diff --git a/step-templates/Azure-Backup-TableStorage-to-Blob.json.human b/step-templates/Azure-Backup-TableStorage-to-Blob.json.human new file mode 100644 index 000000000..08a3b826d --- /dev/null +++ b/step-templates/Azure-Backup-TableStorage-to-Blob.json.human @@ -0,0 +1,96 @@ +{ + "Id": "38791635-a3fc-4b26-bcd7-b65f0f6de5d2", + "Name": "Azure - Backup Table Storage to Blob", + "Description": "This script allow to backup the specified azure table storage into the specified blob.", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "if($IsEnabled -eq \"True\")\r +{\r +Write-Output \"Starting Backup the Azure table 'https://$sourceStorageAccountName.table.core.windows.net/$sourceTableName' to the Blob 'https://$destinationStorageAccountName.blob.core.windows.net/$sourceStorageAccountName-$sourceTableName'\"\r +\r +& \"${Env:ProgramFiles(x86)}\\Microsoft SDKs\\Azure\\AzCopy\\azCopy.exe\" `\r + /Source:https://$sourceStorageAccountName.table.core.windows.net/$sourceTableName/ `\r + /Dest:https://$destinationStorageAccountName.blob.core.windows.net/$sourceStorageAccountName-$sourceTableName/ `\r + /SourceKey:$sourceStorageAccountKey `\r + /Destkey:$destinationStorageAccountKey `\r + /y\r +\r +Write-Output \"Backup Completed\"\r +}\r +else\r +{\r + Write-Output \"This Step is disabled\"\r +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "sourceStorageAccountName", + "Label": "Source Storage Account Name", + "HelpText": "Name of the source storage account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "sourceStorageAccountKey", + "Label": "Source Storage Account Key", + "HelpText": "Key of the source storage account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "sourceTableName", + "Label": "Source Table Name", + "HelpText": "Name of the Source Azure Table", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "destinationStorageAccountName", + "Label": "Destination Storage Account Name", + "HelpText": "Name of the destination storage account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "destinationStorageAccountKey", + "Label": "Destination Storage Account Key", + "HelpText": "Key of the destination storage account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "IsEnabled", + "Label": "IsEnabled", + "HelpText": null, + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "True +False" + } + } + ], + "LastModifiedBy": "phuot", + "$Meta": { + "ExportedAt": "2016-11-30T16:22:50.113Z", + "OctopusVersion": "3.3.22", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/Azure-Container-Copy-to-another-Container.json.human b/step-templates/Azure-Container-Copy-to-another-Container.json.human new file mode 100644 index 000000000..26d94e59d --- /dev/null +++ b/step-templates/Azure-Container-Copy-to-another-Container.json.human @@ -0,0 +1,141 @@ +{ + "Id": "bf955a12-57ee-41b0-af58-672ca48ea07d", + "Name": "Azure - Copy Storage Account Containers", + "Description": "Copy blobs between specified containers across two different storage accounts", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "# Define the source storage account and context +$SourceStorageAccountName = $OctopusParameters['SourceStorageAccountName']; +$SourceStorageAccountKey = $OctopusParameters['SourceStorageAccountKey']; +$SourceContainerName = $OctopusParameters['SourceContainerName']; +$SourceContext = New-AzureStorageContext -StorageAccountName $SourceStorageAccountName -StorageAccountKey $SourceStorageAccountKey +$SourceBlobPrefix = $OctopusParameters['SourceBlobPrefix']; + +# Define the destination storage account and context +$DestinationStorageAccountName = $OctopusParameters['DestinationStorageAccountName']; +$DestinationStorageAccountKey = $OctopusParameters['DestinationStorageAccountKey']; +$DestinationContainerName = $OctopusParameters['DestinationContainerName']; +$DestinationContext = New-AzureStorageContext -StorageAccountName $DestinationStorageAccountName -StorageAccountKey $DestinationStorageAccountKey +$DestinationBlobPrefix = $OctopusParameters['DestinationBlobPrefix']; + +# Check if container exists, otherwise create it +$isContainerExist = Get-AzureStorageContainer -Context $DestinationContext | Where-Object { $_.Name -eq $DestinationContainerName } +if($isContainerExist -eq $null) +{ +    New-AzureStorageContainer -Name $DestinationContainerName -Context $DestinationContext +} + +# Get a reference to blobs in the source container +$blobs = $null +if ($SourceBlobPrefix -eq $null) { + $blobs = Get-AzureStorageBlob -Container $SourceContainerName -Context $SourceContext +} +else { + $blobs = Get-AzureStorageBlob -Container $SourceContainerName -Context $SourceContext -Prefix $SourceBlobPrefix +} + +# Copy blobs from one container to another +if ($DestinationBlobPrefix -eq $null) { +\t$blobs | Start-AzureStorageBlobCopy -DestContainer $DestinationContainerName -DestContext $DestinationContext +} +else { +\t$uri = $SourceContext.BlobEndPoint + $SourceContainerName +\"/\" +\t$blobs | ForEach-Object ` + \t{ Start-AzureStorageBlobCopy ` +\t -SrcUri \"$uri$($_.Name)\" ` + -Context $SourceContext ` +\t -DestContext $DestinationContext ` +\t -DestContainer $DestinationContainerName ` +\t -DestBlob \"$DestinationBlobPrefix/$($_.Name)\" ` +\t } +} + ", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "SourceStorageAccountName", + "Label": "Source Storage Account Name", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SourceStorageAccountKey", + "Label": "Source Storage Account Key", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SourceContainerName", + "Label": "Source Container Name", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SourceBlobPrefix", + "Label": "Source Blob Prefix (Optional)", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DestinationStorageAccountName", + "Label": "Destination Storage Account Name", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DestinationStorageAccountKey", + "Label": "Destination Storage Account Key", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DestinationContainerName", + "Label": "Destination Container Name", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DestinationBlobPrefix", + "Label": "Destination Blob Prefix (Optional)", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "ahmedig", + "$Meta": { + "ExportedAt": "2016-05-12T03:09:48.287+00:00", + "OctopusVersion": "3.3.9", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/Azure-CopySelectiveStorageAccountContainersUsingAZCopy.json.human b/step-templates/Azure-CopySelectiveStorageAccountContainersUsingAZCopy.json.human new file mode 100644 index 000000000..d79405181 --- /dev/null +++ b/step-templates/Azure-CopySelectiveStorageAccountContainersUsingAZCopy.json.human @@ -0,0 +1,139 @@ +{ + "Id": "24ab7967-8ae5-4852-bfdf-4e81d57245f6", + "Name": "Azure - Copy Storage Account Containers AZCopy Inline", + "Description": "Copies Storage Account containers, from a source storage account to destination. It copies the containers with the same names.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$SourceStorageAccountName = $OctopusParameters['SourceStorageAccountName'];\r +$SourceStorageAccountKey = $OctopusParameters['SourceStorageAccountKey'];\r +$DestinationStorageAccountName = $OctopusParameters['DestinationStorageAccountName'];\r +$DestinationStorageAccountKey = $OctopusParameters['DestinationStorageAccountKey'];\r +$ContainersIncluded = $OctopusParameters['ContainersIncluded'];\r +$ContainersExcluded = $OctopusParameters['ContainersExcluded'];\r +\r +$AzCopy = Join-Path ${env:ProgramFiles(x86)} \"Microsoft SDKs\\Azure\\AzCopy\\AzCopy.exe\"\r +\r +function AzCopyContainer($containerName)\r +{\r + &$AzCopy /Source:http://$($SourceStorageAccountName).blob.core.windows.net/$containerName `\r +\t/Dest:http://$($DestinationStorageAccountName).blob.core.windows.net/$containerName `\r +\t/SourceKey:$SourceStorageAccountKey `\r +\t/DestKey:$DestinationStorageAccountKey `\r +\t/S /XO /XN /V | Out-Host\r +}\r +\r +# List all Containers\r +$ctx = New-AzureStorageContext -StorageAccountName $SourceStorageAccountName -StorageAccountKey $SourceStorageAccountKey\r +$containers = Get-AzureStorageContainer -Context $ctx\r +\r +\t\r +# If Containers Included is there => Copy Included Only \r +if($ContainersIncluded)\r +{\r +\t# Parse the Included list\r +\t$ContainersIncluded.Split(\",\") | foreach {\r +\t\tAzCopyContainer $_\r +\t}\r +}\r +\r +# If Containers Excluded is there, and no Included => Copy all except excluded\r +elseif(!$ContainersIncluded -and $ContainersExcluded)\r +{\r +\t#Parse the exclusion list\r +\t[Collections.Generic.List[String]]$lst = $ContainersExcluded.Split(\",\")\r +\r +\t# Loop through all the containers, and\r +\tforeach ($container in $containers) \r +\t{\r +\t\tif($lst.Contains($container.Name)) {\r +\t\t\tcontinue\r +\t\t}\r +\t\telse \r +\t\t{\r +\t\t\t$containerName = $container.Name\r + AzCopyContainer $containerName\r +\t\t}\r +\t} \r +}\r +\r +# Copy all containers\r +else\r +{\r +\t# Loop through all the containers, and\r +\tforeach ($container in $containers) \r +\t{\r +\t\t$containerName = $container.Name\r + AzCopyContainer $containerName\r +\t} \r +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "SourceStorageAccountName", + "Label": "Source Storage Account Name", + "HelpText": "Storage Account Name of the source storage account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SourceStorageAccountKey", + "Label": "Source Storage Account Key", + "HelpText": "Storage Account Key of the source storage account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DestinationStorageAccountName", + "Label": "Destination Storage Account Name", + "HelpText": "Storage Account Name of the destination storage account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DestinationStorageAccountKey", + "Label": "Destination Storage Account Key", + "HelpText": "Storage Account key of the destination storage account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ContainersIncluded", + "Label": "Containers Included", + "HelpText": "A comma separated list of containers that will be copied only, and all the rest will be excluded. If this value is filled with a value, the \"Containers Excluded\" value will be neglected.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ContainersExcluded", + "Label": "Containers Excluded", + "HelpText": "A comma separated list of containers that will be excluded. All containers in source storage account will be copied to destination except these containers. Please note that if the \"Containers Included\" has a value, the \"Containers Excluded\" will be neglected.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "ahmedig", + "$Meta": { + "ExportedAt": "2016-07-24T12:55:38.909+00:00", + "OctopusVersion": "3.3.22", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/Azure-Get-Publishing-Credentials.json.human b/step-templates/Azure-Get-Publishing-Credentials.json.human new file mode 100644 index 000000000..a764454a4 --- /dev/null +++ b/step-templates/Azure-Get-Publishing-Credentials.json.human @@ -0,0 +1,62 @@ +{ + "Id": "94146c4c-28a7-444d-bd09-abdcc860e3b6", + "Name": "Get Azure Web App Publishing Credentials", + "Description": "Gets the publishing credentials for an Azure Web App. They are exported as variables from this step under the names PublishingUsername and PublishingPassword.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 8, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$creds = Invoke-AzureRmResourceAction -ResourceGroupName $ResourceGroup -ResourceType Microsoft.Web/sites/config `\r + -ResourceName $WebApp/publishingCredentials -Action list -ApiVersion 2015-08-01 -Force\r +\r +Set-OctopusVariable -name \"PublishingUsername\" -value $creds.Properties.PublishingUsername\r +Set-OctopusVariable -name \"PublishingPassword\" -value $creds.Properties.PublishingPassword", + "Octopus.Action.Azure.AccountId": "#{AzureAccount}", + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "5a1d5a50-a950-42a4-85d6-25c2b9c45e91", + "Name": "azpubcreds_ResourceGroup", + "Label": "Resource group", + "HelpText": "The name of the resource group that contains the web app for which publishing credentials are required.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "0f86965c-1f40-4778-8378-197ff4199330", + "Name": "azpubcreds_WebApp", + "Label": "Web app", + "HelpText": "The name of the web app for which publishing credentials are required.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "572ee191-a035-47ab-aec3-ef805e690868", + "Name": "azpubcreds_AzureAccount", + "Label": "Azure account", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "jimmcslim", + "$Meta": { + "ExportedAt": "2017-08-23T06:42:23.412Z", + "OctopusVersion": "3.16.0", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/F5-API-enable-disable-member.json.human b/step-templates/F5-API-enable-disable-member.json.human new file mode 100644 index 000000000..589e3faf9 --- /dev/null +++ b/step-templates/F5-API-enable-disable-member.json.human @@ -0,0 +1,246 @@ +{ + "Id": "45d3003a-9443-42a0-aa71-38398eb4f9d6", + "Name": "F5 API Enable, Disable and Force Offline pool member", + "Description": "Enable, Disable and to Force Offline F5 pool member via API. +This step not require iControl snap-in installed.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "#octopus variables +$node = \"#{node}\" +$pool = \"#{pool}\" +$f5pass = \"#{f5pass}\" +$f5user = \"#{f5user}\" +$f5ipv4 = \"#{f5ipv4}\" +$numconn = \"#{numconn}\" +$timeout = \"#{timeout}\" +$action= \"#{action}\" +$f5_ip=$f5ipv4.split(',') + +#whitout ssl certificate +if (-not ([System.Management.Automation.PSTypeName]'ServerCertificateValidationCallback').Type) +{ +$certCallback=@\" + using System; + using System.Net; + using System.Net.Security; + using System.Security.Cryptography.X509Certificates; + public class ServerCertificateValidationCallback + { + public static void Ignore() + { + if(ServicePointManager.ServerCertificateValidationCallback ==null) + { + ServicePointManager.ServerCertificateValidationCallback += + delegate + ( + Object obj, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors errors + ) + { + return true; + }; + } + } + } +\"@ + Add-Type $certCallback + } +[ServerCertificateValidationCallback]::Ignore(); + +#F5 Credentials +$username= $f5user +$password= $f5pass | ConvertTo-SecureString -AsPlainText -Force +$cred= New-Object System.Management.Automation.PSCredential $username, $password +Write-Output \"Cred: $cred\" + +#retrieve Active F5 server +function Get-StatusF5{ + param( + $ipserver, + $credential + ) + $result=Invoke-WebRequest -Uri \"https://$ipserver/mgmt/tm/cm/failover-status\" -Credential $credential -ErrorAction Ignore -UseBasicParsing + $items=$result.Content | ConvertFrom-Json + $status=$items.entries.'https://localhost/mgmt/tm/cm/failover-status/0'.nestedStats.entries.status + return $status +} + +foreach($ipv4 in $f5_ip){ + $state=Get-StatusF5 -ipserver $ipv4 -credential $cred + if (($state.description) -like \"ACTIVE\"){ + $master=$ipv4 + Write-Output \"F5 master ACTIVE: $master\" + } + else{ + Write-Output \"$ipv4 is not master active\" + } +} +if (!$master){ + Write-Error \"ATTENTION - F5 servers are incorrect\" +} + +#retrieve informations +$result=Invoke-WebRequest -Uri \"https://$master/mgmt/tm/ltm/pool/$pool/members\" -Credential $cred -UseBasicParsing +$items=$result.Content | ConvertFrom-Json +$items.items +$name=($items.items | where{$_.name -like \"*$node*\"}).name +Write-Host \"Nome del nodo: $name\" + +#action based on $action +if($action -like \"Enable\"){ + $state ='{\"state\": \"user-up\", \"session\": \"user-enabled\"}' ###ENABLED + Write-Output \"Action: Enable $name\" + Invoke-WebRequest -Uri \"https://$master/mgmt/tm/ltm/pool/$pool/members/~Common~$name\" -Credential $cred -ContentType application/json -Method PUT -Body $state -Verbose -UseBasicParsing +} +else{ + if($action -like \"Disable\"){ + $state ='{\"state\": \"user-up\", \"session\": \"user-disabled\"}' ###Disabled + Write-Output \"Action: Enable $name\" + Invoke-WebRequest -Uri \"https://$master/mgmt/tm/ltm/pool/$pool/members/~Common~$name\" -Credential $cred -ContentType application/json -Method PUT -Body $state -Verbose -UseBasicParsing + } + else{ + if($action -like \"Offline\"){ + $state ='{\"state\": \"user-down\", \"session\": \"user-disabled\"}' ###FORCEDOFFLINE + Invoke-WebRequest -Uri \"https://$master/mgmt/tm/ltm/pool/$pool/members/~Common~$name\" -Credential $cred -ContentType application/json -Method PUT -Body $state -Verbose -UseBasicParsing + $current_conn=$numconn + 00 + + [int]$time = 0 + Write-Output \"Connections accepted: $numconn\" + while($current_conn -gt $numconn){ + if($second -ne $timeout){ + $url=\"https://$master/mgmt/tm/ltm/pool/$pool/members/~Common~$name\" + '/stats?$select=serverside.curConns' + Start-Sleep 1 + [int]$second = $time++ + $result= Invoke-WebRequest -Uri $url -Credential $cred -UseBasicParsing + $item=$result.Content | ConvertFrom-Json + $current_conn=($item.entries.'serverside.curConns').value + Write-Host \"Second: $second - Connections: $current_conn\" + } + else{ + Write-Output \"Timeout - $current_conn connections stopped\" + $current_conn= 0 + } + } + } + else{ + Write-Error \"ACTION IS NOT ACCEPTED\" + } +} +} +Start-sleep 10 +Write-Host \"Go to next step\" +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "c011969a-80e9-47d1-8bd3-d3da6a41ec34", + "Name": "node", + "Label": "Node Member Pool", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "313cf4d5-1006-4e54-9f4a-e4f719ca7988", + "Name": "pool", + "Label": "Pool Name", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "9619ce36-f913-4fa9-91be-9ac650e0d168", + "Name": "f5user", + "Label": "F5 Username", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "6374f90c-ca46-4c13-94df-bb374c9b33f9", + "Name": "f5pass", + "Label": "F5 password", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "e6402134-71af-4b31-b6a4-f2112edb7e25", + "Name": "f5ipv4", + "Label": "F5 Server", + "HelpText": "comma separated (without spaces).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1f402f2b-5907-44ec-87f0-d74bb1901dbd", + "Name": "numconn", + "Label": "Connections limit", + "HelpText": null, + "DefaultValue": "10", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "6b419071-5710-4c47-a1e5-da8434ba190a", + "Name": "timeout", + "Label": "Timeout limit", + "HelpText": null, + "DefaultValue": "60", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "e85c607e-3511-4ae9-8f90-272964fd080f", + "Name": "action", + "Label": "Action member node", + "HelpText": "Change the status of member pool: +- Enabled; +- Disabled; +- Forced Offline.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Enable|Member Enabled in GUI +Disable|Member Disabled in GUI +Offline|Member Forced Offline in GUI" + }, + "Links": {} + } + ], + "LastModifiedBy": "fedelemattia", + "$Meta": { + "ExportedAt": "2016-12-16T14:01:28.267Z", + "OctopusVersion": "3.5.1", + "Type": "ActionTemplate" + }, + "Category": "F5" +} diff --git a/step-templates/GCP-gcloud-run-deploy.json.human b/step-templates/GCP-gcloud-run-deploy.json.human new file mode 100644 index 000000000..c21fe20a8 --- /dev/null +++ b/step-templates/GCP-gcloud-run-deploy.json.human @@ -0,0 +1,162 @@ +{ + "Id": "acbb0a58-e176-4309-8a1f-a0296214d4a4", + "Name": "GCP - gcloud run deploy (bash)", + "Description": "[gcloud run deploy](https://cloud.google.com/sdk/gcloud/reference/run/deploy) - deploy a container to Cloud Run", + "ActionType": "Octopus.GoogleCloudScripting", + "Author": "ryanrousseau", + "Version": 1, + "Packages": [ + { + "Id": "42ba13b1-0b02-48e1-a880-49f2b00ef1a5", + "Name": "GCloudRunDeploy.Container", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "NotAcquired", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "GCloudRunDeploy.Container", + "Purpose": "" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "account=$(get_octopusvariable \"GCloudRunDeploy.Account\") +project=$(get_octopusvariable \"GCloudRunDeploy.Project\") +region=$(get_octopusvariable \"GCloudRunDeploy.Region\") + +service=$(get_octopusvariable \"GCloudRunDeploy.Service\") +image=$(get_octopusvariable \"Octopus.Action.Package[GCloudRunDeploy.Container].Image\") +additionalParams=$(get_octopusvariable \"GCloudRunDeploy.AdditionalParameters\") +printCommand=$(get_octopusvariable \"GCloudRunDeploy.PrintCommand\") + +if [ \"$account\" = \"\" ] ; then + fail_step \"'Account' is a required parameter for this step.\" +fi + +if [ \"$project\" = \"\" ] ; then + fail_step \"'Project' is a required parameter for this step.\" +fi + +if [ \"$region\" = \"\" ] ; then + fail_step \"'Region' is a required parameter for this step.\" +fi + +if [ \"$service\" = \"\" ] ; then + fail_step \"'Service' is a required parameter for this step.\" +fi + +if [ \"$printCommand\" = \"True\" ] ; then + set -x +fi + +gcloud run deploy $service --image=$image ${additionalParams:+ $additionalParams}", + "Octopus.Action.GoogleCloud.UseVMServiceAccount": "False", + "Octopus.Action.GoogleCloudAccount.Variable": "#{GCloudRunDeploy.Account}", + "Octopus.Action.GoogleCloud.ImpersonateServiceAccount": "False", + "Octopus.Action.GoogleCloud.Project": "#{GCloudRunDeploy.Project}", + "Octopus.Action.GoogleCloud.Region": "#{GCloudRunDeploy.Region}", + "Octopus.Action.GoogleCloud.Zone": "#{GCloudRunDeploy.Zone}" + }, + "Parameters": [ + { + "Id": "cc0f6d96-8ae8-49c7-a98e-2c6ec49be818", + "Name": "GCloudRunDeploy.Container", + "Label": "Container", + "HelpText": "The container to deploy to Cloud Run. + +**This step only uses the container image and tag to provide a Release specific value to pass to the `gcloud run deploy` command.** Please make sure that the chosen container is accessible by Google Cloud and the appropriate service accounts.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "7225abe8-e8c0-4c70-b077-e0e603caf330", + "Name": "GCloudRunDeploy.Account", + "Label": "Account", + "HelpText": "Google Cloud Account to use for the command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "GoogleCloudAccount" + } + }, + { + "Id": "7ae8e5b2-2792-415f-b53a-f38ee3a0d605", + "Name": "GCloudRunDeploy.Project", + "Label": "Project", + "HelpText": "Google Cloud project to use with the command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "54794e4f-8681-4198-8157-226909dce509", + "Name": "GCloudRunDeploy.Region", + "Label": "Region", + "HelpText": "Google Cloud region to use with the command. This is used by the underlying Octopus step and not passed to the `gcloud run deploy` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "56ae0926-0f3b-416d-beea-162a02f5f5c0", + "Name": "GCloudRunDeploy.Zone", + "Label": "Zone", + "HelpText": "Google Cloud zone to use with the command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f71d5a0e-215b-4eda-a70d-589313deadd5", + "Name": "GCloudRunDeploy.Service", + "Label": "Service", + "HelpText": "ID of the service or fully qualified identifier for the service. To set the service attribute: + +* provide the argument SERVICE on the command line; +* specify the service name from an interactive prompt. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7425e1db-79a5-4328-8d4b-5f9378e6539c", + "Name": "GCloudRunDeploy.AdditionalParameters", + "Label": "Additional Parameters", + "HelpText": "Provide any additional parameters per [the documentation](https://cloud.google.com/sdk/gcloud/reference/run/deploy). + +Example: `--platform=managed --region=us-central1 --allow-unauthenticated`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c20cdbad-f023-4a47-87a4-d17eee59c3c0", + "Name": "GCloudRunDeploy.PrintCommand", + "Label": "Print Command?", + "HelpText": "Prints the command in the logs using set -x. This will cause a warning when the step runs.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.GoogleCloudScripting", + "$Meta": { + "ExportedAt": "2022-03-17T19:46:39.101Z", + "OctopusVersion": "2022.2.387-hotfix.903", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "ryanrousseau", + "Category": "google-cloud", + "MinimumServerVersion": "2021.2.0" +} diff --git a/step-templates/Jenkins-Queue-Job.json.human b/step-templates/Jenkins-Queue-Job.json.human new file mode 100644 index 000000000..1d1ff5286 --- /dev/null +++ b/step-templates/Jenkins-Queue-Job.json.human @@ -0,0 +1,223 @@ +{ + "Id": "ccb7ad4c-a19e-426f-822e-cd0e0243bda3", + "Name": "Jenkins - Queue Job", + "Description": "Trigger a job in Jenkins", + "ActionType": "Octopus.Script", + "Version": 7, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$jenkinsServer = $OctopusParameters['jqj_JenkinsServer'] +$jenkinsUserName = $OctopusParameters['jqj_JenkinsUserName'] +$jenkinsUserPassword = $OctopusParameters['jqj_JenkinsUserPasword'] +$jobURL = $jenkinsServer + $OctopusParameters['jqj_JobUrl'] +$failBuild = [System.Convert]::ToBoolean($OctopusParameters['jqj_FailBuild']) +$jobTimeout = $OctopusParameters['jqj_JobTimeout'] +$buildParam = $OctopusParameters['jqj_BuildParam'] +$checkIntervals = $OctopusParameters['jqj_checkInterval'] +$fetchBuildWait = $OctopusParameters['jqj_FetchBuildWait'] +$fetchBuildLimit = $OctopusParameters['jqj_FetchBuildLimit'] + +$jobUrlWithParams = \"$jobURL$buildParam\" + +Write-Host \"job url: \" $jobUrlWithParams + +function Get-JenkinsAuth +{ + $params = @{} + if (![string]::IsNullOrWhiteSpace($jenkinsUserName)) { + $securePwd = ConvertTo-SecureString $jenkinsUserPassword -AsPlainText -Force + $credential = New-Object System.Management.Automation.PSCredential ($jenkinsUserName, $securePwd) + $head = @{\"Authorization\" = \"Basic \" + [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($jenkinsUserName + \":\" + $jenkinsUserPassword ))} + $params = @{ + Headers = $head; + Credential = $credential; + ContentType = \"text/plain\"; + } + } + + # If your Jenkins uses the \"Prevent Cross Site Request Forgery exploits\" security option (which it should), + # when you make a POST request, you have to send a CSRF protection token as an HTTP request header. + # https://wiki.jenkins.io/display/JENKINS/Remote+access+API + try { + $tokenUrl = $jenkinsServer + \"crumbIssuer/api/json?tree=crumbRequestField,crumb\" + $crumbResult = Invoke-WebRequest -Uri $tokenUrl -Method Get @params -UseBasicParsing | ConvertFrom-Json + Write-Host \"CSRF protection is enabled, adding CSRF token to request headers\" + $params.Headers += @{$crumbResult.crumbRequestField = $crumbResult.crumb} + } catch { + Write-Host \"Failed to get CSRF token, CSRF may not be enabled\" + Write-Host $Error[0] + } + return $params +} + +try { + Write-Host \"Fetching Jenkins auth params\" + $authParams = Get-JenkinsAuth + + Write-Host \"Start the build\" + $returned = Invoke-WebRequest -Uri $jobUrlWithParams -Method Post -UseBasicParsing @authParams + Write-Host \"Job URL Link: $($returned.Headers['Location'])\" + $jobResult = \"$($returned.Headers['Location'])/api/json\" + $response = Invoke-RestMethod -Uri $jobResult -Method Get @authParams + $buildUrl = $Response.executable.url + $c = 0 + while (($null -eq $buildUrl -or $buildUrl -eq \"\") -and ($c -lt $fetchBuildLimit) ) { + $c += 1 + $response = Invoke-RestMethod -Uri $jobResult -Method Get @authParams + $buildUrl = $Response.executable.url + Start-Sleep -s $fetchBuildWait + } + Write-Host \"Build Number is: $($Response.executable.number)\" + Write-Host \"Job URL Is: $($buildUrl)\" + $buildResult = \"$buildUrl/api/json?tree=result,number,building\" + + $isBuilding = \"True\" + $i = 0 + Write-Host \"Estimate Job Duration: \" $jobTimeout + while ($isBuilding -eq \"True\" -and $i -lt $jobTimeout) { + $i += 5 + Write-Host \"waiting $checkIntervals secs for build to complete\" + Start-Sleep -s $checkIntervals + $retyJobStatus = Invoke-RestMethod -Uri $buildResult -Method Get @authParams + + $isBuilding = $retyJobStatus[0].building + $result = $retyJobStatus[0].result + $buildNumber = $retyJobStatus[0].number + Write-Host \"Retry Job Status: \" $result \" BuildNumber: \" $buildNumber \" IsBuilding: \" $isBuilding + } + if ($failBuild) { + if ($result -ne \"SUCCESS\") { + Write-Host \"BUILD FAILURE: build is unsuccessful or status could not be obtained.\" + exit 1 + } + } +} +catch { + Write-Host \"Exception in jenkins job: $($_.Exception.Message)\" + exit 1 +} +" + }, + "Parameters": [ + { + "Id": "b8337514-3989-4b33-930c-b5ebde5b4be0", + "Name": "jqj_JobUrl", + "Label": "Job Url", + "HelpText": "e.g. job/jobname/build", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a52f7318-6f45-4e9f-b825-b3ae767608f8", + "Name": "jqj_FailBuild", + "Label": "Fail Build", + "HelpText": "Should this fail the deployment?", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "a59c57d3-0177-470c-80e0-f103e57f30d3", + "Name": "jqj_JobTimeout", + "Label": "Timeout Duration(secs)", + "HelpText": "e.g. 60. Specify in secs how long to check for the job status before timing out.", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "70e9cf06-3712-4950-a174-a5c5c7bd5858", + "Name": "jqj_BuildParam", + "Label": "Build Param", + "HelpText": "e.g. ?Param=Value or ?delay=10sec", + "DefaultValue": "/build", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "da8cdc0a-3cd8-4b34-9e5d-13245f77002c", + "Name": "jqj_JenkinsServer", + "Label": "Jenkins Server", + "HelpText": null, + "DefaultValue": "http://jenkins:8080/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1e43a971-d6a2-4692-8dd2-d8b5344b706c", + "Name": "jqj_JenkinsUserName", + "Label": "Jenkins User Name", + "HelpText": "(Optional) User name to use to connect to the Jenkins Server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "fba79fa0-9221-4cd1-9259-5d59e716f0db", + "Name": "jqj_JenkinsUserPasword", + "Label": "Jenkins User Password", + "HelpText": "(Optional) The password to use to connect to the Jenkins Server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "9fea70e1-ff39-4cc1-8937-7cc19b959e17", + "Name": "jqj_checkInterval", + "Label": "Check Interval", + "HelpText": "The sleep time between checking if the job is running in seconds", + "DefaultValue": "20", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "432dbaed-512e-4d29-9b4d-814a8b7c4846", + "Name": "jqj_FetchBuildWait", + "Label": "Fetch Build URL Wait(secs)", + "HelpText": "e.g. 10 Used when getting the build URL. Useful if Jenkins is busy and can't schedule job immediately or there are connection issues. Helps limit excess calls to /api", + "DefaultValue": "10", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cd222e86-cb94-495c-8a5a-8d4cd8b20d86", + "Name": "jqj_FetchBuildLimit", + "Label": "Fetch Build URL Limit", + "HelpText": "e.g. 5. Used to limit the number of times the script asks for the build URL. Used in with FetchBuildURLWait to limit the calls made to /api for build URL.", + "DefaultValue": "5", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2024-07-16T18:49:59.8950000Z", + "LastModifiedBy": "mspikes", + "$Meta": { + "ExportedAt": "2021-09-14T13:38:58.1830000Z", + "OctopusVersion": "2024.1.11966", + "Type": "ActionTemplate" + }, + "Category": "jenkins" +} diff --git a/step-templates/Load-WIF-Issuer-Thumbprints.json.human b/step-templates/Load-WIF-Issuer-Thumbprints.json.human new file mode 100644 index 000000000..4c1961aef --- /dev/null +++ b/step-templates/Load-WIF-Issuer-Thumbprints.json.human @@ -0,0 +1,163 @@ +{ + "Id": "77331575-0628-455d-b484-cfd4703e2081", + "Name": "Load WIF Issuer Thumbprint(s)", + "Description": "Updates the web/app config files' WIF TrustedIssuer thumbprints based on a realtime metadata request. + +Changes are made to the following section: +/configuration/system.identityModel/identityConfiguration/issuerNameRegistry/trustedIssuers", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$FilePath = \"#{FilePath}\"\r +$TrustedIssuerName = \"#{TrustedIssuerName}\"\r +$MetadataUri = \"#{MetadataUri}\"\r +\r +\r +[void][System.Reflection.Assembly]::LoadWithPartialName(\"System.Xml.Linq\")\r +\r +# because Octo calls powershell steps in a stupid manor...\r +$charGT = [System.Text.Encoding]::ASCII.GetString( @(62) )\r +\r +function Get-Thumbprints($MetadataUri) {\r + $MetadataTxt = Invoke-WebRequest -Uri $MetadataUri\r + $MetadataXml = [xml]($MetadataTxt.Content)\r + \r + $outval = @()\r + # new certs\r + \r + $MetadataXml.EntityDescriptor.IDPSSODescriptor.KeyDescriptor | ? { $_.use -eq \"signing\" } | % {\r + $Cert_Bytes = [System.Convert]::FromBase64String( $_.KeyInfo.X509Data.X509Certificate )\r + $Cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2( , $Cert_Bytes ) # powershell is stupid about arrays\r + Write-Host \"Found certificate for [$($_.use)] : [$($cert.NotBefore.ToString(\"yyyyMMdd\")) - $($cert.NotAfter.ToString(\"yyyyMMdd\"))] : Thumbprint [$($Cert.Thumbprint)] for Subject [$($Cert.Subject)]\"\r + $outval += $Cert.Thumbprint\r + }\r + return $outval\r +}\r +\r +function Get-TextIndex([string]$text, [int]$LineNumber = 0, [int]$LinePosition = 0) {\r + # Ported from : https://github.com/futurist/src-location/blob/master/index.js function locationToIndex\r + # NOTE: diff from source to address bug. Test-GetTextIndex validates the changes.\r + $strLF = [char]10 # \ +\r + $strCR = [char]13 # \\r\r + $idx = -1 # text index\r + $lc = 1 # Line Count\r + for($i = 0; $lc -lt $LineNumber -and $i -lt $text.Length; $i++) {\r + $idx++\r + $c = $text[$i] # cur char\r + if ($c -eq $strLF -or $c -eq $strCR) {\r + $lc++\r + if ($c -eq $strCR -and $text[$i + 1] -eq $strLF) { # DOS CRLF\r + $i++\r + $idx++\r + }\r + }\r + }\r + return $idx + $LinePosition\r +}\r +\r +function Replace-TrustedIssuerThumbprints($FilePath, $TrustedIssuerName, $Thumbprints) {\r + # Load the file twice - once as text for manipulation, once as XML for xpath and positions\r + $fileText = [System.IO.File]::ReadAllText($FilePath)\r + $fileXml = [System.Xml.Linq.XDocument]::Load($FilePath, [System.Xml.Linq.LoadOptions]::SetLineInfo -bor [System.Xml.Linq.LoadOptions]::PreserveWhitespace )\r + $IdpsXml = $fileXml.Descendants(\"configuration\")[0].Descendants(\"system.identityModel\")[0].Descendants(\"identityConfiguration\")[0].Descendants(\"issuerNameRegistry\")[0].Descendants(\"trustedIssuers\")[0].Descendants(\"add\")\r +\r + # Figure out which elements to manipulate... First delete from the bottom up, then replace the top-most element\r + $IdpMatches = $IdpsXml | ? { $_.Attribute(\"name\").Value -eq $TrustedIssuerName } | Sort-Object -Property LineNumber, LinePosition -Descending\r + $IdpsToDelete = $IdpMatches | Select-Object -First ($IdpMatches.Count - 1)\r + $IdpsToReplace = $IdpMatches | Select-Object -Last 1\r +\r + # Delete from the bottom up, so that the LineNumber/LinePosition remain valid during the manipulation\r + foreach ($IdP in $IdpsToDelete) {\r + Write-Host ( \"DEL [{0}:{1}] {2}\" -f $IdP.LineNumber, $IdP.LinePosition, $IdP.ToString() )\r +\r + $fileIdxOpen = Get-TextIndex -text $fileText -LineNumber $IdP.LineNumber -LinePosition ( $IdP.LinePosition - 1 )\r + $fileIdxClose = $fileText.IndexOf($charGT, $fileIdxOpen) + 1 # add one to include the closing >\r + $fileSubstr = $fileText.Substring($fileIdxOpen, $fileIdxClose - $fileIdxOpen)\r + Write-Host ( \" [$fileIdxOpen .. $fileIdxClose] : $fileSubstr\" )\r +\r + $fileIdxPrior = $fileText.LastIndexOf($charGT, $fileIdxOpen) + 1\r + $fileText = $fileText.Remove($fileIdxPrior, $fileIdxClose - $fileIdxPrior)\r + }\r + # Replace the top-most element with each thumbprint\r + foreach ($IdP in $IdpsToReplace) {\r + Write-Host ( \"FIX [{0}:{1}] {2}\" -f $IdP.LineNumber, $IdP.LinePosition, $IdP.ToString() )\r +\r + $fileIdxOpen = Get-TextIndex -text $fileText -LineNumber $IdP.LineNumber -LinePosition ( $IdP.LinePosition - 1 )\r + $fileIdxClose = $fileText.IndexOf($charGT, $fileIdxOpen) + 1 # add one to include the closing >\r + $fileSubstr = $fileText.Substring($fileIdxOpen, $fileIdxClose - $fileIdxOpen)\r + Write-Host ( \" [$fileIdxOpen .. $fileIdxClose] : $fileSubstr\" )\r +\r + $fileIdxPrior = $fileText.LastIndexOf($charGT, $fileIdxOpen) + 1\r + $ElementDelim = $fileText.Substring($fileIdxPrior, $fileIdxOpen - $fileIdxPrior)\r + Write-Host ( \" -[{0} .. {1}]\" -f $fileIdxPrior, $fileIdxClose )\r + $fileText = $fileText.Remove($fileIdxPrior, $fileIdxClose - $fileIdxPrior)\r + foreach ($Thumbprint in $Thumbprints) {\r + $newAttribs = [System.Xml.Linq.XAttribute[]]@(\r + ( New-Object System.Xml.Linq.XAttribute(\"thumbprint\", $Thumbprint ) ),\r + ( New-Object System.Xml.Linq.XAttribute(\"name\" , $TrustedIssuerName) )\r + )\r + $newValue = ( New-Object System.Xml.Linq.XElement(\"add\", $newAttribs) ).ToString()\r + $fileText = $fileText.Insert($fileIdxPrior, $ElementDelim + $newValue)\r + }\r + }\r + return $fileText\r +}\r +\r +\r +$ThumbPrints = Get-Thumbprints -MetadataUri $MetadataUri\r +$fileContent = Replace-TrustedIssuerThumbprints -FilePath $FilePath -TrustedIssuerName $TrustedIssuerName -Thumbprints $ThumbPrints\r +[System.IO.File]::WriteAllText($FilePath, $fileContent)\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "9fa8d0c0-53a3-4d38-b183-bab04037869e", + "Name": "FilePath", + "Label": "web config file path", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "e97dcceb-fbf1-41f2-9829-e32c6322fe58", + "Name": "TrustedIssuerName", + "Label": "Trusted Issuer Name", + "HelpText": "", + "DefaultValue": "https://adfs/adfs/services/trust", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "9670671a-5d7e-4232-9d45-5fc47d005167", + "Name": "MetadataUri", + "Label": "Metadata Uri", + "HelpText": null, + "DefaultValue": "https://adfs/FederationMetadata/2007-06/FederationMetadata.xml", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2018-06-07T19:59:41.925Z", + "LastModifiedBy": "sbrickey", + "Category": "XML", + "$Meta": { + "ExportedAt": "2018-06-07T19:59:41.925Z", + "OctopusVersion": "3.17.2", + "Type": "ActionTemplate" + } +} diff --git a/step-templates/Remove-Hosts-File-Entry.json.human b/step-templates/Remove-Hosts-File-Entry.json.human new file mode 100644 index 000000000..fcc29548d --- /dev/null +++ b/step-templates/Remove-Hosts-File-Entry.json.human @@ -0,0 +1,64 @@ +{ + "Id": "cf18f938-a052-4e0a-9549-85bfa9d95a2a", + "Name": "Windows - Remove Hosts File Entry", + "Description": "Remove given IP/host name entry exists in the hosts file. + +This might be helpful in uninstall process.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "$ip = $OctopusParameters['IP'] +$hostName = $OctopusParameters['HostName'] + +$hostsPath = \"$env:windir\\System32\\drivers\\etc\\hosts\" +$hosts = Get-Content $hostsPath +$match = $hosts -match (\"^\\s*$ip\\s+$hostName\" -replace '\\.', '\\.') +$hostsEntry = \"$ip`t$hostName\" + +If ($match) { +write-host $hostsPath $hostsEntry \" exist, then it will be removed.\" +} +else +{ +write-host $hostsPath $hostsEntry \"not exist\" +Exit +} + +(Get-Content -Path $hostsPath) | + ForEach-Object {$_ -Replace \"$hostsEntry\"} | + Set-Content -Path $hostsPath -Verbose", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "d9320ac1-f7cd-4895-9a9b-b2a35bc9bbe3", + "Name": "IP", + "Label": "IP Address", + "HelpText": "The IP address which the host name should resolve to, e.g. 127.0.0.1", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "129047cb-3e0c-4d83-a734-7d9bef6b5023", + "Name": "HostName", + "Label": "Host Name", + "HelpText": "The host name which should resolve to the given IP, e.g. www.mydomain.com", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-08-04T20:32:55.063Z", + "OctopusVersion": "2020.4.6", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "bilalmajali", + "Category": "other" +} diff --git a/step-templates/SQL-Query-Octopus-Database-for-Fragmentation.json.human b/step-templates/SQL-Query-Octopus-Database-for-Fragmentation.json.human new file mode 100644 index 000000000..debba095b --- /dev/null +++ b/step-templates/SQL-Query-Octopus-Database-for-Fragmentation.json.human @@ -0,0 +1,227 @@ +{ + "Id": "b362bd69-4a69-42c1-bcb5-2a134549ef3f", + "Name": "SQL - Query Octopus Database for Fragmentation", + "Description": "This step template will run a fragmentation query on your Octopus database and report the results of the tables. + +If you would like to set this up as a scheduled runbook and get the results in an email, please follow these instructions: +1) Create a Send an Email step after this step in your process +2) Set the body type of that email to HTML, and the body to#{Octopus.Action[STEPNAMEHERE].Output.EmailData} +3) Set the Run Condition of that Send an Email step to Variable, and the value to #{if Octopus.Action[STEPNAMEHERE].Output.Alert== \"True\"}True#{/if}. If you don't do this, you will receive an email regardless of if the threshold was hit.'", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "###PARAMETERS + +[string]$sqlUsername = $OctopusParameters[\"IndexFragmentSQLUsername\"] +[string]$sqlPassword = $OctopusParameters[\"IndexFragmentSQLPassword\"] +[int]$threshold = $OctopusParameters[\"IndexFragmentFragmentation\"] +[string]$SQLServer = $OctopusParameters[\"IndexFragmentSQLServer\"] +[string]$SQLPort = $OctopusParameters[\"IndexFragmentSQLPort\"] +[string]$databaseName = $OctopusParameters[\"IndexFragmentDatabaseName\"] +[string]$pageCount = $OctopusParameters[\"IndexFragmentPageCount\"] + + +if ([string]::IsNullOrWhiteSpace($SQLPort)){ +$SQLPort = \"1433\" +} + +#create the full sql server string +[string]$SQLServerFull = $SQLServer + \",\" + $SQLPort + +#creating the connectionString based on choice of auth +if ([string]::IsNullOrWhiteSpace($sqlUserName)){ +\tWrite-Highlight \"Integrated Authentication being used to connect to SQL.\" + $connectionString = \"Server=$SQLServerFull;Database=$databaseName;integrated security=true;\" +} +else { +\tWrite-Highlight \"SQL Authentication being used to connect to SQL\" + $connectionString = \"Server=$SQLServerFull;Database=$databaseName;User ID=$sqlUsername;Password=$sqlPassword;\" +} + +#function for running the query +function ExecuteSqlQuery ($connectionString, $SQLQuery) { + $Datatable = New-Object System.Data.DataTable + $Connection = New-Object System.Data.SQLClient.SQLConnection + $Connection.ConnectionString = $connectionString +\ttry{ + \t$Connection.Open() + \t$Command = New-Object System.Data.SQLClient.SQLCommand + \t$Command.Connection = $Connection + \t$Command.CommandText = $SQLQuery + \t$Reader = $Command.ExecuteReader() + \t$Datatable.Load($Reader) + } + catch{ + \tWrite-Error $_.Exception.Message + } + finally{ + \tif (($Connection.State) -ne \"Closed\"){ + Write-Highlight \"Closing the SQL Connection.\" + \t$Connection.Close() + } + } + return $Datatable +} + +#Create the query for fragmentation check +$query = @\" +SELECT S.name as 'Schema', +T.name as 'Table', +I.name as 'Index', +DDIPS.avg_fragmentation_in_percent, +DDIPS.page_count +FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS DDIPS +INNER JOIN sys.tables T on T.object_id = DDIPS.object_id +INNER JOIN sys.schemas S on T.schema_id = S.schema_id +INNER JOIN sys.indexes I ON I.object_id = DDIPS.object_id +AND DDIPS.index_id = I.index_id +WHERE DDIPS.database_id = DB_ID() +and I.name is not null +AND DDIPS.avg_fragmentation_in_percent > 0 +ORDER BY DDIPS.avg_fragmentation_in_percent desc +\"@ + +#Run the query against the server and return as a dataset +$resultsDataTable = New-Object System.Data.DataTable +$resultsDataTable = ExecuteSqlQuery $connectionString $query + +#creating variables for later use +$highestFrag = 0 +$array = @() + +#build an array of html so the data is readable +$dataforemail = @() +$dataforemail += \"

SQL Fragmentation Report


\" +$dataforemail += '' +$dataforemail += \"\" +foreach ($row in $resultsDataTable){ +\t#checking if the current row's fragmentation % is higher than our highest if it is, set it +\tif ($row.avg_fragmentation_in_percent -gt $highestFrag -and $row.page_count -gt $pageCount){ +\t\t$highestFrag = $row.avg_fragmentation_in_percent +\t} + #if both thresholds are hit, put the data in HTML format and also an array to later write to console. +\tif ($row.avg_fragmentation_in_percent -gt $threshold -and $row.page_count -gt $pageCount){ + $percent = [math]::Round($row.avg_fragmentation_in_percent,2) +\t\t$dataforemail += \"\" +\t\t$dataforemail += \"\" +\t\t$dataforemail += \"\" + $dataforemail += \"\" + $dataforemail += \"\" +\t\t$dataforemail += \"\" + + $arrayRow = \"\" | Select Table,Index,avg_fragmentation_in_percent,page_count + \t$arrayRow.Table = $Row.Table + \t$arrayRow.Index = $Row.Index + $arrayRow.avg_fragmentation_in_percent = [string]$percent + $arrayRow.page_count = $Row.page_count + \t$array += $arrayRow +\t} +} +$dataforemail += \"
Table IndexFragmentation %Page Count
\" + $row.Table + \"\" + $row.Index + \"\" + [string]$percent + \"\" + $row.page_count + \"
\" + +#if the threshold has been reached, output data and create output variable for sending email. +if ($highestFrag -gt $threshold){ + +\t#convert the array to a string to email +\t[string]$bodyofemail = [string]$dataforemail + +\t#Create all of the necessary variables and output the data +\t\tSet-OctopusVariable -name \"EmailData\" -value \"$dataforemail\" + Set-OctopusVariable -name \"Alert\" -value \"True\" + $output = $array | Out-String + Write-Highlight 'Here are the results for your database fragmentation. The following tables had above the provided fragmentation % and minimum page count. If you would like to get an email alert with the data, please refer to the description of the step template for instructions on setting that up.' + Write-Highlight $output +} +else{ + +\tWrite-Highlight \"No alert is required.\" + Set-OctopusVariable -name \"Alert\" -value \"False\" + + +} +" + }, + "Parameters": [ + { + "Id": "05a6a3f3-3cb9-4d75-abac-b2125fb84a3b", + "Name": "IndexFragmentSQLServer", + "Label": "SQL Server", + "HelpText": "Enter the Hostname or IP address of your server. Include \\Instance if necessary.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8d6081b3-b934-475b-8b73-cde72f5d085d", + "Name": "IndexFragmentSQLPort", + "Label": "SQL Server Port", + "HelpText": "If left blank, 1433 will be used by default.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "42671f3c-763e-4dbd-836d-ed794d4b0005", + "Name": "IndexFragmentDatabaseName", + "Label": "SQL Server Database Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0e439749-1fa9-40f2-9843-126ade5576b1", + "Name": "IndexFragmentFragmentation", + "Label": "Fragmentation Threshold", + "HelpText": "Input the percentage of Fragmentation you would like to be alerted for.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7050d208-a267-412a-9677-95e90909264c", + "Name": "IndexFragmentPageCount", + "Label": "Page Count Threshold", + "HelpText": "Input the minimum page count a table must have to be considered in the results.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4a9d3846-c454-483f-9066-60a1f2cf7413", + "Name": "IndexFragmentSQLUsername", + "Label": "SQL Server Authentication Username", + "HelpText": "Please leave blank if you want to use Integrated Security.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f04df930-bb05-49c1-b6f0-51e97dce3bad", + "Name": "IndexFragmentSQLPassword", + "Label": "SQL Server Authentication Password", + "HelpText": "Please leave blank if you want to use Integrated Security.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-05-25T19:02:40.382Z", + "OctopusVersion": "2020.6.4722", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "millerjn21", + "Category": "sql" +} diff --git a/step-templates/TeamCity-Pin-Build.json.human b/step-templates/TeamCity-Pin-Build.json.human new file mode 100644 index 000000000..2a578852e --- /dev/null +++ b/step-templates/TeamCity-Pin-Build.json.human @@ -0,0 +1,91 @@ +{ + "Id": "b3137de8-93c9-4a10-8f6a-5dd14175b843", + "Name": "Pin TeamCity Build Version", + "Description": "Try to pin the TeamCity build version. (Requires Octopus version to match TeamCity version)", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$buildNumber = $OctopusParameters['buildNumber']\r +$buildTypeId = $OctopusParameters['buildTypeId']\r +\r +$tcUrl = $OctopusParameters['TeamCityUrl']\r +$tcUser = $OctopusParameters['TeamCityUser']\r +$tcPass = $OctopusParameters['TeamCityPassword']\r +\r +[string]$tcRestUrl = $tcUrl + '/httpAuth/app/rest/builds/buildType:{1},number:{0}/pin/'\r +$url = $tcRestUrl -f $buildNumber,$buildTypeId\r +\r +Write-Host \"****************************\"\r +Write-Host \"Pinning build in TeamCity at:\"$url \r +Write-Host \"****************************\"\r +\r +$req = [System.Net.WebRequest]::Create($url)\r +$req.Credentials = new-object System.Net.NetworkCredential($tcUser, $tcPass)\r +$req.Method =\"PUT\"\r +$req.ContentLength = 0\r +\r +$resp = $req.GetResponse()\r +$reader = new-object System.IO.StreamReader($resp.GetResponseStream())\r +$reader.ReadToEnd() | Write-Host\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "buildNumber", + "Label": "Build Number", + "HelpText": null, + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "buildTypeId", + "Label": "Build Configuration ID", + "HelpText": "The build configuration id to look for the build to pin. + +General Settings of the Build Configuration", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityUrl", + "Label": "Url of TeamCity Server", + "HelpText": "The url to the TeamCity server.", + "DefaultValue": "http://localhost:8082", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityUser", + "Label": "TeamCity User", + "HelpText": "The TeamCity user used for pinning the build", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityPassword", + "Label": "TeamCity User Password", + "HelpText": "The password for the TeamCity user.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-02-11T08:20:13.192+00:00", + "OctopusVersion": "2.6.1.796", + "Type": "ActionTemplate" + }, + "Category": "teamcity" +} diff --git a/step-templates/Wait-until-time.json.human b/step-templates/Wait-until-time.json.human new file mode 100644 index 000000000..4118f6f1f --- /dev/null +++ b/step-templates/Wait-until-time.json.human @@ -0,0 +1,58 @@ +{ + "Id": "c8781255-615a-4092-99f1-82ddefdb6d6b", + "Name": "Wait until time", + "Description": "Pauses the process until a given time", + "ActionType": "Octopus.Script", + "Version": 25, + "Properties": { + "Octopus.Action.Script.ScriptBody": "if(($DefaultTargetTime -eq $null-or $DefaultTargetTime -eq '') -and ($TargetTime -eq $null -or $TargetTime -eq'') ){ + Write-Output 'Deploy will start immediately because neither TargetTime or DefaultTargetTime is set' +}else{ + + if($TargetTime -eq $null){ + $deployTime = get-date($DefaultTargetTime) + Write-Output 'DeployTime is set to DefaultTargetTime since TargetTime is not configured as a variable for this build scope.' + }else{ + $deployTime = get-date($TargetTime) + } + if((get-date) -ge $deployTime){ + $deployTime = $deployTime.AddDays(1) + } + Write-Output ('Deploy will pause until ' + $deployTime) + + + do { + \tStart-Sleep 1 + } + until ((get-date) -ge $deployTime) +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "DefaultTargetTime", + "Label": "Time to deploy", + "HelpText": "Will deploy within the next 24 hours if only time is specified or at a specific date if that is specified +A time set here will be used unless the project has a variable TargetTime. +Examples: + +\t03:00\t\t\t\tWill deploy within 24 hours when the time is 03:00 in the morning +\t2099-09-14 05:00\tWill deploy at that day and time +\t2025-10-01\t\t\tWill deploy at 00:00 the first of october 2025 + +If the deployment time changes often it is suggested to create a variable named TargetTime and make it promptable. This will force the user to set the deployment time before the build process starts.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "PeterOsterdahl", + "$Meta": { + "ExportedAt": "2016-09-15T10:46:20.183+00:00", + "OctopusVersion": "3.2.21", + "Type": "ActionTemplate" + }, + "Category": "wait" +} diff --git a/step-templates/add-server-to-azure-load-balancer.json.human b/step-templates/add-server-to-azure-load-balancer.json.human new file mode 100644 index 000000000..8866cda5d --- /dev/null +++ b/step-templates/add-server-to-azure-load-balancer.json.human @@ -0,0 +1,156 @@ +{ + "Id": "540de6f6-f1c6-4e1c-9062-9e78570074f6", + "Name": "Add Server to Azure Load Balancer", + "Description": "Uses Service Principal to Add Server to Azure Load Balancer.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "#region Verify variables + +#Verify psbilbAzureSubscription is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['psbilbAzureSubscription'])) +{ + Throw 'Azure Subscription cannot be null.' +} +$azureSubscription = $OctopusParameters['psbilbAzureSubscription'] +Write-Host ('Azure Subscription: ' + $azureSubscription) + +#Verify psbilbAzureResourceGroup is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['psbilbAzureResourceGroup'])) +{ + Throw 'Azure Resource Group cannot be null.' +} +$azureResourceGroup = $OctopusParameters['psbilbAzureResourceGroup'] +Write-Host ('Azure Resource Group: ' + $azureResourceGroup) + +#Verify psbilbAzureMachineName is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['psbilbAzureMachineName'])) +{ + Throw 'Azure Machine Name cannot be null.' +} +$azureMachineName = $OctopusParameters['psbilbAzureMachineName'] +Write-Host ('Azure Machine Name: ' + $azureMachineName) + +#Verify psbilbAzureLoadBalancer is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['psbilbAzureLoadBalancer'])) +{ + Throw 'Azure Load Balancer cannot be null.' +} +$azureLoadBalancer = $OctopusParameters['psbilbAzureLoadBalancer'] +Write-Host ('Azure Load Balancer: ' + $azureLoadBalancer) + +#Verify psbilbAzureLoadBalancerBackEndPoolName is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['psbilbAzureLoadBalancerBackEndPoolName'])) +{ + Throw 'Azure Load Balancer Backend Pool Name cannot be null.' +} +$azureLoadBalancerBackendPoolName = $OctopusParameters['psbilbAzureLoadBalancerBackEndPoolName'] +Write-Host ('Azure Load Balancer Backend Pool Name: ' + $azureLoadBalancerBackendPoolName) + +#endregion + +#region Process + +Set-AzureRmContext -SubscriptionName $azureSubscription + +$azureVM = Get-AzureRmVM -ResourceGroupName $azureResourceGroup -Name $azureMachineName +If (!$azureVM) +{ + Throw 'Could not retrieve server from Azure needed to remove from Load Balancer.' +} + +$nic = (Get-AzureRmNetworkInterface -ResourceGroupName $azureResourceGroup | Where-Object {$_.VirtualMachine.Id -eq $azureVM.Id}) +If (!$nic) +{ + Throw 'Could not retrieve NIC from Azure needed to remove from Load Balancer.' +} + +$loadBalancer = Get-AzureRmLoadBalancer -Name $azureLoadBalancer -ResourceGroupName $azureResourceGroup +If (!$loadBalancer) +{ + Throw 'Could not retrieve Load Balancer info from Azure.' +} + +$ap = Get-AzureRmLoadBalancerBackendAddressPoolConfig -Name #{psbilbAzureLoadBalancerBackEndPoolName} -LoadBalancer $loadBalancer + +$nic.IpConfigurations[0].LoadBalancerBackendAddressPools = $ap +$nic | Set-AzureRmNetworkInterface + +#endregion +", + "Octopus.Action.Azure.AccountId": "#{psbilbAzureAccount}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "Parameters": [ + { + "Id": "85e7fd91-8976-415a-82b7-1303e150b447", + "Name": "psbilbAzureAccount", + "Label": "Azure Account", + "HelpText": "Service Principal used to connect to Azure", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "e494b221-0b8e-4f02-84f1-079199e9f4b4", + "Name": "psbilbAzureSubscription", + "Label": "Azure Subscription", + "HelpText": "Subscription Load Balancer belongs to. Eg. 00000000-0000-0000-0000-000000000000", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4e0b35a8-c9f7-49e0-b9d2-e8121cf589af", + "Name": "psbilbAzureResourceGroup", + "Label": "Azure Resource Group", + "HelpText": "Resource Group Load Balancer belongs to. Eg. ProductionAus", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "44dfd700-28eb-463c-923a-58cc5ab76eb0", + "Name": "psbilbAzureMachineName", + "Label": "Azure Machine Name", + "HelpText": "Name of Virtual Machine to add to Load Balancer. Eg Web01", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "337bb455-a17c-4251-9543-26cb9ffd7087", + "Name": "psbilbAzureLoadBalancer", + "Label": "Azure Load Balancer", + "HelpText": "Name of Load Balancer. Eg. LoadBalacer01", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0162154d-83ac-41ca-bd6d-8f983187eccd", + "Name": "psbilbAzureLoadBalancerBackEndPoolName", + "Label": "Azure Load Balancer Back End Pool Name", + "HelpText": "Name of Backend Pool on Azure Load Balancer. Eg. WebServers", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-22T10:39:16.450Z", + "OctopusVersion": "2020.2.8", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "benjimac93", + "Category": "Azure" +} diff --git a/step-templates/akamai-content-purge.json.human b/step-templates/akamai-content-purge.json.human new file mode 100644 index 000000000..14cd03a74 --- /dev/null +++ b/step-templates/akamai-content-purge.json.human @@ -0,0 +1,371 @@ +{ + "Id": "0bfe41dc-50b0-44a7-abe3-e6ba2a6d317a", + "Name": "Akamai - Content Purge", + "Description": "Allows to purge content using the Content Control Utility (CCU) v2 REST API.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$clientToken = $OctopusParameters['AkamaiClientToken']\r +$clientAccessToken = $OctopusParameters['AkamaiClientAccessToken']\r +$clientSecret = $OctopusParameters['AkamaiSecret']\r +$queueName = $OctopusParameters['AkamaiQueue']\r +$objects = $OctopusParameters['AkamaiObjects'] -split \",\"\r +$type = $OctopusParameters['AkamaiType']\r +$action = $OctopusParameters['AkamaiAction']\r +$domain = $OctopusParameters['AkamaiDomain']\r +$proxyUser = $OctopusParameters['ProxyUser']\r +$proxyPassword = $OctopusParameters['ProxyPassword']\r +\r +$wait = [bool]::Parse($OctopusParameters['WaitForPurgeToComplete'])\r +$maxChecks = [int]::Parse($OctopusParameters['CheckLimit'])\r +\r +if ($proxyUser) {\r + $securePassword = ConvertTo-SecureString $proxyPassword -AsPlainText -Force\r + $proxyCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $proxyUser,$securePassword\r +\r + (New-Object System.Net.WebClient).Proxy.Credentials=$proxyCredential\r +}\r +\r +# Copied from https://github.com/akamai-open/AkamaiOPEN-powershell/blob/master/Invoke-AkamaiOPEN.ps1\r +function Invoke-AkamaiOpenRequest {\r + param(\r + [Parameter(Mandatory=$true)][string]$Method, \r + [Parameter(Mandatory=$true)][string]$ClientToken, \r + [Parameter(Mandatory=$true)][string]$ClientAccessToken, \r + [Parameter(Mandatory=$true)][string]$ClientSecret, \r + [Parameter(Mandatory=$true)][string]$ReqURL, \r + [Parameter(Mandatory=$false)][string]$Body)\r +\r + #Function to generate HMAC SHA256 Base64\r + Function Crypto ($secret, $message)\r + {\r +\t [byte[]] $keyByte = [System.Text.Encoding]::ASCII.GetBytes($secret)\r +\t [byte[]] $messageBytes = [System.Text.Encoding]::ASCII.GetBytes($message)\r +\t $hmac = new-object System.Security.Cryptography.HMACSHA256((,$keyByte))\r +\t [byte[]] $hashmessage = $hmac.ComputeHash($messageBytes)\r +\t $Crypt = [System.Convert]::ToBase64String($hashmessage)\r +\r +\t return $Crypt\r + }\r +\r + #ReqURL Verification\r + If (($ReqURL -as [System.URI]).AbsoluteURI -eq $null -or $ReqURL -notmatch \"akamai.com\")\r + {\r +\t throw \"Error: Ivalid Request URI\"\r + }\r +\r + #Sanitize Method param\r + $Method = $Method.ToUpper()\r +\r + #Split $ReqURL for inclusion in SignatureData\r + $ReqArray = $ReqURL -split \"(.*\\/{2})(.*?)(\\/)(.*)\"\r +\r + #Timestamp for request signing\r + $TimeStamp = [DateTime]::UtcNow.ToString(\"yyyyMMddTHH:mm:sszz00\")\r +\r + #GUID for request signing\r + $Nonce = [GUID]::NewGuid()\r +\r + #Build data string for signature generation\r + $SignatureData = $Method + \"`thttps`t\"\r + $SignatureData += $ReqArray[2] + \"`t\" + $ReqArray[3] + $ReqArray[4]\r +\r + if (($Body -ne $null) -and ($Method -ceq \"POST\"))\r + {\r +\t $Body_SHA256 = [System.Security.Cryptography.SHA256]::Create()\r +\t $Post_Hash = [System.Convert]::ToBase64String($Body_SHA256.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($Body.ToString())))\r +\r +\t $SignatureData += \"`t`t\" + $Post_Hash + \"`t\"\r + }\r + else\r + {\r +\t $SignatureData += \"`t`t`t\"\r + }\r +\r + $SignatureData += \"EG1-HMAC-SHA256 \"\r + $SignatureData += \"client_token=\" + $ClientToken + \";\"\r + $SignatureData += \"access_token=\" + $ClientAccessToken + \";\"\r + $SignatureData += \"timestamp=\" + $TimeStamp + \";\"\r + $SignatureData += \"nonce=\" + $Nonce + \";\"\r +\r + #Generate SigningKey\r + $SigningKey = Crypto -secret $ClientSecret -message $TimeStamp\r +\r + #Generate Auth Signature\r + $Signature = Crypto -secret $SigningKey -message $SignatureData\r +\r + #Create AuthHeader\r + $AuthorizationHeader = \"EG1-HMAC-SHA256 \"\r + $AuthorizationHeader += \"client_token=\" + $ClientToken + \";\"\r + $AuthorizationHeader += \"access_token=\" + $ClientAccessToken + \";\"\r + $AuthorizationHeader += \"timestamp=\" + $TimeStamp + \";\"\r + $AuthorizationHeader += \"nonce=\" + $Nonce + \";\"\r + $AuthorizationHeader += \"signature=\" + $Signature\r +\r + #Create IDictionary to hold request headers\r + $Headers = @{}\r +\r + #Add Auth header\r + $Headers.Add('Authorization',$AuthorizationHeader)\r +\r + #Add additional headers if POSTing or PUTing\r + If (($Method -ceq \"POST\") -or ($Method -ceq \"PUT\"))\r + {\r +\t $Body_Size = [System.Text.Encoding]::UTF8.GetByteCount($Body)\r +\t $Headers.Add('max-body',$Body_Size.ToString())\r +\r + # turn off the \"Expect: 100 Continue\" header\r + # as it's not supported on the Akamai side.\r + [System.Net.ServicePointManager]::Expect100Continue = $false\r + }\r +\r + #Check for valid Methods and required switches\r + If (($Method -ceq \"POST\") -and ($Body -ne $null))\r + {\r + Invoke-RestMethod -Method $Method -Uri $ReqURL -Headers $Headers -Body $Body -ContentType 'application/json'\r + }\r + elseif (($Method -ceq \"PUT\") -and ($Body -ne $null))\r + {\r +\t #Invoke API call with PUT and return\r +\t Invoke-RestMethod -Method $Method -Uri $ReqURL -Headers $Headers -Body $Body -ContentType 'application/json'\r + }\r + elseif (($Method -ceq \"GET\") -or ($Method -ceq \"DELETE\"))\r + {\r +\t #Invoke API call with GET or DELETE and return\r +\t Invoke-RestMethod -Method $Method -Uri $ReqURL -Headers $Headers\r + }\r + else\r + {\r +\t throw \"Error: Invalid -Method specified or missing required parameter\"\r + }\r +}\r +\r +function Perform-AkamaiRequest {\r + param (\r + [string]$request, \r + [string]$method=\"Get\", \r + [int]$expectedStatusCode=200, \r + $body)\r +\r + $baseUrl = \"http://private-anon-3934daf8d-akamaiopen2purgeccuv2production.apiary-mock.com\"\r + # $baseUrl = \"https://api.ccu.akamai.com\"\r + $uri = \"{0}{1}\" -f $baseUrl,$request\r +\r + if ($uri -match \"mock\"){\r + $requestHeaders = @{'Cache-Control'='no-cache,proxy-revalidate'}\r + $response = Invoke-RestMethod -Uri $uri -Method $method -DisableKeepAlive -Headers $requestHeaders -Body $body\r + } else {\r + $json = ConvertTo-Json $body -Compress\r + $response = Invoke-AkamaiOpenRequest -Method $method -ClientToken $clientToken -ClientAccessToken $clientAccessToken -ClientSecret $clientSecret -ReqURL $uri -Body $json\r + }\r +\r + if ($response.httpStatus -ne $expectedStatusCode){\r + Write-Error \"Request not processed correctly: $($response.detail)\"\r + } elseif ($response.detail) {\r + Write-Verbose $response.detail\r + }\r +\r + Write-Verbose $response\r +\r + $response\r +}\r +\r +function Get-QueueSize {\r + param ([string]$queueName)\r +\r + $queueSize = Perform-AkamaiRequest \"/ccu/v2/queues/$queueName\"\r +\r + $queueSize \r +}\r +\r +function Request-Purge {\r + param ($objects,[string]$type=\"arl\",[string]$action=\"remove\",[string]$domain=\"production\")\r +\r + $body = @{\r + objects = $objects\r + action = $action\r + type = $type\r + domain = $domain\r + }\r +\r + Perform-AkamaiRequest \"/ccu/v2/queues/$queueName\" \"Post\" 201 $body\r +}\r +\r +function Get-PurgeStatus {\r + param ([string]$purgeId)\r +\r + $status = Perform-AkamaiRequest \"/ccu/v2/purges/$purgeId\"\r +\r + Write-Host \"Purge status: $($status.purgeStatus)\"\r +\r + $status\r +}\r +\r +$queueSize = Get-QueueSize $queueName\r +Write-Output \"$($queueName) queue size is $($queueSize.queueLength)\"\r +\r +$purge = Request-Purge $objects $type $action $domain\r +\r +Write-Output \"Purge request created\"\r +Write-Output \"PurgeId: $($purge.purgeId)\"\r +Write-Output \"SupportId: $($purge.supportId)\" \r +\r +if ($wait) {\r + $check = 1\r + $purgeStatus = \"Unknown\"\r +\r + do {\r + if ($check -gt 1) {\r + Write-Output \"Waiting $($purge.pingAfterSeconds) seconds before checking purge status again\"\r + Start-Sleep -Seconds $purge.pingAfterSeconds\r + }\r + $status = Get-PurgeStatus $purge.purgeId \r + $purgeStatus = $status.purgeStatus\r + $check = $check + 1\r + if ($check -gt $maxChecks) { \r + Write-Output \"Maximum number of checks reached\"\r + }\r + } while ($status.purgeStatus -ne \"Done\" -and $check -le $maxChecks)\r +\r + if ($status.purgeStatus -ne \"Done\") {\r + Write-Error \"Purge status is not Done\"\r + }\r +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "AkamaiClientToken", + "Label": "Client Token", + "HelpText": "Authentication token used in client authentication. Available in Luna Portal.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AkamaiClientAccessToken", + "Label": "Client Access Token", + "HelpText": "Authentication token used in client authentication. Available in Luna Portal.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AkamaiSecret", + "Label": "Secret", + "HelpText": "Authentication password used in client authentication. Available in Luna Portal.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "AkamaiQueue", + "Label": "Queue", + "HelpText": "Purge requests queue", + "DefaultValue": "default", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "default +emergency" + } + }, + { + "Name": "AkamaiObjects", + "Label": "Objects", + "HelpText": "A comma separated list of objects to purge, either URLs or CPCODEs", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AkamaiType", + "Label": "Type", + "HelpText": "The type associated with the items in the Objects parameter", + "DefaultValue": "arl", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "arl +cpcode" + } + }, + { + "Name": "AkamaiAction", + "Label": "Action", + "HelpText": "The action to execute on the purge operation", + "DefaultValue": "invalidate", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "invalidate +remove" + } + }, + { + "Name": "AkamaiDomain", + "Label": "Domain", + "HelpText": "The Akamai domain to perform the purge operation on", + "DefaultValue": "production", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "production +staging" + } + }, + { + "Name": "ProxyUser", + "Label": "Proxy User", + "HelpText": "Optional, a user name for the proxy if required in the network", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ProxyPassword", + "Label": "Proxy Password", + "HelpText": "Optional, the password for the account to use if a Proxy User was specified", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "WaitForPurgeToComplete", + "Label": "Wait", + "HelpText": "Indicates if the step should wait for the purge operation to complete", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "false|No +true|Yes" + } + }, + { + "Name": "CheckLimit", + "Label": "Check Limit", + "HelpText": "Maximum number of times to check for the purge operation to complete if set to **Wait**", + "DefaultValue": "2", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText", + "Octopus.SelectOptions": "0 +5 +10" + } + } + ], + "LastModifiedOn": "2016-07-11T14:00:00.000+00:00", + "LastModifiedBy": "roberto-mardeni", + "$Meta": { + "ExportedAt": "2016-07-11T14:27:34.748+00:00", + "OctopusVersion": "3.3.12", + "Type": "ActionTemplate" + }, + "Category": "akamai" +} diff --git a/step-templates/akamai-cpcode-fastpurge.json.human b/step-templates/akamai-cpcode-fastpurge.json.human new file mode 100644 index 000000000..6065272d8 --- /dev/null +++ b/step-templates/akamai-cpcode-fastpurge.json.human @@ -0,0 +1,275 @@ +{ + "Id": "ee4a6957-7d98-4dcf-8f94-78f19ab1c6e0", + "Name": "Akamai - CPCode Fast Purge", + "Description": "Allows to purge CP codes using the Content Control Utility (CCU) v3 REST API.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$clientToken = $OctopusParameters['AkamaiClientToken'] +$clientAccessToken = $OctopusParameters['AkamaiClientAccessToken'] +$clientSecret = $OctopusParameters['AkamaiSecret'] +$cpcode = $OctopusParameters['AkamaiCPCode'] +$akhost = $OctopusParameters['AkamaiHost'] +$action = $OctopusParameters['AkamaiAction'] +$domain = $OctopusParameters['AkamaiDomain'] + +# NOTICE : PowerShell EdgeGrid Client has been deprecated and will reach End of Life soon. For more information, please see https://developer.akamai.com/blog/2018/11/13/akamai-powershell-edgegrid-client-end-life-notice +# Copied from https://github.com/akamai-open/AkamaiOPEN-powershell/blob/master/Invoke-AkamaiOPEN.ps1 +function Invoke-AkamaiOpenRequest { +\tparam( +\t\t[Parameter(Mandatory=$true)] +\t\t[ValidateSet(\"GET\", \"PUT\", \"POST\", \"DELETE\")] +\t\t[string]$Method, +\t\t[Parameter(Mandatory=$true)][string]$ClientToken, +\t\t[Parameter(Mandatory=$true)][string]$ClientAccessToken, +\t\t[Parameter(Mandatory=$true)][string]$ClientSecret, +\t\t[Parameter(Mandatory=$true)][string]$ReqURL, +\t\t[Parameter(Mandatory=$false)][string]$Body, +\t\t[Parameter(Mandatory=$false)][string]$MaxBody = 131072 +\t\t) + +\t#Function to generate HMAC SHA256 Base64 +\tFunction Crypto ($secret, $message) +\t{ +\t\t[byte[]] $keyByte = [System.Text.Encoding]::ASCII.GetBytes($secret) +\t\t[byte[]] $messageBytes = [System.Text.Encoding]::ASCII.GetBytes($message) +\t\t$hmac = new-object System.Security.Cryptography.HMACSHA256((,$keyByte)) +\t\t[byte[]] $hashmessage = $hmac.ComputeHash($messageBytes) +\t\t$Crypt = [System.Convert]::ToBase64String($hashmessage) + +\t\treturn $Crypt +\t} + +\t#ReqURL Verification +\tIf (($ReqURL -as [System.URI]).AbsoluteURI -eq $null -or $ReqURL -notmatch \"akamaiapis.net\") +\t{ +\t\tthrow \"Error: Ivalid Request URI\" +\t} + +\t#Sanitize Method param +\t$Method = $Method.ToUpper() + +\t#Split $ReqURL for inclusion in SignatureData +\t$ReqArray = $ReqURL -split \"(.*\\/{2})(.*?)(\\/)(.*)\" + +\t#Timestamp for request signing +\t$TimeStamp = [DateTime]::UtcNow.ToString(\"yyyyMMddTHH:mm:sszz00\") + +\t#GUID for request signing +\t$Nonce = [GUID]::NewGuid() + +\t#Build data string for signature generation +\t$SignatureData = $Method + \"`thttps`t\" +\t$SignatureData += $ReqArray[2] + \"`t\" + $ReqArray[3] + $ReqArray[4] + +\t#Add body to signature. Truncate if body is greater than max-body (Akamai default is 131072). PUT Medthod does not require adding to signature. +\t +\tif ($Body -and $Method -eq \"POST\") +\t{ +\t $Body_SHA256 = [System.Security.Cryptography.SHA256]::Create() +\t if($Body.Length -gt $MaxBody){ +\t\t$Post_Hash = [System.Convert]::ToBase64String($Body_SHA256.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($Body.Substring(0,$MaxBody)))) +\t } +\t else{ +\t\t$Post_Hash = [System.Convert]::ToBase64String($Body_SHA256.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($Body))) +\t } + +\t $SignatureData += \"`t`t\" + $Post_Hash + \"`t\" +\t} +\telse +\t{ +\t $SignatureData += \"`t`t`t\" +\t} + +\t$SignatureData += \"EG1-HMAC-SHA256 \" +\t$SignatureData += \"client_token=\" + $ClientToken + \";\" +\t$SignatureData += \"access_token=\" + $ClientAccessToken + \";\" +\t$SignatureData += \"timestamp=\" + $TimeStamp + \";\" +\t$SignatureData += \"nonce=\" + $Nonce + \";\" + +\t#Generate SigningKey +\t$SigningKey = Crypto -secret $ClientSecret -message $TimeStamp + +\t#Generate Auth Signature +\t$Signature = Crypto -secret $SigningKey -message $SignatureData + +\t#Create AuthHeader +\t$AuthorizationHeader = \"EG1-HMAC-SHA256 \" +\t$AuthorizationHeader += \"client_token=\" + $ClientToken + \";\" +\t$AuthorizationHeader += \"access_token=\" + $ClientAccessToken + \";\" +\t$AuthorizationHeader += \"timestamp=\" + $TimeStamp + \";\" +\t$AuthorizationHeader += \"nonce=\" + $Nonce + \";\" +\t$AuthorizationHeader += \"signature=\" + $Signature + +\t#Create IDictionary to hold request headers +\t$Headers = @{} + +\t#Add Auth header +\t$Headers.Add('Authorization',$AuthorizationHeader) + +\t#Add additional headers if POSTing or PUTing +\tIf ($Body) +\t{ +\t # turn off the \"Expect: 100 Continue\" header +\t # as it's not supported on the Akamai side. +\t [System.Net.ServicePointManager]::Expect100Continue = $false +\t} +\t +\t#Check for valid Methods and required switches +\t[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 +\tif ($Method -eq \"PUT\" -or $Method -eq \"POST\") { +\t\tif ($Body) { +\t\t\ttry{ +\t\t\t\tInvoke-RestMethod -Method $Method -Uri $ReqURL -Headers $Headers -Body $Body -ContentType 'application/json' +\t\t\t} +\t\t\tcatch{ +\t\t\t\tWrite-Host $_ -fore green +\t\t\t} +\t\t} +\t\telse { +\t\t Invoke-RestMethod -Method $Method -Uri $ReqURL -Headers $Headers -ContentType 'application/json' +\t\t} +\t} +\telse { +\t\t#Invoke API call with GET or DELETE and return +\t\tInvoke-RestMethod -Method $Method -Uri $ReqURL -Headers $Headers +\t} +} + +function Perform-AkamaiRequest { + param ( + [string]$request, + [string]$method=\"Get\", + [int]$expectedStatusCode=200, + $body) + + $baseUrl = \"https://\" + $akhost + $uri = \"{0}{1}\" -f $baseUrl,$request + + $json = ConvertTo-Json $body -Compress + $response = Invoke-AkamaiOpenRequest -Method $method -ClientToken $clientToken -ClientAccessToken $clientAccessToken -ClientSecret $clientSecret -ReqURL $uri -Body $json +\t + if ($response.httpStatus -ne $expectedStatusCode){ + Write-Error \"Request not processed correctly: $($response.detail)\" + } elseif ($response.detail) { + Write-Verbose $response.detail + } + + $response +} + +function Request-Purge { + param ([Int]$cpcode,[string]$action=\"remove\",[string]$domain=\"production\") + + $body = @{ + objects = @($cpcode) + } + + Perform-AkamaiRequest \"/ccu/v3/$action/cpcode/$domain\" \"Post\" 201 $body +} + +$purge = Request-Purge $cpcode $action $domain + +Write-Output \"Purge request created\" +Write-Output \"PurgeId: $($purge.purgeId)\" +Write-Output \"SupportId: $($purge.supportId)\" ", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "293e2cc1-e471-4801-8a9c-42633a3c9122", + "Name": "AkamaiClientToken", + "Label": "Client Token", + "HelpText": "Authentication token used in client authentication. Available in Luna Portal.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "e0edcbea-bfdd-4781-9a8a-55b08eba6ed5", + "Name": "AkamaiClientAccessToken", + "Label": "Client Access Token", + "HelpText": "Authentication token used in client authentication. Available in Luna Portal.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c1ea0502-f68e-4890-99a2-3c721d16b7f0", + "Name": "AkamaiSecret", + "Label": "Secret", + "HelpText": "Authentication password used in client authentication. Available in Luna Portal.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "f54e1b4a-3960-483b-88c9-5ea7a94698a0", + "Name": "AkamaiCPCode", + "Label": "CPCode", + "HelpText": "The CPCode for which to execute the purge operation", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a334eabb-8a36-4c24-b728-52f834f8a893", + "Name": "AkamaiHost", + "Label": "Host", + "HelpText": "Akamai Host (no HTTP/HTTPS). Available in Luna Portal.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "8482001d-a6fc-4e72-b9be-8cc584a39b36", + "Name": "AkamaiAction", + "Label": "Action", + "HelpText": "The action to execute on the purge operation", + "DefaultValue": "invalidate", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "invalidate +remove" + }, + "Links": {} + }, + { + "Id": "e1c3dfa0-7118-4e23-9315-c03ea3662125", + "Name": "AkamaiDomain", + "Label": "Domain", + "HelpText": "The Akamai domain to perform the purge operation on", + "DefaultValue": "production", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "production +staging" + }, + "Links": {} + } + ], + "LastModifiedOn": "2019-02-11T18:51:20.358Z", + "LastModifiedBy": "ajwightm", + "$Meta": { + "ExportedAt": "2019-02-11T18:51:20.358Z", + "OctopusVersion": "3.13.10", + "Type": "ActionTemplate" + }, + "Category": "Akamai" +} diff --git a/step-templates/akamai-fast-content-purge.json.human b/step-templates/akamai-fast-content-purge.json.human new file mode 100644 index 000000000..dd2ea8e51 --- /dev/null +++ b/step-templates/akamai-fast-content-purge.json.human @@ -0,0 +1,292 @@ +{ + "Id": "f8e2e47b-62ba-44a0-881c-d1911dc14428", + "Name": "Akamai - Content Fast Purge", + "Description": "Allows to purge content using the Content Control Utility (CCU) v3 REST API.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$clientToken = $OctopusParameters['AkamaiClientToken']\r +$clientAccessToken = $OctopusParameters['AkamaiClientAccessToken']\r +$clientSecret = $OctopusParameters['AkamaiSecret']\r +$hostname = $OctopusParameters['AkamaiHostname']\r +$objects = $OctopusParameters['AkamaiObjects'] -split \",\"\r +$action = $OctopusParameters['AkamaiAction']\r +$domain = $OctopusParameters['AkamaiDomain']\r +$proxyUser = $OctopusParameters['ProxyUser']\r +$proxyPassword = $OctopusParameters['ProxyPassword']\r +\r +if ($proxyUser) {\r + $securePassword = ConvertTo-SecureString $proxyPassword -AsPlainText -Force\r + $proxyCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $proxyUser,$securePassword\r +\r + (New-Object System.Net.WebClient).Proxy.Credentials=$proxyCredential\r +}\r +\r +# Copied from https://github.com/akamai-open/AkamaiOPEN-powershell/blob/master/Invoke-AkamaiOPEN.ps1\r +function Invoke-AkamaiOpenRequest {\r + param(\r + [Parameter(Mandatory=$true)][string]$Method, \r + [Parameter(Mandatory=$true)][string]$ClientToken, \r + [Parameter(Mandatory=$true)][string]$ClientAccessToken, \r + [Parameter(Mandatory=$true)][string]$ClientSecret, \r + [Parameter(Mandatory=$true)][string]$ReqURL, \r + [Parameter(Mandatory=$false)][string]$Body)\r +\r + #Function to generate HMAC SHA256 Base64\r + Function Crypto ($secret, $message)\r + {\r +\t [byte[]] $keyByte = [System.Text.Encoding]::ASCII.GetBytes($secret)\r +\t [byte[]] $messageBytes = [System.Text.Encoding]::ASCII.GetBytes($message)\r +\t $hmac = new-object System.Security.Cryptography.HMACSHA256((,$keyByte))\r +\t [byte[]] $hashmessage = $hmac.ComputeHash($messageBytes)\r +\t $Crypt = [System.Convert]::ToBase64String($hashmessage)\r +\r +\t return $Crypt\r + }\r +\r + #ReqURL Verification\r + If (($ReqURL -as [System.URI]).AbsoluteURI -eq $null -or $ReqURL -notmatch \"akamai.com\")\r + {\r +\t throw \"Error: Ivalid Request URI\"\r + }\r +\r + #Sanitize Method param\r + $Method = $Method.ToUpper()\r +\r + #Split $ReqURL for inclusion in SignatureData\r + $ReqArray = $ReqURL -split \"(.*\\/{2})(.*?)(\\/)(.*)\"\r +\r + #Timestamp for request signing\r + $TimeStamp = [DateTime]::UtcNow.ToString(\"yyyyMMddTHH:mm:sszz00\")\r +\r + #GUID for request signing\r + $Nonce = [GUID]::NewGuid()\r +\r + #Build data string for signature generation\r + $SignatureData = $Method + \"`thttps`t\"\r + $SignatureData += $ReqArray[2] + \"`t\" + $ReqArray[3] + $ReqArray[4]\r +\r + if (($Body -ne $null) -and ($Method -ceq \"POST\"))\r + {\r +\t $Body_SHA256 = [System.Security.Cryptography.SHA256]::Create()\r +\t $Post_Hash = [System.Convert]::ToBase64String($Body_SHA256.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($Body.ToString())))\r +\r +\t $SignatureData += \"`t`t\" + $Post_Hash + \"`t\"\r + }\r + else\r + {\r +\t $SignatureData += \"`t`t`t\"\r + }\r +\r + $SignatureData += \"EG1-HMAC-SHA256 \"\r + $SignatureData += \"client_token=\" + $ClientToken + \";\"\r + $SignatureData += \"access_token=\" + $ClientAccessToken + \";\"\r + $SignatureData += \"timestamp=\" + $TimeStamp + \";\"\r + $SignatureData += \"nonce=\" + $Nonce + \";\"\r +\r + #Generate SigningKey\r + $SigningKey = Crypto -secret $ClientSecret -message $TimeStamp\r +\r + #Generate Auth Signature\r + $Signature = Crypto -secret $SigningKey -message $SignatureData\r +\r + #Create AuthHeader\r + $AuthorizationHeader = \"EG1-HMAC-SHA256 \"\r + $AuthorizationHeader += \"client_token=\" + $ClientToken + \";\"\r + $AuthorizationHeader += \"access_token=\" + $ClientAccessToken + \";\"\r + $AuthorizationHeader += \"timestamp=\" + $TimeStamp + \";\"\r + $AuthorizationHeader += \"nonce=\" + $Nonce + \";\"\r + $AuthorizationHeader += \"signature=\" + $Signature\r +\r + #Create IDictionary to hold request headers\r + $Headers = @{}\r +\r + #Add Auth header\r + $Headers.Add('Authorization',$AuthorizationHeader)\r +\r + #Add additional headers if POSTing or PUTing\r + If (($Method -ceq \"POST\") -or ($Method -ceq \"PUT\"))\r + {\r +\t $Body_Size = [System.Text.Encoding]::UTF8.GetByteCount($Body)\r +\t $Headers.Add('max-body',$Body_Size.ToString())\r +\r + # turn off the \"Expect: 100 Continue\" header\r + # as it's not supported on the Akamai side.\r + [System.Net.ServicePointManager]::Expect100Continue = $false\r + }\r +\r + #Check for valid Methods and required switches\r + If (($Method -ceq \"POST\") -and ($Body -ne $null))\r + {\r + Invoke-RestMethod -Method $Method -Uri $ReqURL -Headers $Headers -Body $Body -ContentType 'application/json'\r + }\r + elseif (($Method -ceq \"PUT\") -and ($Body -ne $null))\r + {\r +\t #Invoke API call with PUT and return\r +\t Invoke-RestMethod -Method $Method -Uri $ReqURL -Headers $Headers -Body $Body -ContentType 'application/json'\r + }\r + elseif (($Method -ceq \"GET\") -or ($Method -ceq \"DELETE\"))\r + {\r +\t #Invoke API call with GET or DELETE and return\r +\t Invoke-RestMethod -Method $Method -Uri $ReqURL -Headers $Headers\r + }\r + else\r + {\r +\t throw \"Error: Invalid -Method specified or missing required parameter\"\r + }\r +}\r +\r +function Perform-AkamaiRequest {\r + param (\r + [string]$request, \r + [string]$method=\"Get\", \r + [int]$expectedStatusCode=200, \r + $body)\r +\r + $baseUrl = \"http://private-anon-3934daf8d-akamaiopen2purgeccuv3production.apiary-mock.com\"\r + # $baseUrl = \"https://api.ccu.akamai.com\"\r + $uri = \"{0}{1}\" -f $baseUrl,$request\r +\r + if ($uri -match \"mock\"){\r + $requestHeaders = @{'Cache-Control'='no-cache,proxy-revalidate'}\r + $response = Invoke-RestMethod -Uri $uri -Method $method -DisableKeepAlive -Headers $requestHeaders -Body $body\r + } else {\r + $json = ConvertTo-Json $body -Compress\r + $response = Invoke-AkamaiOpenRequest -Method $method -ClientToken $clientToken -ClientAccessToken $clientAccessToken -ClientSecret $clientSecret -ReqURL $uri -Body $json\r + }\r +\r + if ($response.httpStatus -ne $expectedStatusCode){\r + Write-Error \"Request not processed correctly: $($response.detail)\"\r + } elseif ($response.detail) {\r + Write-Verbose $response.detail\r + }\r +\r + Write-Verbose $response\r +\r + $response\r +}\r +\r +function Request-Purge {\r + param ($objects,[string]$hostname,[string]$action=\"remove\",[string]$domain=\"production\")\r +\r + $body = @{\r + objects = $objects\r + }\r +\r + if ($hostname -ne $null -and $hostname -ne \"\") {\r + $body = @{\r + hostname = $hostname\r + objects = $objects\r + }\r + } \r +\r + Perform-AkamaiRequest \"/ccu/v3/$action/$domain\" \"Post\" 201 $body\r +}\r +\r +$purge = Request-Purge $objects $hostname $action $domain\r +\r +Write-Output \"Purge request created\"\r +Write-Output \"PurgeId: $($purge.purgeId)\"\r +Write-Output \"SupportId: $($purge.supportId)\" ", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "AkamaiClientToken", + "Label": "Client Token", + "HelpText": "Authentication token used in client authentication. Available in Luna Portal.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AkamaiClientAccessToken", + "Label": "Client Access Token", + "HelpText": "Authentication token used in client authentication. Available in Luna Portal.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AkamaiSecret", + "Label": "Secret", + "HelpText": "Authentication password used in client authentication. Available in Luna Portal.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "AkamaiHostname", + "Label": "Hostname", + "HelpText": "The hostname for which to execute the purge operation", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AkamaiObjects", + "Label": "Objects", + "HelpText": "A comma separated list of objects to purge, only URLs", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AkamaiAction", + "Label": "Action", + "HelpText": "The action to execute on the purge operation", + "DefaultValue": "invalidate", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "invalidate +remove" + } + }, + { + "Name": "AkamaiDomain", + "Label": "Domain", + "HelpText": "The Akamai domain to perform the purge operation on", + "DefaultValue": "production", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "production +staging" + } + }, + { + "Name": "ProxyUser", + "Label": "Proxy User", + "HelpText": "Optional, a user name for the proxy if required in the network", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ProxyPassword", + "Label": "Proxy Password", + "HelpText": "Optional, the password for the account to use if a Proxy User was specified", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedOn": "2016-07-11T14:00:00.000+00:00", + "LastModifiedBy": "roberto-mardeni", + "$Meta": { + "ExportedAt": "2016-07-11T14:30:28.569+00:00", + "OctopusVersion": "3.3.12", + "Type": "ActionTemplate" + }, + "Category": "akamai" +} diff --git a/step-templates/amazon-chime-post-message.json.human b/step-templates/amazon-chime-post-message.json.human new file mode 100644 index 000000000..8ed1a2ef7 --- /dev/null +++ b/step-templates/amazon-chime-post-message.json.human @@ -0,0 +1,97 @@ +{ + "Id": "6042d737-5902-0729-ae57-8b6650a299da", + "Name": "Amazon Chime - Post Message", + "Description": "Posts a message to a Chime chat room using webhooks.", + "ActionType": "Octopus.Script", + "Version": 14, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$_WebHookUrl = $OctopusParameters['acpm_WebHookURL'] +$_ContentPayload = $OctopusParameters['acpm_ContentPayload'] +$_WarningOnFailure = [System.Convert]::ToBoolean($OctopusParameters['acpm_WarningOnFailure']) + +try { + #Encode the content message as JSON. ConvertTo-JSON adds quotes to the string. + $_EncodedContentPayload = $_ContentPayload | ConvertTo-JSON + + #Create a JSON object that Chime wants + #https://docs.aws.amazon.com/chime/latest/ug/webhooks.html + $_JsonPayload = '{\"Content\":' + $_JsonPayload += $_EncodedContentPayload + $_JsonPayload += '}' + + Write-Host \"Sending message to webhook.\" + Write-Host \"------ Message ------\" + Write-Host \"$_ContentPayload\" + Write-Host \"---- End Message ----\" +\t + #Make the request and send the payload + Invoke-WebRequest \"$_WebHookUrl\" -UseBasicParsing -ContentType \"application/json\" -Method POST -Body $_JsonPayload | Out-Null + Write-Host \"Message successfully sent to webhook.\" +} +catch { + #If WarningOnFailure is not true, then write an error and fail the deployment. + if($_WarningOnFailure -eq $false) { + Write-Error \"Could not send message to Chime web hook.\" + if(!([string]::IsNullOrEmpty($_.Exception.Message))) { + Write-Error \"Exception Message: $($_.Exception.Message)\" + } + } + #Else, just write a warning and continue on. + else { + Write-Warning \"Could not send message to Chime web hook.\" + if(!([string]::IsNullOrEmpty($_.Exception.Message))) { + Write-Warning \"Exception Message: $($_.Exception.Message)\" + } + } +}" + }, + "Parameters": [ + { + "Id": "2d88ad9d-7271-4488-bfdc-5ab045bb02ab", + "Name": "acpm_WebHookURL", + "Label": "Web Hook URL", + "HelpText": "The chat room webhook URL. For more information on Amazon Chime webhooks, visit their [documentation page](https://docs.aws.amazon.com/chime/latest/ug/webhooks.html). + +_Note: Only chat room admins can create webhooks._", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c212ad8e-b56a-469e-a59d-1f23aeb72beb", + "Name": "acpm_ContentPayload", + "Label": "Message", + "HelpText": "The message that you want to display in the Chat Room. + +_Line breaks and web links are supported._", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "6860f750-7715-4a4b-a9d4-345c8269cf6c", + "Name": "acpm_WarningOnFailure", + "Label": "Generate Warning on Failure?", + "HelpText": "If checked and this step template fails to execute correctly, the script will use Write-Warning instead of Write-Error, and the deployment won't fail.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "LastModifiedBy": "vlouwagie", + "$Meta": { + "ExportedAt": "2018-02-18T00:00:01.006Z", + "OctopusVersion": "4.1.5", + "Type": "ActionTemplate" + }, + "Category": "amazon-chime" +} diff --git a/step-templates/amazon-s3-upload.json.human b/step-templates/amazon-s3-upload.json.human new file mode 100644 index 000000000..cff84d465 --- /dev/null +++ b/step-templates/amazon-s3-upload.json.human @@ -0,0 +1,158 @@ +{ + "Id": "302e0653-e84e-4db6-be53-1ee1a56dea88", + "Name": "Amazon S3 Upload", + "Description": "Upload files and folders to an S3 bucket from a specified location. + +Either specify a single file or a folder containing the files and folders to be uploaded. + +**Important!** _For this plugin to function, you must install [AWS Tools for Windows PowerShell](http://aws.amazon.com/powershell/) on your tentacle server and you must restart your tentacle service._", + "ActionType": "Octopus.Script", + "Version": 11, + "Properties": { + "Octopus.Action.Script.ScriptBody": "\r +$recurse = [boolean]::Parse($Recursive)\r +\r +$params = @{}\r +\r +#Sets the Permissions to public if the selection is true\r +if ($MakePublic -eq $True) {\r + $params.add(\"CannedACLName\", \"public-read\")\r +}\r +\r +#Initialises the S3 Credentials based on the Access Key and Secret Key provided, so that we can invoke the APIs further down\r +Set-AWSCredentials -AccessKey $S3AccessKey -SecretKey $S3SecretKey -StoreAs S3Creds\r +\r +#Initialises the Default AWS Region based on the region provided\r +Set-DefaultAWSRegion -Region $S3Region\r +\r +#Gets all relevant files and uploads them\r +function Upload($item) \r +{\r + #Gets all files and child folders within the given directory\r + foreach ($i in Get-ChildItem $item) {\r +\r + #Checks if the item is a folder\r + if($i -is [System.IO.DirectoryInfo]) {\r +\r + #Inserts all files within a folder to AWS \r + Write-S3Object -ProfileName S3Creds -BucketName $S3Bucket -KeyPrefix $S3Prefix$($i.Name) -Folder $i.FullName -Recurse:$recurse @params\r +\r + } else {\r +\r + #Inserts file to AWS\r + Write-S3Object -ProfileName S3Creds -BucketName $S3Bucket -Key $S3Prefix$($i.Name) -File $i.FullName @params\r +\r + }\r + }\r +}\r +\r +Upload($SourceFolderLocation)\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "S3Region", + "Label": "S3 Region", + "HelpText": "A region is the location that your S3 bucket was created. + +Amazon has many different region names and you [can read more about Amazon Region names here](http://docs.aws.amazon.com/general/latest/gr/rande.html). + +**Default Region** +If you didn't specify a region when setting up your S3 buckets, you may be using the default. According to Amazon: +> For accounts created on or after March 8, 2013, the default region is us-west-2; for older accounts, the default region is us-east-1.", + "DefaultValue": "eu-west-1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "us-east-1 +us-west-1 +us-west-2 +ap-south-1 +ap-northeast-2 +ap-southeast-1 +ap-southeast-2 +ap-northeast-1 +eu-central-1 +eu-west-1 +sa-east-1" + } + }, + { + "Name": "S3Bucket", + "Label": "Bucket Name", + "HelpText": "This is the name of the bucket on S3 to which you'd like your files and folders uploaded.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "S3Prefix", + "Label": "Prefix", + "HelpText": "This is the prefix for the path you want the folder to be uploaded to if they differ from the source structure.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SourceFolderLocation", + "Label": "Source Folder", + "HelpText": "This is the local folder located on your tentacle server that you'd like to upload to S3 + +Example: _C:\\Deployment\\S3Distributables_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "S3AccessKey", + "Label": "Access Key ID", + "HelpText": "Your public S3 Key. + +This can be found by clicking _My Account/Consoles_ and navigating to _Security Credentials_.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "S3SecretKey", + "Label": "Secret Access Key", + "HelpText": "Your private S3 Key. + +This can be found by clicking _My Account/Consoles_ and navigating to _Security Credentials_.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MakePublic", + "Label": "Make Public", + "HelpText": null, + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "Recursive", + "Label": "Recursive", + "HelpText": "Do you want to upload to loop through all of the child folders/files and retrieve everything?", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "Phillip Haydon", + "$Meta": { + "ExportedAt": "2018-04-10T11:05:46.352Z", + "OctopusVersion": "2018.2.7", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/ansible-run-playbook.json.human b/step-templates/ansible-run-playbook.json.human new file mode 100644 index 000000000..329844d10 --- /dev/null +++ b/step-templates/ansible-run-playbook.json.human @@ -0,0 +1,47 @@ +{ + "Id": "02d3b753-e0eb-4bda-9bf3-09c77e54fce1", + "Name": "Run Ansible Playbook (bash)", + "Description": "Step template to run an ansible playbook using bash. Requires Ansible and Ansible-Playbook to run successfully.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "ansibleInstalled=$(which -a ansible-playbook >/dev/null; echo $?) + +if [ $ansibleInstalled -ne 0 ];then +\techo \"Ansible Not Installed\" + exit 1; +fi + + +ansible-playbook $(get_octopusvariable \"RunAnsible.Playbook.Path\") +playbookRC=$? + +if [ $playbookRC -ne 0 ]; then + exit $playbookRC; +fi" + }, + "Parameters": [ + { + "Id": "c9c94c61-07a7-4e7e-a52c-fec79ecd50d6", + "Name": "RunAnsible.Playbook.Path", + "Label": "Ansible Playbook Path", + "HelpText": "Enter the full path where the Ansible Playbook yaml is located.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-02-16T11:39:51.509Z", + "OctopusVersion": "2022.4.8394", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "domrichardson", + "Category": "ansible" +} diff --git a/step-templates/ansible-tower-run-template.json.human b/step-templates/ansible-tower-run-template.json.human new file mode 100644 index 000000000..cbab9766e --- /dev/null +++ b/step-templates/ansible-tower-run-template.json.human @@ -0,0 +1,596 @@ +{ + "Id": "9a3d15aa-5a48-4922-9651-cf4ae75730dd", + "Name": "Ansible Tower - Run Template", + "Description": "Run a workflow or job template in Ansible Tower", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# There have been reported issues when using the default JSON parser with Invoke-RestMethod +# on PowerShell 5. So we are going to pull in a different assembly to do the parsing for us. +# This parser appears to be more reliable. +[System.Reflection.Assembly]::LoadWithPartialName(\"System.Web.Extensions\") +$jsonParser = New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer +$jsonParser.MaxJsonLength = 104857600 #100mb as bytes, default is 2mb + +function Write-AnsibleLine([String] $text) { + # split text at ESC-char + $ansi_colors = @( + '[0;30m' #= @{ fg = ConsoleColor.Black } + '[0;31m' #= @{ fg = ConsoleColor.DarkRed } + '[0;32m' #= @{ fg = ConsoleColor.DarkGreen } + '[0;33m' #= @{ fg = ConsoleColor.DarkYellow } + '[0;34m' #= @{ fg = ConsoleColor.DarkBlue } + '[0;35m' #= @{ fg = ConsoleColor.DarkMagenta } + '[0;36m' #= @{ fg = ConsoleColor.DarkCyan } + '[0;37m' #= @{ fg = ConsoleColor.White } + '[0m' #= @{ fg = $null; bg = $null } + '[1;35m' #= Magent (ansible warnings) + '[30;1m' #= @{ fg = ConsoleColor.Grey } + '[31;1m' #= @{ fg = ConsoleColor.Red } + '[32;1m' #= @{ fg = ConsoleColor.Green } + '[33;1m' #= @{ fg = ConsoleColor.Yellow } + '[34;1m' #= @{ fg = ConsoleColor.Blue } + '[35;1m' #= @{ fg = ConsoleColor.Magenta } + '[36;1m' #= @{ fg = ConsoleColor.Cyan } + '[37;1m' #= @{ fg = ConsoleColor.White } + '[0;40m' #= @{ bg = ConsoleColor.Black } + '[0;41m' #= @{ bg = ConsoleColor.DarkRed } + '[0;42m' #= @{ bg = ConsoleColor.DarkGreen } + '[0;43m' #= @{ bg = ConsoleColor.DarkYellow } + '[0;44m' #= @{ bg = ConsoleColor.DarkBlue } + '[0;45m' #= @{ bg = ConsoleColor.DarkMagenta } + '[0;46m' #= @{ bg = ConsoleColor.DarkCyan } + '[0;47m' #= @{ bg = ConsoleColor.White } + '[40;1m' #= @{ bg = ConsoleColor.DarkGrey } + '[41;1m' #= @{ bg = ConsoleColor.Red } + '[42;1m' #= @{ bg = ConsoleColor.Green } + '[43;1m' #= @{ bg = ConsoleColor.Yellow } + '[44;1m' #= @{ bg = ConsoleColor.Blue } + '[45;1m' #= @{ bg = ConsoleColor.Magenta } + '[46;1m' #= @{ bg = ConsoleColor.Cyan } + '[47;1m' #= @{ bg = ConsoleColor.White } + ) + foreach ($segment in $text.split([char] 27)) { + foreach($code in $ansi_colors) { + if($segment.startswith($code)) { + $segment = $segment.replace($code, \"\") + } + } + Write-Host -NoNewline $segment + } + Write-Host \"\" +} + + +Function Resolve-Tower-Asset{ + Param($Name, $Url) + Process { + if($script:Verbose) { Write-Host \"Resolving name $Name\" } + $object = $null + if($Name -match '^[0-9]+$') { + if($script:Verbose) { Write-Host \"Using $Name as ID as its an int already\" } + $url = \"$Url/$Name/\" + try { $object = $jsonParser.Deserialize((Invoke-WebRequest $url -Method GET -Headers $script:auth_headers -UseBasicParsing), [System.Object]) } + catch { + Write-Host \"Error when resolving ID for $Name\" + Write-Host $_ + return $null + } + } else { + if($script:Verbose) { Write-Host \"Looking up ID of name $Name\" } + $url = \"$Url/?name=$Name\" + try { $response = $jsonParser.Deserialize((Invoke-WebRequest $url -Method GET -Headers $script:auth_headers -UseBasicParsing), [System.Object]) } + catch { + Write-Host \"Unable to resolve name $Name\" + Write-Host $_ + return $null + } + if($response.count -eq 0) { + Write-Host \"Got no results when trying to get ID for $Name\" + return $null + } elseif($response.count -ne 1) { + Write-Host \"Did not get a unique job ID for job name $Name\" + return $null + } + if($script:Verbose) { Write-Host \"Resolved to ID $($response.results[0].id)\" } + $object = $response.results[0] + } + return $object + } +} + + +function Get-Auth-Headers { + # If we did not get a TowerOAuthToken or a (TowerUsername and TowerPassword) then we can't even try to auth + if(-not (($TowerUsername -and $TowerPassword) -or $TowerOAuthToken)) { + Fail-Step \"Please pass an OAuth Token and or a Username/Password to authenticate to Tower with\" + } + + if($TowerOAuthToken) { + if($verbose) { Write-Host \"Testing OAuth token\" } + $token_headers = @{ \"Authorization\" = \"Bearer $TowerOAuthToken\" } + try { + # We have to assign it to something or we get a line in the output + $junk = $jsonParser.Deserialize((Invoke-WebRequest \"$api_base/job_templates/?name=Octopus\" -Method GET -Headers $token_headers -UseBasicParsing), [System.Object]) + $script:auth_headers = $token_headers + return + } catch { + Write-Host \"Unable to authenticate to the Tower server with OAuth token\" + Write-Host $_ + } + } + + if(-not ($TowerUsername -and $TowerPassword)) { + Fail-Step \"No username/password to fall back on\" + } + + if($verbose) { Write-Host \"Testing basic auth\" } + $pair = \"${TowerUsername}:${TowerPassword}\" + $bytes = [System.Text.Encoding]::ASCII.GetBytes($pair) + $base64 = [System.Convert]::ToBase64String($bytes) + $basic_auth_value = \"Basic $base64\" + $headers = @{ \"Authorization\" = $basic_auth_value } + try { + # We have to assign it to something or we get a line in the output + $junk = $jsonParser.Deserialize((Invoke-WebRequest \"$api_base/job_templates/?name=Octopus\" -Method GET -Headers $headers -UseBasicParsing), [System.Object]) + $script:auth_headers = $headers + } catch { + Write-Host $_ + Fail-Step \"Username password combination failed to work\" + } + + if ($script:Verbose) { Write-Host \"Attempting to get authentcation Token for $TowerUsername\" } + $body = @{ + username = $TowerUsername + password = $TowerPassword + } | ConvertTo-Json + $url = \"$api_base/authtoken/\" + try { + $auth_token = $jsonParser.Deserialize((Invoke-WebRequest $url -Method POST -Headers $headers -Body $body -ContentType \"application/json\" -UseBasicParsing), [System.Object]) + $script:auth_headers = @{ Authorization = \"Token $($auth_token.token)\" } + return + } catch { + if($_.Exception.Response.StatusCode -eq 404) { + Write-Host(\">>> Server does not support authtoken, try using an OAuth Token\") + Write-Host(\">>> Defaulting to perpetual basic auth. This can be slow for authentication with external sources\") + return + } else { + Write-Host $_ + Fail-Step \"Unable to authenticate to the Tower server for Auth token\" + } + } +} + +function Watch-Job-Complete { + Param($Id) + Process { + $last_log_id = 0 + while($True) { + # First log any events if the user wants them + if($TowerImportLogs) { + $url = \"$api_base/jobs/$Id/job_events/?id__gt=$last_log_id\" + $response = $jsonParser.Deserialize((Invoke-WebRequest $url -Method GET -Headers $script:auth_headers -UseBasicParsing), [System.Object]) + foreach($result in $response.results) { + if($last_log_id -lt $result.id) { $last_log_id = $result.id } + if($result.event_data -and $result.event_data.res -and $result.event_data.res.output) { + foreach($line in $result.event_data.res.output) { + Write-AnsibleLine($line) + } + } else { + $line = $result.stdout + Write-AnsibleLine($line) + } + } + } + + # Now check the status of the job + $url = \"$api_base/jobs/$Id/\" + $response = $jsonParser.Deserialize((Invoke-WebRequest $url -Method GET -Headers $script:auth_headers -UseBasicParsing), [System.Object]) + if($response.finished) { + $response.failed + return + } else { + Start-Sleep -s $SecondsBetweenChecks + } + } + } +} + +function Watch-Workflow-Complete { + Param($Id) + Process { + $workflow_node_id = 0 + while($True) { + # Check to see if there are any jobs we need to follow + $url = \"$tower_base/api/v2/workflow_jobs/$Id/workflow_nodes/?id__gt=$workflow_node_id\" + $response = $jsonParser.Deserialize((Invoke-WebRequest $url -Method GET -Headers $script:auth_headers -UseBasicParsing), [System.Object]) + + # If there are no nodes whose ID is > the last one we looked at we can see if we are complete + if($response.count -eq 0) { + $url = \"$tower_base/api/v2/workflow_jobs/$Id/\" + $response = $jsonParser.Deserialize((Invoke-WebRequest $url -Method GET -Headers $script:auth_headers -UseBasicParsing), [System.Object]) + if($response.finished) { + $response.failed + return + } else { + Start-Sleep -s $SecondsBetweenChecks + } + } else { + foreach($result in $response.results) { + if($result.summary_fields.unified_job_template.unified_job_type -eq 'job') { + $job_id = $result.summary_fields.job.id + if(-not $job_id) { + # This is a job but it hasn't started yet, lets sleep and try again + Start-Sleep -s $SecondsBetweenChecks + break + } + if($script:Verbose) { Write-Host \"Monitoring job $($result.summary_fields.job.name)\" } + # We have to trap the return of Watch-Job-Complete + $junk = Watch-Job-Complete -Id $job_id + } else { + if($script:Verbose) { Write-Host \"Not pulling logs for node $($result.id) which is a $($result.summary_fields.unified_job_template.unified_job_type)\" } + } + $workflow_node_id = $result.id + } + } + } + } +} + + + +##### Main Body + + + + +# Check that we got a TowerJobTemplate, without one we can't do anything +if(-not ($TowerJobType -eq \"job\" -or $TowerJobType -eq \"workflow\")) { Fail-Stop \"The job type must be either job or workflow\" } +if($TowerJobTemplate -eq $null -or $TowerJobTemplate -eq \"\") { Fail-Step \"A Job Name needs to be specified\" } +if($TowerJobTags -eq \"\") { $TowerJobTags = $null } +if($TowerExtraVars -eq \"\") { $TowerExtraVars = $null } +if($TowerLimit -eq \"\") { $TowerLimit = $null } +if($TowerInventory -eq \"\") { $TowerInventory = $null } +if($TowerCredential -eq \"\") { $Credential = $null } +if($TowerImportLogs -and $TowerImportLogs -eq \"True\") { $TowerImportLogs = $True } else { $TowerImportLogs = $False } +if($TowerVerbose -and $TowerVerbose -eq \"True\") { $Verbose = $True} else { $Verbose = $False } +if($TowerSecondsBetweenChecks) { + try { $SecondsBetweenChecks = [int]$TowerSecondsBetweenChecks } + catch { + write-Host \"Failed to parse $TowerSecondsBetweenChecks as integer, defaulting to 3\" + $SecondsBetweenChecks = 3 + } +} else { + $SecondsBetweenChecks = 3 +} +if($TowerTimeLimitInSeconds) { + try { $TowerTimeLimitInSeconds = [int]$TowerTimeLimitInSeconds } + catch { + write-Host \"Failed to parse $TowerTimeLimitInSeconds as integer, defaulting to 600\" + $TowerTimeLimitInSeconds = 600 + } +} else { + $TowerTimeLimitInSeconds = 600 +} +if($TowerIgnoreCert -and $TowerIgnoreCert -eq \"True\") { + # Joyfully borrowed from Markus Kraus' post on + # https://blog.ukotic.net/2017/08/15/could-not-establish-trust-relationship-for-the-ssltls-invoke-webrequest/ + if(-not([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy').Type) { + Add-Type @\" + using System.Net; + using System.Security.Cryptography.X509Certificates; + public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult(ServicePoint srvPoint, X509Certificate certificate, WebRequest request, int certificateProblem) { + return true; + } + } +\"@ + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + [System.Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + } + #endregion +} + +if ($Verbose) { Write-Host \"Beginning Ansible Tower Run on $TowerServer\" } +$tower_url = [System.Uri]$TowerServer +if(-not $tower_url.Scheme) { $tower_url = [System.Uri]\"https://$TowerServer\" } +$tower_base = $tower_url.ToString().TrimEnd(\"/\") +$api_base = \"${tower_base}/api/v2\" + +# First handle authentication +# If we have a TowerOAuthToken try using that +# Else get an authentication token if we have a user name/password +$auth_headers = @{'initial' = 'Data'} +Get-Auth-Headers + + +# If the TowerJobTemplate is actually an ID we can just use that. +# If not we need to lookup the ID from the name +if($TowerJobType -eq 'job') { + $template = Resolve-Tower-Asset -Name $TowerJobTemplate -Url \"$api_base/job_templates\" +} else { + $template = Resolve-Tower-Asset -Name $TowerJobTemplate -Url \"$api_base/workflow_job_templates\" +} +if($template -eq $null) { Fail-Step \"Unable to resolve the job name\" } + +if($TowerExtraVars -ne $null -and $TowerExtraVars -ne '---' -and $template.ask_variables_on_launch -eq $False) { + Write-Warning \"Extra variables defined but prompt for variables on launch is not set in tower job\" +} +if($TowerLimit -ne $null -and $template.ask_limit_on_launch -eq $False) { + Write-Warning \"Limit defined but prompt for limit on launch is not set in tower job\" +} +if($TowerJobTags -ne $null -and $template.ask_tags_on_launch -eq $False) { + Write-Warning \"Job Tags defined but prompt for tags on launch is not set in tower job\" +} +if($TowerInventory -ne $null -and $template.ask_inventory_on_launch -eq $False) { + Write-Warning \"Inventory defined but prompt for inventory on launch is not set in tower job\" +} +if($TowerCredential -ne $null -and $template.ask_credential_on_launch -eq $False) { + Write-Warning \"Credential defined but prompt for credential on launch is not set in tower job\" +} +<# +// Here are some more options we may want to use/check someday +// \"ask_diff_mode_on_launch\": false, +// \"ask_skip_tags_on_launch\": false, +// \"ask_job_type_on_launch\": false, +// \"ask_verbosity_on_launch\": false, +#> + + +# Construct the post body +$post_body = @{} +if($TowerInventory -ne $null) { + $inventory = Resolve-Tower-Asset -Name $TowerInventory -Url \"$api_base/inventories\" + if($inventory -eq $null) { Fail-Step(\"Unable to resolve inventory\") } + $post_body.inventory = $inventory.id +} + +if($TowerCredential -ne $null) { + $credential = Resolve-Tower-Asset -Name $TowerCredential -Url \"$api_base/credentials\" + if($credential -eq $null) { Fail-Step(\"Unable to resolve credential\") } + $post_body.credentials = @($credential.id) +} +if($TowerLimit -ne $null) { $post_body.limit = $TowerLimit } +if($TowerJobTags -ne $null) { $post_body.job_tags = $TowerJobTags } +# Older versions of Tower did not like receiveing \"---\" as extra vars. +if($TowerExtraVars -ne $null -and $TowerExtraVars -ne \"---\") { $post_body.extra_vars = $TowerExtraVars } + +if($Verbose) { Write-Host \"Requesting tower to run $TowerJobTemplate\" } +if($TowerJobType -eq 'job') { + $url = \"$api_base/job_templates/$($template.id)/launch/\" +} else { + $url = \"$api_base/workflow_job_templates/$($template.id)/launch/\" +} +try { + $response = Invoke-WebRequest -Uri $url -Method POST -Headers $auth_headers -Body ($post_body | ConvertTo-JSON) -ContentType \"application/json\" -UseBasicParsing +} catch { + Write-Host \"Failed to make request to invoke job\" + $initialError = $_ + try { + $result = $_.Exception.Response.GetResponseStream() + $reader = New-Object System.IO.StreamReader($result) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $body = $reader.ReadToEnd() | ConvertFrom-Json + <# + Some stuff that we might want to catch includes: + {\"extra_vars\":[\"Must be valid JSON or YAML.\"]} + {\"variables_needed_to_start\":[\"'my_var' value missing\"]} + {\"credential\":[\"Invalid pk \\\"999999\\\" - object does not exist.\"]} + {\"inventory\":[\"Invalid pk \\\"99999999\\\" - object does not exist.\"]} + The last two we don't really care about because we should never hit them + #> + if($body.extra_vars -ne $null) { + Fail-Step \"Failed to launch job: extra vars must be vailid JSON or YAML.\" + } elseif($body.variables_needed_to_start -ne $null) { + Fail-Step \"Failed to launch job: $($body.variables_needed_to_start)\" + } else { + Write-Host $body + Fail-Step \"Failed to launch job for an unknown reason\" + } + } catch { + Write-Host \"Failed to get response body from request\" + Write-Host $initialError + } +} + + +$template_id = $($response | ConvertFrom-Json).id +Write-Host(\"Best Guess Job URL: $tower_base/#/$($TowerJobType)s/$template_id\") + +# For whatever reason, this never fires +#$timer = new-object System.Timers.Timer +#$timer.Interval = 500 #1000 * $TowerTimeLimitInSeconds +#$action = { Fail-Step \"Timed out waiting for Tower to complete tempate run. Template may still be running in Tower.\" } +#$tower_timer = Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action $action +#$timer.AutoReset = $False +#$timer.Enabled = $True + +if($TowerJobType -eq 'job') { + $failed = Watch-Job-Complete -Id $template_id +} else { + $failed = Watch-Workflow-Complete -Id $template_id +} + +#$timer.Stop() +#Unregister-Event $tower_timer.Name + + +if($failed) { + Fail-Step \"Job Failed\" +} else { + Write-Host \"Job Succeeded\" +} +" + }, + "Parameters": [ + { + "Id": "6da92f7b-3e6b-496c-9095-508556a2e69a", + "Name": "TowerServer", + "Label": "Tower Server", + "HelpText": "The connection information of the Tower server to connect to. This can be a single host name or IP (https will be assumed) or a full specification like http://localhost:8081.", + "DefaultValue": "tower.example.com", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2e4148f9-73fd-430b-9ac4-d7aa0789b9d1", + "Name": "TowerUsername", + "Label": "Tower Username", + "HelpText": "The user to connect to Tower as. Be sure this user has permissions to execute the Job/Workflow templates you are attempting to launch.", + "DefaultValue": "admin", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "60f5667e-108a-4966-aaef-feb26e6739ad", + "Name": "TowerPassword", + "Label": "Tower Password", + "HelpText": "The password for the specified user.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "4f8baac7-0b3f-4ffe-af04-deff70484518", + "Name": "TowerOAuthToken", + "Label": "OAuth Token", + "HelpText": "An alternative to username/password which can be used on newer versions of Tower.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ccf787b8-6589-4d59-9f0d-d964f0785252", + "Name": "TowerJobType", + "Label": "Template Type", + "HelpText": "Select the type of template to execute. Supported template types are Workflow and Job", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "job|Job Template +workflow|Workflow Template" + } + }, + { + "Id": "8b6c4e7c-65dd-40f8-8fa3-4371877083fb", + "Name": "TowerJobTemplate", + "Label": "Template Name", + "HelpText": "The Job or Workflow template ID or name. If a name is specified the ID will attempt to be resolved.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6a64ff26-9b22-414d-9995-d1723aafdbb1", + "Name": "TowerExtraVars", + "Label": "Extra Vars", + "HelpText": "Extra variable to pass to the job. This can be either YAML or JSON. +**NOTE:** prompt on launch must be set in your template for this setting to take affect.", + "DefaultValue": "---", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "f65ed9e0-2d50-4698-8e8e-3683ee6e0a41", + "Name": "TowerJobTags", + "Label": "Job Tags", + "HelpText": "Any job tags to pass to Tower. +**NOTE:** prompt on launch must be set in your template for this setting to take affect.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fe800bf5-ef04-43c5-b73d-0c9ca00172a8", + "Name": "TowerLimit", + "Label": "Limit", + "HelpText": "Limit field to be passed to Tower. +**NOTE:** prompt on launch must be set in your template for this setting to take affect.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8572d143-0ccd-4af4-b754-7df663f9e07a", + "Name": "TowerInventory", + "Label": "Inventory", + "HelpText": "The inventory for the job run. +**NOTE:** prompt on launch must be set in your template for this setting to take affect.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "08514a4e-f34c-41cb-94f4-c6ac82f46634", + "Name": "TowerCredential", + "Label": "Credential", + "HelpText": "The credentials to use for this job. +**NOTE:** prompt on launch must be set in your template for this setting to take affect.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9691c6c0-ac17-4bcf-947d-38bbbf027ed3", + "Name": "TowerImportLogs", + "Label": "Import Tower Logs", + "HelpText": "Pull the Tower logs back into the step output", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "7e8c6ea0-637f-4440-9669-55971bc1f825", + "Name": "TowerSecondsBetweenChecks", + "Label": "Second Between Checks", + "HelpText": "How many seconds to pause between checks when monitoring a job. 0 means no checks. Failure to parse this field as an integer will default to 3 seconds.", + "DefaultValue": "3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7f8b11b1-3f4b-41eb-8cf2-38d4e984418e", + "Name": "TowerIgnoreCert", + "Label": "Ignore Certificate", + "HelpText": "**This parameter is intended only for testing.** This tells the step to ignore any https certificate presented to it from the Tower server. Please understand the ramifications before enabling this option.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "4c94cabd-ef0f-4a21-8028-430b7a0465b8", + "Name": "TowerVerbose", + "Label": "Verbose", + "HelpText": "Add additional details of what the plugin is doing.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2021-09-28T13:41:21.989Z", + "OctopusVersion": "2021.1.7500", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "kdblitz", + "Category": "ansible" +} diff --git a/step-templates/apexsql-devops-toolkit-deploy.json.human b/step-templates/apexsql-devops-toolkit-deploy.json.human new file mode 100644 index 000000000..c656e0c39 --- /dev/null +++ b/step-templates/apexsql-devops-toolkit-deploy.json.human @@ -0,0 +1,181 @@ +{ + "Id": "42412324-a768-4943-baf8-bfe26ed2dff3", + "Name": "ApexSQL DevOps toolkit - Deploy", + "Description": "This step will execute schema and data synchronization scripts created as deployment resource after comparison is done. + +ApexSQL DevOps toolkit - Sync and/or ApexSQL DevOps toolkit - Sync data steps are reqiured", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$schemaSyncScript = '' +$dataSyncScript = '' +$schemaSyncQuery = '' +$dataSyncQuery= '' +$query = '' + +function AddArtifact() { + Param( + [Parameter(Mandatory = $true)] + [string]$artifact + ) + if (Test-Path $artifact) { + New-OctopusArtifact $artifact + } +} + +function Get-ParamValue +{ + param + ( + [Parameter(Mandatory = $true)] + [String] $ParamName + ) + if($OctopusParameters -and ($OctopusParameters[\"$($ParamName)\"] -ne $null)) + { + # set the variable value + return $OctopusParameters[\"$($ParamName)\"] + } + else + { + # warning + return $null + } +} + +$exportPath = '#{ExportPath}' +$PackageDownloadStepName = '#{PackageDownloadStepName}' + +$projectId = $OctopusParameters[\"Octopus.Project.Id\"] +$releaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$nugetPackageId = $OctopusParameters[\"Octopus.Action[$PackageDownloadStepName].Package.NuGetPackageId\"] +$exportPath = Join-Path (Join-Path $exportPath $projectId) $releaseNumber + +$defaultSchemaSyncScript = $OctopusParameters[\"Octopus.Action[$PackageDownloadStepName].Output.Package.InstallationDirectoryPath\"] + '\\SchemaSyncScript.sql' +$defaultDataSyncScript = $OctopusParameters[\"Octopus.Action[$PackageDownloadStepName].Output.Package.InstallationDirectoryPath\"] + '\\DataSyncScript.sql' + +New-Item -Path $exportPath -Name \"DeploySummary.txt\" -ItemType \"file\" -Force | Out-Null +$deploySummary = $exportPath + \"\\DeploySummary.txt\" + +$schemaSyncScript = $exportPath + '\\SchemaSyncScript.sql' +$dataSyncScript = $exportPath + '\\DataSyncScript.sql' +$serverName = Get-ParamValue -ParamName 'ServerName' + $database = Get-ParamValue -ParamName 'Database' + $username = Get-ParamValue -ParamName 'Username' + $password = Get-ParamValue -ParamName 'Password' + $auth = '' + + if (-not ($null -eq $username -and $null -eq $password)) + {\t + $auth = \" -U \"\"$($username)\"\" -P \"\"$($password)\"\"\" + } + else + {\t + $auth = \" -E\" + } + +$sqlcmdProps = \"sqlcmd.exe -S \"\"$($serverName)\"\" -d \"\"$($database)\"\"$auth -b -i\" +\tif(Test-Path $schemaSyncScript) + { +\t\t$result = Invoke-Expression -Command \"$sqlcmdProps \"\"$schemaSyncScript\"\"\" + $content = \"Sync summary: \" + $result + if (Test-Path $deploySummary) + { + \tAdd-Content $deploySummary $content + } +\t} + \tif(Test-Path $dataSyncScript) + { +\t\t$result = Invoke-Expression -Command \"$sqlcmdProps \"\"$dataSyncScript\"\"\" + $content = \"Sync data summary: \" + $result + if (Test-Path $deploySummary) + { + \tAdd-Content $deploySummary $content + } +\t} + +\tif(Test-Path $defaultSchemaSyncScript) + { +\t\t$result = Invoke-Expression -Command \"$sqlcmdProps \"\"$defaultSchemaSyncScript\"\"\" +\t} + if(Test-Path $defaultDataSyncScript) + { +\t\t$result = Invoke-Expression -Command \"$sqlcmdProps \"\"$defaultDataSyncScript\"\"\" +\t} + +AddArtifact(\"$deploySummary\")", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "ad2fad54-09c4-4210-adbc-ab238632cd67", + "Name": "DownloadPackageStepName", + "Label": "Retrieve package from", + "HelpText": "Select the step from which the synchronization sripts package can be sourced", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Id": "b899f7a2-49dd-4193-8640-54e815996e02", + "Name": "ExportPath", + "Label": "Export location", + "HelpText": "The location for exported deployment resources. Provide the path used for synchronization steps. All tentacles used in database deployment steps should have access to the chosen location", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f0886e0e-f592-4fd7-ba04-8191224f3436", + "Name": "ServerName", + "Label": "SQL Server", + "HelpText": "Provide the SQL Server name for the deployment target database", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d1358f62-a3ff-4e24-b46c-036e33ff40f4", + "Name": "Database", + "Label": "Database", + "HelpText": "The target database for the deployment of schema and data synchronization scripts.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6572b012-7386-4bae-ae31-e72ac19e5171", + "Name": "Username", + "Label": "Username", + "HelpText": "The account name used for SQL authentication method. Windows authentication method with the account that runs the Tentacle service will be used for SQL Server connection if left empty", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d97dbe49-ccec-4be3-81a8-e356365aa250", + "Name": "Password", + "Label": "Password", + "HelpText": "Enter password for chosen account used for SQL authentication method. Leave empty if Windows authentication method is used", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-20T11:36:52.048Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "ApexSQLtechops", + "Category": "apexsql" +} diff --git a/step-templates/apexsql-devops-toolkit-sync-data.json.human b/step-templates/apexsql-devops-toolkit-sync-data.json.human new file mode 100644 index 000000000..279e3dfe2 --- /dev/null +++ b/step-templates/apexsql-devops-toolkit-sync-data.json.human @@ -0,0 +1,249 @@ +{ + "Id": "71d5998a-3100-4a7e-9565-b65bf0fa2352", + "Name": "ApexSQL DevOps toolkit - Sync data", + "Description": "The step will compare database from a deployment package with target database to create data synchronization script deployment resource. + +[ApexSQL Data Diff](https://www.apexsql.com/sql-tools-datadiff.aspx) is requred", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Get-ApexSQLToolLocation +{ + param + ( + [Parameter(Mandatory = $true)] + [String] $ApplicationName + ) + $key = \"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\ApexSQL $($ApplicationName)_is1\" + if (Test-Path \"HKLM:\\$Key\") + { +\t\t$ApplicationPath = (Get-ItemProperty -Path \"HKLM:\\$key\" -Name InstallLocation).InstallLocation +\t} + else + { +\t\t$reg = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64) + +\t\t$regKey= $reg.OpenSubKey(\"$key\") +\t\tif ($regKey) + { +\t\t\t$ApplicationPath = $regKey.GetValue(\"InstallLocation\") +\t\t} + else + { +\t\t\t$reg = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry32) +\t\t\t$regKey= $reg.OpenSubKey(\"$key\") +\t\t\tif ($regKey) + { +\t\t\t\t$ApplicationPath = $regKey.GetValue(\"InstallLocation\") +\t\t\t} + else + { + return $null +\t\t\t} +\t\t} +\t} + if ($ApplicationPath) + { + return $ApplicationPath + \"ApexSQL\" + $ApplicationName.replace(' ','') + \".com\" + } +} + +function AddArtifact() { + Param( + [Parameter(Mandatory = $true)] + [string]$artifact + ) + if (Test-Path $artifact) { + New-OctopusArtifact $artifact + } +} + +function Get-ParamValue +{ + param + ( + [Parameter(Mandatory = $true)] + [String] $ParamName + ) + if($OctopusParameters -and ($OctopusParameters[\"$($ParamName)\"] -ne $null)) + { + # set the variable value + return $OctopusParameters[\"$($ParamName)\"] + } + else + { + # warning + return $null + } +} + +$exportPath = Get-ParamValue -ParamName 'ExportPath' +$PackageDownloadStepName = Get-ParamValue -ParamName 'PackageDownloadStepName' +$s2 = Get-ParamValue -ParamName 'ServerName' +$d2 = Get-ParamValue -ParamName 'Database' +$u2 = Get-ParamValue -ParamName 'Username' +$p2 = Get-ParamValue -ParamName 'Password' +$projectFilePath = Get-ParamValue -ParamName 'ProjectFilePath' +$additional = Get-ParamValue -ParamName 'Additional' + +$projectId = $OctopusParameters[\"Octopus.Project.Id\"] +$releaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$nugetPackageId = $OctopusParameters[\"Octopus.Action[$PackageDownloadStepName].Package.NuGetPackageId\"] +$exportPath = Join-Path (Join-Path $exportPath $projectId) $releaseNumber + +if (-Not (Test-Path $exportPath)) { New-Item $exportPath -ItemType Directory } + +$FolderList = Get-ChildItem $OctopusParameters[\"Octopus.Action[$PackageDownloadStepName].Output.Package.InstallationDirectoryPath\"] -Directory + +Foreach($f in $Folderlist){ +if ($f.Name -like '*Script*') +\t{ + \t\t$DatabaseScripts = $f.Name + \t} +} + +$sfPath = $OctopusParameters[\"Octopus.Action[$PackageDownloadStepName].Output.Package.InstallationDirectoryPath\"] + '\\' + $DatabaseScripts + +if($null -eq $sfPath) { + throw \"Step: '$PackageDownloadStepName' didn't download any NuGet package.\" +} + +$dataSyncScript = \"DataSyncScript.sql\" +$dataSyncSummary = \"DataSyncSummary.log\" +$dataSyncReport = \"DataSyncReport.html\" + + +$creds2 = '' +if ($u2 -ne $null -and $p2 -ne $null) +{ + $creds2 = \"/user2:`\"$($u2)`\" /password2:`\"$($p2)`\"\" +} + +$project = '' +if($projectFilePath -ne $null) +{ + $project = \"/project: `\"$($projectFilePath)`\"\" +} + +$additionalParams = '' +if($additional -ne $null) +{ + $additionalParams = $additional +} + + +$toolLocation = Get-ApexSQLToolLocation -ApplicationName 'Data Diff' +$toolParams = \" /sf1:`\"$($sfPath)`\" /server2:`\"$($s2)`\" /database2:`\"$($d2)`\" $($creds2)\" +$toolParams += \" /ot:sql /on:`'$($exportPath)\\$($dataSyncScript)`'\" +$toolParams += \" /ot2:html /on2:`\"$($exportPath)\\$($dataSyncReport)`\"\" +$toolParams += \" /cso:`\"$($exportPath)\\$($dataSyncSummary)`\"\" +$toolParams += \" $($project)\" +$toolParams += \" $($additionalParams) /v /f\" +write-host $toolParams +Invoke-Expression -Command (\"& `\"$($toolLocation)`\" $toolParams\") + +AddArtifact(\"$exportPath\\$dataSyncScript\") +AddArtifact(\"$exportPath\\$dataSyncSummary\") +AddArtifact(\"$exportPath\\$dataSyncReport\")", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "3d6da5fc-e18f-4092-9e01-03c35f650644", + "Name": "PackageDownloadStepName", + "Label": "Retrieve package from", + "HelpText": "Select the step from which the database package can be sourced", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Id": "a349fecf-1fe3-4827-8d3b-9f02fb2a12cf", + "Name": "ExportPath", + "Label": "Export location", + "HelpText": "The location for exported deployment resources. This path will be used in the “ApexSQL DevOps toolkit – Deploy” step", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "11c4c432-f021-4671-96e0-04c2b9da1588", + "Name": "ServerName", + "Label": "SQL Server", + "HelpText": "Provide the SQL Server name for the deployment target database", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "96fe6267-ac00-461f-bb97-a33cf628ac94", + "Name": "Database", + "Label": "Database", + "HelpText": "Provide the name of the target database which will be used for comparison with source data located in the deployment package in order to generate deployment resource", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1deec13b-2b56-43fb-a43e-f73c8825a986", + "Name": "Username", + "Label": "Username", + "HelpText": "The account name used for SQL authentication method. Windows authentication method with the account that runs the Tentacle service will be used for SQL Server connection if left empty", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e2abb015-9baa-44cc-8c9a-93ba368d36aa", + "Name": "Password", + "Label": "Password", + "HelpText": "Enter password for chosen account used for SQL authentication method. Leave empty if Windows authentication method is used", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ea6a3d17-1341-4f86-87bf-57c39616132c", + "Name": "ProjectFilePath", + "Label": "Project file path", + "HelpText": "Use to import data comparison options and rows filter template created with ApexSQL Data Diff. Application defaults will be used if not provided + +See also: +[Using the Row filter option in ApexSQL Data Diff](https://knowledgebase.apexsql.com/using-row-filter-feature-in-apexsql-data-diff/)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "24fb5094-7d3e-421a-bddd-5629aba43982", + "Name": "Additional", + "Label": "Additional parameters", + "HelpText": "Enter any CLI options switches used with ApexSQL Data Diff. Options will override existing options imported from project file + +See also: +[ApexSQL Data Diff Command Line Interface (CLI) switches](https://knowledgebase.apexsql.com/apexsql-data-diff-command-line-interface-cli-switches/)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-20T11:41:02.493Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "ApexSQLtechops", + "Category": "apexsql" +} diff --git a/step-templates/apexsql-devops-toolkit-sync.json.human b/step-templates/apexsql-devops-toolkit-sync.json.human new file mode 100644 index 000000000..aaf789311 --- /dev/null +++ b/step-templates/apexsql-devops-toolkit-sync.json.human @@ -0,0 +1,250 @@ +{ + "Id": "3dfd5df3-2e9f-412b-93be-dcf4ef2d3da7", + "Name": "ApexSQL DevOps toolkit - Sync", + "Description": "The step will compare the database from a deployment package with the target database to create a schema synchronization script deployment resource + +[ApexSQL Diff](https://www.apexsql.com/sql-tools-diff.aspx) is required", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Get-ApexSQLToolLocation +{ + param + ( + [Parameter(Mandatory = $true)] + [String] $ApplicationName + ) + $key = \"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\ApexSQL $($ApplicationName)_is1\" + if (Test-Path \"HKLM:\\$Key\") + { +\t\t$ApplicationPath = (Get-ItemProperty -Path \"HKLM:\\$key\" -Name InstallLocation).InstallLocation +\t} + else + { +\t\t$reg = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64) + +\t\t$regKey= $reg.OpenSubKey(\"$key\") +\t\tif ($regKey) + { +\t\t\t$ApplicationPath = $regKey.GetValue(\"InstallLocation\") +\t\t} + else + { +\t\t\t$reg = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry32) +\t\t\t$regKey= $reg.OpenSubKey(\"$key\") +\t\t\tif ($regKey) + { +\t\t\t\t$ApplicationPath = $regKey.GetValue(\"InstallLocation\") +\t\t\t} + else + { + return $null +\t\t\t} +\t\t} +\t} + if ($ApplicationPath) + { + return $ApplicationPath + \"ApexSQL\" + $ApplicationName.replace(' ','') + \".com\" + } +} + +function AddArtifact() { + Param( + [Parameter(Mandatory = $true)] + [string]$artifact + ) + if (Test-Path $artifact) { + New-OctopusArtifact $artifact + } +} + +function Get-ParamValue +{ + param + ( + [Parameter(Mandatory = $true)] + [String] $ParamName + ) + if($OctopusParameters -and ($OctopusParameters[\"$($ParamName)\"] -ne $null)) + { + # set the variable value + return $OctopusParameters[\"$($ParamName)\"] + } + else + { + # warning + return $null + } +} + +$exportPath = Get-ParamValue -ParamName 'ExportPath' +$PackageDownloadStepName = Get-ParamValue -ParamName 'PackageDownloadStepName' +$s2 = Get-ParamValue -ParamName 'ServerName' +$d2 = Get-ParamValue -ParamName 'Database' +$u2 = Get-ParamValue -ParamName 'Username' +$p2 = Get-ParamValue -ParamName 'Password' +$projectFilePath = Get-ParamValue -ParamName 'ProjectFilePath' +$additional = Get-ParamValue -ParamName 'Additional' + +$projectId = $OctopusParameters[\"Octopus.Project.Id\"] +$releaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$nugetPackageId = $OctopusParameters[\"Octopus.Action[$PackageDownloadStepName].Package.NuGetPackageId\"] +$exportPath = Join-Path (Join-Path $exportPath $projectId) $releaseNumber + +if (-Not (Test-Path $exportPath)) { New-Item $exportPath -ItemType Directory } + +$FolderList = Get-ChildItem $OctopusParameters[\"Octopus.Action[$PackageDownloadStepName].Output.Package.InstallationDirectoryPath\"] -Directory + +Foreach($f in $Folderlist){ +if ($f.Name -like '*Script*') +\t{ + \t\t$DatabaseScripts = $f.Name + \t} +} + +$sfPath = $OctopusParameters[\"Octopus.Action[$PackageDownloadStepName].Output.Package.InstallationDirectoryPath\"] + '\\' + $DatabaseScripts + +if($null -eq $sfPath) { + throw \"Step: '$PackageDownloadStepName' didn't download any NuGet package.\" +} + +$schemaSyncScript = \"SchemaSyncScript.sql\" +$schemaSyncSummary = \"SchemaSyncSummary.log\" +$schemaSyncReport = \"SchemaSyncReport.html\" + + +$creds2 = '' +if ($u2 -ne $null -and $p2 -ne $null) +{ + $creds2 = \"/user2:`\"$($u2)`\" /password2:`\"$($p2)`\"\" +} + +$project = '' +if($projectFilePath -ne $null) +{ + $project = \"/project: `\"$($projectFilePath)`\"\" +} + +$additionalParams = '' +if($additional -ne $null) +{ + $additionalParams = $additional +} + + +$toolLocation = Get-ApexSQLToolLocation -ApplicationName 'Diff' +$toolParams = \" /sf1:`\"$($sfPath)`\" /server2:`\"$($s2)`\" /database2:`\"$($d2)`\" $($creds2)\" +$toolParams += \" /ot:sql /on:`'$($exportPath)\\$($schemaSyncScript)`'\" +$toolParams += \" /ot2:html /on2:`\"$($exportPath)\\$($schemaSyncReport)`\"\" +$toolParams += \" /cso:`\"$($exportPath)\\$($schemaSyncSummary)`\"\" +$toolParams += \" $($project)\" +$toolParams += \" $($additionalParams) /v /f\" + +Invoke-Expression -Command (\"& `\"$($toolLocation)`\" $toolParams\") + +AddArtifact(\"$exportPath\\$schemaSyncScript\") +AddArtifact(\"$exportPath\\$schemaSyncSummary\") +AddArtifact(\"$exportPath\\$schemaSyncReport\")", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "86778bfc-aab9-42ea-b67b-ee0c0fe1c203", + "Name": "PackageDownloadStepName", + "Label": "Retrieve package from", + "HelpText": "Select the step from which the database package can be sourced", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Id": "fd75e328-92da-4e88-bb1c-35504fdccf15", + "Name": "ExportPath", + "Label": "Export location", + "HelpText": "The location for exported deployment resources. This path will be used in the “ApexSQL DevOps toolkit – Deploy” step. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a487d044-8a82-4065-bb1d-d7ccb5a65468", + "Name": "ServerName", + "Label": "SQL Server", + "HelpText": "Provide the SQL Server name for the deployment target database", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "dca94a19-8d97-49b5-a4e5-21f32781df32", + "Name": "Database", + "Label": "Database", + "HelpText": "Provide the name of the target database which will be used for comparison with source schema located in the deployment package in order to generate deployment resource. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5465f0aa-7b85-4020-99f9-a9f45f7b39b3", + "Name": "Username", + "Label": "Username", + "HelpText": "The account name used for SQL authentication method. Windows authentication method with the account that runs the Tentacle service will be used for SQL Server connection if left empty", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5396a1bd-5f84-48cd-a452-135aa67a24a4", + "Name": "Password", + "Label": "Password", + "HelpText": "Enter password for chosen account used for SQL authentication method. Leave empty if Windows authentication method is used", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "2a296435-ccf6-444d-8cb6-d0606eb1bce8", + "Name": "ProjectFilePath", + "Label": "Project file path", + "HelpText": "Use to import schema comparison options and objects filter template created with ApexSQL Diff. Application defaults will be used if not provided + +See also: +[How to narrow schema comparison and synchronization to affected objects only](https://knowledgebase.apexsql.com/narrow-schema-comparison-synchronization-affected-objects/) +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e3cfcc10-1e5c-4554-83ca-0d9db66c7848", + "Name": "Additional", + "Label": "Additional parameters", + "HelpText": "Enter any CLI options switches used with ApexSQL Diff. Options will override existing options imported from project file + +See also: +[ApexSQL Diff Command Line Interface (CLI) switches](https://knowledgebase.apexsql.com/apexsql-diff-command-line-interface-cli-switches/)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-20T11:40:28.179Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "ApexSQLtechops", + "Category": "apexsql" +} diff --git a/step-templates/apollo-rover-check-subgraph-schema.json.human b/step-templates/apollo-rover-check-subgraph-schema.json.human new file mode 100644 index 000000000..ac328d81f --- /dev/null +++ b/step-templates/apollo-rover-check-subgraph-schema.json.human @@ -0,0 +1,128 @@ +{ + "Id": "a0f2d754-9cae-488d-820c-4798985fb8d9", + "Name": "Apollo Rover - Check Subgraph Schema", + "Description": "Check subgraph schema against Apollo Studio. This should be run before deploying the subgraph service when promoting to higher environments. + +The script installs the [Apollo Rover CLI](https://apollographql.com/docs/rover) and runs the `rover subgraph check` command.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Name": "SchemaPackage", + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "ApolloSchemaPackage", + "Purpose": "" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "APOLLO_KEY=$(get_octopusvariable \"ApolloKey\") +APOLLO_GRAPH_REF=$(get_octopusvariable \"ApolloGraphRef\") +SUBGRAPH_NAME=$(get_octopusvariable \"ApolloSubgraphName\") +SCHEMA=$(get_octopusvariable \"ApolloSchema\") +ver=$(get_octopusvariable \"ApolloRoverVersion\") + +[ -z \"$ver\" ] || [ \"$ver\" == \"latest\" ] && ROVER_VERSION=\"latest\" || ROVER_VERSION=\"v$ver\" + +missing_params=() + +[ -z \"$APOLLO_KEY\" ] && missing_params+=(\"ApolloKey\") +[ -z \"$APOLLO_GRAPH_REF\" ] && missing_params+=(\"ApolloGraphRef\") +[ -z \"$SUBGRAPH_NAME\" ] && missing_params+=(\"SubgraphName\") +[ -z \"$SCHEMA\" ] && missing_params+=(\"Schema\") + +if [ -n \"$missing_params\" ]; then + >&2 echo \"Missing parameters: ${missing_params[@]}\" + exit 1 +fi + +curl -sSL https://rover.apollo.dev/nix/$ROVER_VERSION -o installer + +if [ \"$(head -n1 installer)\" != \"#!/bin/bash\" ]; then + >&2 echo \"There was a problem fetching $ROVER_VERSION of Rover CLI:\" + >&2 cat installer + rm installer + exit 1 +fi + +sh installer --force 2>&1 +rm installer + +APOLLO_KEY=$APOLLO_KEY ~/.rover/bin/rover subgraph check $APOLLO_GRAPH_REF \\ + --name $SUBGRAPH_NAME \\ + --schema $SCHEMA \\ + 2>&1 +" + }, + "Parameters": [ + { + "Name": "ApolloRoverVersion", + "Label": "Apollo Rover Version", + "HelpText": "The version of the Apollo Rover CLI to use for running `rover subgraph check`. +May be either semver (i.e. `0.19.1`), `latest`, or empty", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApolloKey", + "Label": "Apollo Key", + "HelpText": "The API key to Apollo Studio", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "ApolloGraphRef", + "Label": "Apollo Graph Ref", + "HelpText": "The Apollo Studio graph variant", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApolloSubgraphName", + "Label": "Subgraph Name", + "HelpText": "The name of the subgraph", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApolloSchemaPackage", + "Label": "Schema Package", + "HelpText": "The package containing the schema file", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Name": "ApolloSchema", + "Label": "Schema", + "HelpText": "The path to the schema file", + "DefaultValue": "./SchemaPackage/schema.graphql", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-10-23T17:06:36.419Z", + "OctopusVersion": "2023.4.4265", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "parkerholladay", + "Category": "apollo" +} diff --git a/step-templates/apollo-rover-publish-subgraph-schema.json.human b/step-templates/apollo-rover-publish-subgraph-schema.json.human new file mode 100644 index 000000000..8efb4e045 --- /dev/null +++ b/step-templates/apollo-rover-publish-subgraph-schema.json.human @@ -0,0 +1,140 @@ +{ + "Id": "51536440-5e87-4cc7-a254-acb673f9c2c8", + "Name": "Apollo Rover - Publish Subgraph Schema", + "Description": "Publish subgraph schema to Apollo Studio. This should be run after successfully deploying a subgraph service. + +The script installs the [Apollo Rover CLI](https://apollographql.com/docs/rover) and runs the `rover subgraph publish` command.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Name": "SchemaPackage", + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "ApolloSchemaPackage", + "Purpose": "" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "APOLLO_KEY=$(get_octopusvariable \"ApolloKey\") +APOLLO_GRAPH_REF=$(get_octopusvariable \"ApolloGraphRef\") +SUBGRAPH_NAME=$(get_octopusvariable \"ApolloSubgraphName\") +SUBGRAPH_URL=$(get_octopusvariable \"ApolloSubgraphUrl\") +SCHEMA=$(get_octopusvariable \"ApolloSchema\") +ver=$(get_octopusvariable \"ApolloRoverVersion\") + +[ -z \"$ver\" ] || [ \"$ver\" == \"latest\" ] && ROVER_VERSION=\"latest\" || ROVER_VERSION=\"v$ver\" + +missing_params=() + +[ -z \"$APOLLO_KEY\" ] && missing_params+=(\"ApolloKey\") +[ -z \"$APOLLO_GRAPH_REF\" ] && missing_params+=(\"ApolloGraphRef\") +[ -z \"$SUBGRAPH_NAME\" ] && missing_params+=(\"SubgraphName\") +[ -z \"$SUBGRAPH_URL\" ] && missing_params+=(\"SubgraphUrl\") +[ -z \"$SCHEMA\" ] && missing_params+=(\"Schema\") + +if [ -n \"$missing_params\" ]; then + >&2 echo \"Missing parameters: ${missing_params[@]}\" + exit 1 +fi + +curl -sSL https://rover.apollo.dev/nix/$ROVER_VERSION -o installer + +if [ \"$(head -n1 installer)\" != \"#!/bin/bash\" ]; then + >&2 echo \"There was a problem fetching $ROVER_VERSION of Rover CLI:\" + >&2 cat installer + rm installer + exit 1 +fi + +sh installer --force 2>&1 +rm installer + +APOLLO_KEY=$APOLLO_KEY ~/.rover/bin/rover subgraph publish $APOLLO_GRAPH_REF \\ + --name $SUBGRAPH_NAME \\ + --routing-url $SUBGRAPH_URL \\ + --schema $SCHEMA \\ + 2>&1 +" + }, + "Parameters": [ + { + "Name": "ApolloRoverVersion", + "Label": "Apollo Rover Version", + "HelpText": "The version of the Apollo Rover CLI to use for running `rover subgraph publish`. +May be either semver (i.e. `0.19.1`), `latest`, or empty", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApolloKey", + "Label": "Apollo Key", + "HelpText": "The API key to Apollo Studio", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "ApolloGraphRef", + "Label": "Apollo Graph Ref", + "HelpText": "The Apollo Studio graph variant", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApolloSubgraphName", + "Label": "Subgraph Name", + "HelpText": "The name of the subgraph", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApolloSubgraphUrl", + "Label": "Subgraph Url", + "HelpText": "The graphql endpoint url for the subgraph", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApolloSchemaPackage", + "Label": "Schema Package", + "HelpText": "The package containing the schema file", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Name": "ApolloSchema", + "Label": "Schema", + "HelpText": "The path to the schema file", + "DefaultValue": "./SchemaPackage/schema.graphql", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-10-23T18:58:06.459Z", + "OctopusVersion": "2023.4.4265", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "parkerholladay", + "Category": "apollo" +} diff --git a/step-templates/application-insights-annotate-release-with-rbac.json.human b/step-templates/application-insights-annotate-release-with-rbac.json.human new file mode 100644 index 000000000..8c61db129 --- /dev/null +++ b/step-templates/application-insights-annotate-release-with-rbac.json.human @@ -0,0 +1,160 @@ +{ + "Id": "bc4eae30-786a-4974-a003-948b7a4ed023", + "Name": "Application Insights - Annotate Release with Azure CLI and RBAC", + "Description": "Marks a release point in Application Insights. This step template uses Azure CLI and Role-Based Access Control instead of an API Key. Used application-insights-annotate-release.json as inspiration.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Function to decrypt data +function Convert-PasswordToPlainText { +\t$base64password = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($OctopusParameters[\"AppInsights.ApplicationInsightsAccount.Password\"])) + return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64password)) +} + +# Function to ensure all Unicode characters in a JSON string are properly escaped +function Convert-UnicodeToEscapeHex { + param ( + [parameter(Mandatory = $true)][string]$JsonString + ) + $JsonObject = ConvertFrom-Json -InputObject $JsonString + foreach ($property in $JsonObject.PSObject.Properties) { + $name = $property.Name + $value = $property.Value + if ($value -is [string]) { + $value = [regex]::Unescape($value) + $OutputString = \"\" + foreach ($char in $value.ToCharArray()) { + $dec = [int]$char + if ($dec -gt 127) { + $hex = [convert]::ToString($dec, 16) + $hex = $hex.PadLeft(4, '0') + $OutputString += \"\\u$hex\" + } + else { + $OutputString += $char + } + } + $JsonObject.$name = $OutputString + } + } + return ConvertTo-Json -InputObject $JsonObject -Compress +} + +$applicationName = $OctopusParameters[\"AppInsights.ApplicationName\"] +$resourceGroup = $OctopusParameters[\"AppInsights.ResourceGroup\"] +$releaseName = $OctopusParameters[\"AppInsights.ReleaseName\"] +$properties = $OctopusParameters[\"AppInsights.ReleaseProperties\"] + +# Authenticate via Service Principal +$securePassword = Convert-PasswordToPlainText +$azEnv = if($OctopusParameters[\"AppInsights.ApplicationInsightsAccount.AzureEnvironment\"]) { $OctopusParameters[\"AppInsights.ApplicationInsightsAccount.AzureEnvironment\"] } else { \"AzureCloud\" } + +$azEnv = Get-AzEnvironment -Name $azEnv +if (!$azEnv) { +\tWrite-Error \"No Azure environment could be matched given the name $($OctopusParameters[\"AppInsights.ApplicationInsightsAccount.AzureEnvironment\"])\" +\texit -2 +} + +Write-Verbose \"Authenticating with Service Principal\" + +# Force any output generated to be verbose in Octopus logs. +az login --service-principal -u $OctopusParameters[\"AppInsights.ApplicationInsightsAccount.Client\"] -p $securePassword --tenant $OctopusParameters[\"AppInsights.ApplicationInsightsAccount.TenantId\"] + +Write-Verbose \"Initiating the body of the annotation\" + +$releaseProperties = $null + +if ($properties -ne $null) +{ + $releaseProperties = ConvertFrom-StringData -StringData $properties +} + +$annotation = @{ + Id = [GUID]::NewGuid(); + AnnotationName = $releaseName; + EventTime = (Get-Date).ToUniversalTime().GetDateTimeFormats(\"s\")[0]; + Category = \"Deployment\"; #Application Insights only displays annotations from the \"Deployment\" Category + Properties = ConvertTo-Json $releaseProperties -Compress +} + +$annotation = ConvertTo-Json $annotation -Compress +$annotation = Convert-UnicodeToEscapeHex -JsonString $annotation + +$body = $annotation -replace '(\\\\+)\"', '$1$1\"' -replace \"`\"\", \"`\"`\"\" + +Write-Verbose \"Send the annotation to Application Insights\" + +az rest --method put --uri \"/subscriptions/$($OctopusParameters[\"AppInsights.ApplicationInsightsAccount.SubscriptionNumber\"])/resourceGroups/$($resourceGroup)/providers/microsoft.insights/components/$($applicationName)/Annotations?api-version=2015-05-01\" --body \"$($body) \"", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "ef9d044d-3765-4cb0-af55-22c15ce4013c", + "Name": "AppInsights.ApplicationInsightsAccount", + "Label": "Application Insights Account", + "HelpText": "Azure account for the Application Insights instance", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "98174616-d9dd-4e8e-9b01-2961a061360f", + "Name": "AppInsights.ApplicationName", + "Label": "Application Name", + "HelpText": "The Application Insights Application name.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "41835ca3-76d3-47f8-b863-d26c782c4ba4", + "Name": "AppInsights.ResourceGroup", + "Label": "Resource Group", + "HelpText": "The Resource Group of the Application Insights instance", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e008808c-622d-4efe-91a0-ac666d264996", + "Name": "AppInsights.ReleaseName", + "Label": "Release Name", + "HelpText": "The release name. Typically bound to #{Octopus.Release.Number}", + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "551f06ad-9470-415b-aed9-dd80f3a4123d", + "Name": "AppInsights.ReleaseProperties", + "Label": "Release Properties", + "HelpText": "List of key/value pairs separated by a new-line. For example: + +``` +ReleaseDescription = Release with annotation +TriggerBy = John Doe +```", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2024-05-17T06:54:43.852Z", + "OctopusVersion": "2024.1.12600", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2024-05-17T07:30:00.000Z", + "LastModifiedBy": "NielsDM", + "Category": "azure" +} diff --git a/step-templates/application-insights-annotate-release.json.human b/step-templates/application-insights-annotate-release.json.human new file mode 100644 index 000000000..b0218a382 --- /dev/null +++ b/step-templates/application-insights-annotate-release.json.human @@ -0,0 +1,203 @@ +{ + "Id": "c3278ce2-9cdb-46fd-96ce-e1d03ac327fe", + "Name": "Application Insights - Annotate Release", + "Description": "Marks a release point in Application Insights.", + "ActionType": "Octopus.Script", + "Version": 8, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$securityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 +[Net.ServicePointManager]::SecurityProtocol = $securityProtocol + +$applicationId = $OctopusParameters[\"ApplicationId\"] +$apiKey = $OctopusParameters[\"ApiKey\"] +$releaseName = $OctopusParameters[\"ReleaseName\"] +$properties = $OctopusParameters[\"ReleaseProperties\"] + +$releaseProperties = $null + +if ($properties -ne $null) +{ + $releaseProperties = ConvertFrom-StringData -StringData $properties +} + +# background info on how fwlink works: After you submit a web request, many sites redirect through a series of intermediate pages before you finally land on the destination page. +# So when calling Invoke-WebRequest, the result it returns comes from the final page in any redirect sequence. Hence, I set MaximumRedirection to 0, as this prevents the call to +# be redirected. By doing this, we get a resposne with status code 302, which indicates that there is a redirection link from the response body. We grab this redirection link and +# construct the url to make a release annotation. +# Here's how this logic is going to works +# 1. Client send http request, such as: http://go.microsoft.com/fwlink/?LinkId=625115 +# 2. FWLink get the request and find out the destination URL for it, such as: http://www.bing.com +# 3. FWLink generate a new http response with status code \"302\" and with destination URL \"http://www.bing.com\". Send it back to Client. +# 4. Client, such as a powershell script, knows that status code \"302\" means redirection to new a location, and the target location is \"http://www.bing.com\" +function GetRequestUrlFromFwLink($fwLink) +{ + $request = Invoke-WebRequest -Uri $fwLink -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore + if ($request.StatusCode -eq \"302\") { + return $request.Headers.Location + } + + return $null +} + +function CreateAnnotation($grpEnv) +{ +\t$retries = 1 +\t$success = $false +\twhile (!$success -and $retries -lt 6) { +\t $location = \"$grpEnv/applications/$applicationId/Annotations?api-version=2015-11\" +\t\t +\t\tWrite-Host \"Invoke a web request for $location to create a new release annotation. Attempting $retries\" +\t\tset-variable -Name createResultStatus -Force -Scope Local -Value $null +\t\tset-variable -Name createResultStatusDescription -Force -Scope Local -Value $null +\t\tset-variable -Name result -Force -Scope Local + +\t\ttry { +\t\t\t$result = Invoke-WebRequest -Uri $location -Method Put -Body $bodyJson -Headers $headers -ContentType \"application/json; charset=utf-8\" -UseBasicParsing +\t\t} catch { +\t\t if ($_.Exception){ +\t\t if($_.Exception.Response) { + \t\t\t\t$createResultStatus = $_.Exception.Response.StatusCode.value__ + \t\t\t\t$createResultStatusDescription = $_.Exception.Response.StatusDescription + \t\t\t} + \t\t\telse { + \t\t\t\t$createResultStatus = \"Exception\" + \t\t\t\t$createResultStatusDescription = $_.Exception.Message + \t\t\t} +\t\t } +\t\t} + +\t\tif ($result -eq $null) { +\t\t\tif ($createResultStatus -eq $null) { +\t\t\t\t$createResultStatus = \"Unknown\" +\t\t\t} +\t\t\tif ($createResultStatusDescription -eq $null) { +\t\t\t\t$createResultStatusDescription = \"Unknown\" +\t\t\t} +\t\t} + \t\telse { +\t\t\t $success = $true\t\t\t + } + +\t\tif ($createResultStatus -eq 409 -or $createResultStatus -eq 404 -or $createResultStatus -eq 401) # no retry when conflict or unauthorized or not found +\t\t{ +\t\t\tbreak +\t\t} + +\t\t$retries = $retries + 1 +\t\tsleep 1 +\t} + +\t$createResultStatus +\t$createResultStatusDescription +\treturn +} + +# Need powershell version 3 or greater for script to run +$minimumPowershellMajorVersion = 3 +if ($PSVersionTable.PSVersion.Major -le $minimumPowershellMajorVersion) { + Write-Host \"Need powershell version $minimumPowershellMajorVersion or greater to create release annotation\" + return +} + +$currentTime = (Get-Date).ToUniversalTime() +$annotationDate = $currentTime.ToString(\"MMddyyyy_HHmmss\") +set-variable -Name requestBody -Force -Scope Script +$requestBody = @{} +$requestBody.Id = [GUID]::NewGuid() +$requestBody.AnnotationName = $releaseName +$requestBody.EventTime = $currentTime.GetDateTimeFormats(\"s\")[0] # GetDateTimeFormats returns an array +$requestBody.Category = \"Deployment\" + +if ($releaseProperties -eq $null) { + $properties = @{} +} else { + $properties = $releaseProperties +} +$properties.Add(\"ReleaseName\", $releaseName) + +$requestBody.Properties = ConvertTo-Json($properties) -Compress + +$bodyJson = [System.Text.Encoding]::UTF8.GetBytes(($requestBody | ConvertTo-Json)) +$headers = New-Object \"System.Collections.Generic.Dictionary[[String],[String]]\" +$headers.Add(\"X-AIAPIKEY\", $apiKey) + +set-variable -Name createAnnotationResult1 -Force -Scope Local -Value $null +set-variable -Name createAnnotationResultDescription -Force -Scope Local -Value \"\" + +# get redirect link from fwlink +$requestUrl = GetRequestUrlFromFwLink(\"http://go.microsoft.com/fwlink/?prd=11901&pver=1.0&sbp=Application%20Insights&plcid=0x409&clcid=0x409&ar=Annotations&sar=Create%20Annotation\") +if ($requestUrl -eq $null) { + $output = \"Failed to find the redirect link to create a release annotation\" + throw $output +} + +$createAnnotationResult1, $createAnnotationResultDescription = CreateAnnotation($requestUrl) +if ($createAnnotationResult1) +{ + $output = \"Failed to create an annotation with Id: {0}. Error {1}, Description: {2}.\" -f $requestBody.Id, $createAnnotationResult1, $createAnnotationResultDescription +\t throw $output +} + +$str = \"Release annotation created. Id: {0}.\" -f $requestBody.Id +Write-Host $str", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "4e276bad-2a13-4dc2-bd54-5708a76cf662", + "Name": "ApplicationId", + "Label": "Application Id", + "HelpText": "The Application Insights Application Id.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f818317b-6b62-4695-9272-7062a4c1c601", + "Name": "ApiKey", + "Label": "Api Key", + "HelpText": "The API Key to use to configure the Application Insights application.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a241d2b7-99e0-4ed5-9ed1-5b003d855b33", + "Name": "ReleaseName", + "Label": "Release Name", + "HelpText": "The release name. Typically bound to #{Octopus.Release.Number}", + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bdd7161a-682c-4c6b-98dd-bee990326ed0", + "Name": "ReleaseProperties", + "Label": "Release Properties", + "HelpText": "List of key/value pairs separated by a new-line. For example: + +``` +ReleaseDescription = Release with annotation +TriggerBy = John Doe +```", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedOn": "2019-12-18T22:35:12.542Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2020-04-11T15:22:12.542Z", + "OctopusVersion": "2020.1.9", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/argo-argocd-app-get.json.human b/step-templates/argo-argocd-app-get.json.human new file mode 100644 index 000000000..d88257422 --- /dev/null +++ b/step-templates/argo-argocd-app-get.json.human @@ -0,0 +1,116 @@ +{ + "Id": "f67404e4-3394-4f8d-9739-74a04c99a6f1", + "Name": "Argo - argocd app get", + "Description": "Get an Argo Application details using the [argocd app get](https://argo-cd.readthedocs.io/en/stable/user-guide/commands/argocd_app_get/) CLI command + +_Note:_ This step will only run against an Octopus [kubernetes](https://octopus.com/docs/infrastructure/deployment-targets/kubernetes) deployment target. + +**Pre-requisites:** +- Access to the `argocd` CLI on the target or worker.", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "# argocd is required +if ! [ -x \"$(command -v argocd)\" ]; then +\tfail_step 'argocd command not found' +fi + +# Helper functions +isSet() { [ ! -z \"${1}\" ]; } +isNotSet() { [ -z \"${1}\" ]; } + +# Get variables +argocd_server=$(get_octopusvariable \"ArgoCD.AppGet.ArgoCD_Server\") +argocd_authToken=$(get_octopusvariable \"ArgoCD.AppGet.ArgoCD_Auth_Token\") +applicationName=$(get_octopusvariable \"ArgoCD.AppGet.ApplicationName\") +additionalParameters=$(get_octopusvariable \"ArgoCD.AppGet.AdditionalParameters\") + +# Check required variables +if isNotSet \"${argocd_server}\"; then + fail_step \"argocd_server is not set\" +fi + +if isNotSet \"${argocd_authToken}\"; then + fail_step \"argocd_authToken is not set\" +fi + +if isNotSet \"${applicationName}\"; then + fail_step \"applicationName is not set\" +fi + +if isSet \"${additionalParameters}\"; then + IFS=$'\ +' read -rd '' -a additionalArgs <<< \"$additionalParameters\" +else + additionalArgs=() +fi + +flattenedArgs=\"${additionalArgs[@]}\" + +write_verbose \"ARGOCD_SERVER: '${argocd_server}'\" +write_verbose \"ARGOCD_AUTH_TOKEN: '********'\" + +authArgs=\"--server ${argocd_server} --auth-token ${argocd_authToken}\" +maskedAuthArgs=\"--server ${argocd_server} --auth-token '********'\" + +echo \"Executing: argocd app get ${applicationName} ${maskedAuthArgs} ${flattenedArgs}\" +argocd app get ${applicationName} ${authArgs} ${flattenedArgs}" + }, + "Parameters": [ + { + "Id": "0a5f6eea-c876-4db2-a4ab-ea5b5d35fddb", + "Name": "ArgoCD.AppGet.ArgoCD_Server", + "Label": "ArgoCD Server", + "HelpText": "Enter the name of the ArgoCD Server to connect to. This sets the `--server` parameter used with the CLI.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4c034426-cf1d-4e9a-a69c-4de4aa6cde31", + "Name": "ArgoCD.AppGet.ArgoCD_Auth_Token", + "Label": "ArgoCD Auth Token", + "HelpText": "Enter the name of the ArgoCD Auth Token used to authenticate with. This sets the `--auth-token` parameter used with the CLI.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e738d659-aca8-4fc4-a021-36d57ec71325", + "Name": "ArgoCD.AppGet.ApplicationName", + "Label": "ArgoCD Application Name", + "HelpText": "Enter the name of the application you want to retrieve details for.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "566e77a0-fb80-4c3f-b2ef-cffaa2a2d797", + "Name": "ArgoCD.AppGet.AdditionalParameters", + "Label": "Additional Parameters (optional)", + "HelpText": "Enter additional parameter values(s) to be used when calling the `argocd` CLI. + +**Note:** Multiple parameters can be supplied by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2024-07-22T09:53:25.057Z", + "OctopusVersion": "2024.3.7046", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "argo" +} diff --git a/step-templates/argo-argocd-app-set-with-package.json.human b/step-templates/argo-argocd-app-set-with-package.json.human new file mode 100644 index 000000000..c31a71d4d --- /dev/null +++ b/step-templates/argo-argocd-app-set-with-package.json.human @@ -0,0 +1,171 @@ +{ + "Id": "8bcfe67d-cade-4fe3-a792-ce799dfb9ec1", + "Name": "Argo - argocd app set (with package)", + "Description": "Set application parameters using the [argocd app set](https://argo-cd.readthedocs.io/en/stable/user-guide/commands/argocd_app_set/) CLI command. + +_Note:_ This step will only run against an Octopus [kubernetes](https://octopus.com/docs/infrastructure/deployment-targets/kubernetes) deployment target. + +**Pre-requisites:** +- Access to the `argocd` CLI on the target or worker. +- Selection of a package (for use with setting image parameters)", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "9f2ad876-ad42-428d-bda9-676c6aaa0b60", + "Name": "ArgoCD.AppSet.ContainerImage", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "NotAcquired", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "ArgoCD.AppSet.ContainerImage", + "Purpose": "" + } + } + ], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "# argocd is required +if ! [ -x \"$(command -v argocd)\" ]; then +\tfail_step 'argocd command not found' +fi + +# Helper functions +isSet() { [ ! -z \"${1}\" ]; } +isNotSet() { [ -z \"${1}\" ]; } + +# Get variables +argocd_server=$(get_octopusvariable \"ArgoCD.AppSet.ArgoCD_Server\") +argocd_authToken=$(get_octopusvariable \"ArgoCD.AppSet.ArgoCD_Auth_Token\") +applicationName=$(get_octopusvariable \"ArgoCD.AppSet.ApplicationName\") +applicationParameters=$(get_octopusvariable \"ArgoCD.AppSet.AppParameters\") +additionalParameters=$(get_octopusvariable \"ArgoCD.AppSet.AdditionalParameters\") + +# Check required variables +if isNotSet \"${argocd_server}\"; then + fail_step \"argocd_server is not set\" +fi + +if isNotSet \"${argocd_authToken}\"; then + fail_step \"argocd_authToken is not set\" +fi + +if isNotSet \"${applicationName}\"; then + fail_step \"applicationName is not set\" +fi + +if isSet \"${applicationParameters}\"; then + parameters=\"${applicationParameters//$'\ +'/ \\\\$'\ +'}\" + flattenedParams=\"${applicationParameters//$'\ +'/ }\" + IFS=$'\ +' read -rd '' -a appParameters <<< \"$applicationParameters\" +else + appParameters=() +fi +flattenedParams=\"${appParameters[@]}\" + + +if isSet \"${additionalParameters}\"; then + IFS=$'\ +' read -rd '' -a additionalArgs <<< \"$additionalParameters\" +else + additionalArgs=() +fi + +flattenedArgs=\"${additionalArgs[@]}\" + +write_verbose \"ARGOCD_SERVER: '${argocd_server}'\" +write_verbose \"ARGOCD_AUTH_TOKEN: '********'\" + +authArgs=\"--server ${argocd_server} --auth-token ${argocd_authToken}\" +maskedAuthArgs=\"--server ${argocd_server} --auth-token '********'\" + +echo \"Executing: argocd app set ${applicationName} ${maskedAuthArgs} ${flattenedArgs} \\\\ +${parameters}\" +argocd app set ${applicationName} ${authArgs} ${flattenedArgs} ${flattenedParams}" + }, + "Parameters": [ + { + "Id": "0a5f6eea-c876-4db2-a4ab-ea5b5d35fddb", + "Name": "ArgoCD.AppSet.ArgoCD_Server", + "Label": "ArgoCD Server", + "HelpText": "Enter the name of the ArgoCD Server to connect to. This sets the `--server` parameter used with the CLI.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4c034426-cf1d-4e9a-a69c-4de4aa6cde31", + "Name": "ArgoCD.AppSet.ArgoCD_Auth_Token", + "Label": "ArgoCD Auth Token", + "HelpText": "Enter the name of the ArgoCD Auth Token used to authenticate with. This sets the `--auth-token` parameter used with the CLI.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e738d659-aca8-4fc4-a021-36d57ec71325", + "Name": "ArgoCD.AppSet.ApplicationName", + "Label": "ArgoCD Application Name", + "HelpText": "Enter the ArgoCD application name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b2054ad2-3c41-47bb-ac96-d5d8a6564ea6", + "Name": "ArgoCD.AppSet.ContainerImage", + "Label": "Container image", + "HelpText": "Provide the container image details", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "2adb0917-6b2d-4528-90a4-beff6a01109d", + "Name": "ArgoCD.AppSet.AppParameters", + "Label": "Application Parameters", + "HelpText": "Enter the parameters to set for the application, including the `--parameter` or `-p`. e.g.: +- `-p key1=value1` +- `--parameter key2=value2` + +**Note:** Multiple parameters can be supplied by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "b13a3a5e-ac79-477d-bd51-cf6efd009bd4", + "Name": "ArgoCD.AppSet.AdditionalParameters", + "Label": "Additional Parameters (optional)", + "HelpText": "Enter additional parameter values(s) to be used when calling the `argocd` CLI. + +**Note:** Multiple parameters can be supplied by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2024-07-22T09:55:12.863Z", + "OctopusVersion": "2024.3.7046", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "argo" +} diff --git a/step-templates/argo-argocd-app-set.json.human b/step-templates/argo-argocd-app-set.json.human new file mode 100644 index 000000000..9a25bb92f --- /dev/null +++ b/step-templates/argo-argocd-app-set.json.human @@ -0,0 +1,146 @@ +{ + "Id": "e27c8535-9375-4cd2-97e7-ac73a43e9ef1", + "Name": "Argo - argocd app set", + "Description": "Set application parameters using the [argocd app set](https://argo-cd.readthedocs.io/en/stable/user-guide/commands/argocd_app_set/) CLI command. + +_Note:_ This step will only run against an Octopus [kubernetes](https://octopus.com/docs/infrastructure/deployment-targets/kubernetes) deployment target. + +**Pre-requisites:** +- Access to the `argocd` CLI on the target or worker.", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "# argocd is required +if ! [ -x \"$(command -v argocd)\" ]; then +\tfail_step 'argocd command not found' +fi + +# Helper functions +isSet() { [ ! -z \"${1}\" ]; } +isNotSet() { [ -z \"${1}\" ]; } + +# Get variables +argocd_server=$(get_octopusvariable \"ArgoCD.AppSet.ArgoCD_Server\") +argocd_authToken=$(get_octopusvariable \"ArgoCD.AppSet.ArgoCD_Auth_Token\") +applicationName=$(get_octopusvariable \"ArgoCD.AppSet.ApplicationName\") +applicationParameters=$(get_octopusvariable \"ArgoCD.AppSet.AppParameters\") +additionalParameters=$(get_octopusvariable \"ArgoCD.AppSet.AdditionalParameters\") + +# Check required variables +if isNotSet \"${argocd_server}\"; then + fail_step \"argocd_server is not set\" +fi + +if isNotSet \"${argocd_authToken}\"; then + fail_step \"argocd_authToken is not set\" +fi + +if isNotSet \"${applicationName}\"; then + fail_step \"applicationName is not set\" +fi + +if isSet \"${applicationParameters}\"; then + parameters=\"${applicationParameters//$'\ +'/ \\\\$'\ +'}\" + flattenedParams=\"${applicationParameters//$'\ +'/ }\" + IFS=$'\ +' read -rd '' -a appParameters <<< \"$applicationParameters\" +else + appParameters=() +fi +flattenedParams=\"${appParameters[@]}\" + + +if isSet \"${additionalParameters}\"; then + IFS=$'\ +' read -rd '' -a additionalArgs <<< \"$additionalParameters\" +else + additionalArgs=() +fi + +flattenedArgs=\"${additionalArgs[@]}\" + +write_verbose \"ARGOCD_SERVER: '${argocd_server}'\" +write_verbose \"ARGOCD_AUTH_TOKEN: '********'\" + +authArgs=\"--server ${argocd_server} --auth-token ${argocd_authToken}\" +maskedAuthArgs=\"--server ${argocd_server} --auth-token '********'\" + +echo \"Executing: argocd app set ${applicationName} ${maskedAuthArgs} ${flattenedArgs} \\\\ +${parameters}\" +argocd app set ${applicationName} ${authArgs} ${flattenedArgs} ${flattenedParams}" + }, + "Parameters": [ + { + "Id": "0a5f6eea-c876-4db2-a4ab-ea5b5d35fddb", + "Name": "ArgoCD.AppSet.ArgoCD_Server", + "Label": "ArgoCD Server", + "HelpText": "Enter the name of the ArgoCD Server to connect to. This sets the `--server` parameter used with the CLI.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4c034426-cf1d-4e9a-a69c-4de4aa6cde31", + "Name": "ArgoCD.AppSet.ArgoCD_Auth_Token", + "Label": "ArgoCD Auth Token", + "HelpText": "Enter the name of the ArgoCD Auth Token used to authenticate with. This sets the `--auth-token` parameter used with the CLI.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e738d659-aca8-4fc4-a021-36d57ec71325", + "Name": "ArgoCD.AppSet.ApplicationName", + "Label": "ArgoCD Application Name", + "HelpText": "Enter the ArgoCD application name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2adb0917-6b2d-4528-90a4-beff6a01109d", + "Name": "ArgoCD.AppSet.AppParameters", + "Label": "Application Parameters", + "HelpText": "Enter the parameters to set for the application, including the `--parameter` or `-p`. e.g.: +- `-p key1=value1` +- `--parameter key2=value2` + +**Note:** Multiple parameters can be supplied by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "b13a3a5e-ac79-477d-bd51-cf6efd009bd4", + "Name": "ArgoCD.AppSet.AdditionalParameters", + "Label": "Additional Parameters (optional)", + "HelpText": "Enter additional parameter values(s) to be used when calling the `argocd` CLI. + +**Note:** Multiple parameters can be supplied by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2024-07-22T09:57:16.491Z", + "OctopusVersion": "2024.3.7046", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "argo" +} diff --git a/step-templates/argo-argocd-app-sync.json.human b/step-templates/argo-argocd-app-sync.json.human new file mode 100644 index 000000000..406409a26 --- /dev/null +++ b/step-templates/argo-argocd-app-sync.json.human @@ -0,0 +1,121 @@ +{ + "Id": "655058aa-2e76-4aac-a8eb-728337b5c664", + "Name": "Argo - argocd app sync", + "Description": "Sync an application to its target state using the [argocd app sync](https://argo-cd.readthedocs.io/en/stable/user-guide/commands/argocd_app_sync/) CLI command + +_Note:_ This step will only run against an Octopus [kubernetes](https://octopus.com/docs/infrastructure/deployment-targets/kubernetes) deployment target. + +**Pre-requisites:** +- Access to the `argocd` CLI on the target or worker.", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "# argocd is required +if ! [ -x \"$(command -v argocd)\" ]; then +\tfail_step 'argocd command not found' +fi + +# Helper functions +isSet() { [ ! -z \"${1}\" ]; } +isNotSet() { [ -z \"${1}\" ]; } + +# Get variables +argocd_server=$(get_octopusvariable \"ArgoCD.AppSync.ArgoCD_Server\") +argocd_authToken=$(get_octopusvariable \"ArgoCD.AppSync.ArgoCD_Auth_Token\") +applicationSelector=$(get_octopusvariable \"ArgoCD.AppSync.ApplicationSelector\") +additionalParameters=$(get_octopusvariable \"ArgoCD.AppSync.AdditionalParameters\") + +# Check required variables +if isNotSet \"${argocd_server}\"; then + fail_step \"argocd_server is not set\" +fi + +if isNotSet \"${argocd_authToken}\"; then + fail_step \"argocd_authToken is not set\" +fi + +if isNotSet \"${applicationSelector}\"; then + fail_step \"applicationSelector is not set\" +fi + +if isSet \"${additionalParameters}\"; then + IFS=$'\ +' read -rd '' -a additionalArgs <<< \"$additionalParameters\" +else + additionalArgs=() +fi + +flattenedArgs=\"${additionalArgs[@]}\" + +write_verbose \"ARGOCD_SERVER: '${argocd_server}'\" +write_verbose \"ARGOCD_AUTH_TOKEN: '********'\" + +authArgs=\"--server ${argocd_server} --auth-token ${argocd_authToken}\" +maskedAuthArgs=\"--server ${argocd_server} --auth-token '********'\" + +echo \"Executing: argocd app sync ${applicationSelector} ${maskedAuthArgs} ${flattenedArgs}\" +argocd app sync ${applicationSelector} ${authArgs} ${flattenedArgs}" + }, + "Parameters": [ + { + "Id": "0a5f6eea-c876-4db2-a4ab-ea5b5d35fddb", + "Name": "ArgoCD.AppSync.ArgoCD_Server", + "Label": "ArgoCD Server", + "HelpText": "Enter the name of the ArgoCD Server to connect to. This sets the `--server` parameter used with the CLI.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4c034426-cf1d-4e9a-a69c-4de4aa6cde31", + "Name": "ArgoCD.AppSync.ArgoCD_Auth_Token", + "Label": "ArgoCD Auth Token", + "HelpText": "Enter the name of the ArgoCD Auth Token used to authenticate with. This sets the `--auth-token` parameter used with the CLI.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e738d659-aca8-4fc4-a021-36d57ec71325", + "Name": "ArgoCD.AppSync.ApplicationSelector", + "Label": "ArgoCD Application Selector", + "HelpText": "Enter the ArgoCD application details you want to sync. Valid examples are: +- Application Name(s) e.g.`appname` +- Labels e.g. `-l app.kubernetes.io/instance=my-app` +- Specific resource e.g. `--resource :Service:my-service`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "566e77a0-fb80-4c3f-b2ef-cffaa2a2d797", + "Name": "ArgoCD.AppSync.AdditionalParameters", + "Label": "Additional Parameters (optional)", + "HelpText": "Enter additional parameter values(s) to be used when calling the `argocd` CLI. e.g.: +- `--revisions 0.0.1` +- `--source-positions 1` + +**Note:** Multiple parameters can be supplied by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2024-07-22T09:54:04.913Z", + "OctopusVersion": "2024.3.7046", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "argo" +} diff --git a/step-templates/argo-argocd-app-wait.json.human b/step-templates/argo-argocd-app-wait.json.human new file mode 100644 index 000000000..27ee6d8fa --- /dev/null +++ b/step-templates/argo-argocd-app-wait.json.human @@ -0,0 +1,122 @@ +{ + "Id": "050e7819-ecf7-46de-bcd2-545f0956c1c5", + "Name": "Argo - argocd app wait", + "Description": "Wait for an application to reach a synced and healthy state using the [argocd app wait](https://argo-cd.readthedocs.io/en/stable/user-guide/commands/argocd_app_wait/) CLI command + +_Note:_ This step will only run against an Octopus [kubernetes](https://octopus.com/docs/infrastructure/deployment-targets/kubernetes) deployment target. + +**Pre-requisites:** +- Access to the `argocd` CLI on the target or worker.", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "# argocd is required +if ! [ -x \"$(command -v argocd)\" ]; then +\tfail_step 'argocd command not found' +fi + +# Helper functions +isSet() { [ ! -z \"${1}\" ]; } +isNotSet() { [ -z \"${1}\" ]; } + +# Get variables +argocd_server=$(get_octopusvariable \"ArgoCD.AppWait.ArgoCD_Server\") +argocd_authToken=$(get_octopusvariable \"ArgoCD.AppWait.ArgoCD_Auth_Token\") +applicationSelector=$(get_octopusvariable \"ArgoCD.AppWait.ApplicationSelector\") +additionalParameters=$(get_octopusvariable \"ArgoCD.AppWait.AdditionalParameters\") + +# Check required variables +if isNotSet \"${argocd_server}\"; then + fail_step \"argocd_server is not set\" +fi + +if isNotSet \"${argocd_authToken}\"; then + fail_step \"argocd_authToken is not set\" +fi + +if isNotSet \"${applicationSelector}\"; then + fail_step \"applicationSelector is not set\" +fi + +if isSet \"${additionalParameters}\"; then + IFS=$'\ +' read -rd '' -a additionalArgs <<< \"$additionalParameters\" +else + additionalArgs=() +fi + +flattenedArgs=\"${additionalArgs[@]}\" + +write_verbose \"ARGOCD_SERVER: '${argocd_server}'\" +write_verbose \"ARGOCD_AUTH_TOKEN: '********'\" + +authArgs=\"--server ${argocd_server} --auth-token ${argocd_authToken}\" +maskedAuthArgs=\"--server ${argocd_server} --auth-token '********'\" + +echo \"Executing: argocd app wait ${applicationSelector} ${maskedAuthArgs} ${flattenedArgs}\" +argocd app wait ${applicationSelector} ${authArgs} ${flattenedArgs}" + }, + "Parameters": [ + { + "Id": "0a5f6eea-c876-4db2-a4ab-ea5b5d35fddb", + "Name": "ArgoCD.AppWait.ArgoCD_Server", + "Label": "ArgoCD Server", + "HelpText": "Enter the name of the ArgoCD Server to connect to. This sets the `--server` parameter used with the CLI.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4c034426-cf1d-4e9a-a69c-4de4aa6cde31", + "Name": "ArgoCD.AppWait.ArgoCD_Auth_Token", + "Label": "ArgoCD Auth Token", + "HelpText": "Enter the name of the ArgoCD Auth Token used to authenticate with. This sets the `--auth-token` parameter used with the CLI.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e738d659-aca8-4fc4-a021-36d57ec71325", + "Name": "ArgoCD.AppWait.ApplicationSelector", + "Label": "ArgoCD Application Selector", + "HelpText": "Enter the ArgoCD application details you want to wait. Valid examples are: +- Application Name(s) e.g.`appname` +- Labels e.g. `-l app.kubernetes.io/instance=my-app` +- Specific resource e.g. `--resource :Service:my-service`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "566e77a0-fb80-4c3f-b2ef-cffaa2a2d797", + "Name": "ArgoCD.AppWait.AdditionalParameters", + "Label": "Additional Parameters (optional)", + "HelpText": "Enter additional parameter values(s) to be used when calling the `argocd` CLI. e.g.: +- `--app-namespace` +- `--degraded` +- `--sync` + +**Note:** Multiple parameters can be supplied by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2024-07-22T09:54:34.458Z", + "OctopusVersion": "2024.3.7046", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "argo" +} diff --git a/step-templates/argo-rollouts-get-rollout.json.human b/step-templates/argo-rollouts-get-rollout.json.human new file mode 100644 index 000000000..bcab0eaeb --- /dev/null +++ b/step-templates/argo-rollouts-get-rollout.json.human @@ -0,0 +1,215 @@ +{ + "Id": "c2c71e8e-429d-407e-8448-42e38cfb9c5a", + "Name": "Argo - Rollouts Get Rollout", + "Description": "Gets the status of an Argo Rollout.", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Save-OctopusVariable { + Param( + [string] $name, + [string] $value + ) + $StepName = $OctopusParameters[\"Octopus.Step.Name\"] + + Set-OctopusVariable -Name $name -Value $value + + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$name}\" +} + +# Installs the Argo Rollouts plugin +function Install-Plugin +{ +# Define parameters +\tparam ($PluginUri, + $PluginFilename + ) + + # Check for plugin folder + if ((Test-Path -Path \"$PWD/plugins\") -eq $false) + { +\t\t# Create new plugins folder + New-Item -Path \"$PWD/plugins\" -ItemType \"Directory\" + + # Add to path + $env:PATH = \"$($PWD)/plugins$([IO.Path]::PathSeparator)\" + $env:PATH + } + +\t# Download plugin +\tInvoke-WebRequest -Uri \"$PluginUri\" -OutFile \"$PWD/plugins/$PluginFilename\" + +\t# Make file executable + if ($IsLinux) + { +\t\t# Make it executable + \tchmod +x ./plugins/$PluginFilename + } + + if ($IsWindows) + { + \t# Update filename to include .exe extension + Rename-Item -Path \"$PWD/plugins/$PluginFilename\" -NewName \"$PWD/plugins/$($PluginFilename).exe\" + } +} + +# When listing plugins, kubectl looks in all paths defined in $env:PATH and will fail if the path does not exist +function Verify-Path-Variable +{ +\t# Get current path and split into array + $paths = $env:PATH.Split([IO.Path]::PathSeparator) + $verifiedPaths = @() + + # Loop through paths + foreach ($path in $paths) + { + \t# Check for existence + if ((Test-Path -Path $path) -eq $true) + { + \t# Add to verified + $verifiedPaths += $path + } + } + + # Return verified paths + return ($verifiedPaths -join [IO.Path]::PathSeparator) +} + +function Get-Plugin-Installed +{ +\t# Define parameters + param ( + \t$PluginName, + $InstalledPlugins + ) + + \t$isInstalled = $false + +\tforeach ($plugin in $installedPlugins) + \t{ +\t\tif ($plugin -like \"$($PluginName)*\") + { + \t$isInstalled = $true + \tbreak + } +\t} + + return $isInstalled +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +# Fix ANSI Color on PWSH Core issues when displaying objects +if ($PSEdition -eq \"Core\") { + $PSStyle.OutputRendering = \"PlainText\" +} + +# Check to see if it's running on Windows +if ($IsWindows) { + # Disable the progress bar so downloading files via Invoke-WebRequest are faster + $ProgressPreference = 'SilentlyContinue' +} + +# Set TLS +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Verify all PATH variables are avaialable +$env:PATH = Verify-Path-Variable +if ($IsLinux) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64\" +} + +if ($IsWindows) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-windows-amd64\" +} + +try +{ + # Check to see if plugins are installed + $pluginList = (kubectl plugin list 2>&1) + + # This is the path that Linux will take + if ($lastExitCode -ne 0 -and $pluginList.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { + Install-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + # Parse list + \t$pluginList = $pluginList.Split(\"`n\", [System.StringSplitOptions]::RemoveEmptyEntries) + + if ((Get-Plugin-Installed -PluginName \"kubectl-argo-rollouts\" -InstalledPlugins $pluginList) -eq $false) + { + \tInstall-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \tWrite-Host \"Argo Rollout kubectl plugin found ...\" + } + } +} +catch +{ +\t# On Windows, the executable will cause an error if no plugins found so this the path Windows will take + if ($_.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { +\t\tInstall-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \t# Something else happened, we need to surface the error + throw + } +} + +# Get parameters +$rolloutsName = $OctopusParameters['Argo.Rollout.Name'] +$rolloutsNamespace = $OctopusParameters['Argo.Rollout.Namespace'] + +# Add new arguments +$kubectlArguments = @(\"argo\", \"rollouts\", \"get\", \"rollout\", $rolloutsName, \"--namespace\", $rolloutsNamespace, \"--no-color\") + +kubectl $kubectlArguments" + }, + "Parameters": [ + { + "Id": "5eefb32e-04a6-40ed-9018-3ba12e241b01", + "Name": "Argo.Rollout.Name", + "Label": "Rollout Name", + "HelpText": "Name of the Argo Rollout to promote.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b13022d0-9a74-42cd-8b3e-d3cfa0c4d64c", + "Name": "Argo.Rollout.Namespace", + "Label": "Namespace", + "HelpText": "The namespace to execute the promotion of the rollout against.", + "DefaultValue": "default", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2024-06-12T20:01:43.441Z", + "OctopusVersion": "2024.2.9210", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "argo" +} diff --git a/step-templates/argo-rollouts-pause.json.human b/step-templates/argo-rollouts-pause.json.human new file mode 100644 index 000000000..9f7c9fc55 --- /dev/null +++ b/step-templates/argo-rollouts-pause.json.human @@ -0,0 +1,204 @@ +{ + "Id": "a757705d-2551-42bf-8cef-8dd99bd6b4f9", + "Name": "Argo - Rollouts Pause", + "Description": "Set the rollout paused state to 'true'", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Installs the Argo Rollouts plugin +function Install-Plugin +{ +# Define parameters +\tparam ($PluginUri, + $PluginFilename + ) + + # Check for plugin folder + if ((Test-Path -Path \"$PWD/plugins\") -eq $false) + { +\t\t# Create new plugins folder + New-Item -Path \"$PWD/plugins\" -ItemType \"Directory\" + + # Add to path + $env:PATH = \"$($PWD)/plugins$([IO.Path]::PathSeparator)\" + $env:PATH + } + +\t# Download plugin +\tInvoke-WebRequest -Uri \"$PluginUri\" -OutFile \"$PWD/plugins/$PluginFilename\" + +\t# Make file executable + if ($IsLinux) + { +\t\t# Make it executable + \tchmod +x ./plugins/$PluginFilename + } + + if ($IsWindows) + { + \t# Update filename to include .exe extension + Rename-Item -Path \"$PWD/plugins/$PluginFilename\" -NewName \"$PWD/plugins/$($PluginFilename).exe\" + } +} + +# When listing plugins, kubectl looks in all paths defined in $env:PATH and will fail if the path does not exist +function Verify-Path-Variable +{ +\t# Get current path and split into array + $paths = $env:PATH.Split([IO.Path]::PathSeparator) + $verifiedPaths = @() + + # Loop through paths + foreach ($path in $paths) + { + \t# Check for existence + if ((Test-Path -Path $path) -eq $true) + { + \t# Add to verified + $verifiedPaths += $path + } + } + + # Return verified paths + return ($verifiedPaths -join [IO.Path]::PathSeparator) +} + +function Get-Plugin-Installed +{ +\t# Define parameters + param ( + \t$PluginName, + $InstalledPlugins + ) + + \t$isInstalled = $false + +\tforeach ($plugin in $installedPlugins) + \t{ +\t\tif ($plugin -like \"$($PluginName)*\") + { + \t$isInstalled = $true + \tbreak + } +\t} + + return $isInstalled +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +# Fix ANSI Color on PWSH Core issues when displaying objects +if ($PSEdition -eq \"Core\") { + $PSStyle.OutputRendering = \"PlainText\" +} + +# Check to see if it's running on Windows +if ($IsWindows) { + # Disable the progress bar so downloading files via Invoke-WebRequest are faster + $ProgressPreference = 'SilentlyContinue' +} + +# Set TLS +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Verify all PATH variables are avaialable +$env:PATH = Verify-Path-Variable +if ($IsLinux) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64\" +} + +if ($IsWindows) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-windows-amd64\" +} + +try +{ + # Check to see if plugins are installed + $pluginList = (kubectl plugin list 2>&1) + + # This is the path that Linux will take + if ($lastExitCode -ne 0 -and $pluginList.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { + Install-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + # Parse list + \t$pluginList = $pluginList.Split(\"`n\", [System.StringSplitOptions]::RemoveEmptyEntries) + + if ((Get-Plugin-Installed -PluginName \"kubectl-argo-rollouts\" -InstalledPlugins $pluginList) -eq $false) + { + \tInstall-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \tWrite-Host \"Argo Rollout kubectl plugin found ...\" + } + } +} +catch +{ +\t# On Windows, the executable will cause an error if no plugins found so this the path Windows will take + if ($_.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { + Install-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \t# Something else happened, we need to surface the error + throw + } +} + +# Get parameters +$rolloutsName = $OctopusParameters['Argo.Rollout.Name'] +$rolloutsNamespace = $OctopusParameters['Argo.Rollout.Namespace'] + +# Create arguments array +$kubectlArguments = @(\"argo\", \"rollouts\", \"pause\", $rolloutsName, \"--namespace\", $rolloutsNamespace) + +# Pause rollout +kubectl $kubectlArguments" + }, + "Parameters": [ + { + "Id": "d7665ee0-08b1-4c3a-b7f9-bf9ccfa63219", + "Name": "Argo.Rollout.Name", + "Label": "Rollout Name", + "HelpText": "Name of the Argo Rollout to pause.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8a9ed999-5c3f-4652-abbb-07f592205bfc", + "Name": "Argo.Rollout.Namespace", + "Label": "Namespace", + "HelpText": "The namespace where the rollout exists.", + "DefaultValue": "default", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2024-06-06T19:54:47.156Z", + "OctopusVersion": "2024.1.12815", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "argo" +} diff --git a/step-templates/argo-rollouts-promote.json.human b/step-templates/argo-rollouts-promote.json.human new file mode 100644 index 000000000..0a7a3c418 --- /dev/null +++ b/step-templates/argo-rollouts-promote.json.human @@ -0,0 +1,221 @@ +{ + "Id": "cccd7fe9-b3b5-4f56-84bd-2986d5e68e06", + "Name": "Argo - Rollouts Promote", + "Description": "Promotes an Argo Rollout.", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Installs the Argo Rollouts plugin +function Install-Plugin +{ +# Define parameters +\tparam ($PluginUri, + $PluginFilename + ) + + # Check for plugin folder + if ((Test-Path -Path \"$PWD/plugins\") -eq $false) + { +\t\t# Create new plugins folder + New-Item -Path \"$PWD/plugins\" -ItemType \"Directory\" + + # Add to path + $env:PATH = \"$($PWD)/plugins$([IO.Path]::PathSeparator)\" + $env:PATH + } + +\t# Download plugin +\tInvoke-WebRequest -Uri \"$PluginUri\" -OutFile \"$PWD/plugins/$PluginFilename\" + +\t# Make file executable + if ($IsLinux) + { +\t\t# Make it executable + \tchmod +x ./plugins/$PluginFilename + } + + if ($IsWindows) + { + \t# Update filename to include .exe extension + Rename-Item -Path \"$PWD/plugins/$PluginFilename\" -NewName \"$PWD/plugins/$($PluginFilename).exe\" + } +} + +# When listing plugins, kubectl looks in all paths defined in $env:PATH and will fail if the path does not exist +function Verify-Path-Variable +{ +\t# Get current path and split into array + $paths = $env:PATH.Split([IO.Path]::PathSeparator) + $verifiedPaths = @() + + # Loop through paths + foreach ($path in $paths) + { + \t# Check for existence + if ((Test-Path -Path $path) -eq $true) + { + \t# Add to verified + $verifiedPaths += $path + } + } + + # Return verified paths + return ($verifiedPaths -join [IO.Path]::PathSeparator) +} + +function Get-Plugin-Installed +{ +\t# Define parameters + param ( + \t$PluginName, + $InstalledPlugins + ) + + \t$isInstalled = $false + +\tforeach ($plugin in $installedPlugins) + \t{ +\t\tif ($plugin -like \"$($PluginName)*\") + { + \t$isInstalled = $true + \tbreak + } +\t} + + return $isInstalled +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +# Fix ANSI Color on PWSH Core issues when displaying objects +if ($PSEdition -eq \"Core\") { + $PSStyle.OutputRendering = \"PlainText\" +} + +# Check to see if it's running on Windows +if ($IsWindows) { + # Disable the progress bar so downloading files via Invoke-WebRequest are faster + $ProgressPreference = 'SilentlyContinue' +} + +# Set TLS +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Verify all PATH variables are avaialable +$env:PATH = Verify-Path-Variable +if ($IsLinux) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64\" +} + +if ($IsWindows) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-windows-amd64\" +} + +try +{ + # Check to see if plugins are installed + $pluginList = (kubectl plugin list 2>&1) + + # This is the path that Linux will take + if ($lastExitCode -ne 0 -and $pluginList.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { + Install-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + # Parse list + \t$pluginList = $pluginList.Split(\"`n\", [System.StringSplitOptions]::RemoveEmptyEntries) + + if ((Get-Plugin-Installed -PluginName \"kubectl-argo-rollouts\" -InstalledPlugins $pluginList) -eq $false) + { + \tInstall-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \tWrite-Host \"Argo Rollout kubectl plugin found ...\" + } + } +} +catch +{ +\t# On Windows, the executable will cause an error if no plugins found so this the path Windows will take + if ($_.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { +\t\tInstall-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \t# Something else happened, we need to surface the error + throw + } +} + +# Get parameters +$rolloutsName = $OctopusParameters['Argo.Rollout.Name'] +$rolloutsNamespace = $OctopusParameters['Argo.Rollout.Namespace'] +$rolloutsFullPromotion = [System.Convert]::ToBoolean($OctopusParameters['Argo.Rollout.FullPromotion']) + +# Create arguments array +$kubectlArguments = @(\"argo\", \"rollouts\", \"promote\", $rolloutsName, \"--namespace\", $rolloutsNamespace) + +# Check for additional argument +if ($rolloutsFullPromotion) +{ +\t$kubectlArguments += \"--full\" +} + +# Promote rollout +kubectl $kubectlArguments" + }, + "Parameters": [ + { + "Id": "d1dbd00e-facb-494d-bf10-27ba68334f30", + "Name": "Argo.Rollout.Name", + "Label": "Rollout Name", + "HelpText": "Name of the Argo Rollout to promote.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c9c4937a-e833-4df0-8f31-b53722b881c3", + "Name": "Argo.Rollout.Namespace", + "Label": "Namespace", + "HelpText": "The namespace to execute the promotion of the rollout against.", + "DefaultValue": "default", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "efaf2401-e059-4724-b2b3-30ac8864e450", + "Name": "Argo.Rollout.FullPromotion", + "Label": "Full Promotion", + "HelpText": "Fully promote a rollout to desired version, skipping analysis, pauses, and steps", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2024-06-06T19:55:28.116Z", + "OctopusVersion": "2024.1.12815", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "argo" +} diff --git a/step-templates/argo-rollouts-set-image.json.human b/step-templates/argo-rollouts-set-image.json.human new file mode 100644 index 000000000..19dc33f2f --- /dev/null +++ b/step-templates/argo-rollouts-set-image.json.human @@ -0,0 +1,242 @@ +{ + "Id": "5449ea73-7f77-45d7-a4d7-e56286d679f5", + "Name": "Argo - Rollouts Set Image", + "Description": "Sets a new image tag on an Argo Rollout.", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "67a06773-8f2b-49a7-95bb-1e087a38b3b9", + "Name": "Argo.Rollout.Image", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "NotAcquired", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "Argo.Rollout.Image", + "Purpose": "" + } + } + ], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Install-Plugin +{ +# Define parameters +\tparam ($PluginUri, + $PluginFilename + ) + + # Check for plugin folder + if ((Test-Path -Path \"$PWD/plugins\") -eq $false) + { +\t\t# Create new plugins folder + New-Item -Path \"$PWD/plugins\" -ItemType \"Directory\" + + # Add to path + $env:PATH = \"$($PWD)/plugins$([IO.Path]::PathSeparator)\" + $env:PATH + } + +\t# Download plugin +\tInvoke-WebRequest -Uri \"$PluginUri\" -OutFile \"$PWD/plugins/$PluginFilename\" + +\t# Check for Linux + if ($IsLinux) + { +\t\t# Make it executable + \tchmod +x ./plugins/$PluginFilename + } + + if ($IsWindows) + { + \t# Update filename to include .exe extension + Rename-Item -Path \"$PWD/plugins/$PluginFilename\" -NewName \"$PWD/plugins/$($PluginFilename).exe\" + } +} + +# When listing plugins, kubectl looks in all paths defined in $env:PATH and will fail if the path does not exist +function Verify-Path-Variable +{ +\t# Get current path and split into array + $paths = $env:PATH.Split([IO.Path]::PathSeparator) + $verifiedPaths = @() + + # Loop through paths + foreach ($path in $paths) + { + \t# Check for existence + if ((Test-Path -Path $path) -eq $true) + { + \t# Add to verified + $verifiedPaths += $path + } + } + + # Return verified paths + return ($verifiedPaths -join [IO.Path]::PathSeparator) +} + +function Get-Plugin-Installed +{ +\t# Define parameters + param ( + \t$PluginName, + $InstalledPlugins + ) + + \t$isInstalled = $false + +\tforeach ($plugin in $installedPlugins) + \t{ +\t\tif ($plugin -like \"$($PluginName)*\") + { + \t$isInstalled = $true + \tbreak + } +\t} + + return $isInstalled +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +# Fix ANSI Color on PWSH Core issues when displaying objects +if ($PSEdition -eq \"Core\") { + $PSStyle.OutputRendering = \"PlainText\" +} + +# Check to see if it's running on Windows +if ($IsWindows) { + # Disable the progress bar so downloading files via Invoke-WebRequest are faster + $ProgressPreference = 'SilentlyContinue' +} + +# Set TLS +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Verify all PATH variables are avaialable +$env:PATH = Verify-Path-Variable +if ($IsLinux) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64\" +} + +if ($IsWindows) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-windows-amd64\" +} + + +try +{ + # Check to see if plugins are installed + $pluginList = (kubectl plugin list --name-only=true 2>&1) + + # This is the path that Linux will take + if ($lastExitCode -ne 0 -and $pluginList.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { + # Install the plugin + Install-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + # Parse list + \t$pluginList = $pluginList.Split(\"`n\", [System.StringSplitOptions]::RemoveEmptyEntries) + + if ((Get-Plugin-Installed -PluginName \"kubectl-argo-rollouts\" -InstalledPlugins $pluginList) -eq $false) + { + \tInstall-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \tWrite-Host \"Argo Rollout kubectl plugin found ...\" + } + } +} +catch +{ +\t# On Windows, the executable will cause an error if no plugins found so this the path Windows will take + if ($_.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { + Install-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \t# Something else happened, we need to surface the error + throw + } +} + +# Get variables +$rolloutName = $OctopusParameters['Argo.Rollout.Name'] +$rolloutNamespace = $OctopusParameters['Argo.Rollout.Namespace'] +$containerName = $OctopusParameters[\"Argo.Rollout.Container.Name\"] +$imageName = $OctopusParameters[\"Octopus.Action.Package[Argo.Rollout.Image].PackageId\"] +$imageTag = $OctopusParameters[\"Octopus.Action.Package[Argo.Rollout.Image].PackageVersion\"] + +# Create arguments array +$kubectlArguments = @(\"argo\", \"rollouts\", \"set\", \"image\", $rolloutName, \"$containerName=`\"$($imageName):$($imageTag)`\"\", \"--namespace\", $rolloutNamespace) + +# Set image +kubectl $kubectlArguments" + }, + "Parameters": [ + { + "Id": "892ac50a-e53c-47e5-997d-0ce1935b7303", + "Name": "Argo.Rollout.Name", + "Label": "Rollout Name", + "HelpText": "Name of the Argo Rollout to set the image on.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a08c104a-3877-4989-9436-bf582b3a8505", + "Name": "Argo.Rollout.Namespace", + "Label": "Namespace", + "HelpText": "The namespace to execute the command against.", + "DefaultValue": "default", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "05bf80a9-0419-4d8d-b9ca-457d66a2edb0", + "Name": "Argo.Rollout.Image", + "Label": "Image", + "HelpText": "Select the image to update the rollout to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "115ebf6d-5613-4131-83f5-03d74fbc0f14", + "Name": "Argo.Rollout.Container.Name", + "Label": "Container Name", + "HelpText": "Name of the container to update the image tag on.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2024-06-06T19:56:12.397Z", + "OctopusVersion": "2024.1.12815", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "argo" +} diff --git a/step-templates/argo-rollouts-status.json.human b/step-templates/argo-rollouts-status.json.human new file mode 100644 index 000000000..7dcc83d27 --- /dev/null +++ b/step-templates/argo-rollouts-status.json.human @@ -0,0 +1,286 @@ +{ + "Id": "866c3e69-5bd6-4439-8a2e-8cbf252885a4", + "Name": "Argo - Rollouts Status", + "Description": "Gets the status of an Argo Rollout. +Note: A timeout isn't considered an error, it will return the status at the time of the timeout.", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Supress info messages written to stderr +Write-Host \"##octopus[stderr-progress]\" + +function Save-OctopusVariable { + Param( + [string] $name, + [string] $value + ) + $StepName = $OctopusParameters[\"Octopus.Step.Name\"] + + Set-OctopusVariable -Name $name -Value $value + + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$name}\" +} + +# Installs the Argo Rollouts plugin +function Install-Plugin +{ +# Define parameters +\tparam ($PluginUri, + $PluginFilename + ) + + # Check for plugin folder + if ((Test-Path -Path \"$PWD/plugins\") -eq $false) + { +\t\t# Create new plugins folder + New-Item -Path \"$PWD/plugins\" -ItemType \"Directory\" + + # Add to path + $env:PATH = \"$($PWD)/plugins$([IO.Path]::PathSeparator)\" + $env:PATH + } + +\t# Download plugin +\tInvoke-WebRequest -Uri \"$PluginUri\" -OutFile \"$PWD/plugins/$PluginFilename\" + +\t# Make file executable + if ($IsLinux) + { +\t\t# Make it executable + \tchmod +x ./plugins/$PluginFilename + } + + if ($IsWindows) + { + \t# Update filename to include .exe extension + Rename-Item -Path \"$PWD/plugins/$PluginFilename\" -NewName \"$PWD/plugins/$($PluginFilename).exe\" + } +} + +# When listing plugins, kubectl looks in all paths defined in $env:PATH and will fail if the path does not exist +function Verify-Path-Variable +{ +\t# Get current path and split into array + $paths = $env:PATH.Split([IO.Path]::PathSeparator) + $verifiedPaths = @() + + # Loop through paths + foreach ($path in $paths) + { + \t# Check for existence + if ((Test-Path -Path $path) -eq $true) + { + \t# Add to verified + $verifiedPaths += $path + } + } + + # Return verified paths + return ($verifiedPaths -join [IO.Path]::PathSeparator) +} + +function Get-Plugin-Installed +{ +\t# Define parameters + param ( + \t$PluginName, + $InstalledPlugins + ) + + \t$isInstalled = $false + +\tforeach ($plugin in $installedPlugins) + \t{ +\t\tif ($plugin -like \"$($PluginName)*\") + { + \t$isInstalled = $true + \tbreak + } +\t} + + return $isInstalled +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +# Fix ANSI Color on PWSH Core issues when displaying objects +if ($PSEdition -eq \"Core\") { + $PSStyle.OutputRendering = \"PlainText\" +} + +# Check to see if it's running on Windows +if ($IsWindows) { + # Disable the progress bar so downloading files via Invoke-WebRequest are faster + $ProgressPreference = 'SilentlyContinue' +} + +# Set TLS +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Verify all PATH variables are avaialable +$env:PATH = Verify-Path-Variable +if ($IsLinux) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64\" +} + +if ($IsWindows) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-windows-amd64\" +} + +try +{ + # Check to see if plugins are installed + $pluginList = (kubectl plugin list 2>&1) + + # This is the path that Linux will take + if ($lastExitCode -ne 0 -and $pluginList.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { + Install-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + # Parse list + \t$pluginList = $pluginList.Split(\"`n\", [System.StringSplitOptions]::RemoveEmptyEntries) + + if ((Get-Plugin-Installed -PluginName \"kubectl-argo-rollouts\" -InstalledPlugins $pluginList) -eq $false) + { + \tInstall-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \tWrite-Host \"Argo Rollout kubectl plugin found ...\" + } + } +} +catch +{ +\t# On Windows, the executable will cause an error if no plugins found so this the path Windows will take + if ($_.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { +\t\tInstall-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \t# Something else happened, we need to surface the error + throw + } +} + +# Get parameters +$rolloutsName = $OctopusParameters['Argo.Rollout.Name'] +$rolloutsNamespace = $OctopusParameters['Argo.Rollout.Namespace'] +$rolloutsTimeout = $OctopusParameters['Argo.Rollout.Timeout'] + +# Validate arguments +if ([string]::IsNullOrWhitespace($rolloutsTimeout)) +{ + Write-Error \"Please specify a timeout value\" +} + +# Add new arguments +$kubectlArguments = @(\"argo\", \"rollouts\", \"status\", $rolloutsName, \"--namespace\", $rolloutsNamespace, \"--timeout\", $rolloutsTimeout) + +try +{ + # Get the current status - This was the only way to get it to work cross-platform + $process = Start-Process -FilePath \"kubectl\" -ArgumentList $kubectlArguments -NoNewWindow -RedirectStandardError \"error.txt\" -RedirectStandardOutput \"success.txt\" -Wait -PassThru + $lastExitCode = $process.ExitCode + $rolloutStatus = (Get-Content \"success.txt\") + + # Check the code from the command + if ($lastExitCode -ne 0) + { + # Get the error from the file + $errorMessage = Get-Content \"error.txt\" + + if ($errorMessage -is [array]) + { + $errorMessage = $errorMessage[0] + } + + # Expose the original error + throw $errorMessage + } +} +catch +{ + if ($_.Exception.Message -ne \"Error: Rollout status watch exceeded timeout\") + { + # Timeout is expected, everything else is not + Write-Host \"$(Get-Content \"error.txt\")\" + throw + } + else + { + # It was just a timeout, make last exit 0 + Write-Host \"Wait operation timed out, returning recorded status.\" + $lastExitCode = 0 + } +} + +Write-Host \"Status is $rolloutStatus\" + +if ($rolloutStatus.Contains(\"-\")) +{ + $messages = $rolloutStatus.Split(\"-\") + + Save-OctopusVariable -Name \"Status\" -Value $messages[0].Trim() + Save-OctopusVariable -Name \"StatusMessage\" -Value $messages[1].Trim() +} +else +{ + Save-OctopusVariable -Name \"Status\" -Value $rolloutStatus +}" + }, + "Parameters": [ + { + "Id": "5eefb32e-04a6-40ed-9018-3ba12e241b01", + "Name": "Argo.Rollout.Name", + "Label": "Rollout Name", + "HelpText": "Name of the Argo Rollout to promote.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b13022d0-9a74-42cd-8b3e-d3cfa0c4d64c", + "Name": "Argo.Rollout.Namespace", + "Label": "Namespace", + "HelpText": "The namespace to execute the promotion of the rollout against.", + "DefaultValue": "default", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "065f1110-abe9-4bdf-9a98-af20dcac4e8e", + "Name": "Argo.Rollout.Timeout", + "Label": "Timeout", + "HelpText": "The length of time to watch before giving up. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). Zero means wait forever.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2024-06-12T18:36:25.339Z", + "OctopusVersion": "2024.2.9210", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "argo" +} diff --git a/step-templates/argo-rollouts-undo.json.human b/step-templates/argo-rollouts-undo.json.human new file mode 100644 index 000000000..c445e8766 --- /dev/null +++ b/step-templates/argo-rollouts-undo.json.human @@ -0,0 +1,230 @@ +{ + "Id": "e75786e1-ef06-4ce7-a831-ccde04927c4c", + "Name": "Argo - Rollouts Undo", + "Description": "Rollback to the previous rollout.", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Supress info messages written to stderr +Write-Host \"##octopus[stderr-progress]\" + +# Installs the Argo Rollouts plugin +function Install-Plugin +{ +# Define parameters +\tparam ($PluginUri, + $PluginFilename + ) + + # Check for plugin folder + if ((Test-Path -Path \"$PWD/plugins\") -eq $false) + { +\t\t# Create new plugins folder + New-Item -Path \"$PWD/plugins\" -ItemType \"Directory\" + + # Add to path + $env:PATH = \"$($PWD)/plugins$([IO.Path]::PathSeparator)\" + $env:PATH + } + +\t# Download plugin +\tInvoke-WebRequest -Uri \"$PluginUri\" -OutFile \"$PWD/plugins/$PluginFilename\" + +\t# Make file executable + if ($IsLinux) + { +\t\t# Make it executable + \tchmod +x ./plugins/$PluginFilename + } + + if ($IsWindows) + { + \t# Update filename to include .exe extension + Rename-Item -Path \"$PWD/plugins/$PluginFilename\" -NewName \"$PWD/plugins/$($PluginFilename).exe\" + } +} + +# When listing plugins, kubectl looks in all paths defined in $env:PATH and will fail if the path does not exist +function Verify-Path-Variable +{ +\t# Get current path and split into array + $paths = $env:PATH.Split([IO.Path]::PathSeparator) + $verifiedPaths = @() + + # Loop through paths + foreach ($path in $paths) + { + \t# Check for existence + if ((Test-Path -Path $path) -eq $true) + { + \t# Add to verified + $verifiedPaths += $path + } + } + + # Return verified paths + return ($verifiedPaths -join [IO.Path]::PathSeparator) +} + +function Get-Plugin-Installed +{ +\t# Define parameters + param ( + \t$PluginName, + $InstalledPlugins + ) + + \t$isInstalled = $false + +\tforeach ($plugin in $installedPlugins) + \t{ +\t\tif ($plugin -like \"$($PluginName)*\") + { + \t$isInstalled = $true + \tbreak + } +\t} + + return $isInstalled +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +# Fix ANSI Color on PWSH Core issues when displaying objects +if ($PSEdition -eq \"Core\") { + $PSStyle.OutputRendering = \"PlainText\" +} + +# Check to see if it's running on Windows +if ($IsWindows) { + # Disable the progress bar so downloading files via Invoke-WebRequest are faster + $ProgressPreference = 'SilentlyContinue' +} + +# Set TLS +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Verify all PATH variables are avaialable +$env:PATH = Verify-Path-Variable +if ($IsLinux) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64\" +} + +if ($IsWindows) +{ +\t$pluginUri = \"https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-windows-amd64\" +} + +try +{ + # Check to see if plugins are installed + $pluginList = (kubectl plugin list 2>&1) + + # This is the path that Linux will take + if ($lastExitCode -ne 0 -and $pluginList.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { + Install-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + # Parse list + \t$pluginList = $pluginList.Split(\"`n\", [System.StringSplitOptions]::RemoveEmptyEntries) + + if ((Get-Plugin-Installed -PluginName \"kubectl-argo-rollouts\" -InstalledPlugins $pluginList) -eq $false) + { + \tInstall-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \tWrite-Host \"Argo Rollout kubectl plugin found ...\" + } + } +} +catch +{ +\t# On Windows, the executable will cause an error if no plugins found so this the path Windows will take + if ($_.Exception.Message -eq \"error: unable to find any kubectl plugins in your PATH\") + { + Install-Plugin -PluginUri $pluginUri -PluginFilename \"kubectl-argo-rollouts\" + } + else + { + \t# Something else happened, we need to surface the error + throw + } +} + +# Get parameters +$rolloutsName = $OctopusParameters['Argo.Rollout.Name'] +$rolloutsNamespace = $OctopusParameters['Argo.Rollout.Namespace'] +$rolloutRevision = $OctopusParameters['Argo.Rollout.Revision'] + +# Create arguments array +$kubectlArguments = @(\"argo\", \"rollouts\", \"undo\", $rolloutsName, \"--namespace\", $rolloutsNamespace) + +# Check for revision +if (![string]::IsNullOrWhitespace($rolloutRevision)) +{ +\t# Add argument + $kubectlArguments += @(\"--to-revision=$rolloutRevision\") +} + +# Pause rollout +kubectl $kubectlArguments + +if ($lastExitCode -ne 0) +{ +\tWrite-Error \"Rollout command failed!\" +}" + }, + "Parameters": [ + { + "Id": "f2c7fa50-2faf-4d16-affd-13c00a72afef", + "Name": "Argo.Rollout.Name", + "Label": "Rollout Name", + "HelpText": "Name of the Argo Rollout to Undo", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0910eda0-93f4-4f31-85bd-fafd2d39d3ac", + "Name": "Argo.Rollout.Namespace", + "Label": "Namespace", + "HelpText": "The namespace where the rollout exists.", + "DefaultValue": "default", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b7fb60af-72fc-4a94-b242-40397941aa39", + "Name": "Argo.Rollout.Revision", + "Label": "Revision", + "HelpText": "Revision number to revert to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2024-06-06T20:14:10.831Z", + "OctopusVersion": "2024.1.12815", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "argo" +} diff --git a/step-templates/aspnet-state-database.json.human b/step-templates/aspnet-state-database.json.human new file mode 100644 index 000000000..17e8031ba --- /dev/null +++ b/step-templates/aspnet-state-database.json.human @@ -0,0 +1,79 @@ +{ + "Id": "c81e85e0-0b72-478e-a776-1c658fe696a1", + "Name": "ASPState Database", + "Description": "This uses the .Net framework aspnet_regsql tool to create an ASPState database using the credentials provided. If the username and password are both empty then it will attempt a trusted connection.", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$command = \"$env:windir\\Microsoft.NET\\$FrameworkDirectory\\aspnet_regsql.exe\"\r +$params = @('-ssadd', '-sstype', 'p', '-S', $SqlServer)\r +try\r +{ \r + if ($SqlUsername -ne $null -and $SqlPassword -ne $null)\r + {\r + $params += @('-U', $SqlUsername, '-P', $SqlPassword)\r + } else {\r + $params += @('-E')\r + }\r + \r + & $command @params\r +}\r +catch\r +{ \r + $error[0] | format-list -force\r + Exit 1\r +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "FrameworkDirectory", + "Label": "Framework Directory", + "HelpText": "Used to switch between different bitness and versions of the .Net framework", + "DefaultValue": "Framework64\\v4.0.30319", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Framework\\v2.0.50727| .NET Framework Ver 2.0/3.0/3.5 (32-bit) +Framework64\\v2.0.50727| .NET Framework ver 2.0/3.0/3.5 (64-bit) +Framework\\v4.0.30319| .NET Framework version 4 (32-bit) +Framework64\\v4.0.30319| .NET Framework version 4 (64-bit)" + } + }, + { + "Name": "SqlServer", + "Label": "Sql Server", + "HelpText": "SQL Server Instance with Port", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlUsername", + "Label": "Sql Username (optional)", + "HelpText": "The SQL Account which has access to create a SQL Database", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlPassword", + "Label": "Sql Password (optional)", + "HelpText": "The password for the SQL Account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-06-03T16:12:08.806+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "aspnet" +} diff --git a/step-templates/aspnetcore-set-environment-variable-iis.json.human b/step-templates/aspnetcore-set-environment-variable-iis.json.human new file mode 100644 index 000000000..2adf3bb0d --- /dev/null +++ b/step-templates/aspnetcore-set-environment-variable-iis.json.human @@ -0,0 +1,101 @@ +{ + "Id": "3586C580-7FC8-4FAB-A783-3641D91F7746", + "Author": "waxtell", + "Name": "ASP.NET Core Set Environment Variables Via IIS Config", + "Description": "Set environment variables in IIS config (no web.config)", + "ActionType": "Octopus.Script", + "Version": 2, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "function AddOrReplaceEnvironmentVariable { + param + ( + [string] $variableName, + [string] $variableValue, + [string] $siteName, + [string] $appCmd + ) + + Try { + [xml] $xmlConfig = (&$appCmd list config $sev_siteName -section:system.webServer/aspNetCore) + } + Catch { + Write-Host $sev_siteName 'either does not exist or is not an AspNetCore site!' + exit -1 + } + + if($xmlConfig.selectNodes(\"//environmentVariable[@name='$variableName']\")) { + &$appCmd set config $sev_siteName -section:system.webServer/aspNetCore /-\"environmentVariables.[name='$variableName',value='$variableValue']\" /commit:apphost + } + + &$appCmd set config $sev_siteName -section:system.webServer/aspNetCore /+\"environmentVariables.[name='$variableName',value='$variableValue']\" /commit:apphost +} + +[string] $sev_siteName=$OctopusParameters['sev_siteName'] +[string] $sev_envVariables=$OctopusParameters['sev_envVariables'] +[string] $sev_appCmdPath=$OctopusParameters['sev_appCmdPath'] + +Write-Host \"---------------------------\" +Write-Host $sev_envVariables +Write-Host $sev_appCmdPath +Write-Host \"---------------------------\" + +$appCmd = Join-Path $sev_appCmdPath 'appcmd.exe' + +foreach($line in $sev_envVariables -split '\\r?\ +') { + $indexOfEquals = $line.IndexOf('=') + if ($indexOfEquals -eq -1) { + Write-Host \"Invalid environment variable format: $line\" + continue + } + $key = $line.Substring(0, $indexOfEquals) + $value = $line.Substring($indexOfEquals + 1) + + AddOrReplaceEnvironmentVariable $key $value $sev_siteName $appCmd +} +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "3FE22421-50AB-4CBB-B86A-666EC1609115", + "Name": "sev_siteName", + "Label": "Site Name", + "HelpText": "Name of your ASP.NET Core site", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "78BF70F2-72F5-42E6-A992-C9504A964815", + "Name": "sev_envVariables", + "Label": "Environment Variables", + "HelpText": "Newline separated list of environment variables (varname=varvalue)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "3F851D0C-C93E-403F-861E-5BDA361C1E92", + "Name": "sev_appCmdPath", + "Label": "Path to appcmd.exe", + "HelpText": "Path to the appcmd.exe executable", + "DefaultValue": "c:\\windows\\system32\\inetsrv", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "geeknz", + "$Meta": { + "ExportedAt": "2024-07-15T22:40:29.070Z", + "OctopusVersion": "2024.2.9220", + "Type": "ActionTemplate" + }, + "Category": "dotnetcore" +} diff --git a/step-templates/aspnetcore-set-environment-variable.json.human b/step-templates/aspnetcore-set-environment-variable.json.human new file mode 100644 index 000000000..a41772b0a --- /dev/null +++ b/step-templates/aspnetcore-set-environment-variable.json.human @@ -0,0 +1,138 @@ +{ + "Id": "c7f96ab8-a0d3-4f01-928e-c8cb78ab108c", + "Name": "ASP.NET Core - set runtime environment variable via web.config", + "Description": "ASP.NET Core allows you specify environment variables in web.config - https://docs.microsoft.com/en-us/aspnet/core/hosting/aspnet-core-module#set-environment-variables", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "Param(\r + [string]$anc_WebConfigPath,\r + [string]$anc_EnvironmentVariableName,\r + [string]$anc_EnvironmentVariableValue\r +)\r +\r +$ErrorActionPreference = \"Stop\"\r +\r +function Get-Parameter($Name, [switch]$Required, [switch]$TestPath) {\r +\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + $variable = Get-Variable $Name\r + if ($variable -ne $null) {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null -or $result -eq \"\") {\r + if ($Required) {\r + throw \"Missing parameter value $Name\"\r + }\r + }\r +\r + if ($TestPath) {\r + if (!(Test-Path $result -PathType Leaf)) {\r + throw \"Could not find $result\"\r + }\r + }\r +\r + return $result\r +}\r +\r +& {\r + Param(\r + [string]$anc_WebConfigPath,\r + [string]$anc_EnvironmentVariableName,\r + [string]$anc_EnvironmentVariableValue\r + )\r +\r + $xml = (Get-Content $anc_WebConfigPath) -as [Xml]\r + $aspNetCore = $xml.configuration.location.'system.webServer'.aspNetCore\r + $environmentVariables = $aspNetCore.environmentVariables\r +\r + if (!$environmentVariables) {\r + $environmentVariables = $xml.CreateElement(\"environmentVariables\");\r + $aspNetCore.AppendChild($environmentVariables)\r + }\r +\r + $environmentVariable = $environmentVariables.environmentVariable | Where-Object {$_.name -eq $anc_EnvironmentVariableName}\r +\r + if ($environmentVariable) {\r + $environmentVariable.value = $anc_EnvironmentVariableValue\r + }\r + elseif ($environmentVariables) {\r + $environmentVariable = $xml.CreateElement(\"environmentVariable\");\r + $environmentVariable.SetAttribute(\"name\", $anc_EnvironmentVariableName);\r + $environmentVariable.SetAttribute(\"value\", $anc_EnvironmentVariableValue);\r + $x = $environmentVariables.AppendChild($environmentVariable)\r + }\r + else {\r + throw \"Could not find 'configuration/system.webServer/aspNetCore/environmentVariables' element in web.config\"\r + }\r +\r + try {\r + $xml.Save((Resolve-Path $anc_WebConfigPath))\r + }\r + catch {\r + throw \"Could not save web.config because: $_.Exception.Message\"\r + }\r +} `\r +(Get-Parameter 'anc_WebConfigPath' -Required -TestPath) `\r +(Get-Parameter 'anc_EnvironmentVariableName' -Required) `\r +(Get-Parameter 'anc_EnvironmentVariableValue' -Required)\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "b1b74503-0f3d-4519-8ec6-d6a4aace9086", + "Name": "anc_WebConfigPath", + "Label": "Web.Config Path", + "HelpText": "Typically an output variable from a previous step", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "5fe18f04-de84-445e-bb69-81ed0d39980f", + "Name": "anc_EnvironmentVariableName", + "Label": "Environment Variable Name", + "HelpText": "Name of environment variable to set", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c9099a01-7de3-4724-8d6d-e26d8bf0fd21", + "Name": "anc_EnvironmentVariableValue", + "Label": "Environment Variable Value", + "HelpText": "Value of environment variable to set", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "baynenator", + "$Meta": { + "ExportedAt": "2017-08-31T06:20:39.415Z", + "OctopusVersion": "3.13.8", + "Type": "ActionTemplate" + }, + "Category": "dotnetcore" +} diff --git a/step-templates/automate-manual-intervention-response.json.human b/step-templates/automate-manual-intervention-response.json.human new file mode 100644 index 000000000..28e192173 --- /dev/null +++ b/step-templates/automate-manual-intervention-response.json.human @@ -0,0 +1,209 @@ +{ + "Id": "54f95528-aa1e-4c97-8c16-b2e0d737c43e", + "Name": "Automate Manual Intervention Response", + "Description": "This template will search for deployments that have been paused due to Manual Intervention or Guided Failure and automate the response.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Get-OctopusItems +{ + # Define parameters + param( + $OctopusUri, + $ApiKey, + $SkipCount = 0 + ) + + # Define working variables + $items = @() + $skipQueryString = \"\" + $headers = @{\"X-Octopus-ApiKey\"=\"$ApiKey\"} + + # Check to see if there there is already a querystring + if ($octopusUri.Contains(\"?\")) + { + $skipQueryString = \"&skip=\" + } + else + { + $skipQueryString = \"?skip=\" + } + + $skipQueryString += $SkipCount + + # Get intial set + $resultSet = Invoke-RestMethod -Uri \"$($OctopusUri)$skipQueryString\" -Method GET -Headers $headers + + # Check to see if it returned an item collection + if ($resultSet.Items) + { + # Store call results + $items += $resultSet.Items + + # Check to see if resultset is bigger than page amount + if (($resultSet.Items.Count -gt 0) -and ($resultSet.Items.Count -eq $resultSet.ItemsPerPage)) + { + # Increment skip count + $SkipCount += $resultSet.ItemsPerPage + + # Recurse + $items += Get-OctopusItems -OctopusUri $OctopusUri -ApiKey $ApiKey -SkipCount $SkipCount + } + } + else + { + return $resultSet + } + + + # Return results + return ,$items +} + +$automaticResponseOctopusUrl = $OctopusParameters['AutomateResponse.Octopus.Url'] +$automaticResponseApiKey = $OctopusParameters['AutomateResponse.Api.Key'] +$automaticResponseReasonNotes = $OctopusParameters['AutomateResponse.Reason.Notes'] +$automaticResponseManualInterventionResponseType = $OctopusParameters['AutomateResponse.ManualIntervention'] +$automaticResponseGuidedFailureResponseType = $OctopusParameters['AutomateResponse.GuidedFailure'] +$header = @{ \"X-Octopus-ApiKey\" = $automaticResponseApiKey } + +# Validate response type input +if (![string]::IsNullOrWhitespace($automaticResponseManualInterventionResponseType) -and ![string]::IsNullOrWhitespace($automaticResponseGuidedFailureResponseType)) +{ +\t# Fail step + Write-Error \"Cannot have both a Manual Intervention and Guided Failure selections.\" +} + +if ([string]::IsNullOrWhitespace($automaticResponseManualInterventionResponseType) -and [string]::IsNullOrWhitespace($automaticResponseGuidedFailureResponseType)) +{ +\t# Fail step + Write-Error \"Please select either a Manual Intervention or Guidded Failure response type.\" +} + +# Get space +$spaceId = $OctopusParameters['Octopus.Space.Id'] + +# Get project +$projectId = $OctopusParameters['Octopus.Project.Id'] + +# Get the environemtn +$environmentId = $OctopusParameters['Octopus.Environment.Id'] + +# Get currently executing deployments for project +Write-Host \"Searching for executing deployments ...\" +$executingDeployments = Get-OctopusItems -OctopusUri \"$automaticResponseOctopusUrl/api/$($spaceId)/deployments?projects=$($projectId)&taskState=Executing&environments=$($environmentId)\" -ApiKey $automaticResponseApiKey + +# Check to see if anything was returned for the environment +if ($executingDeployments -is [array]) +{ + # Loop through executing deployments + foreach ($deployment in $executingDeployments) + { + # Get object for manual intervention + Write-Host \"Checking $($deployment.Id) for manual interventions ...\" + $manualIntervention = Get-OctopusItems -OctopusUri \"$automaticResponseOctopusUrl/api/$($spaceId)/interruptions?regarding=$($deployment.Id)&pendingOnly=true\" -ApiKey $automaticResponseApiKey + + # Check to see if a manual intervention was returned + if ($null -ne $manualIntervention.Id) + { + # Take responsibility + Write-Host \"Auto taking resonsibility for manual intervention ...\" + Invoke-RestMethod -Method Put -Uri \"$automaticResponseOctopusUrl/api/$($spaceId)/interruptions/$($manualIntervention.Id)/responsible\" -Headers $header + + # Create response object + $jsonBody = @{ + Notes = $automaticResponseReasonNotes + } + + # Check to see if manual intervention is empty + if (![string]::IsNullOrWhiteSpace($automaticResponseManualInterventionResponseType)) + { + # Add the manual intervention type + Write-Host \"Submitting $automaticResponseManualInterventionResponseType as response ...\" + $jsonBody.Add(\"Result\", $automaticResponseManualInterventionResponseType) + } + + # Check to see if the guided failure is empty + if (![string]::IsNullOrWhiteSpace($automaticResponseGuidedFailureResponseType)) + { + # Add the guided failure response + Write-Host \"Submitting $automaticResponseGuidedFailureResponseType as response ...\" + $jsonBody.Add(\"Guidance\", $automaticResponseGuidedFailureResponseType) + } + + # Post to server + Invoke-RestMethod -Method Post -Uri \"$automaticResponseOctopusUrl/api/$($spaceId)/interruptions/$($manualIntervention.Id)/submit\" -Body ($jsonBody | ConvertTo-Json -Depth 10) -Headers $header + } + } +}" + }, + "Parameters": [ + { + "Id": "f5961f82-66a5-4219-a15c-f4a9feb68904", + "Name": "AutomateResponse.Octopus.Url", + "Label": "Octopus Server Url", + "HelpText": "The Url to your Octopus Server instance.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3752c78e-4df8-4261-bdff-906bed063b12", + "Name": "AutomateResponse.Api.Key", + "Label": "API Key", + "HelpText": "API Key of the account you want to use for the automatic response.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "be44e98e-13a8-4f4e-9b51-99920dfcf763", + "Name": "AutomateResponse.Reason.Notes", + "Label": "Reason Notes", + "HelpText": "This is the `Notes` input for manual intervention.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1bfbcc18-7ed9-4fce-88d1-0b4ab76d2623", + "Name": "AutomateResponse.ManualIntervention", + "Label": "Manual Intervention Response Type", + "HelpText": "Select the response type for the Manual Intervention. Click Default Value to reset to none", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Abort|Abort +Proceed|Proceed" + } + }, + { + "Id": "30405062-6bb6-499f-85ef-78bd378684ba", + "Name": "AutomateResponse.GuidedFailure", + "Label": "Guided Failure Response Type", + "HelpText": "Select the automated response for a Guided Failure. Click Default Value to reset to none.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Fail|Fail +Exclude|Exclude Machine From Deployment +Ignore|Ignore +Retry|Retry" + } + } + ], + "$Meta": { + "ExportedAt": "2021-10-01T17:52:01.610Z", + "OctopusVersion": "2021.2.7580", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "octopus" + } diff --git a/step-templates/aws-add-imagetag-to-all-used-ecr-packages.json.human b/step-templates/aws-add-imagetag-to-all-used-ecr-packages.json.human new file mode 100644 index 000000000..5c7118ce9 --- /dev/null +++ b/step-templates/aws-add-imagetag-to-all-used-ecr-packages.json.human @@ -0,0 +1,73 @@ +{ + "Id": "9894aeda-bf06-4be2-8789-b570c9050d34", + "Name": "Tag all used ECR images", + "Description": "This will apply a tag to all AWS Elastic Container Registry images/packages from the ECR feed that are used in the deployment. That way the lifecycle policies in ECR can be configured to not delete images that are in-use by deployments in various environments.", + "ActionType": "Octopus.AwsRunScript", + "Version": 12, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.AwsAccount.Variable": "#{AwsAccount}", + "Octopus.Action.Aws.Region": "#{AwsRegion}", + "Octopus.Action.Script.ScriptBody": "$DeployTag = $AwsDeployPrefix + $OctopusParameters[\"Octopus.Environment.Name\"] + +#{each action in Octopus.Action} + #{each package in action.Package} + Write-Output \"Package #{package.PackageId} at version #{package.PackageVersion}\" + + $Image = Get-ECRImageBatch -ImageId @{ imageTag=\"#{package.PackageVersion}\" } -RepositoryName \"#{package.PackageId}\" + $ImageDeploy = Get-ECRImageBatch -ImageId @{ imageTag=$DeployTag } -RepositoryName \"#{package.PackageId}\" + + if($Image.Images[0].ImageId.ImageDigest -ne $ImageDeploy.Images[0].ImageId.ImageDigest) { + Write-Output \"Setting tag $DeployTag on image $($Image.Images[0].ImageId.ImageDigest)\" + $Manifest = $Image.Images[0].ImageManifest + Write-ECRImage -RepositoryName \"#{package.PackageId}\" -ImageManifest $Manifest -ImageTag $DeployTag + } +\t#{/each} +#{/each} +" + }, + "Parameters": [ + { + "Id": "3bed9fa6-9d8e-452e-957a-25af1bb6fa58", + "Name": "AwsAccount", + "Label": "Account", + "HelpText": "AWS Account to connect using", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "2c64b94b-deb7-4e4b-b977-ec24f3b86951", + "Name": "AwsRegion", + "Label": "Region", + "HelpText": "AWS Region that is used", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fa47f4c7-b67a-4ee7-8c8f-a890acae795a", + "Name": "AwsDeployPrefix", + "Label": "Deployment Prefix", + "HelpText": "Prefix for the image tags. The Octopus environment is then appended to this.", + "DefaultValue": "deploy-", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "hakanl", + "$Meta": { + "ExportedAt": "2019-02-06T15:41:19.708Z", + "OctopusVersion": "2018.11.2", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-add-remove-elbv2.json.human b/step-templates/aws-add-remove-elbv2.json.human new file mode 100644 index 000000000..444fa537e --- /dev/null +++ b/step-templates/aws-add-remove-elbv2.json.human @@ -0,0 +1,236 @@ +{ + "Id": "2abc4f4f-06f4-4af6-8b10-52651ab4d3d5", + "Name": "AWS - Add or Remove instance from ELBv2", + "Description": "Add or Remove the current instance from an ELBv2 Target Group.", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$accessKey = $OctopusParameters['accessKey'] +$secretKey = $OctopusParameters['secretKey'] +$region = $OctopusParameters['region'] + +$targetGroupArn = $OctopusParameters['targetGroupArn'] + +$action = $OctopusParameters['action'] + +$checkInterval = $OctopusParameters['checkInterval'] +$maxChecks = $OctopusParameters['maxChecks'] + +$awsProfile = (get-date -Format '%y%d%M-%H%m').ToString() # random + +if (Get-Module | Where-Object { $_.Name -like \"AWSPowerShell*\" }) { +\tWrite-Host \"AWS PowerShell module is already loaded.\" +} else { +\t$awsModule = Get-Module -ListAvailable | Where-Object { $_.Name -like \"AWSPowerShell*\" } +\tif (!($awsModule)) { + \tWrite-Error \"AWSPowerShell / AWSPowerShell.NetCore not found\" + return + } else { + \tImport-Module $awsModule.Name + Write-Host \"Imported Module: $($awsModule.Name)\" + } +} + +function GetCurrentInstanceId +{ + Write-Host \"Getting instance id\" + +\t$response = Invoke-RestMethod -Uri \"http://169.254.169.254/latest/meta-data/instance-id\" -Method Get + +\tif ($response) +\t{ +\t\t$instanceId = $response +\t} +\telse +\t{ +\t\tWrite-Error -Message \"Returned Instance ID does not appear to be valid\" +\t\tExit 1 +\t} + +\t$response +} + +function GetTarget +{ + $instanceId = GetCurrentInstanceId + + $target = New-Object -TypeName Amazon.ElasticLoadBalancingV2.Model.TargetDescription + $target.Id = $instanceId + + Write-Host \"Current instance id: $instanceId\" + + return $target +} + +function GetInstanceState +{ +\t$state = (Get-ELB2TargetHealth -TargetGroupArn $targetGroupArn -Target $target -AccessKey $accessKey -SecretKey $secretKey -Region $region).TargetHealth.State + +\tWrite-Host \"Current instance state: $state\" + +\treturn $state +} + +function WaitForState +{ + param([string]$expectedState) + + $instanceState = GetInstanceState -arn $targetGroupArn -target $target + + if ($instanceState -eq $expectedState) + { + return + } + + $checkCount = 0 + + Write-Host \"Waiting for instance state to be $expectedState\" + Write-Host \"Maximum Checks: $maxChecks\" + Write-Host \"Check Interval: $checkInterval\" + + while ($instanceState -ne $expectedState -and $checkCount -le $maxChecks) + {\t +\t $checkCount += 1 +\t +\t Write-Host \"Waiting for $checkInterval seconds for instance state to be $expectedState\" +\t Start-Sleep -Seconds $checkInterval +\t +\t if ($checkCount -le $maxChecks) +\t { +\t\t Write-Host \"$checkCount/$maxChecks Attempts\" +\t } +\t +\t $instanceState = GetInstanceState + } + + if ($instanceState -ne $expectedState) + { +\t Write-Error -Message \"Instance state is not $expectedState, giving up.\" +\t Exit 1 + } +} + +function DeregisterInstance +{ + Write-Host \"Deregistering instance from $targetGroupArn\" + Unregister-ELB2Target -TargetGroupArn $targetGroupArn -Target $target -AccessKey $accessKey -SecretKey $secretKey -Region $region + WaitForState -expectedState \"unused\" + Write-Host \"Instance deregistered\" +} + +function RegisterInstance +{ + Write-Host \"Registering instance with $targetGroupArn\" + Try { + \tRegister-ELB2Target -TargetGroupArn $targetGroupArn -Target $target -AccessKey $accessKey -SecretKey $secretKey -Region $region + } Catch { + \tWrite-Host $Error[0] + } + WaitForState -expectedState \"healthy\" + Write-Host \"Instance registered\" +} + +$target = GetTarget + +switch ($action) +{ + \"deregister\" { DeregisterInstance } + \"register\" { RegisterInstance } +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "81fc5ef2-2df8-4f10-a041-7277792e270a", + "Name": "accessKey", + "Label": "IAM Access Key ID", + "HelpText": "The IAM Access Key ID to use when authenticating with AWS.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c0f7d960-ffce-4046-9996-f7ef39a82306", + "Name": "secretKey", + "Label": "IAM Secret Key", + "HelpText": "The IAM secret access key used to authenticate with AWS.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "ed692709-1332-4b18-9ec8-4bc02609bdee", + "Name": "region", + "Label": "AWS Region", + "HelpText": "The region in which the ELBv2 and Target Group live.", + "DefaultValue": "eu-west-1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "13d12f79-447f-4e2a-b4fb-460117b9301d", + "Name": "targetGroupArn", + "Label": "Target Group ARN", + "HelpText": "The ARN of the target group.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "8c6fa0f4-6ebd-4021-aba9-39c7a5394ecd", + "Name": "action", + "Label": "Action", + "HelpText": "Choose if you want to add or remove the current instance from the selected target group.", + "DefaultValue": "deregister", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "deregister|Remove +register|Add" + }, + "Links": {} + }, + { + "Id": "15e771c5-7fb5-45f3-ad9e-180d90aae785", + "Name": "checkInterval", + "Label": "State Check Interval", + "HelpText": "The number of seconds to wait before checking if the instances has been successfully added or removed from the target group.", + "DefaultValue": "10", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "68c5d2d8-e991-4f64-87df-cd548fc16ffe", + "Name": "maxChecks", + "Label": "Maximum State Checks", + "HelpText": "The maximum number of times to check if the instance has been successfully added or removed from the target group.", + "DefaultValue": "6", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2022-05-16T07:30:05.303Z", + "LastModifiedBy": "phillip-haydon", + "$Meta": { + "ExportedAt": "2022-05-16T07:30:05.303Z", + "OctopusVersion": "2022.1.2584", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-add-tags-to-ec2-instance.json.human b/step-templates/aws-add-tags-to-ec2-instance.json.human new file mode 100644 index 000000000..b09e97cf9 --- /dev/null +++ b/step-templates/aws-add-tags-to-ec2-instance.json.human @@ -0,0 +1,198 @@ +{ + "Id": "82500bb2-f442-44f3-a5a5-7ecb4165ee1f", + "Name": "AWS - Add Tags to EC2 Instance", + "Description": "This step will Add or Remove Tags from an EC2 instance. + +Works well with the \"_AWS - Launch EC2 Instance_\" Community Step Template. + +[AWS Tools for Windows PowerShell](http://aws.amazon.com/powershell/) must be installed on the Server/Target you plan on running this step template on.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +param( + [string]$odInstanceId, + [string]$odAction, + [string]$odTags, + [string]$odAccessKey, + [string]$odSecretKey, + [switch]$whatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if (!$result -or $result -eq $null) { + if ($Default) { + $result = $Default + } elseif ($Required) { + throw \"Missing parameter value $Name\" + } + } + + return $result +} + + +& { + param( + [string]$odInstanceId, + [string]$odAction, + [string]$odTags, + [string]$odAccessKey, + [string]$odSecretKey + ) + + # If AWS key's are not provided as params, attempt to retrieve them from Environment Variables + if ($odAccessKey -or $odSecretKey) { + Set-AWSCredentials -AccessKey $odAccessKey -SecretKey $odSecretKey -StoreAs default + } elseif (([Environment]::GetEnvironmentVariable(\"AWS_ACCESS_KEY\", \"Machine\")) -or ([Environment]::GetEnvironmentVariable(\"AWS_SECRET_KEY\", \"Machine\"))) { + Set-AWSCredentials -AccessKey ([Environment]::GetEnvironmentVariable(\"AWS_ACCESS_KEY\", \"Machine\")) -SecretKey ([Environment]::GetEnvironmentVariable(\"AWS_SECRET_KEY\", \"Machine\")) -StoreAs default + } else { + throw \"AWS API credentials were not available/provided.\" + } + + + + Write-Output (\"------------------------------\") + Write-Output (\"Add/Remove Instance Tags:\") + Write-Output (\"------------------------------\") + + $filterArray = @() + $tagsHash = (ConvertFrom-StringData $odTags).GetEnumerator() + Foreach ($tag in $tagsHash) { + $tagObj = $(Get-EC2Instance -InstanceId $odInstanceId).Instances.Tags | ? {$_.Key -eq $tag.Key -and $_.Value -eq $tag.Value} + $tagObjCount = ($tagObj | measure).Count + if ($tagObjCount -gt 0) { + if ($odAction -eq \"New\") { + Write-Output (\"Cannot Add: The tag '$($tag.Key)=$($tag.Value)' already exists, skipping...\") + } elseif ($odAction -eq \"Remove\") { + Write-Output (\"The tag '$($tag.Key)=$($tag.Value)' exists, deleting...\") + + try { + Remove-EC2Tag -Tags @{key=$tag.Key} -resourceId $odInstanceId -Force + } + catch [Amazon.EC2.AmazonEC2Exception] { + throw $_.Exception.errorcode + '-' + $_.Exception.Message + } + } + } else { + if ($odAction -eq \"New\") { + Write-Output (\"The combination of tag and value '$($tag.Key)=$($tag.Value)' does not exist, Creating/Updating tag...\") + + try { + New-EC2Tag -Tags @{key=$tag.Key;value=$tag.Value} -resourceId $odInstanceId + } + catch [Amazon.EC2.AmazonEC2Exception] { + throw $_.Exception.errorcode + '-' + $_.Exception.Message + } + } elseif ($odAction -eq \"Remove\") { + Write-Output (\"Cannot Remove: The tag '$($tag.Key)=$($tag.Value)' does not exist, skipping...\") + } + } + } + } ` + (Get-Param 'odInstanceId' -Required) ` + (Get-Param 'odAction' -Required) ` + (Get-Param 'odTags' -Required) ` + (Get-Param 'odAccessKey') ` + (Get-Param 'odSecretKey')", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "3670adeb-3e4f-4c55-829b-b39bb709d841", + "Name": "odInstanceId", + "Label": "Instance ID", + "HelpText": "The EC2 Instance ID of the Instance you would like to add Tags to.", + "DefaultValue": "i-xxxxxxxxxxxxxxxxx", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "918acd83-39ae-47d1-9682-12333e723cff", + "Name": "odAction", + "Label": "Action", + "HelpText": "The action you would like to perform - Add or Remove Tags from an EC2 Instance.", + "DefaultValue": "New", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "New|Add tag to an instance +Remove|Remove tag from an instance" + }, + "Links": {} + }, + { + "Id": "9b4d592a-712d-459e-af73-1db757a769d8", + "Name": "odTags", + "Label": "Tags", + "HelpText": "The Tags you would like to Add or Remove from an EC2 Instance, for example: + +- Name=MyProject +- Environment=Prod +- Version=1.3.3.7", + "DefaultValue": "Name=#{Octopus.Project.Name} +Environment=#{Octopus.Environment.Name} +Version=#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "ccd17285-bbb4-4dcb-84a8-330f45199afd", + "Name": "odAccessKey", + "Label": "Access Key (Kind-of Optional)", + "HelpText": "An Access Key with permissions to create the desired EC2 instance. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"AWS\\_ACCESS\\_KEY\". + +Further Reading: +[https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html](https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "6f058614-92a0-4696-bb56-b6342a8d5466", + "Name": "odSecretKey", + "Label": "Secret Key (Kind-of Optional)", + "HelpText": "The Secret Key associated with the above Access Key. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"AWS\\_SECRET\\_KEY\". + +Further Reading: +[https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html](https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "tclydesdale", + "$Meta": { + "ExportedAt": "2018-01-30T12:44:07.356Z", + "OctopusVersion": "4.1.9", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-change-ec2-instance-state.json.human b/step-templates/aws-change-ec2-instance-state.json.human new file mode 100644 index 000000000..007145761 --- /dev/null +++ b/step-templates/aws-change-ec2-instance-state.json.human @@ -0,0 +1,196 @@ +{ + "Id": "302a1282-c139-4e0f-9076-cd4b16d8b795", + "Name": "AWS - Change EC2 Instance State", + "Description": "This step can Start, Stop or Terminate an EC2 instance. + +Works well with the \"_AWS - Launch EC2 Instance_\" Community Step Template. + +[AWS Tools for Windows PowerShell](http://aws.amazon.com/powershell/) must be installed on the Server/Target you plan on running this step template on.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +param( + [string]$odInstanceId, + [string]$odState, + [string]$odAccessKey, + [string]$odSecretKey, + [switch]$whatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if (!$result -or $result -eq $null) { + if ($Default) { + $result = $Default + } elseif ($Required) { + throw \"Missing parameter value $Name\" + } + } + + return $result +} + + +& { + param( + [string]$odInstanceId, + [string]$odState, + [string]$odAccessKey, + [string]$odSecretKey + ) + + # If AWS key's are not provided as params, attempt to retrieve them from Environment Variables + if ($odAccessKey -or $odSecretKey) { + Set-AWSCredentials -AccessKey $odAccessKey -SecretKey $odSecretKey -StoreAs default + } elseif (([Environment]::GetEnvironmentVariable(\"AWS_ACCESS_KEY\", \"Machine\")) -or ([Environment]::GetEnvironmentVariable(\"AWS_SECRET_KEY\", \"Machine\"))) { + Set-AWSCredentials -AccessKey ([Environment]::GetEnvironmentVariable(\"AWS_ACCESS_KEY\", \"Machine\")) -SecretKey ([Environment]::GetEnvironmentVariable(\"AWS_SECRET_KEY\", \"Machine\")) -StoreAs default + } else { + throw \"AWS API credentials were not available/provided.\" + } + + if ($odInstanceId) { + $instanceObj = (Get-EC2Instance $odInstanceId | select -ExpandProperty Instances) + $instanceCount = ($instanceObj | measure).Count + + if ($instanceCount -eq 1) { + $instanceId = ($instanceObj).InstanceId + + Write-Output (\"------------------------------\") + Write-Output (\"Checking/Setting the EC2 Instance state:\") + Write-Output (\"------------------------------\") + + $currentState = (Get-EC2Instance $instanceId).Instances.State.Name + + if ($odState -eq \"running\" -and $currentState -ne \"running\") { + $changeInstanceStateObj = (Start-EC2Instance -InstanceId $instanceId) + } + elseif ($odState -eq \"absent\" -and $currentState -ne \"terminated\") { + $changeInstanceStateObj = (Remove-EC2Instance -InstanceId $instanceId -Force) + } + elseif ($odState -eq \"stopped\" -and $currentState -ne \"stopped\") { + $changeInstanceStateObj = (Stop-EC2Instance -InstanceId $instanceId) + } + + $timeout = new-timespan -Seconds 120 + $sw = [diagnostics.stopwatch]::StartNew() + + while ($true) { + $currentState = (Get-EC2Instance $instanceId).Instances.State.Name + + if ($currentState -eq \"running\" -and $odState -eq \"running\") { + break + } + elseif ($currentState -eq \"terminated\" -and $odState -eq \"absent\") { + break + } + elseif ($currentState -eq \"stopped\" -and $odState -eq \"stopped\") { + break + } + + Write-Output (\"$(Get-Date) | Waiting for Instance '$instanceId' to transition from state: $currentState\") + + if ($sw.elapsed -gt $timeout) { throw \"Timed out waiting for desired state\" } + + Sleep -Seconds 5 + } + + Write-Output (\"------------------------------\") + Write-Output (\"$(Get-Date) | $($instanceId) state: $currentState\") + Write-Output (\"------------------------------\") + } + else + { + Write-Output (\"Instance '$instanceId' could not be found...?\") + } + } + } ` + (Get-Param 'odInstanceId' -Required) ` + (Get-Param 'odState' -Required) ` + (Get-Param 'odAccessKey') ` + (Get-Param 'odSecretKey')" + }, + "Parameters": [ + { + "Id": "3eec5a11-9095-40f6-bebb-71ccb94c73db", + "Name": "odInstanceId", + "Label": "Instance ID", + "HelpText": "The EC2 Instance ID of the Instance you would like to add Tags to.", + "DefaultValue": "i-xxxxxxxxxxxxxxxxx", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "94b68517-858c-4232-bd1a-c760a22750a1", + "Name": "odState", + "Label": "Instance State", + "HelpText": "The State you would like the specified EC2 Instance to be in. For example \"Running\" (ie Start EC2 Instance).", + "DefaultValue": "running", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "running|Start EC2 Instance +stopped|Stop EC2 Instance +absent|Terminate EC2 Instance" + }, + "Links": {} + }, + { + "Id": "e1c98924-6f3b-45fe-a4b8-16329a9a50ea", + "Name": "odAccessKey", + "Label": "Access Key (Kind-of Optional)", + "HelpText": "An Access Key with permissions to create the desired EC2 instance. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"AWS\\_ACCESS\\_KEY\". + +Further Reading: +[https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html](https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "45557d48-5f1a-4523-9f42-f4a53571d0ce", + "Name": "odSecretKey", + "Label": "Secret Key (Kind-of Optional)", + "HelpText": "The Secret Key associated with the above Access Key. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"AWS\\_SECRET\\_KEY\". + +Further Reading: +[https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html](https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "tclydesdale", + "$Meta": { + "ExportedAt": "2018-01-30T13:25:57.198Z", + "OctopusVersion": "4.1.9", + "Type": "ActionTemplate" + }, + "Category": "aws" + } + diff --git a/step-templates/aws-cloudformation-create.json.human b/step-templates/aws-cloudformation-create.json.human new file mode 100644 index 000000000..03904c0f9 --- /dev/null +++ b/step-templates/aws-cloudformation-create.json.human @@ -0,0 +1,375 @@ +{ + "Id": "d6d9d9db-e4ab-487c-967c-860bf8303052", + "Name": "AWS - Create Cloud Formation Stack", + "Description": "Creates a [Amazon Cloud Formation Stack](#http://docs.aws.amazon.com/powershell/latest/reference/items/New-CFNStack.html) with the template specified. + +- Requires the [AWS PowerShell cmdlets](http://aws.amazon.com/powershell/)", + "ActionType": "Octopus.Script", + "Version": 50, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptBody": "#http://docs.aws.amazon.com/powershell/latest/reference/items/New-CFNStack.html\r +\r +#Check for the PowerShell cmdlets\r +try{ \r + Import-Module AWSPowerShell -ErrorAction Stop\r +}catch{\r + \r + $modulePath = \"C:\\Program Files (x86)\\AWS Tools\\PowerShell\\AWSPowerShell\\AWSPowerShell.psd1\"\r + Write-Output \"Unable to find the AWS module checking $modulePath\" \r + \r + try{\r + Import-Module $modulePath\r + \r + }\r + catch{\r + throw \"AWS PowerShell not found! Please make sure to install them from https://aws.amazon.com/powershell/\" \r + }\r +}\r +\r +function Confirm-CFNStackDeleted($credential, $stackName){\r + do{\r + $stack = $null\r + try {\r + $stack = Get-CFNStack -StackName $CloudFormationStackName -Credential $credential -Region $AWSRegion \r + }\r + catch{}\r + \r + if($stack -ne $null){\r +\r +\t\t\t$stack | ForEach-Object {\r +\t\t\t\t$progress = $_.StackStatus.ToString()\r +\t\t\t\t$name = $_.StackName.ToString()\r +\r +\t\t\t\tWrite-Host \"Waiting for Cloud Formation Script to deleted. Stack Name: $name Operation status: $progress\" \r + \r +\t\t\t\tif($progress -ne \"DELETE_COMPLETE\" -and $progress -ne \"DELETE_IN_PROGRESS\"){ \r +\t\t\t\t\t$stack\r +\t\t\t\t\tthrow \"Something went wrong deleting the Cloud Formation Template\" \r +\t\t\t\t} \t \t\t\r +\t\t\t} \r +\t\t\t \r + Start-Sleep -s 15\r + }\r +\r + }until ($stack -eq $null)\r +}\r +\r +function Confirm-CFNStackCompleted($credential, $stackName, $region){\r +\r + $awsValidStatusList = @()\r + $awsValidStatusList += \"CREATE_COMPLETE\"\r + $awsValidStatusList += \"CREATE_IN_PROGRESS\" \r + \r + $awsFailedStatusList = @()\r + $awsFailedStatusList += \"CREATE_FAILED\"\r + $awsFailedStatusList += \"UPDATE_FAILED\"\r + $awsFailedStatusList += \"DELETE_SKIPPED\"\r + $awsFailedStatusList += \"CREATE_FAILED\"\r + $awsFailedStatusList += \"CREATE_FAILED\"\r +\r +\t#http://docs.aws.amazon.com/powershell/latest/reference/Index.html\r + #CREATE_COMPLETE | CREATE_FAILED | CREATE_IN_PROGRESS | DELETE_COMPLETE | DELETE_FAILED | DELETE_IN_PROGRESS | DELETE_SKIPPED | UPDATE_COMPLETE | UPDATE_FAILED | UPDATE_IN_PROGRESS.\r +\t \r + do{\r + $stack = Get-CFNStack -StackName $stackName -Credential $credential -Region $region \r +\t\t$complete = $false\r +\r +\t\t#Depending on the template sometimes there are multiple status per CFN template\r +\r +\t\t$stack | ForEach-Object {\r +\t\t\t$progress = $_.StackStatus.ToString()\r +\t\t\t$name = $_.StackName.ToString()\r +\r +\t\t\tWrite-Host \"Waiting for Cloud Formation Script to complete. Stack Name: $name Operation status: $progress\" \r + \r +\t\t\tif($progress -ne \"CREATE_COMPLETE\" -and $progress -ne \"CREATE_IN_PROGRESS\"){ \r +\t\t\t\t$stack\r +\t\t\t\tthrow \"Something went wrong creating the Cloud Formation Template\" \r +\t\t\t} \t \t\t\r +\t\t}\r +\r +\t\t$inProgress = $stack | Where-Object { $_.StackStatus.ToString() -eq \"CREATE_IN_PROGRESS\" }\r +\t\t\r +\t\tif($inProgress.Count -eq 0){\r +\t\t\t$complete = $true\r +\t\t}\r +\t\t \r + Start-Sleep -s 15\r +\r + }until ($complete -eq $true)\r +}\r +\r +# Check the parameters.\r +if (-NOT $AWSSecretAccessKey) { throw \"You must enter a value for 'AWS Access Key'.\" }\r +if (-NOT $AWSAccessKey) { throw \"You must enter a value for 'AWS Secret Access Key'.\" }\r +if (-NOT $AWSRegion) { throw \"You must enter a value for 'AWS Region'.\" }\r +if (-NOT $CloudFormationStackName) { throw \"You must enter a value for 'AWS Cloud Formation Stack Name'.\" } \r +\r +\r +#Reformat the CloudFormation parameters\r +$paramObject = ConvertFrom-Json $CloudFormationParameters\r +$cloudFormationParams = @()\r +\r +$paramObject.psobject.properties | ForEach-Object { \r + $keyPair = New-Object -Type Amazon.CloudFormation.Model.Parameter\r + $keyPair.ParameterKey = $_.Name\r + $keyPair.ParameterValue = $_.Value\r +\r + $cloudFormationParams += $keyPair\r +} \r +\r +Write-Output \"--------------------------------------------------\"\r +Write-Output \"AWS Region: $AWSRegion\"\r +Write-Output \"AWS Cloud Formation Stack Name: $CloudFormationStackName\"\r +Write-Output \"Use S3 for AWS Cloud Formation Script?: $UseS3ForCloudFormationTemplate\"\r +Write-Output \"Use S3 for AWS Cloud Formation Stack Policy?: $UseS3ForStackPolicy\"\r +Write-Output \"AWS Cloud Formation Script Url: $CloudFormationTemplateURL\"\r +Write-Output \"AWS Cloud Formation Stack Policy Url: $CloudFormationStackPolicyURL\"\r +Write-Output \"AWS Cloud Formation Parameters:\"\r +Write-Output $cloudFormationParams\r +Write-Output \"--------------------------------------------------\"\r +\r +#Set up the credentials and the dependencies\r +Set-DefaultAWSRegion -Region $AWSRegion\r +$credential = New-AWSCredentials -AccessKey $AWSAccessKey -SecretKey $AWSSecretAccessKey \r +\r +#Check to see if the stack exists\r +try{\r + $stack = Get-CFNStack -StackName $CloudFormationStackName -Credential $credential -Region $AWSRegion \r +}\r +catch{} #Do nothing as this will throw if the stack does not exist\r +\r +if($stack -ne $null){\r + if($DeleteExistingStack -eq $false) {\r + Write-Output \"Stack with name $CloudFormationStackName already exists. If you wish to automatically delete existing stacks, set 'Delete Existing Stack' to True.\"\r + exit -1\r + }\r + Write-Output \"Stack found, removing the existing Cloud Formation Stack\" \r + \r + Remove-CFNStack -Credential $credential -StackName $CloudFormationStackName -Region $AWSRegion -Force\r + Confirm-CFNStackDeleted -credential $credential -stackName $CloudFormationStackName\r +}\r +\r +if($UseS3ForCloudFormationTemplate -eq $true){ \r +\r + if (-NOT $CloudFormationTemplateURL) { throw \"You must enter a value for 'AWS Cloud Formation Template'.\" } \r +\r + if($UseS3ForStackPolicy -eq $true){\r + Write-Output \"Using Cloud Formation Stack Policy from $CloudFormationStackPolicyURL\"\r + New-CFNStack -Credential $credential -OnFailure $CloudFormationOnFailure -TemplateUrl $CloudFormationTemplateURL -StackName $CloudFormationStackName -Region $AWSRegion -Parameter $cloudFormationParams -Capability $CloudFormationCapability -StackPolicyURL $CloudFormationStackPolicyURL\r + }\r + else {\r + New-CFNStack -Credential $credential -OnFailure $CloudFormationOnFailure -TemplateUrl $CloudFormationTemplateURL -StackName $CloudFormationStackName -Region $AWSRegion -Parameter $cloudFormationParams -Capability $CloudFormationCapability \r + }\r +\r + Confirm-CFNStackCompleted -credential $credential -stackName $CloudFormationStackName -region $AWSRegion\r +}\r +else{\r + \r + Write-Output \"Using Cloud Formation script from Template\"\r +\r + $validTemplate = Test-CFNTemplate -TemplateBody $CloudFormationTemplate -Region $AWSRegion -Credential $credential\r + $statusCode = $validTemplate.HttpStatusCode.ToString()\r +\r + Write-Output \"Validation Response: $statusCode\"\r +\r + if($validTemplate.HttpStatusCode){\r +\r + if($UseS3ForStackPolicy -eq $true){\r + Write-Output \"Using Cloud Formation Stack Policy from $CloudFormationStackPolicyURL\"\r + New-CFNStack -Credential $credential -OnFailure $CloudFormationOnFailure -TemplateBody $CloudFormationTemplate -StackName $CloudFormationStackName -Region $AWSRegion -Parameter $cloudFormationParams -Capability $CloudFormationCapability -StackPolicyURL $CloudFormationStackPolicyURL\r + }\r + else {\r + New-CFNStack -Credential $credential -OnFailure $CloudFormationOnFailure -TemplateBody $CloudFormationTemplate -StackName $CloudFormationStackName -Region $AWSRegion -Parameter $cloudFormationParams -Capability $CloudFormationCapability\r + }\r +\r + Confirm-CFNStackCompleted -credential $credential -stackName $CloudFormationStackName -region $AWSRegion\r + }\r + else{\r + throw \"AWS Cloud Formation template is not valid\"\r + } \r +}\r +\r +$stack = Get-CFNStack -StackName $CloudFormationStackName -Credential $credential -Region $AWSRegion \r +\r +Set-OctopusVariable -name \"AWSCloudFormationStack\" -value $stack\r +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "13f780df-407d-48b9-b7d8-7bd92f47da38", + "Name": "AWSSecretAccessKey", + "Label": "AWS Secret Access Key", + "HelpText": "The [secret access key](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html) to use when executing the script", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "520b8d18-fedd-49fc-a80c-957ea00fe532", + "Name": "AWSAccessKey", + "Label": "AWS Access Key", + "HelpText": "The [access key](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html) to use when executing the script", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "ff776b86-a3f3-4d8c-b3e1-1e7ac629d503", + "Name": "AWSRegion", + "Label": "AWS Region", + "HelpText": "The Amazon Region see (http://docs.aws.amazon.com/powershell/latest/reference/items/Get-AWSRegion.html) for further info", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "us-east-2|US East (Ohio) +us-east-1|US East (N. Virginia) +us-west-1|US West (N. California) +us-west-2|US West (Oregon) +ca-central-1|Canada (Central) +ap-south-1|Asia Pacific (Mumbai) +ap-northeast-2|Asia Pacific (Seoul) +ap-southeast-1|Asia Pacific (Singapore) +ap-southeast-2|Asia Pacific (Sydney) +ap-northeast-1|Asia Pacific (Tokyo) +eu-central-1|EU (Frankfurt) +eu-west-1|EU (Ireland) +eu-west-2|EU (London) +sa-east-1|South America (São Paulo)" + }, + "Links": {} + }, + { + "Id": "772a57e1-824b-46bf-b652-31c27b3b8473", + "Name": "CloudFormationStackName", + "Label": "AWS Cloud Formation Stack Name", + "HelpText": "The name of the AWS Cloud Formation Stack", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "8c6b8792-fc24-4f40-953e-4d8511b5e00d", + "Name": "CloudFormationCapability", + "Label": "AWS Cloud Formation Capability", + "HelpText": "The capability required for the tempate see [docs](http://docs.aws.amazon.com/powershell/latest/reference/items/New-CFNStack.html)", + "DefaultValue": "CAPABILITY_IAM", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "51433c58-fc29-4e73-aae0-5a40799fa16f", + "Name": "CloudFormationOnFailure", + "Label": "Action on Failure", + "HelpText": "Defaults to ROLLBACK. See [docs](http://docs.aws.amazon.com/powershell/latest/reference/items/New-CFNStack.html)", + "DefaultValue": "ROLLBACK", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a8919189-c361-4b05-ae4b-9aba650d9d0f", + "Name": "UseS3ForCloudFormationTemplate", + "Label": "Use AWS S3 storage for the Cloud Formation Template", + "HelpText": "Whether to use S3 storage to source the Cloud Formation script. See [docs](http://docs.aws.amazon.com/powershell/latest/reference/items/New-CFNStack.html)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "7169c2f4-e1ff-45fa-99f1-7fcb49b1e313", + "Name": "CloudFormationTemplate", + "Label": "The Cloud Formation Template", + "HelpText": "The Cloud Formation Template in the format, see [docs](http://aws.amazon.com/cloudformation/aws-cloudformation-templates/)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "48351ea3-fedb-43ec-b594-c2bbc11efb46", + "Name": "CloudFormationTemplateURL", + "Label": "The location in S3 for the Cloud Formation Template", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a01f0c6c-4a0c-4f32-9327-d6a22c10d10c", + "Name": "CloudFormationParameters", + "Label": "Cloud Formation Parameters", + "HelpText": "See [docs](http://docs.aws.amazon.com/powershell/latest/reference/items/New-CFNStack.html) + +Should be provided as a JSON formatted object. + +e.g.`{ \"Key1\": \"Value1\", \"Key2\": \"Value2\" }`", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "ca922e15-fd36-4a0d-8717-660afc5e7aa9", + "Name": "UseS3ForStackPolicy", + "Label": "Use AWS S3 Storage for Cloud Formation Stack Policy", + "HelpText": "See [docs](http://docs.aws.amazon.com/powershell/latest/reference/items/New-CFNStack.html)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "869f2c5b-511f-4dee-bde7-30bee105b81d", + "Name": "CloudFormationStackPolicyURL", + "Label": "The URL for the Cloud Formation Policy in S3", + "HelpText": "See [docs](http://docs.aws.amazon.com/powershell/latest/reference/items/New-CFNStack.html)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "671b3717-d0b9-4285-915b-5a52ad52ff78", + "Name": "DeleteExistingStack", + "Label": "Delete Existing Stack", + "HelpText": "A boolean to state whether or not to delete the existing stack if one with the same name is found. If this is set to false and a stack with the same name is found, the step will fail.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "LastModifiedBy": "kirkholloway", + "$Meta": { + "ExportedAt": "2017-02-06T22:56:26.736Z", + "OctopusVersion": "3.7.5", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-cloudfront-invalidate-cache.json.human b/step-templates/aws-cloudfront-invalidate-cache.json.human new file mode 100644 index 000000000..bf70361eb --- /dev/null +++ b/step-templates/aws-cloudfront-invalidate-cache.json.human @@ -0,0 +1,96 @@ +{ + "Id": "de9a03dc-25e1-40fe-8047-716e4462bd23", + "Name": "Amazon Cloudfront Cache Invalidation", + "Description": "Invalidate AWS Cloudfront cache. This template uses the AWS CLI tool. ALL step fields need to be populated for this template to work. + +AWS CLI needs to be installed on your deployment server for this to work properly: +https://aws.amazon.com/cli/", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "Write-Host \"Setting up AWS profile environment\"\r +aws configure set aws_access_key_id $AccessKey --profile $CredentialsProfileName\r +aws configure set aws_secret_access_key $SecretKey --profile $CredentialsProfileName\r +aws configure set default.region $Region --profile $CredentialsProfileName\r +aws configure set preview.cloudfront true --profile $CredentialsProfileName\r +\r +Write-Host \"Initiating AWS cloudfront invalidation of the following paths:\"\r +Write-Host $InvalidationPaths\r +aws cloudfront create-invalidation --profile $CredentialsProfileName --distribution-id $DistributionId --paths $InvalidationPaths\r +\r +Write-Host \"Please note that it may take up to 15-20 minutes for AWS to complete the cloudfront cache invalidation\"", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "CredentialsProfileName", + "Label": "AWS credentials profile name", + "HelpText": "Used to store your AWS credentials to: ~/.aws/ + +HAS TO BE UNIQUE - check that no other Octopus projects is using the same AWS credential profile name.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Region", + "Label": "Region", + "HelpText": "AWS Cloudfront Region", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DistributionId", + "Label": "Distribution Id", + "HelpText": "AWS Cloudfront Distribution Id", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AccessKey", + "Label": "Access Key", + "HelpText": "AWS Access Key", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SecretKey", + "Label": "Secret Key", + "HelpText": "AWS Secret Key", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "InvalidationPaths", + "Label": "Invalidation Paths", + "HelpText": "Space-delimited list of paths to invalidate. + +For example: +/index.html /images/*", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedBy": "dovetail-technologies", + "$Meta": { + "ExportedAt": "2017-01-05T09:27:06.133Z", + "OctopusVersion": "3.3.15", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-configure-lambda-alias.json.human b/step-templates/aws-configure-lambda-alias.json.human new file mode 100644 index 000000000..c24495170 --- /dev/null +++ b/step-templates/aws-configure-lambda-alias.json.human @@ -0,0 +1,282 @@ +{ + "Id": "5a98a4ba-55ee-4200-aa80-df330f377a6a", + "Name": "AWS - Configure Lambda Alias", + "Description": "Configures an AWS Lambda Alias. Allows you to specify how much traffic is routed to a specific version of the Lambda. + +**Please Note:** Your AWS Lambda function **MUST** have at least one published version for this step to work. + +This step uses the following AWS CLI commands to deploy the AWS Lambda. You will be required to install the AWS CLI on your server/worker for this to work. The AWS CLI is pre-installed on the [dynamic workers](https://octopus.com/docs/infrastructure/workers/dynamic-worker-pools) in Octopus Cloud as well as the provided docker containers for [Execution Containers](https://octopus.com/docs/deployment-process/execution-containers-for-workers). + +- [create-alias](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-alias.html) +- [get-alias](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/get-alias.html) +- [get-function](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/get-function.html) +- [list-versions-by-function](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/list-versions-by-function.html) +- [update-alias](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/update-alias.html)", + "ActionType": "Octopus.AwsRunScript", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.AwsAccount.Variable": "#{AWS.Lambda.Account}", + "Octopus.Action.Aws.Region": "#{AWS.Lambda.Region}", + "Octopus.Action.Script.ScriptBody": "$functionName = $OctopusParameters[\"AWS.Lambda.Function.Name\"] +$functionAliasName = $OctopusParameters[\"AWS.Lambda.Alias.Name\"] +$functionAliasPercent = $OctopusParameters[\"AWS.Lambda.Alias.Percent\"] +$functionVersion = $OctopusParameters[\"AWS.Lambda.Alias.FunctionVersion\"] + +if ([string]::IsNullOrWhiteSpace($functionName)) +{ +\tWrite-Error \"The parameter Function Name is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionAliasName)) +{ +\tWrite-Error \"The parameter Alias Name is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionVersion)) +{ +\tWrite-Error \"The parameter Function Version is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionAliasPercent)) +{ +\tWrite-Error \"The parameter Alias Percent is required.\" + Exit 1 +} + +$newVersionPercent = [int]$functionAliasPercent + +if ($newVersionPercent -le 0 -or $newVersionPercent -gt 100) +{ + Write-Error \"The parameter Alias Percent must be between 1 and 100.\" + exit 1 +} + +Write-Host \"Function Name: $functionName\" +Write-Host \"Function Version: $functionVersion\" +Write-Host \"Function Alias Name: $functionAliasName\" +Write-Host \"Function Alias Percent: $functionAliasPercent\" + +$versionToUpdateTo = $functionVersion +if ($functionVersion.ToLower().Trim() -eq \"latest\" -or $functionVersion.ToLower().Trim() -eq \"previous\") +{ +\tWrite-Highlight \"The function version specified is $functionVersion, attempting to find the specific version number.\" + $versionOutput = aws lambda list-versions-by-function --function-name \"$functionName\" --no-paginate + $versionOutput = $versionOutput | ConvertFrom-JSON + $versionList = @($versionOutput.Versions) + + if ($functionVersion.ToLower().Trim() -eq \"previous\" -and $versionList.Count -gt 2) + { + \t$versionArrayIndex = $versionList.Count - 2 + } + else + { + \t$versionArrayIndex = $versionList.Count - 1 + } + + $versionToUpdateTo = $versionList[$versionArrayIndex].Version + Write-Highlight \"The alias will update to version $versionToUpdateTo\" +} + +try +{ + Write-Host \"Publish set to yes with a function alias specified. Attempting to find existing alias.\" + $aliasInformation = aws lambda get-alias --function-name \"$functionName\" --name \"$functionAliasName\" 2> $null + + Write-Host \"The exit code from the alias lookup was $LASTEXITCODE\" + if ($LASTEXITCODE -eq 255 -or $LASTEXITCODE -eq 254) + { + \t Write-Highlight \"The function's alias $functionAliasName does not exist.\" + Write-Host \"If you see an error right here you can safely ignore that.\" +\t $aliasInformation = $null + } + else + { +\t Write-Highlight \"The function's alias $functionAliasName already exists.\" +\t $aliasInformation = $aliasInformation | ConvertFrom-JSON +\t Write-Host $aliasInformation + } +} +catch +{ + Write-Host \"The alias specified $functionAliasName does not exist for $functionName. Will create a new alias with that name.\" + $aliasInformation = $null +} + +if ($null -ne $aliasInformation) +{ + \tWrite-Host \"Comparing the existing alias version $($aliasInformation.FunctionVersion) with the the published version $versionToUpdateTo\" + + \tif ($aliasInformation.FunctionVersion -ne $versionToUpdateTo) + { + \tWrite-Host \"The alias $functionAliasName version $($aliasInformation.FunctionVersion) does not equal the published version $versionToUpdateTo\" + + if ($newVersionPercent -eq 100) + { + \tWrite-Highlight \"The percent for the new version of the function is 100%, updating the alias $functionAliasName to function version $versionToUpdateTo\" + \t$newAliasInformation = aws lambda update-alias --function-name \"$functionName\" --name \"$functionAliasName\" --function-version \"$versionToUpdateTo\" --routing-config \"AdditionalVersionWeights={}\" + } + else + { \t + $newVersionPercent = $newVersionPercent / [double]100 + + Write-Highlight \"Updating the alias $functionAliasName so $functionAliasPercent of all traffic is routed to $versionToUpdateTo\" +\t\t\t + \t $newAliasInformation = aws lambda update-alias --function-name \"$functionName\" --name \"$functionAliasName\" --routing-config \"AdditionalVersionWeights={\"\"$versionToUpdateTo\"\"=$newVersionPercent}\" + } + } + elseif ($newVersionPercent -eq 100) + { + \tWrite-Highlight \"The alias $functionAliasName is already pointed to $versionToUpdateTo and the percent sent in is 100, updating the function so all traffic is routed to that version.\" + $newAliasInformation = aws lambda update-alias --function-name \"$functionName\" --name \"$functionAliasName\" --routing-config \"AdditionalVersionWeights={}\" + } + else + { + \t\tWrite-Highlight \"The alias $functionAliasName is already pointed to $versionToUpdateTo. Leaving as is.\" + } +} +else +{ + \tWrite-Highlight \"Creating the alias $functionAliasName with the version $versionToUpdateTo\" + \t$newAliasInformation = aws lambda create-alias --function-name \"$functionName\" --name \"$functionAliasName\" --function-version \"$versionToUpdateTo\" +} + +if ($null -ne $newAliasInformation) +{ +\tWrite-Host ($newAliasInformation | ConvertFrom-JSON) +} + +Write-Highlight \"The alias has finished updating.\"" + }, + "Parameters": [ + { + "Id": "261c7c1d-d3b2-4c73-9d81-00cba3c97842", + "Name": "AWS.Lambda.Function.Name", + "Label": "Function Name", + "HelpText": "Required. + +The name of the function the alias will be attached to. See [documentation](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html#options) + +Examples: +- Function name - my-function . +- Function ARN - arn:aws:lambda:us-west-2:123456789012:function:my-function . +- Partial ARN - 123456789012:function:my-function . + +The length constraint applies only to the full ARN. If you specify only the function name, it is limited to 64 characters in length.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a0c0d1ba-03a8-42e6-b275-b8991f4573af", + "Name": "AWS.Lambda.Account", + "Label": "AWS Account", + "HelpText": "Required. + +The AWS Account with permissions to create / update AWS Lambdas.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "8eb17fdf-4b22-4a4e-9105-459cca33cca2", + "Name": "AWS.Lambda.Region", + "Label": "Region", + "HelpText": "Required. + +The region where the function is in.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "us-east-2|US East (Ohio) +us-east-1|US East (N. Virginia) +us-west-1|US West (N. California) +us-west-2|US West (Oregon) +af-south-1|Africa (Cape Town) +ap-east-1|Asia Pacific (Hong Kong) +ap-south-1|Asia Pacific (Mumbai) +ap-northeast-3|Asia Pacific (Osaka-Local) +ap-northeast-2|Asia Pacific (Seoul) +ap-southeast-1|Asia Pacific (Singapore) +ap-southeast-2|Asia Pacific (Sydney) +ap-northeast-1|Asia Pacific (Tokyo) +ca-central-1|Canada (Central) +eu-central-1|Europe (Frankfurt) +eu-west-1|Europe (Ireland) +eu-west-2|Europe (London) +eu-south-1|Europe (Milan) +eu-west-3|Europe (Paris) +eu-north-1|Europe (Stockholm) +me-south-1|Middle East (Bahrain) +sa-east-1|South America (São Paulo)" + } + }, + { + "Id": "ee765e89-3807-4422-9fe4-b695b2e8a88e", + "Name": "AWS.Lambda.Alias.Name", + "Label": "Alias Name", + "HelpText": "Required. + +The [alias](https://docs.aws.amazon.com/lambda/latest/dg/versioning-aliases.html) for a Lambda function version. Use aliases to provide clients with a function identifier that you can update to invoke a different version.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3b382f4a-556c-4303-9844-993b3df18830", + "Name": "AWS.Lambda.Alias.Percent", + "Label": "Alias Version Percent", + "HelpText": "Required. + +The percent (between 0 and 100) of traffic to send to the version. + +- If the alias does not exist, this parameter is ignored and 100% of traffic is sent to the new alias. +- If the alias exists and the function version is the same as the specified version no updates are performed. +- If the alias exists and the value is 100, the alias is updated to the version specified in the `Function Version` parameter and all traffic will be routed to that. +- If the alias exists and the value is less than 100, the alias function is updated to send that percent of traffic to the version specified in the `Function Version` parameter. + +**Please note:** Existing percentages will be automatically updated. For example, the alias is configured to send 100% of the traffic to version 3, and you provide 20 for version 4. Version 3 will be automatically adjusted to get 80% of the traffic and version 4 will get 20% of the traffic.", + "DefaultValue": "100", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ff3335b7-8b39-45c4-878d-4158412d65dc", + "Name": "AWS.Lambda.Alias.FunctionVersion", + "Label": "Function Version", + "HelpText": "Required. + +The function version to route traffic to for this alias. Please note the function version <> to the package version. The options are: + +- **Latest**: This step will find the most recent version of the function and use that. +- **Previous**: This step will find the second to the most recent version of the function and use that. +- **[Number]**: A specific version number to assign to the alias. + +If you are using the community step template [AWS - Deploy Lambda Function](https://library.octopus.com/step-templates/9b5ee984-bdd2-49f0-a78a-07e21e60da8a/actiontemplate-aws-deploy-lambda-function), that sets the output variable `PublishedVersion` you can leverage.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.AwsRunScript", + "$Meta": { + "ExportedAt": "2022-10-04T18:36:08.312Z", + "OctopusVersion": "2022.3.10594", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "aws" +} diff --git a/step-templates/aws-configure-lambda-api-gateway-integration.json.human b/step-templates/aws-configure-lambda-api-gateway-integration.json.human new file mode 100644 index 000000000..1bcaab5b1 --- /dev/null +++ b/step-templates/aws-configure-lambda-api-gateway-integration.json.human @@ -0,0 +1,476 @@ +{ + "Id": "7e818c42-8c59-4a14-b612-2b9cb69fcf4a", + "Name": "AWS - Configure Lambda API Gateway Integration", + "Description": "Configures an API v2 Gateway to connect to and invoke a Lambda Function. That includes: + +- Integration on the API Gateway +- Route on the API Gateway +- Permission Policy on AWS Lambda (Aliases are supported) + +**Please Note:** Your AWS Lambda function **MUST** exist prior to running this step. + +This step uses the following AWS CLI commands to create the integration and route. You will be required to install the AWS CLI on your server/worker for this to work. The AWS CLI is pre-installed on the [dynamic workers](https://octopus.com/docs/infrastructure/workers/dynamic-worker-pools) in Octopus Cloud as well as the provided docker containers for [Execution Containers](https://octopus.com/docs/deployment-process/execution-containers-for-workers). + +APIGatewayV2 CLI Methods +- [create-integration](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigatewayv2/create-integration.html) +- [create-route](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigatewayv2/create-route.html) +- [get-apis](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigatewayv2/get-apis.html) +- [get-integrations](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigatewayv2/get-integrations.html) +- [get-routes](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigatewayv2/get-routes.html) +- [get-vpc-links](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigatewayv2/get-vpc-links.html) +- [update-integration](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigatewayv2/update-integration.html) + +Lambda CLI Methods +- [add-permission](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/add-permission.html) +- [get-policy](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/get-policy.html) +- [remove-permission](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/remove-permission.html) + + +## Output Variables + +This step template sets the following output variables: + +- `ApiGatewayEndPoint`: The endpoint of the API Gateway +- `ApiGatewayId`: The id of the API Gateway +- `ApiGatewayArn`: The ARN of the API Gateway", + "ActionType": "Octopus.AwsRunScript", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.AwsAccount.Variable": "#{AWS.Api.Gateway.Account}", + "Octopus.Action.Aws.Region": "#{AWS.Api.Gateway.Region}", + "Octopus.Action.Script.ScriptBody": "$ApiGatewayName = $OctopusParameters[\"AWS.Api.Gateway.Name\"] +$ApiRouteKey = $OctopusParameters[\"AWS.Api.Gateway.Route.Key\"] +$ApiLambdaUri = $OctopusParameters[\"AWS.Api.Gateway.Lambda.Arn\"] +$ApiPayloadFormatVersion = $OctopusParameters[\"AWS.Api.Gateway.Integration.PayloadFormatVersion\"] +$ApiConnection = $OctopusParameters[\"AWS.Api.Gateway.Integration.Connection\"] +$ApiIntegrationMethod = $OctopusParameters[\"AWS.Api.Gateway.Integration.HttpMethod\"] +$ApiLambdaAlias = $OctopusParameters[\"AWS.Api.Gateway.Lambda.Alias\"] +$ApiRegion = $OctopusParameters[\"AWS.Api.Gateway.Region\"] + +$stepName = $OctopusParameters[\"Octopus.Step.Name\"] + +if ([string]::IsNullOrWhiteSpace($ApiGatewayName)) +{ +\tWrite-Error \"The parameter Gateway Name is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($ApiRouteKey)) +{ +\tWrite-Error \"The parameter Route Key is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($ApiLambdaUri)) +{ +\tWrite-Error \"The parameter Lambda ARN is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($ApiPayloadFormatVersion)) +{ +\tWrite-Error \"The parameter Payload Format Version is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($ApiIntegrationMethod)) +{ +\tWrite-Error \"The parameter Http Method is required.\" + Exit 1 +} + +Write-Host \"Gateway Name: $ApiGatewayName\" +Write-Host \"Route Key: $ApiRouteKey\" +Write-Host \"Lambda ARN: $ApiLambdaUri\" +Write-Host \"Lambda Alias: $ApiLambdaAlias\" +Write-Host \"Payload Format Version: $ApiPayloadFormatVersion\" +Write-Host \"VPC Connection: $ApiConnection\" +Write-host \"API Region: $apiRegion\" + +if ([string]::IsNullOrWhiteSpace($apiLambdaAlias) -eq $false) +{ +\tWrite-Host \"Alias specified, adding it to the Lambda ARN\" +\t$apiLambdaIntegrationArn = \"$($apiLambdaUri):$($apiLambdaAlias)\" +} +else +{ +\tWrite-Host \"No alias specified, going directly to the lambda function\" +\t$apiLambdaIntegrationArn = $apiLambdaUri +} + +$apiQueryOutput = aws apigatewayv2 get-apis --no-paginate +$apiQueryOutput = ($apiQueryOutput | ConvertFrom-JSON) + +$apiList = @($apiQueryOutput.Items) +$apiGatewayToUpdate = $null +foreach ($api in $apiList) +{ +\tif ($api.Name.ToLower().Trim() -eq $apiGatewayName.ToLower().Trim()) + { + \tWrite-Highlight \"Found the gateway $apiGatewayName\" + \t$apiGatewayToUpdate = $api + break + } +} + +if ($null -eq $apiGatewayToUpdate) +{ +\tWrite-Error \"Unable to find the gateway with the name $apiGatewayName\" + exit 1 +} + +Write-Host $apiGatewayToUpdate + +$apiId = $apiGatewayToUpdate.ApiId +Write-Host \"The id of the api gateway is $apiId\" + +$apiConnectionType = \"INTERNET\" +if ([string]::IsNullOrWhiteSpace($ApiConnection) -eq $false) +{ +\t$apiConnectionType = \"VPC_LINK\" + $existingVPCLinks = aws apigatewayv2 get-vpc-links --no-paginate + $existingVPCLinks = ($existingVPCLinks | ConvertFrom-JSON) + + $existingVPCLinkList = @($existingVPCLinks.Items) + foreach ($vpc in $existingVPCLinkList) + { + \tif ($vpc.Name.ToLower().Trim() -eq $ApiConnection.ToLower().Trim()) + { + \tWrite-Host \"The name $($vpc.Name) matches $apiConnection\" + \t$apiConnectionId = $vpc.VpcLinkId + break + } + elseif ($vpc.VpcLinkId.ToLower().Trim() -eq $apiConnection.ToLower().Trim()) + { + \tWrite-Host \"The vpc link id $($vpc.VpcLinkId) matches $apiConnection\" + \t$apiConnectionId = $vpc.VpcLinkId + break + } + } + + if ([string]::IsNullOrWhiteSpace($apiConnectionId) -eq $true) + { + \tWrite-Error \"The VPC Connection $apiConnection could not be found. Please check the name or ID and try again. Please note: names can be updated, if you are matching by name double check nothing has changed.\" + exit 1 + } +} + +$apiIntegrations = aws apigatewayv2 get-integrations --api-id \"$apiId\" --no-paginate +$apiIntegrations = ($apiIntegrations | ConvertFrom-JSON) + +$integrationList = @($apiIntegrations.Items) +$integrationToUpdate = $null +foreach ($integration in $integrationList) +{ +\tif ($integration.IntegrationUri -eq $apiLambdaIntegrationArn -and $integration.ConnectionType -eq $apiConnectionType -and $integration.IntegrationType -eq \"AWS_PROXY\" -and $integration.PayloadFormatVersion -eq $ApiPayloadFormatVersion) + { + \tWrite-Highlight \"Found the existing integration $($integration.Id)\" + \t$integrationToUpdate = $integration + break + } +} + +if ($null -ne $integrationToUpdate) +{ +\tWrite-Highlight \"Updating existing integration\" +} +else +{ +\tWrite-Highlight \"Creating new integration\" + if ($apiConnectionType -eq \"INTERNET\") + { + \tWrite-Host \"Command line: aws apigatewayv2 create-integration --api-id \"\"$apiId\"\" --connection-type \"\"$apiConnectionType\"\" --integration-method \"\"$ApiIntegrationMethod\"\" --integration-type \"\"AWS_PROXY\"\" --integration-uri \"\"$apiLambdaIntegrationArn\"\" --payload-format-version \"\"$ApiPayloadFormatVersion\"\" \" + \t$integrationToUpdate = aws apigatewayv2 create-integration --api-id \"$apiId\" --connection-type \"$apiConnectionType\" --integration-method \"$ApiIntegrationMethod\" --integration-type \"AWS_PROXY\" --integration-uri \"$apiLambdaIntegrationArn\" --payload-format-version \"$ApiPayloadFormatVersion\" + } + else + { + \tWrite-Host \"Command line: aws apigatewayv2 create-integration --api-id \"\"$apiId\"\" --connection-type \"\"$apiConnectionType\"\" --connection-id \"\"$ApiConnectionId\"\" --integration-method \"\"$ApiIntegrationMethod\"\" --integration-type \"\"AWS_PROXY\"\" --integration-uri \"\"$apiLambdaIntegrationArn\"\" --payload-format-version \"\"$ApiPayloadFormatVersion\"\" \" + \t$integrationToUpdate = aws apigatewayv2 create-integration --api-id \"$apiId\" --connection-type \"$apiConnectionType\" --connection-id \"$ApiConnectionId\" --integration-method \"$ApiIntegrationMethod\" --integration-type \"AWS_PROXY\" --integration-uri \"$apiLambdaIntegrationArn\" --payload-format-version \"$ApiPayloadFormatVersion\" + } + + $integrationToUpdate = ($integrationToUpdate | ConvertFrom-JSON) +} + +If ($null -eq $integrationToUpdate) +{ +\tWrite-Error \"There was an error finding or creating the integration.\" + Exit 1 +} + +Write-Host \"$integrationToUpdate\" + +Write-Host \"Command line: aws apigatewayv2 update-integration --api-id \"\"$apiId\"\" --integration-id \"\"$($integrationToUpdate.IntegrationId)\"\" --integration-method \"\"$ApiIntegrationMethod\"\" \" +$updateResult = aws apigatewayv2 update-integration --api-id \"$apiId\" --integration-id \"$($integrationToUpdate.IntegrationId)\" --integration-method \"$ApiIntegrationMethod\" + +Write-Host \"Command line: aws apigatewayv2 get-routes --api-id \"\"$apiId\"\" --no-paginate\" +$apiRoutes = aws apigatewayv2 get-routes --api-id \"$apiId\" --no-paginate +$apiRoutes = ($apiRoutes | ConvertFrom-JSON) + +$routeList = @($apiRoutes.Items) +$routeToUpdate = $null +$routePath = \"$ApiIntegrationMethod $ApiRouteKey\" +$routeTarget = \"integrations/$($integrationToUpdate.IntegrationId)\" +foreach ($route in $routeList) +{ +\tWrite-Host \"Comparing $($route.RouteKey) with $routePath and $($route.Target) with $routeTarget\" +\tif ($route.RouteKey -eq $routePath -and $route.Target -eq $routeTarget) + { + \tWrite-Highlight \"Found the existing path $($route.RouteId)\" + \t$routeToUpdate = $route + break + } +} + +if ($null -eq $routeToUpdate) +{ +\tWrite-Highlight \"The route with the path $routePath pointing to integration $($integrationToUpdate.IntegrationId) does not exist. Creating that one now.\" + $routeResult = aws apigatewayv2 create-route --api-id \"$apiId\" --route-key \"$routePath\" --target \"$routeTarget\" +} +else +{ +\tWrite-Highlight \"The route with the path $routePath pointing to integration $($integrationToUpdate.IntegrationId) already exists. Leaving that alone.\" +} + +$accountInfo = aws sts get-caller-identity +$accountInfo = ($accountInfo | ConvertFrom-JSON) + +if ($apiRoute -notcontains \"*default\") +{ +\t$routeKeyToUse = $apiRouteKey + $statementIdToUse = \"$($ApiGatewayName)$($apiRouteKey.Replace(\"/\", \"-\"))\" +} +else +{ +\t$routeKeyToUse = \"\" + $statementIdToUse = \"$ApiGatewayName\" +} +$sourceArn = \"arn:aws:execute-api:$($apiRegion):$($accountInfo.Account):$($apiId)/*/*$routeKeyToUse\" + +Write-Host \"Source ARN: $sourceArn\" +$hasExistingPolicy = $false +$deleteExistingPolicy = $false + +try +{ +\tWrite-Host \"Getting existing policies\" +\t$existingPolicy = aws lambda get-policy --function-name \"$apiLambdaIntegrationArn\" 2> $null + + if ($LASTEXITCODE -eq 255 -or $LASTEXITCODE -eq 254) + { + \tWrite-Host \"Last exit code was $LASTEXITCODE the policy does not exist\" + \t$hasExistingPolicy = $false + } + else + { + \tWrite-Host \"The policy exists\" + \t$hasExistingPolicy = $true + } + + $existingPolicy = ($existingPolicy | ConvertFrom-JSON) + Write-Host $existingPolicy + + $policyObject = ($existingPolicy.Policy | ConvertFrom-JSON) + +\t$statementList = @($policyObject.Statement) + Write-Host \"Statement List $statementList\" + + foreach ($existingStatement in $statementList) + { + \tWrite-Host $existingStatement + \tWrite-Host \"Comparing $($existingStatement.Sid) with $statementIdToUse and $($existingStatement.Condition.ArnLike.'AWS:SourceArn') with $sourceArn\" + \tif ($existingStatement.Sid -eq \"$statementIdToUse\" -and $existingStatement.Condition.ArnLike.'AWS:SourceArn' -ne \"$sourceArn\") + { + \tWrite-Host \"The policy exists but it is not pointing to the write source arn, recreating it.\" + \t$deleteExistingPolicy = $true + } + } +} +catch +{ +\tWrite-Host \"Error pulling back the policies, this typically means the policy does not exist\" +\t$hasExistingPolicy = $false +} + +if ($hasExistingPolicy -eq $true -and $deleteExistingPolicy -eq $true) +{ +\tWrite-Highlight \"Removing the existing policy $statementIdToUse\" + aws lambda remove-permission --function-name \"$apiLambdaIntegrationArn\" --statement-id \"$statementIdToUse\" +} + +if ($hasExistingPolicy -eq $false -or $deleteExistingPolicy -eq $true) +{ +\tWrite-Highlight \"Adding the policy $statementIdToUse\" +\taws lambda add-permission --function-name \"$apiLambdaIntegrationArn\" --statement-id \"$statementIdToUse\" --action \"lambda:InvokeFunction\" --principal \"apigateway.amazonaws.com\" --qualifier \"$ApiLambdaAlias\" --source-arn \"$sourceArn\" +} + +Write-Highlight \"Setting the output variable 'Octopus.Action[$($stepName)].Output.ApiGatewayEndPoint' to $($apiGatewayToUpdate.ApiEndpoint)\" +Set-OctopusVariable -name \"ApiGatewayEndPoint\" -value \"$($apiGatewayToUpdate.ApiEndpoint)\" + +Write-Highlight \"Setting the output variable 'Octopus.Action[$($stepName)].Output.ApiGatewayId' to $apiId\" +Set-OctopusVariable -name \"ApiGatewayId\" -value \"$apiId\" + +Write-Highlight \"Setting the output variable 'Octopus.Action[$($stepName)].Output.ApiGatewayArn' to $sourceArn\" +Set-OctopusVariable -name \"ApiGatewayArn\" -value \"$sourceArn\" +" + }, + "Parameters": [ + { + "Id": "6a63ef8c-5f0b-4501-beca-fc3839f7f5c9", + "Name": "AWS.Api.Gateway.Name", + "Label": "API Gateway Name", + "HelpText": "Required. + +The name of the API gateway to create the integration and route.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0fb1f1fa-35f6-46b3-bf4a-527ddd98fb80", + "Name": "AWS.Api.Gateway.Account", + "Label": "AWS Account", + "HelpText": "Required. + +The account used to update the API Gateway.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "a42498df-fadd-45d1-9271-78e80edb9623", + "Name": "AWS.Api.Gateway.Region", + "Label": "AWS Region", + "HelpText": "Required. + +The region where the API gateway is in.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "us-east-2|US East (Ohio) +us-east-1|US East (N. Virginia) +us-west-1|US West (N. California) +us-west-2|US West (Oregon) +af-south-1|Africa (Cape Town) +ap-east-1|Asia Pacific (Hong Kong) +ap-south-1|Asia Pacific (Mumbai) +ap-northeast-3|Asia Pacific (Osaka-Local) +ap-northeast-2|Asia Pacific (Seoul) +ap-southeast-1|Asia Pacific (Singapore) +ap-southeast-2|Asia Pacific (Sydney) +ap-northeast-1|Asia Pacific (Tokyo) +ca-central-1|Canada (Central) +eu-central-1|Europe (Frankfurt) +eu-west-1|Europe (Ireland) +eu-west-2|Europe (London) +eu-south-1|Europe (Milan) +eu-west-3|Europe (Paris) +eu-north-1|Europe (Stockholm) +me-south-1|Middle East (Bahrain) +sa-east-1|South America (São Paulo)" + } + }, + { + "Id": "bd681a63-f62c-4dcb-a401-ae43428075ed", + "Name": "AWS.Api.Gateway.Integration.Connection", + "Label": "VPC Connection", + "HelpText": "Optional. + +The VPC connection you wish to use with this link. Supports: + +- VPC Link Name - The name of the VPC link specified when it was created. +- VPC Link Id - The 6-character alphanumeric key assigned by AWS to the VPC Link. + +**Please note**: Sending a value in will change the connection type from `INTERNET` to `VPC_LINK`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "607e787d-d0bd-47b9-984e-2195d58278ed", + "Name": "AWS.Api.Gateway.Route.Key", + "Label": "Route Key", + "HelpText": "Required. + +The route key for the route. Examples: + +- `$default` +- `/signup`", + "DefaultValue": "$default", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f255a0c5-f2e6-4739-af59-a0329c6ddb14", + "Name": "AWS.Api.Gateway.Integration.HttpMethod", + "Label": "HTTP Method", + "HelpText": "Required. + +The HTTP method of the route and integration you wish to create.", + "DefaultValue": "ANY", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "ANY|ANY +POST|POST +PUT|PUT +GET|GET +DELETE|DELETE" + } + }, + { + "Id": "6552c873-6576-4272-a724-a3b257b1b13e", + "Name": "AWS.Api.Gateway.Lambda.Arn", + "Label": "Lambda ARN", + "HelpText": "Required. + +The ARN of the AWS Lambda to connect to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "261a89c2-dc9b-4608-8f76-fcb07385725c", + "Name": "AWS.Api.Gateway.Lambda.Alias", + "Label": "Lambda Alias", + "HelpText": "Required. + +The alias in the lambda function to use when the API Gateway invokes the function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4c281de7-1922-4d78-b16d-9c60cd2d2477", + "Name": "AWS.Api.Gateway.Integration.PayloadFormatVersion", + "Label": "Payload Format Version", + "HelpText": "Required. + +The payload format version specifies the format of the data that API Gateway sends to a Lambda integration, and how API Gateway interprets the response from Lambda.", + "DefaultValue": "2.0", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "1.0|1.0 +2.0|2.0" + } + } + ], + "StepPackageId": "Octopus.AwsRunScript", + "$Meta": { + "ExportedAt": "2022-10-04T18:34:13.219Z", + "OctopusVersion": "2022.3.10594", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "aws" +} diff --git a/step-templates/aws-configure-lambda.json.human b/step-templates/aws-configure-lambda.json.human new file mode 100644 index 000000000..bd92353ef --- /dev/null +++ b/step-templates/aws-configure-lambda.json.human @@ -0,0 +1,694 @@ +{ + "Id": "db4f7564-1b04-41c6-a3a6-aca911236aee", + "Name": "AWS - Configure Lambda Function", + "Description": "Creates or updates a lambda function using code from an S3 bucket or an [container image](https://docs.aws.amazon.com/lambda/latest/dg/lambda-images.html) in the Amazon ECR registry. + +This step uses the following AWS CLI commands to create or update the AWS Lambda function. You will be required to install the AWS CLI on your server/worker for this to work. The AWS CLI is pre-installed on the [dynamic workers](https://octopus.com/docs/infrastructure/workers/dynamic-worker-pools) in Octopus Cloud as well as the provided docker containers for [Execution Containers](https://octopus.com/docs/deployment-process/execution-containers-for-workers). + +- [create-function](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html) +- [get-function](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/get-function.html) +- [publish-version](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/publish-version.html) +- [tag-resource](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/tag-resource.html) +- [untag-resource](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/untag-resource.html) +- [update-function-code](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/update-function-code.html) +- [update-function-configuration](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/update-function-configuration.html) + +## Code Options + +You can specify either a .zip file hosted in S3 or a container hosted in Amazon ECR. + +If you decide on S3 the key is the \"path\" to the .zip file in the S3 bucket. For example: `#{Octopus.Release.Number}/#{Octopus.Environment.Name}/AcceptMessage.zip` + +If you decide on the container, you must include the version number in the URI. Typically, most people use `latest`. + +## Output Variables + +This step template sets the following output variables: + +- `LambdaArn`: The ARN of the Lambda Function +- `PublishedVersion`: The most recent version published (only set when Publish is set to `Yes`).", + "ActionType": "Octopus.AwsRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.AwsAccount.Variable": "#{AWS.Lambda.Account}", + "Octopus.Action.Aws.Region": "#{AWS.Lambda.Region}", + "Octopus.Action.Script.ScriptBody": "$functionName = $OctopusParameters[\"AWS.Lambda.FunctionName\"] +$functionRole = $OctopusParameters[\"AWS.Lambda.FunctionRole\"] +$functionRunTime = $OctopusParameters[\"AWS.Lambda.Runtime\"] +$functionHandler = $OctopusParameters[\"AWS.Lambda.FunctionHandler\"] +$functionMemorySize = $OctopusParameters[\"AWS.Lambda.MemorySize\"] +$functionDescription = $OctopusParameters[\"AWS.Lambda.Description\"] +$functionVPCSubnetId = $OctopusParameters[\"AWS.Lambda.VPCSubnetIds\"] +$functionVPCSecurityGroupId = $OctopusParameters[\"AWS.Lambda.VPCSecurityGroupIds\"] +$functionEnvironmentVariables = $OctopusParameters[\"AWS.Lambda.EnvironmentVariables\"] +$functionEnvironmentVariablesKey = $OctopusParameters[\"AWS.Lambda.EnvironmentVariablesKey\"] +$functionTimeout = $OctopusParameters[\"AWS.Lambda.FunctionTimeout\"] +$functionTags = $OctopusParameters[\"AWS.Lambda.Tags\"] +$functionFileSystemConfig = $OctopusParameters[\"AWS.Lambda.FileSystemConfig\"] +$functionDeadLetterConfig = $OctopusParameters[\"AWS.Lambda.DeadLetterConfig\"] +$functionTracingConfig = $OctopusParameters[\"AWS.Lambda.TracingConfig\"] +$functionPublishOption = $OctopusParameters[\"AWS.Lambda.Publish\"] + +$functionReleaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$functionRunbookRun = $OctopusParameters[\"Octopus.RunbookRun.Id\"] +$stepName = $OctopusParameters[\"Octopus.Step.Name\"] + +$regionName = $OctopusParameters[\"AWS.Lambda.Region\"] +$functionCodeS3Bucket = $OctopusParameters[\"AWS.Lambda.Code.S3Bucket\"] +$functionCodeS3Key = $OctopusParameters[\"AWS.Lambda.Code.S3Key\"] +$functionCodeS3Version = $OctopusParameters[\"AWS.Lambda.Code.S3ObjectVersion\"] +$functionCodeImageUri = $OctopusParameters[\"AWS.Lambda.Code.ImageUri\"] +$functionCodeVersion = $OctopusParameters[\"AWS.Lambda.Code.Version\"] + +if ([string]::IsNullOrWhiteSpace($functionName)) +{ +\tWrite-Error \"The parameter Function Name is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionRole)) +{ +\tWrite-Error \"The parameter Role is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionCodeS3Bucket) -and [string]::IsNullOrWhiteSpace($functionCodeImageUri)) +{ +\tWrite-Error \"You must specify either a S3 Bucket or an Image URI for the lambda to run.\" + exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionCodeS3Bucket) -and [string]::IsNullOrWhiteSpace($functionCodeS3Key) -eq $false) +{ +\tWrite-Error \"The S3 Key was specified but the S3 bucket was not specified, the S3 Bucket parameter is required when the key is specified.\" + exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionRunTime)) +{ +\tWrite-Error \"The parameter Run Time is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionHandler)) +{ +\tWrite-Error \"The parameter Handler is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionPublishOption)) +{ +\tWrite-Error \"The parameter Publish is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionReleaseNumber) -eq $false) +{ + $deployVersionTag = \"Octopus-Release=$functionReleaseNumber\" +} +else +{ +\t$deployVersionTag = \"Octopus-Runbook-Run=$functionRunbookRun\" +} + +Write-Host \"Function Name: $functionName\" +Write-Host \"Function Role: $functionRole\" +Write-Host \"Function Runtime: $functionRunTime\" +Write-Host \"Function Handler: $functionHandler\" +Write-Host \"Function Memory Size: $functionMemorySize\" +Write-Host \"Function Description: $functionDescription\" +Write-Host \"Function Subnet Ids: $functionVPCSubnetId\" +Write-Host \"Function Security Group Ids: $functionVPCSecurityGroupId\" +Write-Host \"Function Environment Variables: $functionEnvironmentVariables\" +Write-Host \"Function Environment Variables Key: $functionEnvironmentVariablesKey\" +Write-Host \"Function Timeout: $functionTimeout\" +Write-Host \"Function Tags: $functionTags\" +Write-Host \"Function File System Config: $functionFileSystemConfig\" +Write-Host \"Function Dead Letter Config: $functionDeadLetterConfig\" +Write-Host \"Function Tracing Config: $functionTracingConfig\" +Write-Host \"Function S3 Bucket: $functionCodeS3Bucket\" +Write-host \"Function S3 Key: $functionCodeS3Key\" +Write-Host \"Function S3 Object Version: $functionCodeS3Version\" +Write-Host \"Function Image Uri: $functionCodeImageUri\" +Write-Host \"Function Publish: $functionPublishOption\" + +Write-Host \"Attempting to find the function $functionName in the region $regionName\" +$hasExistingFunction = $true +try +{ +\t$existingFunction = aws lambda get-function --function-name \"$functionName\" + + Write-Host \"The exit code from the lookup was $LASTEXITCODE\" + if ($LASTEXITCODE -eq 255 -or $LASTEXITCODE -eq 254) + { + \t$hasExistingFunction = $false + } + + $existingFunction = $existingFunction | ConvertFrom-Json +} +catch +{ +\tWrite-Host \"The function was not found\" +\t$hasExistingFunction = $false +} + +Write-Host \"Existing functions: $hasExistingFunction\" +Write-Host $existingFunction + +$aliasInformation = $null +if ($hasExistingFunction -eq $false) +{ +\t$functionCodeLocation = \"ImageUri=$functionCodeImageUri\" + if ([string]::IsNullOrWhiteSpace($functionCodeS3Bucket) -eq $false) + { + \tWrite-Host \"S3 Bucket Specified, using that as the code source.\" + $functionCodeLocation = \"S3Bucket=$functionCodeLocation\" + + if ([string]::IsNullOrWhiteSpace($functionCodeS3Key) -eq $false) + { + \tWrite-Host \"S3 Key Specified\" + \t$functionCodeLocation += \",S3Key=$functionCodeS3Key\" + } + + if ([string]::IsNullOrWhiteSpace($functionCodeS3Version) -eq $false) + { + \tWrite-Host \"Object Version Specified\" + \t$functionCodeLocation += \",S3Key=$functionCodeS3Version\" + } + } + +\tWrite-Highlight \"Creating $functionName in $regionName\" +\t$functionInformation = aws lambda create-function --function-name \"$functionName\" --code \"$functionCodeLocation\" --handler $functionHandler --runtime $functionRuntime --role $functionRole --memory-size $functionMemorySize +} +else +{ +\tif ([string]::IsNullOrWhiteSpace($functionCodeS3Bucket) -eq $false) + { + \tWrite-Host \"S3 Bucket specified, updating the function $functionName to use that.\" + + \tif ([string]::IsNullOrWhiteSpace($functionCodeS3Key) -eq $false -and [string]::IsNullOrWhiteSpace($functionCodeS3Version) -eq $false) + { + \tWrite-host \"Both the S3 Key and the Object Version specified\" +\t\t $updatedConfig = aws lambda update-function-code --function-name \"$functionName\" --s3-bucket \"$functionCodeS3Bucket\" --s3-key \"$functionCodeS3Key\" --s3-object-version \"$functionCodeS3Version\" + } + elseif ([string]::IsNullOrWhiteSpace($functionCodeS3Key) -eq $false -and [string]::IsNullOrWhiteSpace($functionCodeS3Version) -eq $true) + { + \tWrite-host \"Only the S3 key was specified\" +\t\t $updatedConfig = aws lambda update-function-code --function-name \"$functionName\" --s3-bucket \"$functionCodeS3Bucket\" --s3-key \"$functionCodeS3Key\" + } + else + { + \tWrite-host \"Only the Object Version was specified\" +\t\t $updatedConfig = aws lambda update-function-code --function-name \"$functionName\" --s3-bucket \"$functionCodeS3Bucket\" --s3-object-version \"$functionCodeS3Version\" + } + } + else + { + \tWrite-Host \"Image URI specified, updating the function $functionName to use that.\" +\t $updatedConfig = aws lambda update-function-code --function-name \"$functionName\" --image-uri \"$functionCodeImageUri\" + } + + Write-Highlight \"Updating the $functionName base configuration\" + $functionInformation = aws lambda update-function-configuration --function-name \"$functionName\" --role $functionRole --handler $functionHandler --runtime $functionRuntime --memory-size $functionMemorySize +} + +$functionInformation = $functionInformation | ConvertFrom-JSON +$functionArn = $functionInformation.FunctionArn + +Write-Host \"Function ARN: $functionArn\" + +if ([string]::IsNullOrWhiteSpace($functionEnvironmentVariables) -eq $false) +{ +\tWrite-Highlight \"Environment variables specified, updating environment variables configuration for $functionName\" +\t$environmentVariables = \"Variables={$functionEnvironmentVariables}\" + + if ([string]::IsNullOrWhiteSpace($functionEnvironmentVariablesKey) -eq $true) + { + \t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --environment \"$environmentVariables\" + } + else + { + \t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --environment \"$environmentVariables\" --kms-key-arn \"$functionEnvironmentVariablesKey\" + } +} + +if ([string]::IsNullOrWhiteSpace($functionTimeout) -eq $false) +{ +\tWrite-Highlight \"Timeout specified, updating timeout configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --timeout \"$functionTimeout\" +} + +if ([string]::IsNullOrWhiteSpace($functionTags) -eq $false) +{ +\tWrite-Highlight \"Tags specified, updating tags configuration for $functionName\" +\t$updatedConfig = aws lambda tag-resource --resource \"$functionArn\" --tags \"$functionTags\" +} + +if ([string]::IsNullOrWhiteSpace($deployVersionTag) -eq $false) +{ +\tWrite-Highlight \"Deploy version tag found with value of $deployVersionTag, updating tags configuration for $functionName\" + aws lambda untag-resource --resource \"$functionArn\" --tag-keys \"Octopus-Release\" \"Octopus-Runbook-Run\" +\t$updatedConfig = aws lambda tag-resource --resource \"$functionArn\" --tags \"$deployVersionTag\" +} + +if ([string]::IsNullOrWhiteSpace($functionVPCSubnetId) -eq $false -and [string]::IsNullOrWhiteSpace($functionVPCSecurityGroupId) -eq $false) +{ +\tWrite-Highlight \"VPC subnets and security group specified, updating vpc configuration for $functionName\" +\t$vpcConfig = \"SubnetIds=$functionVPCSubnetId,SecurityGroupIds=$functionVPCSecurityGroupId\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --vpc-config \"$vpcConfig\" +} + +if ([string]::IsNullOrWhiteSpace($functionDescription) -eq $false) +{ +\tWrite-Highlight \"Description specified, updating description configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --description \"$functionDescription\"\t +} + +if ([string]::IsNullOrWhiteSpace($functionFileSystemConfig) -eq $false) +{ +\tWrite-Highlight \"File System Config specified, updating file system configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --file-system-configs \"$functionFileSystemConfig\"\t +} + +if ([string]::IsNullOrWhiteSpace($functionDeadLetterConfig) -eq $false) +{ +\tWrite-Highlight \"Dead Letter specified, updating dead letter configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --dead-letter-config \"$functionDeadLetterConfig\"\t +} + +if ([string]::IsNullOrWhiteSpace($functionTracingConfig) -eq $false) +{ +\tWrite-Highlight \"Tracing config specified, updating tracing configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --tracing-config \"$functionTracingConfig\"\t +} + +Write-Host $updatedConfig | ConvertFrom-JSON + +if ($functionPublishOption -eq \"Yes\") +{ +\t$functionVersionNumber = $functionCodeVersion +\tif ([string]::IsNullOrWhiteSpace($functionCodeVersion) -eq $true) + { + \t$functionVersionNumber = $functionReleaseNumber + } + +\tWrite-Highlight \"Publishing the function with the description $functionVersionNumber to create a snapshot of the current code and configuration of this function in AWS.\" +\t$publishedVersion = aws lambda publish-version --function-name \"$functionArn\" --description \"$functionVersionNumber\" + + $publishedVersion = $publishedVersion | ConvertFrom-JSON + + Write-Highlight \"Setting the output variable 'Octopus.Action[$($stepName)].Output.PublishedVersion' to $($publishedVersion.Version)\" + Set-OctopusVariable -name \"PublishedVersion\" -value \"$($publishedVersion.Version)\" +} + +Write-Highlight \"Setting the output variable 'Octopus.Action[$($stepName)].Output.LambdaArn' to $functionArn\" +Set-OctopusVariable -name \"LambdaArn\" -value \"$functionArn\" + +Write-Highlight \"AWS Lambda $functionName successfully deployed.\"", + "Octopus.Action.SubstituteInFiles.Enabled": "True", + "Octopus.Action.SubstituteInFiles.OutputEncoding": "utf-8", + "OctopusUseBundledTooling": "False" + }, + "Parameters": [ + { + "Id": "bf72bc3e-3ce6-4b63-b23f-1171b5cc72dd", + "Name": "AWS.Lambda.FunctionName", + "Label": "Function Name", + "HelpText": "Required. + +The name of the function to create or update. See [documentation](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html#options) + +Examples: +- Function name - my-function . +- Function ARN - arn:aws:lambda:us-west-2:123456789012:function:my-function . +- Partial ARN - 123456789012:function:my-function . + +The length constraint applies only to the full ARN. If you specify only the function name, it is limited to 64 characters in length. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "13dcec09-00f8-4af0-80e4-23bcb47eaf17", + "Name": "AWS.Lambda.Account", + "Label": "AWS Account", + "HelpText": "Required. + +The AWS Account with permissions to create / update AWS Lambdas. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "8fd7ff24-7557-4f96-a809-ce611c473b13", + "Name": "AWS.Lambda.Region", + "Label": "Region", + "HelpText": "Required. + +The region where the function will live.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "us-east-2|US East (Ohio) +us-east-1|US East (N. Virginia) +us-west-1|US West (N. California) +us-west-2|US West (Oregon) +af-south-1|Africa (Cape Town) +ap-east-1|Asia Pacific (Hong Kong) +ap-south-1|Asia Pacific (Mumbai) +ap-northeast-3|Asia Pacific (Osaka-Local) +ap-northeast-2|Asia Pacific (Seoul) +ap-southeast-1|Asia Pacific (Singapore) +ap-southeast-2|Asia Pacific (Sydney) +ap-northeast-1|Asia Pacific (Tokyo) +ca-central-1|Canada (Central) +eu-central-1|Europe (Frankfurt) +eu-west-1|Europe (Ireland) +eu-west-2|Europe (London) +eu-south-1|Europe (Milan) +eu-west-3|Europe (Paris) +eu-north-1|Europe (Stockholm) +me-south-1|Middle East (Bahrain) +sa-east-1|South America (São Paulo)" + } + }, + { + "Id": "d45499b4-5f4a-4bae-a4b9-336e97a75cdc", + "Name": "AWS.Lambda.FunctionRole", + "Label": "Function Role", + "HelpText": "Required. + +The Amazon Resource Name (ARN) of the function’s execution role. This role must exist prior to this step is run. See [documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html) for more detail on creating an execution role.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e4426d6c-2814-451c-9575-d2d216ac2778", + "Name": "AWS.Lambda.Code.S3Bucket", + "Label": "S3 Bucket", + "HelpText": "Optional. + +The S3 bucket where the code is stored. The bucket can be in a different Amazon Web Services account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bc282ee2-2341-4b36-a1fc-181dbf72692f", + "Name": "AWS.Lambda.Code.S3Key", + "Label": "S3 Key", + "HelpText": "Optional. + +The Amazon S3 key of the deployment package. You must upload the function as a .zip file. The key must be the path to the .zip file. + +For example: `#{Octopus.Release.Number}/#{Octopus.Environment.Name}/AcceptMessage.zip` + +**Please note**: If this is specified then the `S3 Bucket` is required.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "49002ff9-f4f4-4114-a363-2eeedd62f1a7", + "Name": "AWS.Lambda.Code.S3ObjectVersion", + "Label": "S3 Object Version", + "HelpText": "Optional. + +For versioned objects, the version of the deployment package object to use.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "02f25886-8101-4fe2-b9e4-844ea3498522", + "Name": "AWS.Lambda.Code.ImageUri", + "Label": "Image URI", + "HelpText": "Optional. + +URI of a [container image](https://docs.aws.amazon.com/lambda/latest/dg/lambda-images.html) in the Amazon ECR registry. + +You must include the version number in the URI, or `latest` to use the latest image. + +**Please note**: You can either run the lambda from an S3 bucket OR an image URI. If the S3 bucket parameter is specified this value will be ignored.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5d930ed5-1e7e-4b5c-8bc4-26ab5e0aeff5", + "Name": "AWS.Lambda.Code.Version", + "Label": "Code Version", + "HelpText": "Optional. + +The version of the package being uploaded to S3 or Amazon ECR in this deployment. If you are publishing the function with this deployment, this will be what is added to the description. + +**Please Note**: If this is left blank the release number will be used in the published description. + +You can use a variable to reference the package version from a previous step. + +`#{Octopus.Action[_name_].Package.PackageVersion}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "926b93c6-a47a-4899-9865-e7329b93b4b8", + "Name": "AWS.Lambda.Runtime", + "Label": "Runtime", + "HelpText": "Required. + +The runtime of the AWS Lambda. See [documentation](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html#options) for more details on what runtimes are supported.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "nodejs|nodejs +nodejs4.3|nodejs4.3 +nodejs4.3-edge|nodejs4.3-edge +nodejs6.10|nodejs6.10 +nodejs8.10|nodejs8.10 +nodejs10.x|nodejs10.x +nodejs12.x|nodejs12.x +nodejs14.x|nodejs14.x +java8|java8 +java8.al2|java8.al2 +java11|java11 +python2.7|python2.7 +python3.6|python3.6 +python3.7|python3.7 +python3.8|python3.8 +python3.9|python3.9 +dotnetcore1.0|dotnetcore1.0 +dotnetcore2.0|dotnetcore2.0 +dotnetcore2.1|dotnetcore2.1 +dotnetcore3.1|dotnetcore3.1 +nodejs4.3-edge|nodejs4.3-edge +go1.x|go1.x +ruby2.5|ruby2.5 +ruby2.7|ruby2.7 +provided|provided +provided.al2|provided.al2" + } + }, + { + "Id": "1b8bfb82-3736-4d9a-8b05-a39319eb5735", + "Name": "AWS.Lambda.FunctionHandler", + "Label": "Handler", + "HelpText": "Required. + +The name of the method within your code that Lambda calls to execute your function. The format includes the file name. It can also include namespaces and other qualifiers, depending on the runtime. For more information, see [Programming Model](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-features.html)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "68428e8e-abc3-4f29-a5bb-fe635281d073", + "Name": "AWS.Lambda.MemorySize", + "Label": "Memory Size", + "HelpText": "Required. + +The amount of memory that your function has access to. Increasing the function’s memory also increases its CPU allocation. The default value is 128 MB. The value must be a multiple of 64 MB.", + "DefaultValue": "128", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "948280bb-50af-495b-9d1d-2f7567a0b0cc", + "Name": "AWS.Lambda.Description", + "Label": "Description", + "HelpText": "Optional. + +A description of the function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ed2ab9bb-d8a3-4ab4-a576-36b6c0a8f75d", + "Name": "AWS.Lambda.VPCSubnetIds", + "Label": "VPC Subnet Ids", + "HelpText": "Optional. + +Format: `SubnetId1,SubnetId2` + +For network connectivity to AWS resources in a VPC, specify a list of security groups and subnets in the VPC. When you connect a function to a VPC, it can only access resources and the internet through that VPC. For more information, see [VPC Settings](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8c2793a7-1a88-40a2-be28-3a38c7b40658", + "Name": "AWS.Lambda.VPCSecurityGroupIds", + "Label": "VPC Security Group Ids", + "HelpText": "Optional. + +Format: `SecurityGroupId1,SecurityGroupId2` + +For network connectivity to AWS resources in a VPC, specify a list of security groups and subnets in the VPC. When you connect a function to a VPC, it can only access resources and the internet through that VPC. For more information, see [VPC Settings](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ba3edded-1e19-47c4-990a-ebdf4eb0bcca", + "Name": "AWS.Lambda.EnvironmentVariables", + "Label": "Environment Variables", + "HelpText": "Optional. + +Format: `KeyName1=string,KeyName2=string` + +Environment variables that are accessible from function code during execution.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "58d76440-e4f5-46fb-a095-84aedd904a18", + "Name": "AWS.Lambda.EnvironmentVariablesKey", + "Label": "Environment Variables Encryption Key", + "HelpText": "Optional. + +The ARN of the AWS Key Management Service (AWS KMS) key that’s used to encrypt your function’s environment variables. If it’s not provided, AWS Lambda uses a default service key. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5b9b3111-5349-49e6-ab0d-f386a53bdd7c", + "Name": "AWS.Lambda.FunctionTimeout", + "Label": "Timeout", + "HelpText": "Optional. + +The amount of time that Lambda allows a function to run before stopping it. The default is 3 seconds. The maximum allowed value is 900 seconds.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "567f5eeb-e174-4c8f-8b17-36bd9457ea29", + "Name": "AWS.Lambda.Tags", + "Label": "Tags", + "HelpText": "Optional. + +Format: `KeyName1=string,KeyName2=string` + +A list of tags to apply to the function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "297f4f8e-3837-43a3-b844-a0e0d02e9d5b", + "Name": "AWS.Lambda.FileSystemConfig", + "Label": "File System Config", + "HelpText": "Optional. + +Format: `Arn=string,LocalMountPath=string` + +Connection settings for an Amazon EFS file system.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c720a63a-7b77-4a13-b6ed-6d44126e9372", + "Name": "AWS.Lambda.TracingConfig", + "Label": "Tracing Config", + "HelpText": "Optional. + +Format: `Mode=string` + +Set Mode to Active to sample and trace a subset of incoming requests with AWS X-Ray.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "92fd8a1f-e681-4e1c-b382-3df1de12194e", + "Name": "AWS.Lambda.DeadLetterConfig", + "Label": "Dead Letter Config", + "HelpText": "Optional. + +Format: `TargetArn=string` + +A dead letter queue configuration that specifies the queue or topic where Lambda sends asynchronous events when they fail processing. For more information, see [Dead Letter Queues](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html#dlq). +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "67f8da1a-08f1-4cde-a60f-238d1fb08c98", + "Name": "AWS.Lambda.Publish", + "Label": "Publish", + "HelpText": "Required. + +Creates a [version](https://docs.aws.amazon.com/lambda/latest/dg/versioning-aliases.html) from the current code and configuration of a function. Use versions to create a snapshot of your function code and configuration that doesn’t change. + +**Important**: Lambda doesn’t publish a version if the function’s configuration and code haven’t changed since the last version. Use UpdateFunctionCode or UpdateFunctionConfiguration to update the function before publishing a version.", + "DefaultValue": "Yes", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Yes|Yes +No|No" + } + } + ], + "StepPackageId": "Octopus.AwsRunScript", + "$Meta": { + "ExportedAt": "2021-11-04T14:40:20.783Z", + "OctopusVersion": "2021.3.7807", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "bobjwalker", + "Category": "aws" +} diff --git a/step-templates/aws-create-a-route-53-resource-record.json.human b/step-templates/aws-create-a-route-53-resource-record.json.human new file mode 100644 index 000000000..71a091339 --- /dev/null +++ b/step-templates/aws-create-a-route-53-resource-record.json.human @@ -0,0 +1,353 @@ +{ + "Id": "9271327d-6e8a-49d1-881a-3925c75aef56", + "Name": "AWS - Create a Route 53 Resource Record", + "Description": "This step will Update, Create or Delete a Resource Record from an Route 53 hosted Domain Name. + +Works well with the \"_AWS - Launch EC2 Instance_\" Community Step Template. + +[AWS Tools for Windows PowerShell](http://aws.amazon.com/powershell/) must be installed on the Server/Target you plan on running this step template on.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +param( + [string]$odZoneId, + [string]$odAction, + [string]$odName, + [string]$odResourceAddress, + [string]$odType, + [string]$odTtl, + [string]$odWait, + [string]$odComment, + [string]$odAccessKey, + [string]$odSecretKey, + [switch]$whatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if (!$result -or $result -eq $null) { + if ($Default) { + $result = $Default + } elseif ($Required) { + throw \"Missing parameter value $Name\" + } + } + + return $result +} + +# More custom functions would go here + +& { + param( + [string]$odZoneId, + [string]$odAction, + [string]$odName, + [string]$odResourceAddress, + [string]$odType, + [string]$odTtl, + [string]$odWait, + [string]$odComment, + [string]$odAccessKey, + [string]$odSecretKey + ) + + # If AWS key's are not provided as params, attempt to retrieve them from Environment Variables + if ($odAccessKey -or $odSecretKey) { + Set-AWSCredentials -AccessKey $odAccessKey -SecretKey $odSecretKey -StoreAs default + } elseif (([Environment]::GetEnvironmentVariable(\"AWS_ACCESS_KEY\", \"Machine\")) -or ([Environment]::GetEnvironmentVariable(\"AWS_SECRET_KEY\", \"Machine\"))) { + Set-AWSCredentials -AccessKey ([Environment]::GetEnvironmentVariable(\"AWS_ACCESS_KEY\", \"Machine\")) -SecretKey ([Environment]::GetEnvironmentVariable(\"AWS_SECRET_KEY\", \"Machine\")) -StoreAs default + } else { + throw \"AWS API credentials were not available/provided.\" + } + + If($odAction -ne \"CREATE\" -and $odAction -ne \"DELETE\" -and $odAction -ne \"UPSERT\") { throw \"Invalid Action provided. Please use CREATE, DELETE or UPSERT.\" } + + if ($odName -notmatch '.+?\\.$') { $odName += '.' } + + $change = (New-Object Amazon.Route53.Model.Change) + $change.Action = $odAction + $change.ResourceRecordSet = (New-Object Amazon.Route53.Model.ResourceRecordSet) + $change.ResourceRecordSet.Name = $odName + $change.ResourceRecordSet.Type = $odType + $change.ResourceRecordSet.TTL = $odTtl + + if ($odResourceAddress -like '*,*') { + $($odResourceAddress -split ',') | Foreach-Object { + $change.ResourceRecordSet.ResourceRecords.Add(@{Value=$($_)}) + } + } else { + $change.ResourceRecordSet.ResourceRecords.Add(@{Value=$odResourceAddress}) + } + + Write-Output (\"------------------------------\") + Write-Output (\"Checking if Resource Record Set Exists:\") + Write-Output (\"------------------------------\") + + $resourceRecordSetObj = $(Get-R53ResourceRecordSet -HostedZoneId $odZoneId).ResourceRecordSets | Where {$_.Name -eq $odName} + $resourceRecordSetCount = ($resourceRecordSetObj | measure).Count + + if ($odAction -eq \"DELETE\") { + if ($resourceRecordSetCount -gt 0) { + Write-Output (\"The record '$($odName)' exists, deleting...\") + } else { + Write-Output (\"Cannot Delete: The record '$($odName)' does not exist, skipping...\") + Exit + } + } elseif ($odAction -eq \"CREATE\") { + if ($resourceRecordSetCount -gt 0) { + Write-Output (\"Cannot Create: The record '$($odName)' already exists, skipping...\") + Exit + } else { + Write-Output (\"The record '$($odName)' does not exist, creating record...\") + } + } elseif ($odAction -eq \"UPSERT\") { + if ($resourceRecordSetCount -gt 0) { + Write-Output (\"The record '$($odName)' already exists, updating record...\") + } else { + Write-Output (\"The record '$($odName)' does not exist, creating record...\") + } + } else { throw \"OMG - Unexpected result\" } + + $params = @{ + HostedZoneId=$odZoneId + ChangeBatch_Comment=$odComment + ChangeBatch_Change=$change + } + + Write-Output (\"------------------------------\") + Write-Output (\"Listing DNS change/s to be made:\") + Write-Output (\"------------------------------\") + + $($params.ChangeBatch_Change) | Foreach-Object { + $resourceRecords=\" | \" + + $($_.ResourceRecordSet.ResourceRecords) | Foreach-Object { + $resourceRecords += $_.Value + \",\" + } + + Write-Output ($($_.Action.Value) + \" | \" + $($_.ResourceRecordSet.Name) + \" | \" + $($_.ResourceRecordSet.Type) + $($resourceRecords -replace \".$\")) + } + + + + $timeout = new-timespan -Seconds 30 + $sw = [diagnostics.stopwatch]::StartNew() + $attempt = 1 + + while ($true) { + try { + $result = Edit-R53ResourceRecordSet @params + break + } + catch [Amazon.Route53.AmazonRoute53Exception] { + Write-Output (\"$($_.Exception.errorcode)-$($_.Exception.Message)\") + + if ($attempt -eq 3) { + throw $_.Exception.errorcode + '-' + $_.Exception.Message + } + + if ($sw.elapsed -gt $timeout) {throw \"Timed out waiting for 'Edit-R53ResourceRecordSet' to succeed\"} + + Write-Output (\"Attempt no.$($attempt) failed - Trying again in 5 seconds...\") + Sleep -Seconds 5 + + $attempt++ + } + } + + + Write-Output (\"------------------------------\") + Write-Output (\"Checking the R53 Change status:\") + Write-Output (\"------------------------------\") + + $timeout = new-timespan -Seconds 120 + $sw = [diagnostics.stopwatch]::StartNew() + + while ($true) { + $currentState = (Get-R53Change -Id $result.Id).Status + + if ($currentState -eq \"INSYNC\") {break} + if ([bool]($odWait -eq $false)) {break} + + Write-Output (\"$(Get-Date) | Waiting for R53 Change '$($result.Id)' to transition from state: $currentState\") + + if ($sw.elapsed -gt $timeout) {throw \"Timed out waiting for desired state\"} + + Sleep -Seconds 5 + } + Write-Output (\"$(Get-Date) | R53 Change state: $currentState\") + } ` + (Get-Param 'odZoneId' -Required) ` + (Get-Param 'odAction' -Required) ` + (Get-Param 'odName' -Required) ` + (Get-Param 'odResourceAddress' -Required) ` + (Get-Param 'odType' -Required) ` + (Get-Param 'odTtl' -Required) ` + (Get-Param 'odWait' -Required) ` + (Get-Param 'odComment') ` + (Get-Param 'odAccessKey') ` + (Get-Param 'odSecretKey')" + }, + "Parameters": [ + { + "Id": "6ec4f68e-02b7-4015-88aa-34a2a7dd8568", + "Name": "odZoneId", + "Label": "Hosted Zone ID", + "HelpText": "The Hosted Zone ID of the Domain Name you would like to work with.", + "DefaultValue": "Zxxxxxxxxxxxxx", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2e0fa53b-d85a-4afd-9fb8-a44ddd28b598", + "Name": "odAction", + "Label": "Action", + "HelpText": "The Action you would like to perform - Update, Create or Delete a Resource Record from an Route 53 hosted Domain Name.", + "DefaultValue": "UPSERT", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "UPSERT|Update a Resource Record if exists, or Create a new one +CREATE|Create a Resource Record +DELETE|Delete a Resource Record" + }, + "Links": {} + }, + { + "Id": "843ef155-8583-417f-90dd-6e880ca175e2", + "Name": "odName", + "Label": "Name", + "HelpText": "The Name is the full Domain Name Address you would like to work with. +For example: +- database01.example.com", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "956cb942-2d09-4bb4-b0a0-4c855d4f9b29", + "Name": "odResourceAddress", + "Label": "Resource Address", + "HelpText": "The Address (IP or Domain Name) of the Resource you would like the Resource Record to point to. For example: +- 8.8.8.8 _(e.g. A Record)_ +- webserver01.example.com _(e.g. CNAME Record)_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "d2734830-3915-4759-9b06-5a174540c337", + "Name": "odType", + "Label": "Record Type", + "HelpText": "The Type of Resource Record you would like to work with. For example: +- A _(e.g. 8.8.8.8)_ +- CNAME _(e.g. webserver01.example.com)_ + +Further Reading: +[https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html)", + "DefaultValue": "A", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a62ded34-e6f4-4736-bea6-3c7294a5e6dd", + "Name": "odTtl", + "Label": "TTL (Time to Live)", + "HelpText": "The amount of time (in seconds) a Resource Record should be cached for. + +Further Reading: +[https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-values-basic.html#rrsets-values-basic-ttl](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-values-basic.html#rrsets-values-basic-ttl)", + "DefaultValue": "600", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "d14fbeaf-907b-4f5f-9efc-04a5ae2031f8", + "Name": "odWait", + "Label": "Wait for Sync", + "HelpText": "If the Step should wait for the Resource Record to sync to all Name Servers before proceeding to the next Step.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "d009432c-d746-42d1-b214-42ba78760d5b", + "Name": "odComment", + "Label": "Comment (Optional)", + "HelpText": "The Comment provided in this field is for auditing purposes, and is not visible in the AWS Route 53 Web Interface (it's only accessible via the API).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "078ab1ac-4560-4241-8127-29bb293622b5", + "Name": "odAccessKey", + "Label": "Access Key (Kind-of Optional)", + "HelpText": "An Access Key with permissions to create the desired EC2 instance. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"AWS\\_ACCESS\\_KEY\". + +Further Reading: +[https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html](https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "0e06989b-4f09-4652-8c92-398d078c3cb5", + "Name": "odSecretKey", + "Label": "Secret Key (Kind-of Optional)", + "HelpText": "The Secret Key associated with the above Access Key. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"AWS\\_SECRET\\_KEY\". + +Further Reading: +[https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html](https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "tclydesdale", + "$Meta": { + "ExportedAt": "2018-01-31T13:17:01.348Z", + "OctopusVersion": "4.1.9", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-create-a-security-group.json.human b/step-templates/aws-create-a-security-group.json.human new file mode 100644 index 000000000..6ea13f3da --- /dev/null +++ b/step-templates/aws-create-a-security-group.json.human @@ -0,0 +1,349 @@ +{ + "Id": "051ee152-1ef8-4937-a616-b56eb94dad25", + "Name": "AWS - Create a Security Group", + "Description": "This step will Create a Security Group within a Virtual Private Cloud (VPC). + +Works well with the \"_AWS - Launch EC2 Instance_\" Community Step Template. + +[AWS Tools for Windows PowerShell](http://aws.amazon.com/powershell/) must be installed on the Server/Target you plan on running this step template on. If you would like to add comments to rules, then you will need at least version 3.3.42.0 installed.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +param( + [string]$odGroupName, + [string]$odGroupDescription, + [string]$odVpcId, + [string]$odRules, + [string]$odInstanceId, + [string]$odAccessKey, + [string]$odSecretKey, + [switch]$whatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if (!$result -or $result -eq $null) { + if ($Default) { + $result = $Default + } elseif ($Required) { + throw \"Missing parameter value $Name\" + } + } + + return $result +} + +& { + param( + [string]$odGroupName, + [string]$odGroupDescription, + [string]$odVpcId, + [string]$odRules, + [string]$odInstanceId, + [string]$odAccessKey, + [string]$odSecretKey + ) + + # If AWS key's are not provided as params, attempt to retrieve them from Environment Variables + if ($odAccessKey -or $odSecretKey) { + Set-AWSCredentials -AccessKey $odAccessKey -SecretKey $odSecretKey -StoreAs default + } elseif (([Environment]::GetEnvironmentVariable(\"AWS_ACCESS_KEY\", \"Machine\")) -or ([Environment]::GetEnvironmentVariable(\"AWS_SECRET_KEY\", \"Machine\"))) { + Set-AWSCredentials -AccessKey ([Environment]::GetEnvironmentVariable(\"AWS_ACCESS_KEY\", \"Machine\")) -SecretKey ([Environment]::GetEnvironmentVariable(\"AWS_SECRET_KEY\", \"Machine\")) -StoreAs default + } else { + throw \"AWS API credentials were not available/provided.\" + } + + + + Write-Output (\"------------------------------\") + Write-Output (\"Checking the Security Group:\") + Write-Output (\"------------------------------\") + + $filterArray = @() + $filterArray += @{ name=\"vpc-id\";value=$odVpcId } + $filterArray += @{ name=\"group-name\";value=$odGroupName } + $filterArray += @{ name=\"description\";value=$odGroupDescription } + + $securityGroupObj = (Get-EC2SecurityGroup -Filter $filterArray) + $securityGroupCount = ($securityGroupObj | measure).Count + $securityGroupId = $null + + if ($securityGroupCount -gt 1) { + throw \"More than one security group exists with the same vpcid/name/description - I don't know what to do!?\" + } + elseif ($securityGroupCount -eq 1) { + Write-Output (\"$(Get-Date) | Security group already exists...\") + + $securityGroupId = ($securityGroupObj).GroupId + } + elseif ($securityGroupCount -eq 0) { + Write-Output (\"$(Get-Date) | Creating security group...\") + + $securityGroupId = (New-EC2SecurityGroup -VpcId $odVpcId -GroupName $odGroupName -GroupDescription $odGroupDescription) + + Write-Output (\"Security Group Created: $($securityGroupId)\") + } + + if ($securityGroupId -and $OctopusParameters) { + Set-OctopusVariable -name \"GroupId\" -value $securityGroupId + } + + if ($odRules) { + (ConvertFrom-StringData $odRules).GetEnumerator() | Foreach-Object { + $ruleSplit = $_.Value.Split(\"|\") + + $direction = $ruleSplit[0] + $ipProtocol = $ruleSplit[1] + $fromPort = $ruleSplit[2] + $toPort = $ruleSplit[3] + $ipRanges = $ruleSplit[4] + + Write-Output (\"------------------------------\") + Write-Output (\"Creating new $($direction) rule for Security Group $($securityGroupId):\") + Write-Output (\"------------------------------\") + + $failCount = 0 + while ($true) { + try { + if ($direction -eq \"Ingress\") { + $check_ipPermissionObj = ($securityGroupObj | Select -ExpandProperty IpPermissions | ? {$_.IpProtocol -eq $ipProtocol -and $_.FromPort -eq $fromPort -and $_.ToPort -eq $toPort}) + } + elseif ($direction -eq \"Egress\") { + $check_ipPermissionObj = ($securityGroupObj | Select -ExpandProperty IpPermissionsEgress | ? {$_.IpProtocol -eq $ipProtocol -and $_.FromPort -eq $fromPort -and $_.ToPort -eq $toPort}) + } + + break + } + catch { + $failCount++ + } + + if ($failCount -eq 3) { throw \"Could not register the task after three attempts!\" } + } + + + + $check_ipRangesObj = ($check_ipPermissionObj | Select -ExpandProperty IpRanges | ? {$_ -eq $ipRanges}) + $check_ipRangesObjCount = ($check_ipRangesObj | measure).Count + + if ($check_ipRangesObjCount -gt 0) { + Write-Output (\"$(Get-Date) | Rule '$($_.Key)' already exists...\") + } + else { + Write-Output (\"$(Get-Date) | Creating new rule '$($_.Key)'...\") + + $ipPermissionObj = (New-Object \"Amazon.EC2.Model.IpPermission\") + $ipPermissionObj.IpProtocol = $ipProtocol + $ipPermissionObj.FromPort = $fromPort + $ipPermissionObj.ToPort = $toPort + + + try { + $ipRangesObj = (New-Object \"Amazon.EC2.Model.IpRange\") + $ipRangesObj.CidrIp = $ipRanges + $ipRangesObj.Description = $_.Key + $ipPermissionObj.Ipv4Ranges = $ipRangesObj + } + catch { + Write-Output (\"$(Get-Date) | Cannot create 'Amazon.EC2.Model.IpRange' object, possibly running an old version of the 'AWS Tools for Windows PowerShell'\") + Write-Output (\"$(Get-Date) | Attempting to use the old method, but the old method does not allow rule comments/descriptions\") + + $ipRangesObj = (New-Object \"System.Collections.Generic.List[string]\") + $ipRangesObj.Add($ipRanges) + $ipPermissionObj.IpRanges = $ipRangesObj + } + + Write-Output $ipPermissionObj + + try { + if ($direction -eq \"Ingress\") { + Grant-EC2SecurityGroupIngress -GroupId $securityGroupId -IpPermission $ipPermissionObj + } + elseif ($direction -eq \"Egress\") { + Grant-EC2SecurityGroupEgress -GroupId $securityGroupId -IpPermission $ipPermissionObj + } + } + catch [Amazon.EC2.AmazonEC2Exception] { + throw $_.Exception.errorcode + '-' + $_.Exception.Message + } + + Write-Output (\"------------------------------\") + Write-Output (\"New $($direction) ruleset looks like:\") + Write-Output (\"------------------------------\") + + $securityGroupObj = (Get-EC2SecurityGroup -Filter $filterArray) + + if ($direction -eq \"Ingress\") { + Write-Output $securityGroupObj | Select -ExpandProperty IpPermissions | ? {$_.IpProtocol -eq $ipProtocol -and $_.FromPort -eq $fromPort -and $_.ToPort -eq $toPort} + } + elseif ($direction -eq \"Egress\") { + Write-Output $securityGroupObj | Select -ExpandProperty IpPermissionsEgress | ? {$_.IpProtocol -eq $ipProtocol -and $_.FromPort -eq $fromPort -and $_.ToPort -eq $toPort} + } + } + } + } + + + + + if ($odInstanceId) { + $filterArray = @() + $filterArray += @{ name=\"instance-id\";value=$odInstanceId } + + $instanceObj = (Get-EC2Instance -Filter $filterArray | select -ExpandProperty Instances) + $instanceCount = ($instanceObj | measure).Count + + if ($instanceCount -gt 1) { + throw \"More than one instance exists with the same instance id - I don't know what to do!?\" + } + elseif ($instanceCount -eq 1) { + Write-Output (\"$(Get-Date) | Found instance '$($odInstanceId)'!\") + + $securityGroupArray = @() + $securityGroupArray += ($instanceObj.NetworkInterfaces | Where-Object {$(Get-EC2NetworkInterface -NetworkInterfaceId $($_.NetworkInterfaceId))} | Select -ExpandProperty Groups | Select GroupId | Select -Expand GroupId) + + if ($securityGroupArray -contains $securityGroupId) { + Write-Output (\"$(Get-Date) | Security Group '$($securityGroupId)' is already associated with the Instance '$($odInstanceId)'...\") + } + else { + Write-Output (\"$(Get-Date) | Adding Security Group '$($securityGroupId)' to the Instance '$($odInstanceId)'!\") + + $securityGroupArray += $securityGroupId + $instanceObj.NetworkInterfaces | Where-Object {$(Edit-EC2NetworkInterfaceAttribute -NetworkInterfaceId $($_.NetworkInterfaceId) -Groups $securityGroupArray)} + } + } + + Write-Output (\"------------------------------\") + Write-Output (\"Security Groups for instance '$($odInstanceId)':\") + Write-Output (\"------------------------------\") + + $instanceObj = (Get-EC2Instance -Filter $filterArray | select -ExpandProperty Instances) + Write-Output $instanceObj.NetworkInterfaces | Where-Object {$(Get-EC2NetworkInterface -NetworkInterfaceId $($_.NetworkInterfaceId))} | Select -ExpandProperty Groups + } + } ` + (Get-Param 'odGroupName' -Required) ` + (Get-Param 'odGroupDescription' -Required) ` + (Get-Param 'odVpcId' -Required) ` + (Get-Param 'odRules') ` + (Get-Param 'odInstanceId') ` + (Get-Param 'odAccessKey') ` + (Get-Param 'odSecretKey')" + }, + "Parameters": [ + { + "Id": "868cba60-5638-4078-aa56-b65bba16f9aa", + "Name": "odGroupName", + "Label": "Security Group Name", + "HelpText": "The Name you would like to assign to the new Security Group.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "55c75754-48ad-44ba-854f-7d937ace11b4", + "Name": "odGroupDescription", + "Label": "Security Group Description", + "HelpText": "The Description you would like to assign to the new Security Group.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "332e2d15-e8f0-4012-9eaa-a28011ab1ef3", + "Name": "odVpcId", + "Label": "Virtual Private Cloud (VPC) ID", + "HelpText": "The Virtual Private Cloud (VPC) ID of the VPC you would like the Security Group to be created in.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b8041ec7-6d50-4f8d-babe-9b6b3fef2ac5", + "Name": "odRules", + "Label": "Rules (Optional)", + "HelpText": "The Rules you would like to add to the Security Group. For example: +- RDP=Ingress|tcp|3389|3389|52.64.52.64/32 + +The format being: +- comment=direction|protocol|fromport|toport|iprange", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "ae77469f-6cad-42a7-8fbc-592d2ee85c3d", + "Name": "odInstanceId", + "Label": "Instance ID (Optional)", + "HelpText": "The EC2 Instance ID of the Instance you would like to add the Security Group to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "ee896a8c-beda-4609-a104-bdf6fd3799cd", + "Name": "odAccessKey", + "Label": "Access Key (Kind-of Optional)", + "HelpText": "An Access Key with permissions to create the desired EC2 instance. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"AWS\\_ACCESS\\_KEY\". + +Further Reading: +[https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html](https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "004f625b-6d07-4a12-8a80-ea74943fb283", + "Name": "odSecretKey", + "Label": "Secret Key (Kind-of Optional)", + "HelpText": "The Secret Key associated with the above Access Key. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"AWS\\_SECRET\\_KEY\". + +Further Reading: +[https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html](https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "tclydesdale", + "$Meta": { + "ExportedAt": "2018-02-01T14:11:31.262Z", + "OctopusVersion": "4.1.9", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-deploy-lambda-iam.json.human b/step-templates/aws-deploy-lambda-iam.json.human new file mode 100644 index 000000000..d0637221d --- /dev/null +++ b/step-templates/aws-deploy-lambda-iam.json.human @@ -0,0 +1,631 @@ +{ + "Id": "9e2fa6bc-0ce7-4dbe-b6f9-4d14d4877e42", + "Name": "AWS - Deploy Lambda Function using IAM Role auth", + "Description": "Deploys a Zip file to an AWS Lambda function using the IAM role configured on the machine. + +This step can perform variable substitution, however, the recommended approach to changing a lambda configuration per environment is to use [environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html) + +This step uses the following AWS CLI commands to deploy the AWS Lambda. You will be required to install the AWS CLI on your server/worker for this to work. The AWS CLI is pre-installed on the [dynamic workers](https://octopus.com/docs/infrastructure/workers/dynamic-worker-pools) in Octopus Cloud as well as the provided docker containers for [Execution Containers](https://octopus.com/docs/deployment-process/execution-containers-for-workers). + +- [create-function](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html) +- [get-function](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/get-function.html) +- [publish-version](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/publish-version.html) +- [tag-resource](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/tag-resource.html) +- [untag-resource](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/untag-resource.html) +- [update-function-code](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/update-function-code.html) +- [update-function-configuration](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/update-function-configuration.html) + +This step template is worker-friendly, you can pass in a package reference rather than having to reference a previous step that downloaded the package. This step requires **Octopus Deploy 2019.10.0** or higher. + +## Output Variables + +This step template sets the following output variables: + +- `LambdaArn`: The ARN of the Lambda Function +- `PublishedVersion`: The most recent version published (only set when Publish is set to `Yes`).", + "ActionType": "Octopus.AwsRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "8dbae499-5aa8-438e-a2fe-ae29fb8f0a39", + "Name": "AWS.Lambda.Package", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "AWS.Lambda.Package", + "Purpose": "" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "True", + "Octopus.Action.Aws.Region": "#{AWS.Lambda.Region}", + "Octopus.Action.Script.ScriptBody": "$functionName = $OctopusParameters[\"AWS.Lambda.FunctionName\"] +$functionRole = $OctopusParameters[\"AWS.Lambda.FunctionRole\"] +$functionRunTime = $OctopusParameters[\"AWS.Lambda.Runtime\"] +$functionHandler = $OctopusParameters[\"AWS.Lambda.FunctionHandler\"] +$functionMemorySize = $OctopusParameters[\"AWS.Lambda.MemorySize\"] +$functionDescription = $OctopusParameters[\"AWS.Lambda.Description\"] +$functionVPCSubnetId = $OctopusParameters[\"AWS.Lambda.VPCSubnetIds\"] +$functionVPCSecurityGroupId = $OctopusParameters[\"AWS.Lambda.VPCSecurityGroupIds\"] +$functionEnvironmentVariables = $OctopusParameters[\"AWS.Lambda.EnvironmentVariables\"] +$functionEnvironmentVariablesKey = $OctopusParameters[\"AWS.Lambda.EnvironmentVariablesKey\"] +$functionTimeout = $OctopusParameters[\"AWS.Lambda.FunctionTimeout\"] +$functionTags = $OctopusParameters[\"AWS.Lambda.Tags\"] +$functionFileSystemConfig = $OctopusParameters[\"AWS.Lambda.FileSystemConfig\"] +$functionDeadLetterConfig = $OctopusParameters[\"AWS.Lambda.DeadLetterConfig\"] +$functionTracingConfig = $OctopusParameters[\"AWS.Lambda.TracingConfig\"] +$functionVersionNumber = $OctopusParameters[\"Octopus.Action.Package[AWS.Lambda.Package].PackageVersion\"] +$functionPublishOption = $OctopusParameters[\"AWS.Lambda.Publish\"] + +$functionReleaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$functionRunbookRun = $OctopusParameters[\"Octopus.RunbookRun.Id\"] +$stepName = $OctopusParameters[\"Octopus.Step.Name\"] + +$regionName = $OctopusParameters[\"AWS.Lambda.Region\"] +$newArchiveFileName = \"$($OctopusParameters[\"Octopus.Action.Package[AWS.Lambda.Package].ExtractedPath\"])/$($OctopusParameters[\"Octopus.Action.Package[AWS.Lambda.Package].PackageId\"]).$($OctopusParameters[\"Octopus.Action.Package[AWS.Lambda.Package].PackageVersion\"]).zip\" + +if ([string]::IsNullOrWhiteSpace($functionName)) +{ +\tWrite-Error \"The parameter Function Name is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionRole)) +{ +\tWrite-Error \"The parameter Role is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionRunTime)) +{ +\tWrite-Error \"The parameter Run Time is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionHandler)) +{ +\tWrite-Error \"The parameter Handler is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionPublishOption)) +{ +\tWrite-Error \"The parameter Publish is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionReleaseNumber) -eq $false) +{ + $deployVersionTag = \"Octopus-Release=$functionReleaseNumber\" +} +else +{ +\t$deployVersionTag = \"Octopus-Runbook-Run=$functionRunbookRun\" +} + +Write-Host \"Function Name: $functionName\" +Write-Host \"Function Role: $functionRole\" +Write-Host \"Function Runtime: $functionRunTime\" +Write-Host \"Function Handler: $functionHandler\" +Write-Host \"Function Memory Size: $functionMemorySize\" +Write-Host \"Function Description: $functionDescription\" +Write-Host \"Function Subnet Ids: $functionVPCSubnetId\" +Write-Host \"Function Security Group Ids: $functionVPCSecurityGroupId\" +Write-Host \"Function Environment Variables: $functionEnvironmentVariables\" +Write-Host \"Function Environment Variables Key: $functionEnvironmentVariablesKey\" +Write-Host \"Function Timeout: $functionTimeout\" +Write-Host \"Function Tags: $functionTags\" +Write-Host \"Function File System Config: $functionFileSystemConfig\" +Write-Host \"Function Dead Letter Config: $functionDeadLetterConfig\" +Write-Host \"Function Tracing Config: $functionTracingConfig\" +Write-Host \"Function file path: fileb://$newArchiveFileName\" +Write-Host \"Function Publish: $functionPublishOption\" + +Write-Host \"Re-packaging archive ...\" + +# Repackage the files into a zip file +Compress-Archive -Path \"$($OctopusParameters[\"Octopus.Action.Package[AWS.Lambda.Package].ExtractedPath\"])/*\" -DestinationPath $newArchiveFileName + +Write-Host \"Attempting to find the function $functionName in the region $regionName\" +$hasExistingFunction = $true + +try +{ + $existingFunction = aws lambda get-function --function-name \"$functionName\" 2> $null + + Write-Host \"The exit code from the lookup was $LASTEXITCODE\" + if ($LASTEXITCODE -eq 255 -or $LASTEXITCODE -eq 254) + { + \t$hasExistingFunction = $false + } + + $existingFunction = $existingFunction | ConvertFrom-Json +} +catch +{ +\tWrite-Host \"The function was not found\" +\t$hasExistingFunction = $false +} + +Write-Host \"Existing functions: $hasExistingFunction\" +Write-Host $existingFunction + +$aliasInformation = $null +if ($hasExistingFunction -eq $false) +{ +\tWrite-Highlight \"Creating $functionName in $regionName\" +\t$functionInformation = aws lambda create-function --function-name \"$functionName\" --zip-file fileb://$newArchiveFileName --handler $functionHandler --runtime $functionRuntime --role $functionRole --memory-size $functionMemorySize +} +else +{ +\tWrite-Highlight \"Updating the $functionName code\" + $updatedConfig = aws lambda update-function-code --function-name \"$functionName\" --zip-file fileb://$newArchiveFileName + + Write-Highlight \"Waiting for update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" + + Write-Highlight \"Updating the $functionName base configuration\" + $functionInformation = aws lambda update-function-configuration --function-name \"$functionName\" --role $functionRole --handler $functionHandler --runtime $functionRuntime --memory-size $functionMemorySize + + Write-Highlight \"Waiting for base configuration update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +$functionInformation = $functionInformation | ConvertFrom-JSON +$functionArn = $functionInformation.FunctionArn + +Write-Host \"Function ARN: $functionArn\" + +if ([string]::IsNullOrWhiteSpace($functionEnvironmentVariables) -eq $false) +{ +\tWrite-Highlight \"Environment variables specified, updating environment variables configuration for $functionName\" +\t$environmentVariables = \"Variables={$functionEnvironmentVariables}\" + + if ([string]::IsNullOrWhiteSpace($functionEnvironmentVariablesKey) -eq $true) + { + \t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --environment \"$environmentVariables\" + } + else + { + \t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --environment \"$environmentVariables\" --kms-key-arn \"$functionEnvironmentVariablesKey\" + } + + Write-Highlight \"Waiting for environment variable update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionTimeout) -eq $false) +{ +\tWrite-Highlight \"Timeout specified, updating timeout configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --timeout \"$functionTimeout\" + + Write-Highlight \"Waiting for timeout upate to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionTags) -eq $false) +{ +\tWrite-Highlight \"Tags specified, updating tags configuration for $functionName\" +\t$updatedConfig = aws lambda tag-resource --resource \"$functionArn\" --tags \"$functionTags\" +} + +if ([string]::IsNullOrWhiteSpace($deployVersionTag) -eq $false) +{ +\tWrite-Highlight \"Deploy version tag found with value of $deployVersionTag, updating tags configuration for $functionName\" + aws lambda untag-resource --resource \"$functionArn\" --tag-keys \"Octopus-Release\" \"Octopus-Runbook-Run\" +\t$updatedConfig = aws lambda tag-resource --resource \"$functionArn\" --tags \"$deployVersionTag\" +} + +if ([string]::IsNullOrWhiteSpace($functionVPCSubnetId) -eq $false -and [string]::IsNullOrWhiteSpace($functionVPCSecurityGroupId) -eq $false) +{ +\tWrite-Highlight \"VPC subnets and security group specified, updating vpc configuration for $functionName\" +\t$vpcConfig = \"SubnetIds=$functionVPCSubnetId,SecurityGroupIds=$functionVPCSecurityGroupId\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --vpc-config \"$vpcConfig\" + + Write-Highlight \"Waiting for vpc configuration to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionDescription) -eq $false) +{ +\tWrite-Highlight \"Description specified, updating description configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --description \"$functionDescription\" + + Write-Highlight \"Waiting for description configuration ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionFileSystemConfig) -eq $false) +{ +\tWrite-Highlight \"File System Config specified, updating file system configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --file-system-configs \"$functionFileSystemConfig\"\t + + Write-Highlight \"Wating for file system configuration update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionDeadLetterConfig) -eq $false) +{ +\tWrite-Highlight \"Dead Letter specified, updating dead letter configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --dead-letter-config \"$functionDeadLetterConfig\"\t + + Write-Highlight \"Waitng for Dead Letter configuration update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionTracingConfig) -eq $false) +{ +\tWrite-Highlight \"Tracing config specified, updating tracing configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --tracing-config \"$functionTracingConfig\"\t + + Write-Highlight \"Waiting for tracing configuration to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +Write-Host $updatedConfig | ConvertFrom-JSON + +if ($functionPublishOption -eq \"Yes\") +{ +\tWrite-Highlight \"Publishing the function with the description $functionVersionNumber to create a snapshot of the current code and configuration of this function in AWS.\" +\t$publishedVersion = aws lambda publish-version --function-name \"$functionArn\" --description \"$functionVersionNumber\" + + $publishedVersion = $publishedVersion | ConvertFrom-JSON + + Write-Highlight \"Setting the output variable 'Octopus.Action[$($stepName)].Output.PublishedVersion' to $($publishedVersion.Version)\" + Set-OctopusVariable -name \"PublishedVersion\" -value \"$($publishedVersion.Version)\" +} + +Write-Highlight \"Setting the output variable 'Octopus.Action[$($stepName)].Output.LambdaArn' to $functionArn\" +Set-OctopusVariable -name \"LambdaArn\" -value \"$functionArn\" + +Write-Highlight \"AWS Lambda $functionName successfully deployed.\"", + "OctopusUseBundledTooling": "False", + "Octopus.Action.EnabledFeatures": "Octopus.Features.JsonConfigurationVariables,Octopus.Features.SubstituteInFiles", + "Octopus.Action.Package.JsonConfigurationVariablesTargets": "#{AWS.Lambda.StructuredConfigurationVariables}", + "Octopus.Action.SubstituteInFiles.TargetFiles": "#{AWS.Lambda.SubstituteVariablesinTemplates}" + }, + "Parameters": [ + { + "Id": "1575a1fe-24df-4953-a27b-aec5d741245a", + "Name": "AWS.Lambda.FunctionName", + "Label": "Function Name", + "HelpText": "Required. + +The name of the function to create or update. See [documentation](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html#options) + +Examples: +- Function name - my-function . +- Function ARN - arn:aws:lambda:us-west-2:123456789012:function:my-function . +- Partial ARN - 123456789012:function:my-function . + +The length constraint applies only to the full ARN. If you specify only the function name, it is limited to 64 characters in length. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e1a04b5b-cd19-43cb-a8f3-925817d73272", + "Name": "AWS.Lambda.Region", + "Label": "Region", + "HelpText": "Required. + +The region where the function will live.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "us-east-2|US East (Ohio) +us-east-1|US East (N. Virginia) +us-west-1|US West (N. California) +us-west-2|US West (Oregon) +af-south-1|Africa (Cape Town) +ap-east-1|Asia Pacific (Hong Kong) +ap-south-1|Asia Pacific (Mumbai) +ap-northeast-3|Asia Pacific (Osaka-Local) +ap-northeast-2|Asia Pacific (Seoul) +ap-southeast-1|Asia Pacific (Singapore) +ap-southeast-2|Asia Pacific (Sydney) +ap-northeast-1|Asia Pacific (Tokyo) +ca-central-1|Canada (Central) +eu-central-1|Europe (Frankfurt) +eu-west-1|Europe (Ireland) +eu-west-2|Europe (London) +eu-south-1|Europe (Milan) +eu-west-3|Europe (Paris) +eu-north-1|Europe (Stockholm) +me-south-1|Middle East (Bahrain) +sa-east-1|South America (São Paulo)" + } + }, + { + "Id": "dd56868a-019f-4f3b-8231-72b8cce0d862", + "Name": "AWS.Lambda.Package", + "Label": "Package", + "HelpText": "Required. + +The package containing the code you wish to deploy to the AWS Lambda function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "15353fc5-0d45-40f7-a6ec-803371aa2fa9", + "Name": "AWS.Lambda.FunctionRole", + "Label": "Function Role", + "HelpText": "Required. + +The Amazon Resource Name (ARN) of the function’s execution role. This role must exist prior to this step is run. See [documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html) for more detail on creating an execution role.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6bb03f9d-5ed6-4e7d-8cd8-71620cd81281", + "Name": "AWS.Lambda.Runtime", + "Label": "Runtime", + "HelpText": "Required. + +The runtime of the AWS Lambda. See [documentation](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html#options) for more details on what runtimes are supported.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "nodejs|nodejs +nodejs4.3|nodejs4.3 +nodejs4.3-edge|nodejs4.3-edge +nodejs6.10|nodejs6.10 +nodejs8.10|nodejs8.10 +nodejs10.x|nodejs10.x +nodejs12.x|nodejs12.x +nodejs14.x|nodejs14.x +java8|java8 +java8.al2|java8.al2 +java11|java11 +python2.7|python2.7 +python3.6|python3.6 +python3.7|python3.7 +python3.8|python3.8 +python3.9|python3.9 +dotnetcore1.0|dotnetcore1.0 +dotnetcore2.0|dotnetcore2.0 +dotnetcore2.1|dotnetcore2.1 +dotnetcore3.1|dotnetcore3.1 +dotnet6|dotnet6 +nodejs4.3-edge|nodejs4.3-edge +go1.x|go1.x +ruby2.5|ruby2.5 +ruby2.7|ruby2.7 +provided|provided +provided.al2|provided.al2" + } + }, + { + "Id": "385948fb-d456-415b-ab33-77896e53823b", + "Name": "AWS.Lambda.FunctionHandler", + "Label": "Handler", + "HelpText": "Required. + +The name of the method within your code that Lambda calls to execute your function. The format includes the file name. It can also include namespaces and other qualifiers, depending on the runtime. For more information, see [Programming Model](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-features.html)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "de6cb1c3-1906-4f01-895e-634aad21540c", + "Name": "AWS.Lambda.MemorySize", + "Label": "Memory Size", + "HelpText": "Required. + +The amount of memory that your function has access to. Increasing the function’s memory also increases its CPU allocation. The default value is 128 MB. The value must be a multiple of 64 MB.", + "DefaultValue": "128", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7a225558-f530-407c-90d5-419dbd3c9b8f", + "Name": "AWS.Lambda.Description", + "Label": "Description", + "HelpText": "Optional. + +A description of the function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f0661cd1-6df2-4320-a41f-e8e7076a7518", + "Name": "AWS.Lambda.VPCSubnetIds", + "Label": "VPC Subnet Ids", + "HelpText": "Optional. + +Format: `SubnetId1,SubnetId2` + +For network connectivity to AWS resources in a VPC, specify a list of security groups and subnets in the VPC. When you connect a function to a VPC, it can only access resources and the internet through that VPC. For more information, see [VPC Settings](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "92eb428e-6837-4c78-a7ce-a4d43fb3955b", + "Name": "AWS.Lambda.VPCSecurityGroupIds", + "Label": "VPC Security Group Ids", + "HelpText": "Optional. + +Format: `SecurityGroupId1,SecurityGroupId2` + +For network connectivity to AWS resources in a VPC, specify a list of security groups and subnets in the VPC. When you connect a function to a VPC, it can only access resources and the internet through that VPC. For more information, see [VPC Settings](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "312fabde-f65d-49aa-99ab-c4eb41a5d9bb", + "Name": "AWS.Lambda.EnvironmentVariables", + "Label": "Environment Variables", + "HelpText": "Optional. + +Format: `KeyName1=string,KeyName2=string` + +Environment variables that are accessible from function code during execution.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f38bc9ee-c221-4f41-b344-b78d29b3f1bc", + "Name": "AWS.Lambda.EnvironmentVariablesKey", + "Label": "Environment Variables Encryption Key", + "HelpText": "Optional. + +The ARN of the AWS Key Management Service (AWS KMS) key that’s used to encrypt your function’s environment variables. If it’s not provided, AWS Lambda uses a default service key. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e3432e86-c5c9-46ca-8bef-5f247938b285", + "Name": "AWS.Lambda.FunctionTimeout", + "Label": "Timeout", + "HelpText": "Optional. + +The amount of time that Lambda allows a function to run before stopping it. The default is 3 seconds. The maximum allowed value is 900 seconds.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2b6e7aa2-512e-44e4-aad9-d16280752714", + "Name": "AWS.Lambda.Tags", + "Label": "Tags", + "HelpText": "Optional. + +Format: `KeyName1=string,KeyName2=string` + +A list of tags to apply to the function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a69663ca-02fa-4d1d-abd9-0f554fd78013", + "Name": "AWS.Lambda.FileSystemConfig", + "Label": "File System Config", + "HelpText": "Optional. + +Format: `Arn=string,LocalMountPath=string` + +Connection settings for an Amazon EFS file system.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "17ff862b-2d83-4819-a5b5-6bae7c45b71a", + "Name": "AWS.Lambda.TracingConfig", + "Label": "Tracing Config", + "HelpText": "Optional. + +Format: `Mode=string` + +Set Mode to Active to sample and trace a subset of incoming requests with AWS X-Ray.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "558e6e77-d9e4-483d-8f43-c1059e67cc60", + "Name": "AWS.Lambda.DeadLetterConfig", + "Label": "Dead Letter Config", + "HelpText": "Optional. + +Format: `TargetArn=string` + +A dead letter queue configuration that specifies the queue or topic where Lambda sends asynchronous events when they fail processing. For more information, see [Dead Letter Queues](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html#dlq). +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5ad9e9f4-48c3-4c3f-a34d-a0f14af4318a", + "Name": "AWS.Lambda.Publish", + "Label": "Publish", + "HelpText": "Required. + +Creates a [version](https://docs.aws.amazon.com/lambda/latest/dg/versioning-aliases.html) from the current code and configuration of a function. Use versions to create a snapshot of your function code and configuration that doesn’t change. + +**Important**: Lambda doesn’t publish a version if the function’s configuration and code haven’t changed since the last version. Use UpdateFunctionCode or UpdateFunctionConfiguration to update the function before publishing a version.", + "DefaultValue": "Yes", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Yes|Yes +No|No" + } + }, + { + "Id": "50051025-3ec9-43b0-a752-b8b08d0d159e", + "Name": "AWS.Lambda.StructuredConfigurationVariables", + "Label": "Structured Configuration Variables", + "HelpText": "To ensure you get the correct path, preface all entries with +`#{Octopus.Action.Package[AWS.Lambda.Package].ExtractedPath}/` + +Target files need to be new line seperated, relative to the package contents. Extended wildcard syntax is supported. E.g., appsettings.json, Config\\*.xml, **\\specific-folder\\*.yaml. Learn more about Structured Configuration Variables and view examples. + +Note: To avoid unnecessary warnings, the default value is a blank space.", + "DefaultValue": " ", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "8363b78f-f808-4b73-bee1-7b60d9186abf", + "Name": "AWS.Lambda.SubstituteVariablesinTemplates", + "Label": "Substitute Variables in Templates", + "HelpText": "To ensure you get the correct path, preface all entries with +`#{Octopus.Action.Package[AWS.Lambda.Package].ExtractedPath}/` + +A newline-separated list of file names to transform, relative to the package contents. Extended wildcard syntax is supported. E.g., Notes.txt, Config\\*.json, **\\specific-folder\\*.config. +This field supports extended template syntax. Conditional if and unless: +`#{if MyVar}...#{/if}` +Iteration over variable sets or comma-separated values with each: +`#{each mv in MyVar}...#{mv}...#{/each}` + +Note: To avoid unnecessary warnings, the default value is a blank space.", + "DefaultValue": " ", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.AwsRunScript", + "$Meta": { + "ExportedAt": "2023-06-27T18:05:45.154Z", + "OctopusVersion": "2023.3.4135", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "aws" +} diff --git a/step-templates/aws-deploy-lambda-image.json.human b/step-templates/aws-deploy-lambda-image.json.human new file mode 100644 index 000000000..4d231be5b --- /dev/null +++ b/step-templates/aws-deploy-lambda-image.json.human @@ -0,0 +1,610 @@ +{ + "Id": "480a3d93-7bad-42d4-b439-cced56ae792a", + "Name": "AWS - Deploy Image Lambda Function", + "Description": "Deploys an image to an AWS Lambda function. + +This step uses the following AWS CLI commands to deploy the AWS Lambda. You will be required to install the AWS CLI on your server/worker for this to work. The AWS CLI is pre-installed on the [dynamic workers](https://octopus.com/docs/infrastructure/workers/dynamic-worker-pools) in Octopus Cloud as well as the provided docker containers for [Execution Containers](https://octopus.com/docs/deployment-process/execution-containers-for-workers). + +- [create-function](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html) +- [get-function](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/get-function.html) +- [publish-version](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/publish-version.html) +- [tag-resource](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/tag-resource.html) +- [untag-resource](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/untag-resource.html) +- [update-function-code](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/update-function-code.html) +- [update-function-configuration](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/update-function-configuration.html) + +This step template is worker-friendly, you can pass in a package reference rather than having to reference a previous step that downloaded the package. This step requires **Octopus Deploy 2019.10.0** or higher. + +## Output Variables + +This step template sets the following output variables: + +- `LambdaArn`: The ARN of the Lambda Function +- `PublishedVersion`: The most recent version published (only set when Publish is set to `Yes`).", + "ActionType": "Octopus.AwsRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "8dbae499-5aa8-438e-a2fe-ae29fb8f0a39", + "Name": "AWS.Lambda.Package", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "NotAcquired", + "Properties": { + "Extract": "False", + "SelectionMode": "deferred", + "PackageParameterName": "AWS.Lambda.Package", + "Purpose": "" + } + } + ], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.AwsAccount.Variable": "#{AWS.Lambda.Account}", + "Octopus.Action.Aws.Region": "#{AWS.Lambda.Region}", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\"; + +$functionName = $OctopusParameters[\"AWS.Lambda.FunctionName\"] +$functionRole = $OctopusParameters[\"AWS.Lambda.FunctionRole\"] +$functionRunTime = $OctopusParameters[\"AWS.Lambda.Runtime\"] +$functionMemorySize = $OctopusParameters[\"AWS.Lambda.MemorySize\"] +$functionDescription = $OctopusParameters[\"AWS.Lambda.Description\"] +$functionVPCSubnetId = $OctopusParameters[\"AWS.Lambda.VPCSubnetIds\"] +$functionVPCSecurityGroupId = $OctopusParameters[\"AWS.Lambda.VPCSecurityGroupIds\"] +$functionEnvironmentVariables = $OctopusParameters[\"AWS.Lambda.EnvironmentVariables\"] +$functionEnvironmentVariablesKey = $OctopusParameters[\"AWS.Lambda.EnvironmentVariablesKey\"] +$functionTimeout = $OctopusParameters[\"AWS.Lambda.FunctionTimeout\"] +$functionTags = $OctopusParameters[\"AWS.Lambda.Tags\"] +$functionFileSystemConfig = $OctopusParameters[\"AWS.Lambda.FileSystemConfig\"] +$functionDeadLetterConfig = $OctopusParameters[\"AWS.Lambda.DeadLetterConfig\"] +$functionTracingConfig = $OctopusParameters[\"AWS.Lambda.TracingConfig\"] +$functionVersionNumber = $OctopusParameters[\"Octopus.Action.Package[AWS.Lambda.Package].PackageVersion\"] +$functionPublishOption = $OctopusParameters[\"AWS.Lambda.Publish\"] + +$functionReleaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$functionRunbookRun = $OctopusParameters[\"Octopus.RunbookRun.Id\"] +$stepName = $OctopusParameters[\"Octopus.Step.Name\"] + +$regionName = $OctopusParameters[\"AWS.Lambda.Region\"] + +if ($null -ne $OctopusParameters[\"Octopus.Action.Package[AWS.Lambda.Package].Image\"]) { + $imageUri = $OctopusParameters[\"Octopus.Action.Package[AWS.Lambda.Package].Image\"] +} + +if ([string]::IsNullOrWhiteSpace($functionName)) { + Write-Error \"The parameter Function Name is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionRole)) { + Write-Error \"The parameter Role is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionReleaseNumber) -eq $false) { + $deployVersionTag = \"Octopus-Release=$functionReleaseNumber\" +} +else { + $deployVersionTag = \"Octopus-Runbook-Run=$functionRunbookRun\" +} + +Write-Host \"Function Name: $functionName\" +Write-Host \"Function Role: $functionRole\" +Write-Host \"Function Memory Size: $functionMemorySize\" +Write-Host \"Function Description: $functionDescription\" +Write-Host \"Function Subnet Ids: $functionVPCSubnetId\" +Write-Host \"Function Security Group Ids: $functionVPCSecurityGroupId\" +Write-Host \"Function Timeout: $functionTimeout\" +Write-Host \"Function Tags: $functionTags\" +Write-Host \"Function File System Config: $functionFileSystemConfig\" +Write-Host \"Function Dead Letter Config: $functionDeadLetterConfig\" +Write-Host \"Function Tracing Config: $functionTracingConfig\" +Write-Host \"Function Publish: $functionPublishOption\" +Write-Host \"Function Image URI: $imageUri\" +Write-Host \"Function Environment Variables: $functionEnvironmentVariables\" +Write-Host \"Function Environment Variables Key: $functionEnvironmentVariablesKey\" + +if (![string]::IsNullOrWhitespace($OctopusParameters[\"AWS.Lambda.Image.Entrypoint\"])) +{ + Write-Host \"Image Entrypoint override: $($OctopusParameters[\"AWS.Lambda.Image.Entrypoint\"])\" +} + +if (![string]::IsNullOrWhitespace($OctopusParameters[\"AWS.Lambda.Image.Command\"])) +{ + Write-Host \"Image Command override: $($OctopusParameters[\"AWS.Lambda.Image.Command\"])\" +} + + +Write-Host \"Attempting to find the function $functionName in the region $regionName\" +$hasExistingFunction = $true + +try { + $existingFunction = aws lambda get-function --function-name \"$functionName\" 2> $null + + Write-Host \"The exit code from the lookup was $LASTEXITCODE\" + if ($LASTEXITCODE -eq 255 -or $LASTEXITCODE -eq 254) { + $hasExistingFunction = $false + } + + $existingFunction = $existingFunction | ConvertFrom-Json +} +catch { + Write-Host \"The function was not found\" + $hasExistingFunction = $false +} + +Write-Host \"Existing functions: $hasExistingFunction\" +Write-Host $existingFunction + +# Init argument variable +$lambdaArguments = @(\"lambda\") +$waitArguments = @(\"lambda\", \"wait\") + +if ($hasExistingFunction -eq $false) { + Write-Highlight \"Creating $functionName in $regionName\" + + $waitArguments += @(\"function-active-v2\") + + $lambdaArguments += @(\"create-function\", \"--role\", $functionRole, \"--memory-size\", $functionMemorySize) + + if ($null -ne $imageUri) { + Write-Host \"Deploying Lambda container ...\" + $lambdaArguments += @(\"--code\", \"ImageUri=$imageUri\", \"--package-type\", \"Image\") + + if (![string]::IsNullOrWhitespace($OctopusParameters['AWS.Lambda.Image.Entrypoint']) -or ![string]::IsNullOrWhitespace($OctopusParameters['AWS.Lambda.Image.Command'])) { + $lambdaArguments += \"--image-config\" + + if (![string]::IsNullOrWhitespace($OctopusParameters['AWS.Lambda.Image.Entrypoint'])) { + $lambdaArguments += \"EntryPoint=$($OctopusParameters['AWS.Lambda.Image.Entrypoint'])\" + } + + if (![string]::IsNullOrWhitespace($OctopusParameters['AWS.Lambda.Image.Command'])) { + $lambdaArguments += \"Command=$($OctopusParameters['AWS.Lambda.Image.Command'])\" + } + } + } +} +else { + Write-Highlight \"Updating the $functionName code\" + + $waitArguments += @(\"function-updated\") + $lambdaArguments += \"update-function-code\" + + if ($null -ne $imageUri) { + Write-Host \"Deploying Lambda container ...\" + $lambdaArguments += @(\"--image-uri\", $imageUri) + } +} + +$waitArguments += @(\"--function-name\", \"$functionName\") + +$lambdaArguments += @(\"--function-name\", \"$functionName\") + +# Wait for function to be done creating +Write-Host \"Running aws $lambdaArguments ...\" +$functionInformation = (aws $lambdaArguments) +(aws $waitArguments) + + +if ($hasExistingFunction -eq $true) { + # update configuration + $lambdaArguments = @(\"lambda\", \"update-function-configuration\", \"--function-name\", \"$functionName\", \"--role\", $functionRole, \"--memory-size\", $functionMemorySize) + + if ($null -ne $imageUri) { + Write-Highlight \"Updating the $functionName image configuration\" + if (![string]::IsNullOrWhitespace($OctopusParameters['AWS.Lambda.Image.Entrypoint']) -or ![string]::IsNullOrWhitespace($OctopusParameters['AWS.Lambda.Image.Command'])) { + $lambdaArguments += \"--image-config\" + + if (![string]::IsNullOrWhitespace($OctopusParameters['AWS.Lambda.Image.Entrypoint'])) { + $lambdaArguments += \"EntryPoint=$($OctopusParameters['AWS.Lambda.Image.Entrypoint'])\" + } + + if (![string]::IsNullOrWhitespace($OctopusParameters['AWS.Lambda.Image.Command'])) { + $lambdaArguments += \"Command=$($OctopusParameters['AWS.Lambda.Image.Command'])\" + } + } + } + + $functionInformation = (aws $lambdaArguments) + Write-Highlight \"Waiting for configuration update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +$functionInformation = $functionInformation | ConvertFrom-JSON +$functionArn = $functionInformation.FunctionArn + +Write-Host \"Function ARN: $functionArn\" + +if ([string]::IsNullOrWhiteSpace($functionEnvironmentVariables) -eq $false) { + Write-Highlight \"Environment variables specified, updating environment variables configuration for $functionName\" + $environmentVariables = \"Variables={$functionEnvironmentVariables}\" + + if ([string]::IsNullOrWhiteSpace($functionEnvironmentVariablesKey) -eq $true) { + $updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --environment \"$environmentVariables\" + } + else { + $updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --environment \"$environmentVariables\" --kms-key-arn \"$functionEnvironmentVariablesKey\" + } + + Write-Highlight \"Waiting for environment variable update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionTimeout) -eq $false) { + Write-Highlight \"Timeout specified, updating timeout configuration for $functionName\" + $updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --timeout \"$functionTimeout\" + + Write-Highlight \"Waiting for timeout upate to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionTags) -eq $false) { + Write-Highlight \"Tags specified, updating tags configuration for $functionName\" + $updatedConfig = aws lambda tag-resource --resource \"$functionArn\" --tags \"$functionTags\" +} + +if ([string]::IsNullOrWhiteSpace($deployVersionTag) -eq $false) { + Write-Highlight \"Deploy version tag found with value of $deployVersionTag, updating tags configuration for $functionName\" + aws lambda untag-resource --resource \"$functionArn\" --tag-keys \"Octopus-Release\" \"Octopus-Runbook-Run\" + $updatedConfig = aws lambda tag-resource --resource \"$functionArn\" --tags \"$deployVersionTag\" +} + +if ([string]::IsNullOrWhiteSpace($functionVPCSubnetId) -eq $false -and [string]::IsNullOrWhiteSpace($functionVPCSecurityGroupId) -eq $false) { + Write-Highlight \"VPC subnets and security group specified, updating vpc configuration for $functionName\" + $vpcConfig = \"SubnetIds=$functionVPCSubnetId,SecurityGroupIds=$functionVPCSecurityGroupId\" + $updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --vpc-config \"$vpcConfig\" + + Write-Highlight \"Waiting for vpc configuration to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionDescription) -eq $false) { + Write-Highlight \"Description specified, updating description configuration for $functionName\" + $updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --description \"$functionDescription\" + + Write-Highlight \"Waiting for description configuration ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionFileSystemConfig) -eq $false) { + Write-Highlight \"File System Config specified, updating file system configuration for $functionName\" + $updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --file-system-configs \"$functionFileSystemConfig\"\t + + Write-Highlight \"Wating for file system configuration update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionDeadLetterConfig) -eq $false) { + Write-Highlight \"Dead Letter specified, updating dead letter configuration for $functionName\" + $updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --dead-letter-config \"$functionDeadLetterConfig\"\t + + Write-Highlight \"Waitng for Dead Letter configuration update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionTracingConfig) -eq $false) { + Write-Highlight \"Tracing config specified, updating tracing configuration for $functionName\" + $updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --tracing-config \"$functionTracingConfig\"\t + + Write-Highlight \"Waiting for tracing configuration to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +Write-Host $updatedConfig | ConvertFrom-JSON + +if ($functionPublishOption -eq \"Yes\") { + Write-Highlight \"Publishing the function with the description $functionVersionNumber to create a snapshot of the current code and configuration of this function in AWS.\" + $publishedVersion = aws lambda publish-version --function-name \"$functionArn\" --description \"$functionVersionNumber\" + + $publishedVersion = $publishedVersion | ConvertFrom-JSON + + Write-Highlight \"Setting the output variable 'Octopus.Action[$($stepName)].Output.PublishedVersion' to $($publishedVersion.Version)\" + Set-OctopusVariable -name \"PublishedVersion\" -value \"$($publishedVersion.Version)\" +} + +Write-Highlight \"Setting the output variable 'Octopus.Action[$($stepName)].Output.LambdaArn' to $functionArn\" +Set-OctopusVariable -name \"LambdaArn\" -value \"$functionArn\" + +Write-Highlight \"AWS Lambda $functionName successfully deployed.\"", + "OctopusUseBundledTooling": "False" + }, + "Parameters": [ + { + "Id": "8c0297c2-5a7c-4776-9ce7-8bd3dbe93e45", + "Name": "AWS.Lambda.FunctionName", + "Label": "Function Name", + "HelpText": "Required. + +The name of the function to create or update. See [documentation](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html#options) + +Examples: +- Function name - my-function . +- Function ARN - arn:aws:lambda:us-west-2:123456789012:function:my-function . +- Partial ARN - 123456789012:function:my-function . + +The length constraint applies only to the full ARN. If you specify only the function name, it is limited to 64 characters in length. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "711a1557-03c2-4c78-87aa-730389673884", + "Name": "AWS.Lambda.Account", + "Label": "AWS Account", + "HelpText": "Required. + +The AWS Account with permissions to create / update AWS Lambdas. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "304f276c-ed3e-4766-93e1-3fb25a727ccd", + "Name": "AWS.Lambda.Region", + "Label": "Region", + "HelpText": "Required. + +The region where the function will live.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "us-east-2|US East (Ohio) +us-east-1|US East (N. Virginia) +us-west-1|US West (N. California) +us-west-2|US West (Oregon) +af-south-1|Africa (Cape Town) +ap-east-1|Asia Pacific (Hong Kong) +ap-south-1|Asia Pacific (Mumbai) +ap-northeast-3|Asia Pacific (Osaka-Local) +ap-northeast-2|Asia Pacific (Seoul) +ap-southeast-1|Asia Pacific (Singapore) +ap-southeast-2|Asia Pacific (Sydney) +ap-northeast-1|Asia Pacific (Tokyo) +ca-central-1|Canada (Central) +eu-central-1|Europe (Frankfurt) +eu-west-1|Europe (Ireland) +eu-west-2|Europe (London) +eu-south-1|Europe (Milan) +eu-west-3|Europe (Paris) +eu-north-1|Europe (Stockholm) +me-south-1|Middle East (Bahrain) +sa-east-1|South America (São Paulo)" + } + }, + { + "Id": "d9d48dc2-d671-41b9-8a5e-59efbf4e29e3", + "Name": "AWS.Lambda.Package", + "Label": "Image", + "HelpText": "Required. + +The registry containing the image you wish to deploy to the AWS Lambda function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "a44a4664-f3b7-41f7-bc9c-cd7e06be7dbe", + "Name": "AWS.Lambda.FunctionRole", + "Label": "Function Role", + "HelpText": "Required. + +The Amazon Resource Name (ARN) of the function’s execution role. This role must exist prior to this step is run. See [documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html) for more detail on creating an execution role.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "12b11c8c-b31d-42b1-a4a4-c7fa0cf6219d", + "Name": "AWS.Lambda.MemorySize", + "Label": "Memory Size", + "HelpText": "Required. + +The amount of memory that your function has access to. Increasing the function’s memory also increases its CPU allocation. The default value is 128 MB. The value must be a multiple of 64 MB.", + "DefaultValue": "128", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c16ab198-835b-4c1d-b289-b87ad5fe3df2", + "Name": "AWS.Lambda.Description", + "Label": "Description", + "HelpText": "Optional. + +A description of the function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "903476ad-6eca-4ddf-a656-f121c12e8785", + "Name": "AWS.Lambda.VPCSubnetIds", + "Label": "VPC Subnet Ids", + "HelpText": "Optional. + +Format: `SubnetId1,SubnetId2` + +For network connectivity to AWS resources in a VPC, specify a list of security groups and subnets in the VPC. When you connect a function to a VPC, it can only access resources and the internet through that VPC. For more information, see [VPC Settings](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "457ac64f-5538-49b4-b7a2-f6ab97b5affe", + "Name": "AWS.Lambda.VPCSecurityGroupIds", + "Label": "VPC Security Group Ids", + "HelpText": "Optional. + +Format: `SecurityGroupId1,SecurityGroupId2` + +For network connectivity to AWS resources in a VPC, specify a list of security groups and subnets in the VPC. When you connect a function to a VPC, it can only access resources and the internet through that VPC. For more information, see [VPC Settings](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6ba57e09-dfea-4dcb-9372-6e9fc5d43051", + "Name": "AWS.Lambda.Image.Entrypoint", + "Label": "Entrypoint override", + "HelpText": "Optional for Image Package type. + +Comma-delimited list of commands to override the Image Entrypoint. + +Format: `entrypoint1, entrypoint2` + +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f85d4334-e896-488f-aeef-bf6d66efc8a5", + "Name": "AWS.Lambda.Image.Command", + "Label": "Command override", + "HelpText": "Optional for Image Package type. + +Comma-delimited list of commands to override the Image Entrypoint. + +Format: `command1, command2`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "383e3565-d091-447e-828e-f6ab50d79150", + "Name": "AWS.Lambda.EnvironmentVariables", + "Label": "Environment Variables", + "HelpText": "Optional. + +Format: `KeyName1=string,KeyName2=string` + +Environment variables that are accessible from function code during execution.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "53366a6a-783e-4db9-b8af-eb5665ea805f", + "Name": "AWS.Lambda.EnvironmentVariablesKey", + "Label": "Environment Variables Encryption Key", + "HelpText": "Optional. + +The ARN of the AWS Key Management Service (AWS KMS) key that’s used to encrypt your function’s environment variables. If it’s not provided, AWS Lambda uses a default service key. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b2c16584-9f90-4dc1-85de-539cddd6482b", + "Name": "AWS.Lambda.FunctionTimeout", + "Label": "Timeout", + "HelpText": "Optional. + +The amount of time that Lambda allows a function to run before stopping it. The default is 3 seconds. The maximum allowed value is 900 seconds.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e67b885a-5972-458d-9d67-2b5b8860fe61", + "Name": "AWS.Lambda.Tags", + "Label": "Tags", + "HelpText": "Optional. + +Format: `KeyName1=string,KeyName2=string` + +A list of tags to apply to the function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bb8af54e-5f79-45c4-b150-bdf4ee377b7b", + "Name": "AWS.Lambda.FileSystemConfig", + "Label": "File System Config", + "HelpText": "Optional. + +Format: `Arn=string,LocalMountPath=string` + +Connection settings for an Amazon EFS file system.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "59047de4-a921-4437-a2a2-c9e684bcf21e", + "Name": "AWS.Lambda.TracingConfig", + "Label": "Tracing Config", + "HelpText": "Optional. + +Format: `Mode=string` + +Set Mode to Active to sample and trace a subset of incoming requests with AWS X-Ray.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "37a91d63-9c42-4d49-832b-dbe64d81f6da", + "Name": "AWS.Lambda.DeadLetterConfig", + "Label": "Dead Letter Config", + "HelpText": "Optional. + +Format: `TargetArn=string` + +A dead letter queue configuration that specifies the queue or topic where Lambda sends asynchronous events when they fail processing. For more information, see [Dead Letter Queues](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html#dlq). +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1db6270a-9138-48d8-8d34-05c39ade10a8", + "Name": "AWS.Lambda.Publish", + "Label": "Publish", + "HelpText": "Required. + +Creates a [version](https://docs.aws.amazon.com/lambda/latest/dg/versioning-aliases.html) from the current code and configuration of a function. Use versions to create a snapshot of your function code and configuration that doesn’t change. + +**Important**: Lambda doesn’t publish a version if the function’s configuration and code haven’t changed since the last version. Use UpdateFunctionCode or UpdateFunctionConfiguration to update the function before publishing a version.", + "DefaultValue": "Yes", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Yes|Yes +No|No" + } + } + ], + "StepPackageId": "Octopus.AwsRunScript", + "$Meta": { + "ExportedAt": "2024-06-03T15:07:31.689Z", + "OctopusVersion": "2024.3.1025", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "aws" +} diff --git a/step-templates/aws-deploy-lambda.json.human b/step-templates/aws-deploy-lambda.json.human new file mode 100644 index 000000000..9e390afcd --- /dev/null +++ b/step-templates/aws-deploy-lambda.json.human @@ -0,0 +1,601 @@ +{ + "Id": "9b5ee984-bdd2-49f0-a78a-07e21e60da8a", + "Name": "AWS - Deploy Lambda Function", + "Description": "Deploys a Zip file to an AWS Lambda function. + +This step does **not** perform variable substitution (it used to). It takes the .zip file from the specified feed and uploads it to AWS as is. The recommended approach to changing a lambda configuration per environment is to use [environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html) + +This step uses the following AWS CLI commands to deploy the AWS Lambda. You will be required to install the AWS CLI on your server/worker for this to work. The AWS CLI is pre-installed on the [dynamic workers](https://octopus.com/docs/infrastructure/workers/dynamic-worker-pools) in Octopus Cloud as well as the provided docker containers for [Execution Containers](https://octopus.com/docs/deployment-process/execution-containers-for-workers). + +- [create-function](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html) +- [get-function](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/get-function.html) +- [publish-version](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/publish-version.html) +- [tag-resource](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/tag-resource.html) +- [untag-resource](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/untag-resource.html) +- [update-function-code](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/update-function-code.html) +- [update-function-configuration](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/update-function-configuration.html) + +This step template is worker-friendly, you can pass in a package reference rather than having to reference a previous step that downloaded the package. This step requires **Octopus Deploy 2019.10.0** or higher. + +## Output Variables + +This step template sets the following output variables: + +- `LambdaArn`: The ARN of the Lambda Function +- `PublishedVersion`: The most recent version published (only set when Publish is set to `Yes`).", + "ActionType": "Octopus.AwsRunScript", + "Version": 8, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "8dbae499-5aa8-438e-a2fe-ae29fb8f0a39", + "Name": "AWS.Lambda.Package", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "False", + "SelectionMode": "deferred", + "PackageParameterName": "AWS.Lambda.Package" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.AwsAccount.Variable": "#{AWS.Lambda.Account}", + "Octopus.Action.Aws.Region": "#{AWS.Lambda.Region}", + "Octopus.Action.Script.ScriptBody": "$functionName = $OctopusParameters[\"AWS.Lambda.FunctionName\"] +$functionRole = $OctopusParameters[\"AWS.Lambda.FunctionRole\"] +$functionRunTime = $OctopusParameters[\"AWS.Lambda.Runtime\"] +$functionHandler = $OctopusParameters[\"AWS.Lambda.FunctionHandler\"] +$functionMemorySize = $OctopusParameters[\"AWS.Lambda.MemorySize\"] +$functionDescription = $OctopusParameters[\"AWS.Lambda.Description\"] +$functionVPCSubnetId = $OctopusParameters[\"AWS.Lambda.VPCSubnetIds\"] +$functionVPCSecurityGroupId = $OctopusParameters[\"AWS.Lambda.VPCSecurityGroupIds\"] +$functionEnvironmentVariables = $OctopusParameters[\"AWS.Lambda.EnvironmentVariables\"] +$functionEnvironmentVariablesKey = $OctopusParameters[\"AWS.Lambda.EnvironmentVariablesKey\"] +$functionTimeout = $OctopusParameters[\"AWS.Lambda.FunctionTimeout\"] +$functionTags = $OctopusParameters[\"AWS.Lambda.Tags\"] +$functionFileSystemConfig = $OctopusParameters[\"AWS.Lambda.FileSystemConfig\"] +$functionDeadLetterConfig = $OctopusParameters[\"AWS.Lambda.DeadLetterConfig\"] +$functionTracingConfig = $OctopusParameters[\"AWS.Lambda.TracingConfig\"] +$functionVersionNumber = $OctopusParameters[\"Octopus.Action.Package[AWS.Lambda.Package].PackageVersion\"] +$functionPublishOption = $OctopusParameters[\"AWS.Lambda.Publish\"] + +$functionReleaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$functionRunbookRun = $OctopusParameters[\"Octopus.RunbookRun.Id\"] +$stepName = $OctopusParameters[\"Octopus.Step.Name\"] + +$regionName = $OctopusParameters[\"AWS.Lambda.Region\"] +$newArchiveFileName = $OctopusParameters[\"Octopus.Action.Package[AWS.Lambda.Package].PackageFilePath\"] + +if ([string]::IsNullOrWhiteSpace($functionName)) +{ +\tWrite-Error \"The parameter Function Name is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionRole)) +{ +\tWrite-Error \"The parameter Role is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionRunTime)) +{ +\tWrite-Error \"The parameter Run Time is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionHandler)) +{ +\tWrite-Error \"The parameter Handler is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionPublishOption)) +{ +\tWrite-Error \"The parameter Publish is required.\" + Exit 1 +} + +if ([string]::IsNullOrWhiteSpace($functionReleaseNumber) -eq $false) +{ + $deployVersionTag = \"Octopus-Release=$functionReleaseNumber\" +} +else +{ +\t$deployVersionTag = \"Octopus-Runbook-Run=$functionRunbookRun\" +} + +Write-Host \"Function Name: $functionName\" +Write-Host \"Function Role: $functionRole\" +Write-Host \"Function Runtime: $functionRunTime\" +Write-Host \"Function Handler: $functionHandler\" +Write-Host \"Function Memory Size: $functionMemorySize\" +Write-Host \"Function Description: $functionDescription\" +Write-Host \"Function Subnet Ids: $functionVPCSubnetId\" +Write-Host \"Function Security Group Ids: $functionVPCSecurityGroupId\" +Write-Host \"Function Environment Variables: $functionEnvironmentVariables\" +Write-Host \"Function Environment Variables Key: $functionEnvironmentVariablesKey\" +Write-Host \"Function Timeout: $functionTimeout\" +Write-Host \"Function Tags: $functionTags\" +Write-Host \"Function File System Config: $functionFileSystemConfig\" +Write-Host \"Function Dead Letter Config: $functionDeadLetterConfig\" +Write-Host \"Function Tracing Config: $functionTracingConfig\" +Write-Host \"Function file path: fileb://$newArchiveFileName\" +Write-Host \"Function Publish: $functionPublishOption\" + +Write-Host \"Attempting to find the function $functionName in the region $regionName\" +$hasExistingFunction = $true + +try +{ + $existingFunction = aws lambda get-function --function-name \"$functionName\" 2> $null + + Write-Host \"The exit code from the lookup was $LASTEXITCODE\" + if ($LASTEXITCODE -eq 255 -or $LASTEXITCODE -eq 254) + { + \t$hasExistingFunction = $false + } + + $existingFunction = $existingFunction | ConvertFrom-Json +} +catch +{ +\tWrite-Host \"The function was not found\" +\t$hasExistingFunction = $false +} + +Write-Host \"Existing functions: $hasExistingFunction\" +Write-Host $existingFunction + +$aliasInformation = $null +if ($hasExistingFunction -eq $false) +{ +\tWrite-Highlight \"Creating $functionName in $regionName\" +\t$functionInformation = aws lambda create-function --function-name \"$functionName\" --zip-file fileb://$newArchiveFileName --handler $functionHandler --runtime $functionRuntime --role $functionRole --memory-size $functionMemorySize +} +else +{ +\tWrite-Highlight \"Updating the $functionName code\" + $updatedConfig = aws lambda update-function-code --function-name \"$functionName\" --zip-file fileb://$newArchiveFileName + + Write-Highlight \"Waiting for update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" + + Write-Highlight \"Updating the $functionName base configuration\" + $functionInformation = aws lambda update-function-configuration --function-name \"$functionName\" --role $functionRole --handler $functionHandler --runtime $functionRuntime --memory-size $functionMemorySize + + Write-Highlight \"Waiting for base configuration update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +$functionInformation = $functionInformation | ConvertFrom-JSON +$functionArn = $functionInformation.FunctionArn + +Write-Host \"Function ARN: $functionArn\" + +if ([string]::IsNullOrWhiteSpace($functionEnvironmentVariables) -eq $false) +{ +\tWrite-Highlight \"Environment variables specified, updating environment variables configuration for $functionName\" +\t$environmentVariables = \"Variables={$functionEnvironmentVariables}\" + + if ([string]::IsNullOrWhiteSpace($functionEnvironmentVariablesKey) -eq $true) + { + \t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --environment \"$environmentVariables\" + } + else + { + \t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --environment \"$environmentVariables\" --kms-key-arn \"$functionEnvironmentVariablesKey\" + } + + Write-Highlight \"Waiting for environment variable update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionTimeout) -eq $false) +{ +\tWrite-Highlight \"Timeout specified, updating timeout configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --timeout \"$functionTimeout\" + + Write-Highlight \"Waiting for timeout upate to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionTags) -eq $false) +{ +\tWrite-Highlight \"Tags specified, updating tags configuration for $functionName\" +\t$updatedConfig = aws lambda tag-resource --resource \"$functionArn\" --tags \"$functionTags\" +} + +if ([string]::IsNullOrWhiteSpace($deployVersionTag) -eq $false) +{ +\tWrite-Highlight \"Deploy version tag found with value of $deployVersionTag, updating tags configuration for $functionName\" + aws lambda untag-resource --resource \"$functionArn\" --tag-keys \"Octopus-Release\" \"Octopus-Runbook-Run\" +\t$updatedConfig = aws lambda tag-resource --resource \"$functionArn\" --tags \"$deployVersionTag\" +} + +if ([string]::IsNullOrWhiteSpace($functionVPCSubnetId) -eq $false -and [string]::IsNullOrWhiteSpace($functionVPCSecurityGroupId) -eq $false) +{ +\tWrite-Highlight \"VPC subnets and security group specified, updating vpc configuration for $functionName\" +\t$vpcConfig = \"SubnetIds=$functionVPCSubnetId,SecurityGroupIds=$functionVPCSecurityGroupId\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --vpc-config \"$vpcConfig\" + + Write-Highlight \"Waiting for vpc configuration to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionDescription) -eq $false) +{ +\tWrite-Highlight \"Description specified, updating description configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --description \"$functionDescription\" + + Write-Highlight \"Waiting for description configuration ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionFileSystemConfig) -eq $false) +{ +\tWrite-Highlight \"File System Config specified, updating file system configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --file-system-configs \"$functionFileSystemConfig\"\t + + Write-Highlight \"Wating for file system configuration update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionDeadLetterConfig) -eq $false) +{ +\tWrite-Highlight \"Dead Letter specified, updating dead letter configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --dead-letter-config \"$functionDeadLetterConfig\"\t + + Write-Highlight \"Waitng for Dead Letter configuration update to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +if ([string]::IsNullOrWhiteSpace($functionTracingConfig) -eq $false) +{ +\tWrite-Highlight \"Tracing config specified, updating tracing configuration for $functionName\" +\t$updatedConfig = aws lambda update-function-configuration --function-name \"$functionArn\" --tracing-config \"$functionTracingConfig\"\t + + Write-Highlight \"Waiting for tracing configuration to complete ...\" + aws lambda wait function-updated --function-name \"$functionName\" +} + +Write-Host $updatedConfig | ConvertFrom-JSON + +if ($functionPublishOption -eq \"Yes\") +{ +\tWrite-Highlight \"Publishing the function with the description $functionVersionNumber to create a snapshot of the current code and configuration of this function in AWS.\" +\t$publishedVersion = aws lambda publish-version --function-name \"$functionArn\" --description \"$functionVersionNumber\" + + $publishedVersion = $publishedVersion | ConvertFrom-JSON + + Write-Highlight \"Setting the output variable 'Octopus.Action[$($stepName)].Output.PublishedVersion' to $($publishedVersion.Version)\" + Set-OctopusVariable -name \"PublishedVersion\" -value \"$($publishedVersion.Version)\" +} + +Write-Highlight \"Setting the output variable 'Octopus.Action[$($stepName)].Output.LambdaArn' to $functionArn\" +Set-OctopusVariable -name \"LambdaArn\" -value \"$functionArn\" + +Write-Highlight \"AWS Lambda $functionName successfully deployed.\"", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "bf72bc3e-3ce6-4b63-b23f-1171b5cc72dd", + "Name": "AWS.Lambda.FunctionName", + "Label": "Function Name", + "HelpText": "Required. + +The name of the function to create or update. See [documentation](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html#options) + +Examples: +- Function name - my-function . +- Function ARN - arn:aws:lambda:us-west-2:123456789012:function:my-function . +- Partial ARN - 123456789012:function:my-function . + +The length constraint applies only to the full ARN. If you specify only the function name, it is limited to 64 characters in length. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "13dcec09-00f8-4af0-80e4-23bcb47eaf17", + "Name": "AWS.Lambda.Account", + "Label": "AWS Account", + "HelpText": "Required. + +The AWS Account with permissions to create / update AWS Lambdas. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "8fd7ff24-7557-4f96-a809-ce611c473b13", + "Name": "AWS.Lambda.Region", + "Label": "Region", + "HelpText": "Required. + +The region where the function will live.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "us-east-2|US East (Ohio) +us-east-1|US East (N. Virginia) +us-west-1|US West (N. California) +us-west-2|US West (Oregon) +af-south-1|Africa (Cape Town) +ap-east-1|Asia Pacific (Hong Kong) +ap-south-1|Asia Pacific (Mumbai) +ap-northeast-3|Asia Pacific (Osaka-Local) +ap-northeast-2|Asia Pacific (Seoul) +ap-southeast-1|Asia Pacific (Singapore) +ap-southeast-2|Asia Pacific (Sydney) +ap-northeast-1|Asia Pacific (Tokyo) +ca-central-1|Canada (Central) +eu-central-1|Europe (Frankfurt) +eu-west-1|Europe (Ireland) +eu-west-2|Europe (London) +eu-south-1|Europe (Milan) +eu-west-3|Europe (Paris) +eu-north-1|Europe (Stockholm) +me-south-1|Middle East (Bahrain) +sa-east-1|South America (São Paulo)" + } + }, + { + "Id": "11928ce7-e1b8-451c-9a9b-481acac60611", + "Name": "AWS.Lambda.Package", + "Label": "Package", + "HelpText": "Required. + +The package containing the code you wish to deploy to the AWS Lambda function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "d45499b4-5f4a-4bae-a4b9-336e97a75cdc", + "Name": "AWS.Lambda.FunctionRole", + "Label": "Function Role", + "HelpText": "Required. + +The Amazon Resource Name (ARN) of the function’s execution role. This role must exist prior to this step is run. See [documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html) for more detail on creating an execution role.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "926b93c6-a47a-4899-9865-e7329b93b4b8", + "Name": "AWS.Lambda.Runtime", + "Label": "Runtime", + "HelpText": "Required. + +The runtime of the AWS Lambda. See [documentation](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html#options) for more details on what runtimes are supported.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "nodejs|nodejs +nodejs4.3|nodejs4.3 +nodejs4.3-edge|nodejs4.3-edge +nodejs6.10|nodejs6.10 +nodejs8.10|nodejs8.10 +nodejs10.x|nodejs10.x +nodejs12.x|nodejs12.x +nodejs14.x|nodejs14.x +java8|java8 +java8.al2|java8.al2 +java11|java11 +python2.7|python2.7 +python3.6|python3.6 +python3.7|python3.7 +python3.8|python3.8 +python3.9|python3.9 +dotnetcore1.0|dotnetcore1.0 +dotnetcore2.0|dotnetcore2.0 +dotnetcore2.1|dotnetcore2.1 +dotnetcore3.1|dotnetcore3.1 +dotnet6|dotnet6 +nodejs4.3-edge|nodejs4.3-edge +go1.x|go1.x +ruby2.5|ruby2.5 +ruby2.7|ruby2.7 +provided|provided +provided.al2|provided.al2" + } + }, + { + "Id": "1b8bfb82-3736-4d9a-8b05-a39319eb5735", + "Name": "AWS.Lambda.FunctionHandler", + "Label": "Handler", + "HelpText": "Required. + +The name of the method within your code that Lambda calls to execute your function. The format includes the file name. It can also include namespaces and other qualifiers, depending on the runtime. For more information, see [Programming Model](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-features.html)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "68428e8e-abc3-4f29-a5bb-fe635281d073", + "Name": "AWS.Lambda.MemorySize", + "Label": "Memory Size", + "HelpText": "Required. + +The amount of memory that your function has access to. Increasing the function’s memory also increases its CPU allocation. The default value is 128 MB. The value must be a multiple of 64 MB.", + "DefaultValue": "128", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "948280bb-50af-495b-9d1d-2f7567a0b0cc", + "Name": "AWS.Lambda.Description", + "Label": "Description", + "HelpText": "Optional. + +A description of the function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ed2ab9bb-d8a3-4ab4-a576-36b6c0a8f75d", + "Name": "AWS.Lambda.VPCSubnetIds", + "Label": "VPC Subnet Ids", + "HelpText": "Optional. + +Format: `SubnetId1,SubnetId2` + +For network connectivity to AWS resources in a VPC, specify a list of security groups and subnets in the VPC. When you connect a function to a VPC, it can only access resources and the internet through that VPC. For more information, see [VPC Settings](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8c2793a7-1a88-40a2-be28-3a38c7b40658", + "Name": "AWS.Lambda.VPCSecurityGroupIds", + "Label": "VPC Security Group Ids", + "HelpText": "Optional. + +Format: `SecurityGroupId1,SecurityGroupId2` + +For network connectivity to AWS resources in a VPC, specify a list of security groups and subnets in the VPC. When you connect a function to a VPC, it can only access resources and the internet through that VPC. For more information, see [VPC Settings](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ba3edded-1e19-47c4-990a-ebdf4eb0bcca", + "Name": "AWS.Lambda.EnvironmentVariables", + "Label": "Environment Variables", + "HelpText": "Optional. + +Format: `KeyName1=string,KeyName2=string` + +Environment variables that are accessible from function code during execution.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "58d76440-e4f5-46fb-a095-84aedd904a18", + "Name": "AWS.Lambda.EnvironmentVariablesKey", + "Label": "Environment Variables Encryption Key", + "HelpText": "Optional. + +The ARN of the AWS Key Management Service (AWS KMS) key that’s used to encrypt your function’s environment variables. If it’s not provided, AWS Lambda uses a default service key. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5b9b3111-5349-49e6-ab0d-f386a53bdd7c", + "Name": "AWS.Lambda.FunctionTimeout", + "Label": "Timeout", + "HelpText": "Optional. + +The amount of time that Lambda allows a function to run before stopping it. The default is 3 seconds. The maximum allowed value is 900 seconds.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "567f5eeb-e174-4c8f-8b17-36bd9457ea29", + "Name": "AWS.Lambda.Tags", + "Label": "Tags", + "HelpText": "Optional. + +Format: `KeyName1=string,KeyName2=string` + +A list of tags to apply to the function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "297f4f8e-3837-43a3-b844-a0e0d02e9d5b", + "Name": "AWS.Lambda.FileSystemConfig", + "Label": "File System Config", + "HelpText": "Optional. + +Format: `Arn=string,LocalMountPath=string` + +Connection settings for an Amazon EFS file system.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c720a63a-7b77-4a13-b6ed-6d44126e9372", + "Name": "AWS.Lambda.TracingConfig", + "Label": "Tracing Config", + "HelpText": "Optional. + +Format: `Mode=string` + +Set Mode to Active to sample and trace a subset of incoming requests with AWS X-Ray.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "92fd8a1f-e681-4e1c-b382-3df1de12194e", + "Name": "AWS.Lambda.DeadLetterConfig", + "Label": "Dead Letter Config", + "HelpText": "Optional. + +Format: `TargetArn=string` + +A dead letter queue configuration that specifies the queue or topic where Lambda sends asynchronous events when they fail processing. For more information, see [Dead Letter Queues](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html#dlq). +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "67f8da1a-08f1-4cde-a60f-238d1fb08c98", + "Name": "AWS.Lambda.Publish", + "Label": "Publish", + "HelpText": "Required. + +Creates a [version](https://docs.aws.amazon.com/lambda/latest/dg/versioning-aliases.html) from the current code and configuration of a function. Use versions to create a snapshot of your function code and configuration that doesn’t change. + +**Important**: Lambda doesn’t publish a version if the function’s configuration and code haven’t changed since the last version. Use UpdateFunctionCode or UpdateFunctionConfiguration to update the function before publishing a version.", + "DefaultValue": "Yes", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Yes|Yes +No|No" + } + } + ], + "$Meta": { + "ExportedAt": "2022-09-16T00:50:35.270Z", + "OctopusVersion": "2022.3.10382", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "aws" + } diff --git a/step-templates/aws-deregister-elb-instance.json.human b/step-templates/aws-deregister-elb-instance.json.human new file mode 100644 index 000000000..51a8aa254 --- /dev/null +++ b/step-templates/aws-deregister-elb-instance.json.human @@ -0,0 +1,114 @@ +{ + "Id": "bcb05502-ae9a-48a0-a153-1cb3057efb61", + "Name": "AWS - Deregister ELB Instance", + "Description": "Removes the current instance from the ELB", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Part 1 of 2\r +# Part 1 Deregisters an EC2 instance from an ELB\r +# Part 2 Registers an EC2 instance with an ELB and waits for it to be InService\r +\r +$ec2Region = $OctopusParameters['ec2Region']\r +$ec2User = $OctopusParameters['ec2ClientId']\r +$ec2Credentials = $OctopusParameters['ec2Credentials']\r +$elbName = $OctopusParameters['elbName']\r +$instanceId = \"\"\r +\r +# Load EC2 credentials (not sure if this is needed when executed from an EC2 box)\r +try\r +{\r +\tWrite-Host \"Loading AWS Credentials...\"\r +\tImport-Module AWSPowerShell\r +\tSet-AWSCredentials -AccessKey $ec2User -SecretKey $ec2Credentials\r +\tSet-DefaultAWSRegion $ec2Region\r +\tWrite-Host \"AWS Credentials Loaded.\"\r +}\r +catch\r +{\r +\tWrite-Error -Message \"Failed to load AWS Credentials.\" -Exception $_.Exception\r +\tExit 1\r +}\r +\r +# Get EC2 Instance\r +try\r +{\r +\t$response = Invoke-RestMethod -Uri \"http://169.254.169.254/latest/meta-data/instance-id\" -Method Get\r +\tif ($response)\r +\t{\r +\t\t$instanceId = $response\r +\t}\r +\telse\r +\t{\r +\t\tWrite-Error -Message \"Returned Instance ID does not appear to be valid\"\r +\t\tExit 1\r +\t}\r +}\r +catch\r +{\r +\tWrite-Error -Message \"Failed to load instance ID from AWS.\" -Exception $_.Exception\r +\tExit 1\r +}\r +\r +# Deregister the current EC2 instance\r +Write-Host \"Deregistering instance $instanceId from $elbName\"\r +try\r +{\r +\tRemove-ELBInstanceFromLoadBalancer -LoadBalancerName $elbName -Instance $instanceId -Force\r +\tWrite-Host \"Instance deregistered\"\r +}\r +catch\r +{\r +\tWrite-Error -Message \"Failed to deregister instance.\" -Exception $_.Exception\r +\tExit 1\r +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ec2ClientId", + "Label": "AWS EC2 Client Id", + "HelpText": "The client id used to authenticate with AWS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "ec2Credentials", + "Label": "AWS EC2 Client Secret", + "HelpText": "The client secret used to authenticate with AWS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "elbName", + "Label": "AWS ELB Name", + "HelpText": "The name of the AWS ELB to remove the instance from", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ec2Region", + "Label": "AWS EC2 Region", + "HelpText": "The region in which the ELB lives", + "DefaultValue": "us-east-1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-04-30T13:24:08.210+00:00", + "LastModifiedBy": "DudeSolutions", + "$Meta": { + "ExportedAt": "2015-04-30T13:24:16.921+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-ec2-get-public-hostname.json.human b/step-templates/aws-ec2-get-public-hostname.json.human new file mode 100644 index 000000000..d65899406 --- /dev/null +++ b/step-templates/aws-ec2-get-public-hostname.json.human @@ -0,0 +1,29 @@ +{ + "Id": "42d2f9b7-12cc-4844-a767-5f4a29c68dab", + "Name": "AWS - EC2 - Get Public Hostname", + "Description": "Gets the public hostname from `http://instance-data/latest/meta-data/public-hostname` on the EC2 instance and stores it in the `Hostname` variable.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$downloader = new-object System.Net.WebClient\r +$hostname = $downloader.DownloadString(\"http://instance-data/latest/meta-data/public-hostname\")\r +Set-OctopusVariable -name \"Hostname\" -value $hostname\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [], + "LastModifiedOn": "2017-02-23T19:37:06.120+00:00", + "LastModifiedBy": "natelowry", + "$Meta": { + "ExportedAt": "2017-02-23T19:32:18.146Z", + "OctopusVersion": "3.8.2", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-elastic-beanstalk-dotnet-webapp-deploy.json.human b/step-templates/aws-elastic-beanstalk-dotnet-webapp-deploy.json.human new file mode 100644 index 000000000..19b38a6ba --- /dev/null +++ b/step-templates/aws-elastic-beanstalk-dotnet-webapp-deploy.json.human @@ -0,0 +1,181 @@ +{ + "Id": "11060b54-15bc-4b12-a912-197c6c18b7b7", + "Name": "AWS Elastic Beanstalk .Net WebApp Deploy", + "Description": "Deploy a .Net WebApp build to AWS Elastic Beanstalk. This template uses the awsdeploy tool. ALL step fields need to be populated for this template to work. + +AWS Toolkit needs to be installed on your deployment server for this to work properly: +https://aws.amazon.com/visualstudio/", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$env:Path += \";C:\\Program Files (x86)\\AWS Tools\\Deployment Tool\\;C:\\Program Files (x86)\\IIS\\Microsoft Web Deploy V3\\\"\r +$AwsDeployConfigFileName = \"aws-deploy.config\"\r +$AwsDeployConfigFile = Join-Path $BuildDirectory $AwsDeployConfigFileName\r +$MSDeployParamsFile = Join-Path $BuildDirectory $MSDeployParamsFilePath\r +$DeployArchive = Join-Path $MSDeployOutputDirectory.trim().replace(\" \", \"_\") \"deploy.zip\"\r +\r +if (!(Test-Path $AwsDeployConfigFile))\r +{\r + # Create an empty, dummy file (not used, awsdeploy params used instead)\r + New-Item -path $BuildDirectory -name $AwsDeployConfigFileName -type \"file\"\r +}\r +\r +if (Test-Path $DeployArchive)\r +{\r + # Delete deploy archive if it exists\r + Remove-Item $DeployArchive\r +}\r +\r +$EscapedBuildDirectory = $BuildDirectory -replace \"\\\\\",\"\\\\\"\r +$EscapedBuildDirectory = $EscapedBuildDirectory -replace \"\\.\",\"\\.\"\r +$MSDeployParamsContent = (Get-Content $MSDeployParamsFile)\r +$MSDeployParamsContent = $MSDeployParamsContent -replace \"{BUILD_DIRECTORY}\",$EscapedBuildDirectory\r +Set-Content $MSDeployParamsFile $MSDeployParamsContent\r +\r +Write-Host \"Creating WebDeploy package file $DeployArchive with the contents of directory $BuildDirectory\"\r +msdeploy.exe -verb:sync `\r + -source:iisApp=\"$BuildDirectory\" `\r + -dest:package=\"$DeployArchive\" `\r + -declareParamFile=\"$MSDeployParamsFile\"\r +\r +Write-Host \"Starting AWSdeploy\"\r +awsdeploy -r -v `\r + \"-DAWSProfileName=$($ProfileName)\" `\r + \"-DApplication.Name=$($ApplicationName)\" `\r + \"-DEnvironment.Name=$($EnvironmentName)\" `\r + \"-DRegion=$($Region)\" `\r + \"-DUploadBucket=$($UploadBucket)\" `\r + \"-DAWSAccessKey=$($AccessKey)\" `\r + \"-DAWSSecretKey=$($SecretKey)\" `\r + \"-DTemplate=ElasticBeanstalk\" `\r + \"-DDeploymentPackage=$($DeployArchive)\" `\r + \"$AwsDeployConfigFile\"\r +\r +# Sleep to give time to the deployment process to start\r +Start-Sleep -Seconds 5\r +\r +$i = 0\r +$isReady = $FALSE\r +# Wait no more than 10 minutes for the deployment to finish (or 120 sleeps of 5 seconds)\r +while ((!$isReady) -and ($i -lt 120)) {\r + $i++\r + $ebHealth = Get-EBEnvironment -AccessKey \"$AccessKey\" -SecretKey \"$SecretKey\" -Region \"$Region\" -EnvironmentName \"$EnvironmentName\"\r +\r + if ($ebHealth.Status -eq \"Ready\") {\r + Write-Host \"Deployment successful.\"\r + $isReady=$TRUE;\r + } else {\r + Write-Host \"Deployment status: $($ebHealth.Status)\"\r + }\r + Start-Sleep -Seconds 5\r +}\r +\r +if (!$isReady) {\r + Write-Host \"Deployment failed. Please check your AWS console.\"\r +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "ProfileName", + "Label": "Profile Name", + "HelpText": "AWS Profile Name", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApplicationName", + "Label": "Application Name", + "HelpText": "AWS Application Name", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "EnvironmentName", + "Label": "Environment Name", + "HelpText": "AWS Environment Name", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Region", + "Label": "Region", + "HelpText": "AWS Application Region", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "UploadBucket", + "Label": "Upload Bucket", + "HelpText": "AWS Application Upload Bucket", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AccessKey", + "Label": "Access Key", + "HelpText": "AWS Access Key", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SecretKey", + "Label": "Secret Key", + "HelpText": "AWS Secret Key", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BuildDirectory", + "Label": "Build Directory", + "HelpText": "Path to the compiled code that needs to be packaged by MSDeploy.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "MSDeployOutputDirectory", + "Label": "MSDeploy Output Directory", + "HelpText": "Path where MSDeploy should put the newly created package.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "MSDeployParamsFilePath", + "Label": "MSDeploy Params File Path", + "HelpText": "Subpath (relative to your #{BuildDirectory}) to the \"parameters.xml\" file. Include filename.", + "DefaultValue": "parameters.xml", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "dovetail-technologies", + "$Meta": { + "ExportedAt": "2017-01-05T09:21:06.133Z", + "OctopusVersion": "3.3.15", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-execute-powershell.json.human b/step-templates/aws-execute-powershell.json.human new file mode 100644 index 000000000..b3f58f5ef --- /dev/null +++ b/step-templates/aws-execute-powershell.json.human @@ -0,0 +1,96 @@ +{ + "Id": "551a6f13-9e4c-4d45-8387-f424d5ca02a4", + "Name": "Execute AWS Powershell Script", + "Description": "This combines two previous library templates of [checking for Chocolatey being installed](https://library.octopus.com//step-templates/c364b0a5-a0b7-48f8-a1a4-35e9f54a82d3/actiontemplate-chocolatey-ensure-installed), and [installing something via Chocolatey](https://library.octopus.com/step-templates/b2385b12-e5b5-440f-bed8-6598c29b2528/actiontemplate-chocolatey-install-package), in this case awstools.powershell, and then adds on a third piece of running a custom Powershell script using AWS Powershell tools", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Write-Output \"Ensuring the Chocolatey package manager is installed...\" + +$chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"Machine\") + \"\\bin\" +$chocolateyExe = \"$chocolateyBin\\choco.exe\" +$chocInstalled = Test-Path $chocolateyExe + +if (-not $chocInstalled) { + Write-Output \"Chocolatey not found, installing...\" + + $installPs1 = ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1')) + Invoke-Expression $installPs1 + + Write-Output \"Chocolatey installation complete.\" +} else { + Write-Output \"Chocolatey was found at $chocolateyBin and won't be reinstalled.\" +} + +$ChocolateyPackageId = 'awstools.powershell' + +if (-not $ChocolateyPackageId) { + throw \"Please specify the ID of an application package to install.\" +} + +if (-not $ChocolateyPackageVersion) { + Write-Output \"Installing package $ChocolateyPackageId from the Chocolatey package repository...\" + & $chocolateyExe install $ChocolateyPackageId +} else { + Write-Output \"Installing package $ChocolateyPackageId version $ChocolateyPackageVersion from the Chocolatey package repository...\" + & $chocolateyExe install $ChocolateyPackageId --version $ChocolateyPackageVersion +} + +Import-Module \"C:\\Program Files (x86)\\AWS Tools\\PowerShell\\AWSPowerShell\\AWSPowerShell.psd1\" + +Set-AWSCredentials -AccessKey $AccessKey -SecretKey $SecretKey -StoreAs AWSKeyProfile + +Initialize-AWSDefaults -ProfileName AWSKeyProfile -Region $Region + + +Invoke-Expression $AWSScript", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "SecretKey", + "Label": "SecretKey", + "HelpText": "Enter your AWS Secret Key here. This will be used to authenticate the session with AWS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AccessKey", + "Label": "AccessKey", + "HelpText": "Enter your AWS Access Key here. This will be used to authenticate the session with AWS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Region", + "Label": "Region", + "HelpText": "Enter the region for where you will be executing your powershell scripts against. If you are unsure of the region you are in, check the list found [here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AWSScript", + "Label": "AWSScript", + "HelpText": "This is the Powershell Script that contains commands using the AWSPowershell module that you want to execute", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedOn": "2023-06-05T12:27:05.077+00:00", + "LastModifiedBy": "pauby", + "$Meta": { + "ExportedAt": "2023-06-05T12:27:05.077+00:00", + "OctopusVersion": "2.5.8.447", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-lambda-create.json.human b/step-templates/aws-lambda-create.json.human new file mode 100644 index 000000000..fe77cc6dd --- /dev/null +++ b/step-templates/aws-lambda-create.json.human @@ -0,0 +1,287 @@ +{ + "Id": "001609c0-35d0-4faa-95c3-a995faaeaa5e", + "Name": "AWS - Create Lambda (deprecated)", + "Description": "Creates a [AWS Lambda Function](#https://aws.amazon.com/lambda/) from the specified zip. + +If the function exists, it will update the function code and update function configuration. +- Requires the [AWS PowerShell cmdlets](http://aws.amazon.com/powershell/) + +To create environment variables, add variables in project starting with 'env.'. +For example, to create environment variable S3BucketName = MyTestFolder, create variable 'env.S3BucketName' = 'MyTestFolder'. This function has been deprecated in favor of the new AWS Deploy Lambda step template. That new step requires 2019.10.x to run properly. This step is left in place for older versions of Octopus Deploy to still use.", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Check for the PowerShell cmdlets (from AWS - Create Cloud Formation Stack Octopus Step).\r +try{ \r + Import-Module AWSPowerShell -ErrorAction Stop\r +}catch{\r + \r + $modulePath = \"C:\\Program Files (x86)\\AWS Tools\\PowerShell\\AWSPowerShell\\AWSPowerShell.psd1\"\r + Write-Output \"Unable to find the AWS module checking $modulePath\" \r + \r + try{\r + Import-Module $modulePath \r + }\r + catch{\r + throw \"AWS PowerShell not found! Please make sure to install them from https://aws.amazon.com/powershell/\" \r + }\r +}\r +\r +function Get-EnvironmentVariables () {\r + $resultEV = @{}\r + $environmentVariableConst = 'env.'\r +\r + $envVariables = $OctopusParameters.Keys | ? {$_ -like $environmentVariableConst + '*' }\r + \r + foreach($item in $envVariables)\r + {\r + $key = $item.Replace($environmentVariableConst, '')\r + $value = $OctopusParameters[$item]\r +\r + $resultEV.Add($key, $value)\r + }\r + \r + return $resultEV\r +}\r +\r +# Get the parameters.\r +$functionName = $OctopusParameters['FunctionName']\r +$functionZip = $OctopusParameters['FunctionZip']\r +$handler = $OctopusParameters['Handler']\r +$runtime = $OctopusParameters['Runtime']\r +$role = $OctopusParameters['Role']\r +$description = $OctopusParameters['Description']\r +$memorySize = $OctopusParameters['MemorySize']\r +$timeout = $OctopusParameters['Timeout']\r +$awsRegion = $OctopusParameters['AWSRegion']\r +$awsSecretAccessKey = $OctopusParameters['AWSSecretAccessKey']\r +$awsAccessKey = $OctopusParameters['AWSAccessKey']\r +$AWSCL_VpcConfig_SubnetId = $OctopusParameters['AWSCL_VpcConfig_SubnetId']\r +$vpcSubnetIds = if($AWSCL_VpcConfig_SubnetId) { $AWSCL_VpcConfig_SubnetId.Split(',') }\r +$AWSCL_VpcConfig_SecurityGroupId = $OctopusParameters['AWSCL_VpcConfig_SecurityGroupId']\r +if($AWSCL_VpcConfig_SecurityGroupId) { $vpcSecurityGroupIds = $AWSCL_VpcConfig_SecurityGroupId.Split(',') }\r +\r +# Check the parameters.\r +if (-NOT $awsSecretAccessKey) { throw \"You must enter a value for 'AWS Access Key'.\" }\r +if (-NOT $awsAccessKey) { throw \"You must enter a value for 'AWS Secret Access Key'.\" }\r +if (-NOT $awsRegion) { throw \"You must enter a value for 'AWS Region'.\" }\r +if (-NOT $functionName) { throw \"You must enter a value for 'Function Name'.\" }\r +if (-NOT $functionZip) { throw \"You must enter a value for 'Function Zip'.\" }\r +if (-NOT $handler) { throw \"You must enter a value for 'Handler'.\" }\r +if (-NOT $runtime) { throw \"You must enter a value for 'Runtime'.\" }\r +if (-NOT $role) { throw \"You must enter a value for 'Role'.\" }\r +if (-NOT $memorySize) { throw \"You must enter a value for 'Memory Size'.\" }\r +if (-NOT $timeout) { throw \"You must enter a value for 'Timeout'.\" }\r +\r +Write-Output \"--------------------------------------------------\"\r +Write-Output \"AWS Region: $awsRegion\"\r +Write-Output \"AWS Lambda Function Name: $functionName\"\r +Write-Output \"AWS Lambda Handler: $handler\"\r +Write-Output \"AWS Lambda Runtime: $runtime\"\r +Write-Output \"AWS Lambda Memory Size: $memorySize\"\r +Write-Output \"AWS Lambda Timeout: $timeout\"\r +Write-Output \"AWS Lambda Role: $role\"\r +Write-Output \"--------------------------------------------------\"\r +\r +# Set up the credentials and the dependencies.\r +Set-DefaultAWSRegion -Region $awsRegion\r +$credential = New-AWSCredentials -AccessKey $awsAccessKey -SecretKey $awsSecretAccessKey\r +\r +$awsEnvironmentVariables = Get-EnvironmentVariables\r +\r +# Check if the function exists, with a try catch\r +try {\r + Get-LMFunction -Credential $credential -FunctionName $functionName -Region $awsRegion\r + \r + Write-Host 'Updating Lambda function code.'\r +\r + # Update the function.\r + Update-LMFunctionCode -Credential $credential -Region $awsRegion -FunctionName $functionName -ZipFilename $functionZip\r + \r + Write-Host 'Updating Lambda function configuration.'\r +\r + Update-LMFunctionConfiguration -Credential $credential -Region $awsRegion -FunctionName $functionName -Description $description -Handler $handler -MemorySize $memorySize -Role $role -Runtime $runtime -Timeout $timeout -Environment_Variable $awsEnvironmentVariables -VpcConfig_SecurityGroupId $vpcSecurityGroupIds -VpcConfig_SubnetId $vpcSubnetIds\r + \r + # Feedback\r + Write-Output \"--------------------------------------------------\"\r + Write-Output \"AWS Lambda Function updated.\"\r + Write-Output \"--------------------------------------------------\"\r +}\r +catch {\r + # Create the function.\r + Publish-LMFunction -Credential $credential -Region $awsRegion -FunctionName $functionName -FunctionZip $functionZip -Handler $handler -Runtime $runtime -Role $role -Description $description -MemorySize $memorySize -Timeout $timeout -Environment_Variable $awsEnvironmentVariables -VpcConfig_SecurityGroupId $vpcSecurityGroupIds -VpcConfig_SubnetId $vpcSubnetIds\r +\r + # Feedback\r + Write-Output \"--------------------------------------------------\"\r + Write-Output \"AWS Lambda Function created.\"\r + Write-Output \"--------------------------------------------------\"\r +}\r +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Id": "d0fe2641-f1ea-4760-aac0-da3b0a68d27a", + "Name": "AWSSecretAccessKey", + "Label": "AWS Secret Access Key", + "HelpText": "The [secret access key](http://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys) to use when executing the script", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "2102fea4-4491-40da-b91c-d0044a131e4c", + "Name": "AWSAccessKey", + "Label": "AWS Access Key", + "HelpText": "The [access key](http://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys) to use when executing the script", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "cc68295e-5bd7-42a0-a093-21cf99cbedb8", + "Name": "AWSRegion", + "Label": "AWS Region", + "HelpText": "The Amazon Region see [https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/) for further info", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2bc4b496-8267-41ef-a319-d2d028e3d8ae", + "Name": "FunctionName", + "Label": "AWS Lambda Function Name", + "HelpText": "The name of the AWS Lambda Function", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b1e02e55-b7fb-434e-aa3b-10e2f136e16a", + "Name": "FunctionZip", + "Label": "AWS Lambda Function Zip Location", + "HelpText": "The zip location of the AWS Lambda Function", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c51f5b24-3b86-4285-b406-5cc90c6b95a4", + "Name": "Handler", + "Label": "AWS Lambda Function Handler", + "HelpText": "The handler signature (ASSEMBLY::TYPE::METHOD) of the AWS Lambda Function", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "0373bf3a-e961-4722-8cf9-e607b42ca344", + "Name": "Runtime", + "Label": "AWS Lambda Function Runtime", + "HelpText": "The runtime of the AWS Lambda Function", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "nodejs|nodejs +nodejs4.3|nodejs4.3 +nodejs6.10|nodejs6.10 +java8|java8 +python2.7|python2.7 +python3.6|python3.6 +dotnetcore1.0|dotnetcore1.0 +dotnetcore2.0|dotnetcore2.0 +nodejs4.3-edge|nodejs4.3-edge +go1.x|go1.x" + }, + "Links": {} + }, + { + "Id": "17791000-4b1c-425e-992a-3909b085a3ad", + "Name": "Role", + "Label": "AWS Lambda Function Role", + "HelpText": "The role of the AWS Lambda Function, in [ARN](http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) format", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "eef1166c-1d9c-483f-95e4-5603d6f04e70", + "Name": "Description", + "Label": "AWS Lambda Function Description", + "HelpText": "The description of the AWS Lambda Function", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "ac367ef3-7aef-45c3-89f2-1e7ef2821cf3", + "Name": "MemorySize", + "Label": "AWS Lambda Function Memory Size", + "HelpText": "The memory size of the AWS Lambda Function. The default value is 128 MB. The value must be a multiple of 64 MB", + "DefaultValue": "128", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "48d6f0dc-b8de-4903-8411-5bde136c2ccd", + "Name": "Timeout", + "Label": "AWS Lambda Function Timeout", + "HelpText": "The timeout of the AWS Lambda Function", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "5fd40dab-518e-4329-9bff-c61a6e4177fa", + "Name": "AWSCL_VpcConfig_SubnetId", + "Label": "AWS Lambda Function VPC Subnet Id", + "HelpText": "A list of one or more subnet IDs in your VPC.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "4918ab99-d21c-4002-9a04-c5073979ed4b", + "Name": "AWSCL_VpcConfig_SecurityGroupId", + "Label": "AWS Lambda Function VPC Security Group Id", + "HelpText": "A list of one or more security groups IDs in your VPC.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2021-02-26T12:09:57.788Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2018-04-16T10:25:57.788Z", + "OctopusVersion": "2018.3.6", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-launch-ec2-instance.json.human b/step-templates/aws-launch-ec2-instance.json.human new file mode 100644 index 000000000..5498b943b --- /dev/null +++ b/step-templates/aws-launch-ec2-instance.json.human @@ -0,0 +1,478 @@ +{ + "Id": "eed13694-0208-4614-aca9-e82309838dd8", + "Name": "AWS - Launch EC2 Instance", + "Description": "This step will launch an EC2 instance, or start an instance previously created by this step. + +The goal being that only one specific instance will be created, and on subsequent deploys, if the instance is not running it will be started. If the instance was terminated, a new instance will be created in its place. Basically, this step says \"_I will make sure the specified Instance will be running after this step_\". + +This step makes the following Octopus \"Output\" Variables available to subsequent steps: +- **InstanceId** (e.g. i-a1b2c3d4e5f6g7h8i) +- **PrivateIpAddress** (e.g. 192.168.0.100) +- **PublicIpAddress** (e.g. 52.64.52.64) +- Usage: _#{Octopus.Action[AWS - Launch EC2 Instance].Output.**InstanceId**}_ + +Now what...? Well, you could use the \"UserData\" field to [automatically install and register a Tentacle](https://octopus.com/blog/auto-provision-ec2-instances-with-tentacle-installed). + +Note for UserData: _If you're creating your own image (AMI), you may need to [re-enable User Data](https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/UsingConfig_WinAMI.html#UsingConfigInterface_WinAMI), then create the image._ + +With a couple of extra step templates you can: +- Wait for the Instance to register as a Deployment Target (_Octopus - Wait for Deployment Target registration_) +- Include the new Instance in subsequent deployment steps (_Health Check_) + +[AWS Tools for Windows PowerShell](http://aws.amazon.com/powershell/) must be installed on the Server/Target you plan on running this step template on.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +param( + [string]$odTags, + [string]$odImageId, + [string]$odInstanceType, + [string]$odSubnetId, + [string]$odSecurityGroupId, + [string]$odKeyName, + [string]$odRegion, + [string]$odUserData, + [decimal]$odSpotPrice, + [string]$odSpotProduct, + [string]$odAccessKey, + [string]$odSecretKey, + [switch]$whatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if (!$result -or $result -eq $null) { + if ($Default) { + $result = $Default + } elseif ($Required) { + Throw \"Missing parameter value $Name\" + } + } + + return $result +} + + +function removeTags($instanceId, $tags) +{ + (ConvertFrom-StringData $tags).GetEnumerator() | Foreach-Object { + try { + Remove-EC2Tag -Tags @{key=$_.Key} -resourceId $instanceId -Force + } + catch [Amazon.EC2.AmazonEC2Exception] { + Throw $_.Exception.errorcode + '-' + $_.Exception.Message + } + } +} + +function NewTags($instanceId, $tags) +{ + (ConvertFrom-StringData $tags).GetEnumerator() | Foreach-Object { + try { + New-EC2Tag -Tags @{key=$_.Key;value=$_.Value} -resourceId $instanceId + } + catch [Amazon.EC2.AmazonEC2Exception] { + Throw $_.Exception.errorcode + '-' + $_.Exception.Message + } + } +} + + +& { + param( + [string]$odTags, + [string]$odImageId, + [string]$odInstanceType, + [string]$odSubnetId, + [string]$odSecurityGroupId, + [string]$odKeyName, + [string]$odRegion, + [string]$odUserData, + [decimal]$odSpotPrice, + [string]$odSpotProduct, + [string]$odAccessKey, + [string]$odSecretKey + ) + + # If AWS key's are not provided as params, attempt to retrieve them from Environment Variables + if ($odAccessKey -or $odSecretKey) { + Set-AWSCredentials -AccessKey $odAccessKey -SecretKey $odSecretKey -StoreAs default + } elseif (([Environment]::GetEnvironmentVariable(\"AWS_ACCESS_KEY\", \"Machine\")) -or ([Environment]::GetEnvironmentVariable(\"AWS_SECRET_KEY\", \"Machine\"))) { + Set-AWSCredentials -AccessKey ([Environment]::GetEnvironmentVariable(\"AWS_ACCESS_KEY\", \"Machine\")) -SecretKey ([Environment]::GetEnvironmentVariable(\"AWS_SECRET_KEY\", \"Machine\")) -StoreAs default + } else { + Throw \"AWS API credentials were not available/provided.\" + } + + if ($odTags) { + $filterArray = @() + (ConvertFrom-StringData $odTags).GetEnumerator() | Foreach-Object { + $filterHash = @{ Name=\"tag:\"+$_.Key;value=$_.Value } + $filterArray += $filterHash + } + + $instanceObj = (Get-EC2Instance -Filter $filterArray | select -ExpandProperty Instances) + $instanceCount = ($instanceObj | measure).Count + $instanceId = $null + $currentState = \"missing\" + + if ($instanceCount -gt 1) { + Throw \"More than one instance exists with the same tags - I don't know what to do!?\" + } + elseif ($instanceCount -eq 1) { + $instanceId = ($instanceObj).InstanceId + $currentState = (Get-EC2Instance $instanceId).Instances.State.Name + + Write-Output (\"------------------------------\") + Write-Output (\"Checking the EC2 Instance status:\") + Write-Output (\"------------------------------\") + + $timeout = (New-Timespan -Seconds 90) + $sw = [diagnostics.stopwatch]::StartNew() + + while ($true) { + $currentState = (Get-EC2Instance $instanceId).Instances.State.Name + + if ($currentState -eq \"running\") { + break + } + elseif ($currentState -eq \"terminated\") { + (removeTags $instanceId $odTags) | out-null + $instanceId = $null + break + } + elseif ($currentState -eq \"stopped\") { + (Start-EC2Instance -InstanceIds $instanceId) | out-null + } + + Write-Output (\"$(Get-Date) | Waiting for Instance '$instanceId' to transition from state: $currentState\") + + if ($sw.elapsed -gt $timeout) {Throw \"Timed out waiting for desired state\"} + + Sleep -Seconds 5 + } + + Write-Output (\"$(Get-Date) | Instance state: $currentState\") + } + + # If the instance doesn't exist, create it! + if ($instanceId -eq $null) { + Write-Output (\"------------------------------\") + Write-Output (\"Creating a new EC2 instance:\") + Write-Output (\"------------------------------\") + + $encodedUserData = $null + if ($odUserData -ne $null) { $encodedUserData = [System.Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes($odUserData) ) } + + if (!$odSpotPrice) { + Write-Output (\"$(Get-Date) | Attempting to create a new EC2 instance from ImageId: $odImageId\") + + try { + $ec2Instances = (New-EC2Instance -ImageId $odImageId -KeyName $odKeyName -SecurityGroupId $odSecurityGroupId -InstanceType $odInstanceType -UserData $encodedUserData -SubnetId $odSubnetId -Region $odRegion) + + $instanceId = $ec2Instances.Instances[0].InstanceId + } + catch [Amazon.EC2.AmazonEC2Exception] { + Write-Output ($_.Exception.errorcode) + Write-Output ($_.Exception.Message) + Throw \"Couldn't launch EC2 instance, sorry...\" + } + } + elseif ($odSpotPrice) { + Write-Output (\"$(Get-Date) | Attempting to create a new EC2 spot-instance from ImageId: $odImageId\") + + try { + $spotInstancePricingObj = (Get-EC2SpotPriceHistory -InstanceType $odInstanceType -Region $odRegion -Filter @{ Name=\"product-description\";value=\"$odSpotProduct\" } -MaxResult 10) + + Write-Output (\"------------------------------\") + Write-Output (\"Listing the 10 most recent spot price changes for \" + $odRegion + \":\") + Write-Output (\"------------------------------\") + + [decimal]$highPrice=0 + $spotInstancePricingObj | Foreach-Object { + if ($highPrice -lt $_.Price) { $highPrice=$_.Price } + Write-Output ($_.AvailabilityZone + \" | \" + $_.InstanceType + \" | \" + $_.Price + \" | \" + $_.ProductDescription + \" | \" + $_.Timestamp) + } + + if ($odSpotPrice -lt ($highPrice*1.1)) { Write-Output (\"WARNING: Requested spot price (\" + $odSpotPrice + \") may be too low: Below 10% of recent high (\" + $highPrice + \")\") } + if ($odSpotPrice -gt ($highPrice*5)) { Write-Output (\"WARNING: Requested spot price (\" + $odSpotPrice + \") may be too high: Over 5x recent high (\" + $highPrice + \")\") } + } + catch [Amazon.EC2.AmazonEC2Exception] { + Write-Output ($_.Exception.errorcode) + Write-Output ($_.Exception.Message) + Throw \"Couldn't gather spot pricing details, sorry...\" + } + + try { + $if0 = (New-Object Amazon.EC2.Model.InstanceNetworkInterfaceSpecification) + $if0.DeviceIndex = 0 + $if0.SubnetId = $odSubnetId + $if0.Groups.Add($odSecurityGroupId) + + $spotInstanceRequestObj = (Request-EC2SpotInstance -SpotPrice $odSpotPrice -InstanceCount 1 -Type one-time -LaunchSpecification_ImageId $odImageId -LaunchSpecification_KeyName $odKeyName -LaunchSpecification_InstanceType $odInstanceType -LaunchSpecification_UserData $encodedUserData -Region $odRegion -LaunchSpecification_NetworkInterfaces $if0) + + Write-Output (\"------------------------------\") + Write-Output (\"Checking the spot request status:\") + Write-Output (\"------------------------------\") + + $timeout = (New-Timespan -Seconds 300) + $sw = [diagnostics.stopwatch]::StartNew() + + while ($true) + { + if ($sw.elapsed -gt $timeout) { Throw \"Timed out waiting for spot instance - please check manually!\" } + + $spotcurrentState = (Get-EC2SpotInstanceRequest -SpotInstanceRequestId ($spotInstanceRequestObj.SpotInstanceRequestId)).State + Write-Output (\"$(Get-Date) | Current State: $spotcurrentState | Desired State: active\") + if ($spotcurrentState -eq \"active\") { break } + + Sleep -Seconds 5 + } + + Write-Output (\"------------------------------\") + Write-Output (\"Spot Request details:\") + Write-Output (Get-EC2SpotInstanceRequest -SpotInstanceRequestId ($spotInstanceRequestObj.SpotInstanceRequestId)) + Write-Output (\"------------------------------\") + + $instanceId = (Get-EC2SpotInstanceRequest -SpotInstanceRequestId ($spotInstanceRequestObj.SpotInstanceRequestId)).InstanceId + } + catch [Amazon.EC2.AmazonEC2Exception] { + Write-Output ($_.Exception.errorcode) + Write-Output ($_.Exception.Message) + Throw \"Couldn't launch spot instance\" + } + } + + if ($odTags) { NewTags $instanceId $odTags } + } + + if ($instanceId) { + if ($currentState -ne \"running\") { + Write-Output (\"------------------------------\") + Write-Output (\"Checking the EC2 Instance status:\") + Write-Output (\"------------------------------\") + + Write-Output (\"$(Get-Date) | Instance state: $currentState\") + + $timeout = (New-Timespan -Seconds 90) + $sw = [diagnostics.stopwatch]::StartNew() + + while ($true) { + $currentState = (Get-EC2Instance $instanceId).Instances.State.Name + + if ($currentState -eq \"running\") { break } + + Write-Output (\"$(Get-Date) | Waiting for Instance '$instanceId' to transition from state: $currentState\") + + if ($sw.elapsed -gt $timeout) {Throw \"Timed out waiting for desired state\"} + + Sleep -Seconds 5 + } + + Write-Output (\"$(Get-Date) | Instance state: $currentState\") + } + + + Write-Output (\"------------------------------\") + Write-Output (\"Instance details:\") + Write-Output ((Get-EC2Instance $instanceId).Instances) + Write-Output (\"------------------------------\") + + $privateIpAddress = (Get-EC2Instance $instanceId).Instances.PrivateIpAddress + $publicIpAddress = (Get-EC2Instance $instanceId).Instances.PublicIpAddress + + if($OctopusParameters) { + Set-OctopusVariable -name \"InstanceId\" -value $instanceId + Set-OctopusVariable -name \"PrivateIpAddress\" -value $privateIpAddress + Set-OctopusVariable -name \"PublicIpAddress\" -value $publicIpAddress + } + } + } + } ` + (Get-Param 'odTags' -Required) ` + (Get-Param 'odImageId' -Required) ` + (Get-Param 'odInstanceType' -Required) ` + (Get-Param 'odSubnetId' -Required) ` + (Get-Param 'odSecurityGroupId' -Required) ` + (Get-Param 'odKeyName' -Required) ` + (Get-Param 'odRegion' -Required) ` + (Get-Param 'odUserData') ` + (Get-Param 'odSpotPrice') ` + (Get-Param 'odSpotProduct') ` + (Get-Param 'odAccessKey') ` + (Get-Param 'odSecretKey')", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "45aa8520-eb05-48a9-b4ef-30d11b5e6a73", + "Name": "odTags", + "Label": "Tags", + "HelpText": "The Tags to be applied to the new instance. These tags will determine if a new instance will be created, or if an instance matching the tags should be in a running state.", + "DefaultValue": "Name=#{Octopus.Project.Name} +Environment=#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "2411ffb3-c3fc-402a-9448-249415000c14", + "Name": "odImageId", + "Label": "ImageId", + "HelpText": "The Amazon Machine Image (AMI) Id. This can be the id of an image you've created yourself, or one of the AWS Community AMIs.", + "DefaultValue": "ami-xxxxxxxx", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a2cc4774-b60c-4dad-9c44-074d0564b626", + "Name": "odInstanceType", + "Label": "InstanceType", + "HelpText": "The Instance Type (Model), e.g. t2.micro, m3.large, c4.large, etc. +Further reading: https://aws.amazon.com/ec2/instance-types/", + "DefaultValue": "t2.micro", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "dd2c238d-4758-4027-8901-b050e991bf83", + "Name": "odSubnetId", + "Label": "SubnetId", + "HelpText": "The Subnet Id you would like the Instance to launch in.", + "DefaultValue": "subnet-xxxxxxxx", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "349022a4-0ad3-46bd-b408-e456c5616577", + "Name": "odSecurityGroupId", + "Label": "SecurityGroupId", + "HelpText": "The Security Group Id you would like the Instance to launch with.", + "DefaultValue": "sg-xxxxxxxx", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "54508ead-07db-4f73-b8a9-8c3b8c91bdb1", + "Name": "odKeyName", + "Label": "KeyName", + "HelpText": "The name of the Key Pair that will be used to log into the new instance.", + "DefaultValue": "AutoDeployKeyNameExample", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "01097d0d-4ce5-4c38-afd4-527919a800f7", + "Name": "odRegion", + "Label": "Region", + "HelpText": "The AWS Region you would like the Instance to launch in, e.g. ap-southeast-2, us-west-2, eu-west-1, etc. +Further reading: https://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region", + "DefaultValue": "ap-southeast-2", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b4dfba68-93fe-496a-acd4-00e01d818312", + "Name": "odUserData", + "Label": "UserData (Optional)", + "HelpText": "The UserData is a script that will be executed on the initial startup of your EC2 Instance. +Further Reading: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html", + "DefaultValue": "tzutil.exe /s 'AUS Eastern Standard Time'", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "5842e13b-af7a-4a18-bfa4-4891aded2ae9", + "Name": "odSpotPrice", + "Label": "SpotPrice (Optional)", + "HelpText": "The Spot Price is a variable hourly rate (often discounted), which is calculated based on the current demand for any particular Instance Type (Model). +Further Reading: https://aws.amazon.com/ec2/spot/pricing/", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "caa5f631-f0c5-4925-8a57-a80abb5e4300", + "Name": "odSpotProduct", + "Label": "SpotProduct (Optional)", + "HelpText": "The Spot Product is basically the Operating System you plan to utilise.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Linux/Unix|Linux/Unix +SUSE Linux|SUSE Linux +Windows|Windows" + }, + "Links": {} + }, + { + "Id": "43206cff-be8c-4805-9045-2f6bf302e056", + "Name": "odAccessKey", + "Label": "AccessKey (Kind-of Optional)", + "HelpText": "An Access Key with permissions to create the desired EC2 instance. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"AWS\\_ACCESS\\_KEY\" +Further Reading: +https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b0d6b3e7-3c90-4f0e-b919-78ef04608cb3", + "Name": "odSecretKey", + "Label": "SecretKey (Kind-of Optional)", + "HelpText": "The Secret Key associated with the above Access Key. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"AWS\\_SECRET\\_KEY\" +Further Reading: +https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "tclydesdale", + "$Meta": { + "ExportedAt": "2018-01-29T12:56:55.311Z", + "OctopusVersion": "4.1.9", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-linux-install-tentacle.json.human b/step-templates/aws-linux-install-tentacle.json.human new file mode 100644 index 000000000..2d28773f2 --- /dev/null +++ b/step-templates/aws-linux-install-tentacle.json.human @@ -0,0 +1,309 @@ +{ + "Id": "43354ae1-c301-42f8-971d-6e659e4807b3", + "Name": "AWS Linux - Install Octopus Tentacle", + "Description": "This step template will install the latest tentacle on an AWS hosted, Linux virtual machine. This will also open the firewall for inbound traffic on port 10933 on the Security Group. +
+*Note: Expects the AWS CLI and Powershell to be installed on the worker running this task*
+*Note: Firewall ports will not be opened on the remote machine*
+*Note: Target machines must be added to your AWS System Manager (SSM)*", + "ActionType": "Octopus.AwsRunScript", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{awsInstallLinuxTentacle.AWSAccount}", + "Octopus.Action.Script.ScriptBody": "$sgName = $OctopusParameters[\"awsInstallLinuxTentacle.awsSGName\"] +$instanceId = $OctopusParameters[\"awsInstallLinuxTentacle.awsVmInstanceId\"] +$serverUri = $OctopusParameters[\"instrallTentacle.octoServerUrl\"] +$apiKey = $OctopusParameters[\"awsInstallLinuxTentacle.octoApiKey\"] +$rolesRaw = $OctopusParameters[\"awsInstallLinuxTentacle.octopusRoles\"] +$enviroRaw = $OctopusParameters[\"awsInstallLinuxTentacle.octopusEnvironments\"] +$octoThumb = $OctopusParameters[\"awsInstallLinuxTentacle.octoServerThumb\"] +$comStyle = $OctopusParameters[\"awsInstallLinuxTentacle.tentacleType\"] +$hostname = $OctopusParameters[\"awsInstallLinuxTentacle.tentacleHostName\"] +$tentacleName = $OctopusParameters[\"awsInstallLinuxTentacle.tentacleName\"] +$portNumber = $OctopusParameters[\"awsInstallLinuxTentacle.portNumber\"] + +Write-Host \"Parsing Parameters\" + +if([string]::IsNullOrEmpty($sgName)) +{ +\tthrow \"Security Group name must be provided\" +} + +if([string]::IsNullOrEmpty($instanceId)) +{ +\tthrow \"Instance Id must be provided\" +} + +if([string]::IsNullOrEmpty($apiKey)) +{ +\tthrow \"apiKey must be provided\" +} + +if([string]::IsNullOrEmpty($rolesRaw)) +{ +\tthrow \"At least one role must be defined\" +} + +if([string]::IsNullOrEmpty($enviroRaw)) +{ +\tthrow \"At least one environment must be defined\" +} + +if([string]::IsNullOrEmpty($octoThumb)) +{ +\tthrow \"octo thumbprint must be provided\" +} + +$roles = \"\" +$rolesRaw -split \"`n\" | ForEach-Object { $roles += \"--role $_ \"} +$roles = $roles.TrimEnd(' ') + +$environments = \"\" +$enviroRaw -split \"`n\" | ForEach-Object { $environments += \"--env $_ \"} +$environments = $environments.TrimEnd(' ') + +if($comStyle -eq \"TentaclePassive\") +{ +\tif([string]::IsNullOrEmpty($hostname)) + { + \t$hostname = aws ec2 describe-instances --filters \"Name=instance-id,Values=$instanceId\" --query \"Reservations[].Instances[].NetworkInterfaces[].Association.PublicIp\" --output=text + $hostname = $hostname.Trim(\"`n\") + } + + $noListen = \"--port $portNumber --noListen `\"false`\"\" + $comStyle += \" --publicHostName='$hostname'\" + $openFirewall = 'true' +} +else +{ +\t$noListen = \"--noListen `\"true`\"\" + $openFirewall = 'false' +} + +if([string]::IsNullOrEmpty($tentacleName)) +{ +\t$tentacleName = $hostname +} + +if($openFirewall -eq 'true') +{ +\tWrite-Host \"Checking SG...\" -NoNewline + $sgCheck = aws ec2 describe-security-groups --group-names $sgName --output json --filters Name=ip-permission.from-port,Values=$portNumber Name=ip-permission.cidr,Values='0.0.0.0/0' | convertfrom-json + + if($sgCheck.SecurityGroups.count -eq 0) + { +\t\tWrite-Host \"Creating SG Rule\" + \taws ec2 authorize-security-group-ingress --group-name $sgName --ip-permissions IpProtocol=tcp,ToPort=$portNumber,FromPort=$portNumber,IpRanges='[{CidrIp=0.0.0.0/0,Description=\"OctopusListeningTentacle\"}]' +\t} + else + { + \tWrite-Host \"Found Existing SG Rule\" + } +} + +Write-Verbose \"hostname: $hostname`nnoListen: $noListen\" + +$remoteScript = @\" +{ +\t\"commands\": [ + \t\"#!/bin/bash\", +\t\t\"curl -L https://octopus.com/downloads/latest/Linux_x64TarGz/OctopusTentacle -o /tmp/tentacle-linux_x64.tar.gz -fsS\", +\t\t\"if [ ! -d \\\"/opt/octopus\\\" ]; then sudo mkdir /opt/octopus; fi\", +\t\t\"tar xvzf /tmp/tentacle-linux_x64.tar.gz -C /opt/octopus\", +\t\t\"rm /tmp/tentacle-linux_x64.tar.gz\", +\t\t\"cd /opt/octopus/tentacle\", +\t\t\"sudo /opt/octopus/tentacle/Tentacle create-instance --config '/etc/octopus/default/tentacle-default.config'\", +\t\t\"sudo chmod a+rwx /etc/octopus/default/tentacle-default.config\", +\t\t\"/opt/octopus/tentacle/Tentacle new-certificate --if-blank\", +\t\t\"/opt/octopus/tentacle/Tentacle configure --port $portNumber --noListen False --reset-trust --app '/home/Octopus/Applications'\", +\t\t\"/opt/octopus/tentacle/Tentacle configure --trust $octoThumb\", +\t\t\"echo 'Registering the Tentacle $name with server $serverUri in environment $environments with role $roles'\", +\t\t\"/opt/octopus/tentacle/Tentacle register-with --server '$serverUri' --apiKey '$apikey' $environments $roles --comms-style $comStyle --name '$tentacleName' --force\", +\t\t\"sudo /opt/octopus/tentacle/Tentacle service --install --start\" +\t] +} +\"@ + +Write-Host \"Installing tentacle on remote machine\" +$guid = (new-guid).guid +Set-Content -Value $remoteScript -Path \"$env:Temp/$guid.json\" + +write-verbose $remoteScript + +write-verbose \"aws ssm send-command --document-name `\"AWS-RunShellScript`\" --instance-ids `\"$instanceId`\" --parameters `\"file://$env:Temp/$guid.json`\"\" +try { +\t$cmdResponse = aws ssm send-command --document-name \"AWS-RunShellScript\" --instance-ids \"$instanceId\" --parameters \"file://$env:Temp/$guid.json\" --query \"Command\" --output json | convertfrom-json + $cmdId = $cmdResponse.CommandId + $errorResponse = aws ssm get-command-invocation --command-id \"$cmdId\" --instance-id \"$instanceId\" --output json | convertfrom-json + + while($errorResponse.Status -eq 'InProgress') + { + \twrite-verbose \"`nStatus: $($errorResponse.Status)\" + \t$errorResponse = aws ssm get-command-invocation --command-id \"$cmdId\" --instance-id \"$instanceId\" --output json | convertfrom-json + } + + write-verbose \"`nErrorResponse: $errorResponse`n\" + + if(![string]::IsNullOrEmpty($errorResponse.StandardErrorContent)) + { + \tthrow $errorResponse.StandardErrorContent + } +} +finally { +\tremove-item \"$env:Temp\\$guid.json\" +} +", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.AwsAccount.Variable": "#{awsInstallLinuxTentacle.AWSAccount}", + "Octopus.Action.Aws.Region": "#{awsInstallLinuxTentacle.awsRegion}" + }, + "Parameters": [ + { + "Id": "dea51842-271f-48fa-901e-243488049f97", + "Name": "awsInstallLinuxTentacle.AWSAccount", + "Label": "AWS Account", + "HelpText": "AWS account with permissions to the virtual machine in which to install the tentacle on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "6a97548c-3b97-4a19-8b9a-45aa58ac62d9", + "Name": "awsInstallLinuxTentacle.awsRegion", + "Label": "AWS Region", + "HelpText": "The name of the aws region. I.E. `us-east-1`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d9efb017-65c0-4010-b09f-0a6a34fc009f", + "Name": "awsInstallLinuxTentacle.awsSGName", + "Label": "Security Group Name", + "HelpText": "The name of the AWS security group to create an exception in", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e4358aec-2798-412a-bcd2-1c7e41a0728d", + "Name": "awsInstallLinuxTentacle.awsVmInstanceId", + "Label": "VM Instance ID", + "HelpText": "The instance ID of the virtual machine to target when ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8dbe0a29-4e0f-43f8-8ef2-ae23a2884e85", + "Name": "awsInstallLinuxTentacle.octoServerThumb", + "Label": "Server Thumbprint", + "HelpText": "The Thumbprint of the octopus server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "a0859551-6c84-45d1-af2b-c57becb4e8ef", + "Name": "awsInstallLinuxTentacle.octoApiKey", + "Label": "Octo User API Key", + "HelpText": "The API key used to configure the tentacle.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "dd07815d-bfb7-43b9-90f8-21ec5a2d2953", + "Name": "awsInstallLinuxTentacle.octopusRoles", + "Label": "Roles", + "HelpText": "Roles to assign to this tentacle installation.
+*Note: Each role should be on it's own line*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "d3ffb38e-7fcf-4547-be9a-6cd8e46f8e5e", + "Name": "awsInstallLinuxTentacle.octopusEnvironments", + "Label": "Environments", + "HelpText": "Environments to assign this tentacle installation to.
+*Note: Each environment should be on its own line*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "15150827-7bd3-4146-a798-66344851f602", + "Name": "instrallTentacle.octoServerUrl", + "Label": "Octo Server Url", + "HelpText": "The server url to register the tentacle with. Defaults to the base url", + "DefaultValue": "#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9bcd6bb5-2db3-453f-b4f6-6a78f3c39b59", + "Name": "awsInstallLinuxTentacle.tentacleType", + "Label": "Tentacle Type", + "HelpText": "Select between a listing or polling tentacle", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "TentaclePassive|Listening +TentacleActive|Polling" + } + }, + { + "Id": "6bb98570-fe3e-4ab7-adfb-79ef7ce5c2ce", + "Name": "awsInstallLinuxTentacle.tentacleHostName", + "Label": "Tentacle Host Name", + "HelpText": "The host name to register the listening tentacles with. Octopus deploy server uses this value to reach out to the vm.
+*Note: Leave blank to automatically use assigned public IP address.*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "386d9f39-9e90-4215-b739-53a1af5bd105", + "Name": "awsInstallLinuxTentacle.tentacleName", + "Label": "Tentacle Name", + "HelpText": "The name of the tentacle to use in the infrastructure area of octopus deploy. Will use the host name if not provided", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "50621684-c2a5-47c0-9bfe-12f004022e60", + "Name": "awsInstallLinuxTentacle.portNumber", + "Label": "Port Number", + "HelpText": "Port number used when installing and registered tentacle. This is also the port opened on the firewall", + "DefaultValue": "10933", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-08-23T12:40:10.975Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "benjimac93", + "Category": "aws" +} diff --git a/step-templates/aws-rds-restore-s3.json.human b/step-templates/aws-rds-restore-s3.json.human new file mode 100644 index 000000000..9ced2a340 --- /dev/null +++ b/step-templates/aws-rds-restore-s3.json.human @@ -0,0 +1,148 @@ +{ + "Id": "55848421-44b9-403c-b1f0-ba8a84b1f177", + "Name": "AWS RDS SQL Server - Restore from S3 Bucket", + "Description": "Will restore a database backup from an S3 bucket", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Write-Host \"SqlLoginWhoHasRights $rdsSqlRestoreBackupSqlLoginUserWhoHasCreateUserRights\" +Write-Host \"CreateSqlServer $rdsSqlRestoreBackupSqlServer\" +Write-Host \"CreateDatabaseName $rdsSqlRestoreBackupDatabaseName\" +Write-Host \"Backup S3 Bucket $rdsSqlRestoreBackupS3Bucket\" +Write-Host \"Backup File Name $rdsSqlRestoreBackupFileName\" + +if ([string]::IsNullOrWhiteSpace($rdsSqlRestoreBackupSqlLoginUserWhoHasCreateUserRights) -eq $true){ +\tWrite-Host \"No username found, using integrated security\" + $connectionString = \"Server=$rdsSqlRestoreBackupSqlServer;Database=msdb;integrated security=true;\" +} +else { +\tWrite-Host \"Username found, using SQL Authentication\" + $connectionString = \"Server=$rdsSqlRestoreBackupSqlServer;Database=msdb;User ID=$rdsSqlRestoreBackupSqlLoginUserWhoHasCreateUserRights;Password=$rdsSqlRestoreBackupSqlLoginPasswordWhoHasRights;\" +} + +$sqlConnection = New-Object System.Data.SqlClient.SqlConnection +$sqlConnection.ConnectionString = $connectionString + +$command = New-Object System.Data.SqlClient.SqlCommand(\"dbo.rds_restore_database\", $sqlConnection) +$command.CommandType = [System.Data.CommandType]'StoredProcedure' + +$backupDestParamValue = \"arn:aws:s3:::$rdsSqlRestoreBackupS3Bucket/$rdsSqlRestoreBackupFileName\" +$command.Parameters.AddWithValue(\"s3_arn_to_restore_from\", $backupDestParamValue) +$command.Parameters.AddWithValue(\"with_norecovery\", 0) +$command.Parameters.AddWithValue(\"restore_db_name\", $rdsSqlRestoreBackupDatabaseName) + +$taskStatusCommand = New-Object System.Data.SqlClient.SqlCommand(\"dbo.rds_task_status\", $sqlConnection) +$taskStatusCommand.CommandType = [System.Data.CommandType]'StoredProcedure' +$taskStatusCommand.Parameters.AddWithValue(\"db_name\", $rdsSqlRestoreBackupDatabaseName) + +$taskStatusAdapter = New-Object System.Data.SqlClient.SqlDataAdapter $taskStatusCommand + +Write-Host \"Opening the connection to $rdsSqlRestoreBackupSqlServer\" +$sqlConnection.Open() + +Write-Host \"Executing backup\" +$command.ExecuteNonQuery() + +Write-Host \"Closing the connection to $rdsSqlRestoreBackupSqlServer\" +$sqlConnection.Close() + +Write-Host \"Getting status of backup\" +$backupIsActive = $true + +While ($backupIsActive) +{ +\tWrite-Host \"Opening the connection to $rdsSqlRestoreBackupSqlServer\" +\t$sqlConnection.Open() + + $taskStatusDataSet = New-Object System.Data.DataSet +\t$taskStatusAdapter.Fill($taskStatusDataSet) + $taskStatus = $taskStatusDataSet.Tables[0].Rows[0][\"lifecycle\"] + $taskComplete = $taskStatusDataSet.Tables[0].Rows[0][\"% complete\"] + + Write-Host \"The task is $taskComplete% complete.\" + $backupIsActive = $taskStatus -eq \"CREATED\" -or $taskStatus -eq \"IN_PROGRESS\" + + Write-Host \"Closing the connection to $rdsSqlRestoreBackupSqlServer\" +\t$sqlConnection.Close() + + Start-Sleep -Seconds 5 +}" + }, + "Parameters": [ + { + "Id": "3e45bb88-3632-4115-a0d5-54680615f0ca", + "Name": "rdsSqlRestoreBackupSqlServer", + "Label": "SQL Server", + "HelpText": "The SQL Server to perform the work on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "880a4e73-64fe-4a36-b4c3-a281b64e3c23", + "Name": "rdsSqlRestoreBackupSqlLoginUserWhoHasCreateUserRights", + "Label": "SQL Login", + "HelpText": "The login of the user who has permissions to create a database. + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6689fbe5-f47a-4800-945d-df50fc19c7b0", + "Name": "rdsSqlRestoreBackupSqlLoginPasswordWhoHasRights", + "Label": "SQL Password", + "HelpText": "The password of the user who has permissions to create SQL Logins + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "1a358cca-5cd1-41ae-b763-fcaf2c7350f9", + "Name": "rdsSqlRestoreBackupDatabaseName", + "Label": "Database Name", + "HelpText": "The name of the database to restore to", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c5e2fa69-1b42-4427-91d2-10e1a13af744", + "Name": "rdsSqlRestoreBackupS3Bucket", + "Label": "S3 Bucket Name", + "HelpText": "The name of the bucket (including any sub directories).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ea9d9eb8-9c0f-4c69-a6d4-4d00e43383af", + "Name": "rdsSqlRestoreBackupFileName", + "Label": "Backup File Name and Extension", + "HelpText": "The name of the back up file (including the extension).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-18T13:27:31.393Z", + "OctopusVersion": "2020.4.0-ci0428", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "octobob", + "Category": "aws" +} diff --git a/step-templates/aws-rds-sql-backup-s3.json.human b/step-templates/aws-rds-sql-backup-s3.json.human new file mode 100644 index 000000000..b9667fc6c --- /dev/null +++ b/step-templates/aws-rds-sql-backup-s3.json.human @@ -0,0 +1,148 @@ +{ + "Id": "3dd60fea-b98a-4760-8867-cbd049f7aa31", + "Name": "AWS RDS SQL Server - Backup to S3 Bucket", + "Description": "Will create a database user using an existing server user if that database user does not exist without using SMO.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Write-Host \"SqlLoginWhoHasRights $rdsSqlBackupSqlLoginUserWhoHasCreateUserRights\" +Write-Host \"CreateSqlServer $rdsSqlBackupSqlServer\" +Write-Host \"CreateDatabaseName $rdsSqlBackupDatabaseName\" +Write-Host \"Backup S3 Bucket $rdsSqlBackupS3Bucket\" +Write-Host \"Backup File Name $rdsSqlBackupFileName\" + +if ([string]::IsNullOrWhiteSpace($rdsSqlBackupSqlLoginUserWhoHasCreateUserRights) -eq $true){ +\tWrite-Host \"No username found, using integrated security\" + $connectionString = \"Server=$rdsSqlBackupSqlServer;Database=msdb;integrated security=true;\" +} +else { +\tWrite-Host \"Username found, using SQL Authentication\" + $connectionString = \"Server=$rdsSqlBackupSqlServer;Database=msdb;User ID=$rdsSqlBackupSqlLoginUserWhoHasCreateUserRights;Password=$rdsSqlBackupSqlLoginPasswordWhoHasRights;\" +} + +$sqlConnection = New-Object System.Data.SqlClient.SqlConnection +$sqlConnection.ConnectionString = $connectionString + +$command = New-Object System.Data.SqlClient.SqlCommand(\"dbo.rds_backup_database\", $sqlConnection) +$command.CommandType = [System.Data.CommandType]'StoredProcedure' + +$backupDestParamValue = \"arn:aws:s3:::$rdsSqlBackupS3Bucket/$rdsSqlBackupFileName\" +$command.Parameters.AddWithValue(\"s3_arn_to_backup_to\", $backupDestParamValue) +$command.Parameters.AddWithValue(\"overwrite_S3_backup_file\", 1) +$command.Parameters.AddWithValue(\"source_db_name\", $rdsSqlBackupDatabaseName) + +$taskStatusCommand = New-Object System.Data.SqlClient.SqlCommand(\"dbo.rds_task_status\", $sqlConnection) +$taskStatusCommand.CommandType = [System.Data.CommandType]'StoredProcedure' +$taskStatusCommand.Parameters.AddWithValue(\"db_name\", $rdsSqlBackupDatabaseName) + +$taskStatusAdapter = New-Object System.Data.SqlClient.SqlDataAdapter $taskStatusCommand + +Write-Host \"Opening the connection to $rdsSqlBackupSqlServer\" +$sqlConnection.Open() + +Write-Host \"Executing backup\" +$command.ExecuteNonQuery() + +Write-Host \"Closing the connection to $rdsSqlBackupSqlServer\" +$sqlConnection.Close() + +Write-Host \"Getting status of backup\" +$backupIsActive = $true + +While ($backupIsActive) +{ +\tWrite-Host \"Opening the connection to $rdsSqlBackupSqlServer\" +\t$sqlConnection.Open() + + $taskStatusDataSet = New-Object System.Data.DataSet +\t$taskStatusAdapter.Fill($taskStatusDataSet) + $taskStatus = $taskStatusDataSet.Tables[0].Rows[0][\"lifecycle\"] + $taskComplete = $taskStatusDataSet.Tables[0].Rows[0][\"% complete\"] + + Write-Host \"The task is $taskComplete% complete.\" + $backupIsActive = $taskStatus -eq \"CREATED\" -or $taskStatus -eq \"IN_PROGRESS\" + + Write-Host \"Closing the connection to $rdsSqlBackupSqlServer\" +\t$sqlConnection.Close() + + Start-Sleep -Seconds 5 +}" + }, + "Parameters": [ + { + "Id": "3e45bb88-3632-4115-a0d5-54680615f0ca", + "Name": "rdsSqlBackupSqlServer", + "Label": "SQL Server", + "HelpText": "The SQL Server to perform the work on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "880a4e73-64fe-4a36-b4c3-a281b64e3c23", + "Name": "rdsSqlBackupSqlLoginUserWhoHasCreateUserRights", + "Label": "SQL Login", + "HelpText": "The login of the user who has permissions to create a database. + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6689fbe5-f47a-4800-945d-df50fc19c7b0", + "Name": "rdsSqlBackupSqlLoginPasswordWhoHasRights", + "Label": "SQL Password", + "HelpText": "The password of the user who has permissions to create SQL Logins + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "1a358cca-5cd1-41ae-b763-fcaf2c7350f9", + "Name": "rdsSqlBackupDatabaseName", + "Label": "Database Name", + "HelpText": "The name of the database to create the user on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c5e2fa69-1b42-4427-91d2-10e1a13af744", + "Name": "rdsSqlBackupS3Bucket", + "Label": "S3 Bucket Name", + "HelpText": "The name of the bucket (including any sub directories).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ea9d9eb8-9c0f-4c69-a6d4-4d00e43383af", + "Name": "rdsSqlBackupFileName", + "Label": "Backup File Name and Extension", + "HelpText": "The name of the back up file (including the extension).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-18T14:30:56.431Z", + "OctopusVersion": "2020.4.0-ci0428", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "octobob", + "Category": "aws" +} diff --git a/step-templates/aws-register-elb-instance.json.human b/step-templates/aws-register-elb-instance.json.human new file mode 100644 index 000000000..c3c056ce9 --- /dev/null +++ b/step-templates/aws-register-elb-instance.json.human @@ -0,0 +1,171 @@ +{ + "Id": "cc1ae822-3dcf-4041-a790-69a1267552c2", + "Name": "AWS - Register ELB Instance", + "Description": "Registers an instance with an ELB", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Part 2 of 2\r +# Part 1 Deregisters an EC2 instance from an ELB\r +# Part 2 Registers an EC2 instance with an ELB and waits for it to be InService\r +\r +$ec2Region = $OctopusParameters['ec2Region']\r +$ec2User = $OctopusParameters['ec2ClientId']\r +$ec2Credentials = $OctopusParameters['ec2Credentials']\r +$elbName = $OctopusParameters['elbName']\r +$instanceId = \"\"\r +$registrationCheckInterval = $OctopusParameters['registrationCheckInterval']\r +$maxRegistrationCheckCount = $OctopusParameters['maxRegistrationCheckCount']\r +\r +# Load EC2 credentials (not sure if this is needed when executed from an EC2 box)\r +try\r +{\r +\tWrite-Host \"Loading AWS Credentials...\"\r +\tImport-Module AWSPowerShell\r +\tSet-AWSCredentials -AccessKey $ec2User -SecretKey $ec2Credentials\r +\tSet-DefaultAWSRegion $ec2Region\r +\tWrite-Host \"AWS Credentials Loaded.\"\r +}\r +catch\r +{\r +\tWrite-Error -Message \"Failed to load AWS Credentials.\" -Exception $_.Exception\r +\tExit 1\r +}\r +\r +# Get EC2 Instance\r +try\r +{\r +\t$response = Invoke-RestMethod -Uri \"http://169.254.169.254/latest/meta-data/instance-id\" -Method Get\r +\tif ($response)\r +\t{\r +\t\t$instanceId = $response\r +\t}\r +\telse\r +\t{\r +\t\tWrite-Error -Message \"Returned Instance ID does not appear to be valid\"\r +\t\tExit 1\r +\t}\r +}\r +catch\r +{\r +\tWrite-Error -Message \"Failed to load instance ID from AWS.\" -Exception $_.Exception\r +\tExit 1\r +}\r +\r +# Register the current EC2 instance\r +Write-Host \"Registering instance $instanceId with $elbName.\"\r +try\r +{\r +\tRegister-ELBInstanceWithLoadBalancer -LoadBalancerName $elbName -Instance $instanceId\r +\tWrite-Host \"Instance Registered, waiting for registration to complete.\"\r +\t\r +\t$instanceState = (Get-ELBInstanceHealth -LoadBalancerName $elbName -Instance $instanceId).State\r +\tWrite-Host \"Current State: $instanceState\"\r +\t\r +\t$checkCount = 0\r +\t\r +\tWrite-Host \"Retry Parameters:\"\r +\tWrite-Host \"Maximum Checks: $maxRegistrationCheckCount\"\r +\tWrite-Host \"Check Interval: $registrationCheckInterval\"\r +\t\r +\tWhile ($instanceState -ne \"InService\" -and $checkCount -le $maxRegistrationCheckCount)\r +\t{\t\r +\t\t$checkCount += 1\r +\t\t\r +\t\t# Wait a bit until we check the status\r +\t\tWrite-Host \"Waiting for $registrationCheckInterval seconds for instance to register\"\r +\t\tStart-Sleep -Seconds $registrationCheckInterval\r +\t\t\r +\t\tif ($checkCount -le $maxRegistrationCheckCount)\r +\t\t{\r +\t\t\tWrite-Host \"$checkCount/$maxRegistrationCheckCount Attempts\"\r +\t\t}\r +\t\t\r +\t\t$instanceState = (Get-ELBInstanceHealth -LoadBalancerName $elbName -Instance $instanceId).State\r +\t\t\r +\t\tWrite-Host \"Current instance state: $instanceState\"\r +\t}\r +\t\r +\tif ($instanceState -eq \"InService\")\r +\t{\r +\t\tWrite-Host \"Registration complete.\"\r +\t}\r +\telse\r +\t{\r +\t\tWrite-Error -Message \"Failed to register instance: $instanceState\"\r +\t\tExit 1\r +\t}\r +}\r +catch\r +{\r +\tWrite-Error -Message \"Failed to Register instance.\" -Exception $_.Exception\r +\tExit 1\r +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ec2ClientId", + "Label": "AWS EC2 Client Id", + "HelpText": "The client id to use when authenticating with AWS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "ec2Credentials", + "Label": "AWS EC2 Client Secret", + "HelpText": "The client secret used to authenticate with AWS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "elbName", + "Label": "AWS ELB Name", + "HelpText": "The name of the AWS ELB to add the instance to", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "registrationCheckInterval", + "Label": "Registration Check Interval", + "HelpText": "The number of seconds to wait before checking to see if the Instance has properly registered with the ELB", + "DefaultValue": "10", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "maxRegistrationCheckCount", + "Label": "Maximum Registration Checks", + "HelpText": "The maximum number of registration checks to perform before the step fails.", + "DefaultValue": "10", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ec2Region", + "Label": "AWS EC2 Region", + "HelpText": "The region in which the ELB lives", + "DefaultValue": "us-east-1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-04-30T13:23:14.350+00:00", + "LastModifiedBy": "DudeSolutions", + "$Meta": { + "ExportedAt": "2015-04-30T13:25:21.128+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "aws" +} diff --git a/step-templates/aws-secrets-manager-retrieve-secrets.json.human b/step-templates/aws-secrets-manager-retrieve-secrets.json.human new file mode 100644 index 000000000..51afc8d15 --- /dev/null +++ b/step-templates/aws-secrets-manager-retrieve-secrets.json.human @@ -0,0 +1,330 @@ +{ + "Id": "5d5bd3ae-09a0-41ac-9a45-42a96ee6206a", + "Name": "AWS Secrets Manager - Retrieve Secrets", + "Description": "This step retrieves one or more secrets from AWS [Secrets Manager](https://aws.amazon.com/secrets-manager) and creates [sensitive output variables](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) for each value retrieved. The step supports creating a variable for each key-value in a secret that's retrieved, or you can specify individual keys. These values can be used in other steps in your deployment or runbook process. + +--- + +**Specifying Secret names/keys to retrieve:** + +Specify the names of the secrets to be returned from AWS Secrets Manager, in the format: + +`SecretName SecretVersionId SecretVersionStage | KeyNames | OutputVariableName` where: + +- `SecretName` is the name of the secret to retrieve. You can specify either the `Amazon Resource Name (ARN)` or the friendly name of the secret. +- `SecretVersionId` is the unique identifier of the version of the secret that you want to retrieve. If this value isn't specified, the version with the `VersionStage` value as specified in `SecretVersionStage` will be retrieved. +- `SecretVersionStage` specifies the secret version that you want to retrieve by the staging label attached to the version. *Staging labels are used to keep track of different versions during the rotation process*. If this value isn't specified, the version with the `VersionStage` value of `AWSCURRENT` will be retrieved. +- `KeyNames` are the names of the keys stored in the secret that you wish to retrieve values for. Multiple fields can be retrieved separated by a space. Alternatively, you can specify all fields using the special keyword `all` or `*`. +- `OutputVariableName` is the _optional_ Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) name to store the secret's value in. If multiple fields are specified the field name will be appended to this value. *If this value isn't specified, an output name will be generated dynamically*. + +**Examples:** + +Given a secret named `OctoSamples-usercredentials`: + +1. `OctoSamples-usercredentials | Username | octousername` + + This would retrieve the secret and extract the value from the key-value named `Username` and save it into a sensitive output variable named `octousername`. + +2. `OctoSamples-usercredentials | Username Password | octocreds` + + This would retrieve the secret named `OctoSamples-usercredentials`, and then extract the values from the key-values named `Username` and `Password` and save them to two sensitive output variables named `octocreds.Username` and `octocreds.Password`. + +3. `OctoSamples-usercredentials | * | octocreds` + + This would retrieve the secret named `OctoSamples-usercredentials`, and then extract all key-values from the secret and save them to sensitive output variables *prefixed* with `octocreds`. + +4. `OctoSamples-usercredentials | all` + + This would retrieve the secret named `OctoSamples-usercredentials`, and then extract all key-values from the secret and save them to sensitive output variables *prefixed* with `OctoSamples-usercredentials`. + +--- + +**AWS Dependencies:** + +There are some dependencies/requirements for this step to work successfully. + +1. **CLI** - This step uses AWS tooling pre-installed on the target or worker. + + Scripts executed in this step need to use the [AWS CLI](https://aws.amazon.com/cli/) to authenticate to AWS and perform other actions. If the CLI can't be found, the step will fail. + +2. **AWS Account** - An [AWS account](https://octopus.com/docs/infrastructure/accounts/aws) with permissions to retrieve secrets from AWS Secrets Manager is also required. + +--- + +**Notes:** +- Tested on Octopus **2021.2**. +- Tested on both Windows Server 2019 and Ubuntu 20.04. + +", + "ActionType": "Octopus.AwsRunScript", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +# Variables +$SecretNames = $OctopusParameters[\"AWS.SecretsManager.RetrieveSecrets.SecretNames\"] +$PrintVariableNames = $OctopusParameters[\"AWS.SecretsManager.RetrieveSecrets.PrintVariableNames\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($SecretNames)) { + throw \"Required parameter AWS.SecretsManager.RetrieveSecrets.SecretNames not specified\" +} + +# Functions +function Format-SecretName { + [CmdletBinding()] + Param( + [string] $Name, + [string] $VersionId, + [string] $VersionStage, + [string[]] $Keys + ) + $displayName = \"'$Name'\" + if (![string]::IsNullOrWhiteSpace($VersionId)) { + $displayName += \" $VersionId\" + } + if (![string]::IsNullOrWhiteSpace($VersionStage)) { + $displayName += \" $VersionStage\" + } + if ($Keys.Count -gt 0) { + $displayName += \" ($($Keys -Join \",\"))\" + } + return $displayName +} + +function Save-OctopusVariable { + Param( + [string] $name, + [string] $value + ) + if ($script:storedVariables -icontains $name) { + Write-Warning \"A variable with name '$name' has already been created. Check your secret name parameters as this will likely cause unexpected behavior and should be investigated.\" + } + Set-OctopusVariable -Name $name -Value $value -Sensitive + $script:storedVariables += $name + + if ($PrintVariableNames -eq $True) { + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$name}\" + } +} + +# End Functions + +$script:storedVariables = @() +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] +$Secrets = @() + +# Extract secret names +@(($SecretNames -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + Write-Verbose \"Working establishing secret definition for: '$_'\" + $secretDefinition = ($_ -Split \"\\|\") + + # Establish the secret name/version requirements + $secretName = $secretDefinition[0].Trim() + $secretVersionId = \"\" + $secretVersionStage = \"\" + $secretNameAndVersion = ($secretName -Split \" \") + + if ($secretNameAndVersion.Count -gt 1) { + $secretName = $secretNameAndVersion[0].Trim() + $secretVersionId = $secretNameAndVersion[1].Trim() + if ($secretNameAndVersion.Count -eq 3) { + $secretVersionStage = $secretNameAndVersion[2].Trim() + } + } + + if ([string]::IsNullOrWhiteSpace($secretName)) { + throw \"Unable to establish secret name from: '$($_)'\" + } + + # Establish the secret field(s)/output variable name requirements. + $VariableName = \"\" + $Keys = @() + if ($secretDefinition.Count -gt 1) { + $KeyNames = $secretDefinition[1].Trim() + $Keys = @(($KeyNames -Split \" \")) + $EmptyKeys = $Keys | Where-Object { [string]::IsNullOrWhiteSpace($_) } + if ($Keys.Count -le 0 -or $EmptyKeys.Count -gt 0) { + throw \"No keys (field names) were specified for '$_'. To retrieve all keys in a secret, add the word ALL or the wildcard (*) character.\" + } + + if ($secretDefinition.Count -gt 2) { + $VariableName = $secretDefinition[2].Trim() + } + } + else { + throw \"No keys (field names) were specified for '$_'. To retrieve all keys in a secret, add the word ALL or the wildcard (*) character.\" + } + + $secret = [PsCustomObject]@{ + Name = $secretName + SecretVersionId = $secretVersionId + SecretVersionStage = $secretVersionStage + Keys = $Keys + variableNameOrPrefix = $VariableName + } + $Secrets += $secret + } +} + +Write-Verbose \"Secrets to retrieve: $($Secrets.Count)\" +Write-Verbose \"Print variables: $PrintVariableNames\" + +$retrievedSecrets = @{} + +# Retrieve Secrets +foreach ($secret in $secrets) { + $name = $secret.Name + $versionId = $secret.SecretVersionId + $versionStage = $secret.SecretVersionStage + $variableNameOrPrefix = $secret.variableNameOrPrefix + $keys = $secret.Keys + + # Should we extract only specified keys, or all values? + $SpecifiedKeys = $True + if ($keys.Count -eq 1 -and ($keys[0] -ieq \"all\" -or $keys[0] -ieq \"*\")) { + $SpecifiedKeys = $False + } + + $displayName = Format-SecretName -Name $name -VersionId $versionId -VersionStage $versionStage -Keys $keys + Write-Verbose \"Retrieving Secret $displayName\" + $_secretIdentifier = \"$name\" + + $params = @(\"--secret-id $name\") + if (![string]::IsNullOrWhiteSpace($versionId)) { + $params += \"--version-id $versionId\" + $_secretIdentifier += \"_$versionId\" + } + if (![string]::IsNullOrWhiteSpace($versionStage)) { + $params += \"--version-stage $versionStage\" + $_secretIdentifier += \"_$versionStage\" + } + + # Check to see if we've already retrieved this secret value to save on requests + if (-not $retrievedSecrets.ContainsKey($_secretIdentifier)) { + $command = \"aws secretsmanager get-secret-value $($params -Join \" \")\" + Write-Verbose \"Invoking command: $command\" + $response = Invoke-Expression -Command $command + if ([string]::IsNullOrWhiteSpace($response)) { + throw \"Error: Secret $displayName not found or has no versions.\" + } + Write-Verbose \"Added secret to retrieved collection ($_secretIdentifier)\" + $retrievedSecrets.Add($_secretIdentifier, $response) + } + else { + Write-Verbose \"Rehydrating previously stored secret ($_secretIdentifier) instead of calling AWS.\" + $response = $retrievedSecrets.$_secretIdentifier + } + + try { + $AwsSecret = $response | ConvertFrom-Json + $AwsSecretValue = $AwsSecret.SecretString | ConvertFrom-Json + $secretKeyValues = $AwsSecretValue | Get-Member | Where-Object { $_.MemberType -eq \"NoteProperty\" } | Select-Object -ExpandProperty \"Name\" + } + catch { + Write-Error \"Error converting JSON value returned from AWS for $displayName.`n`nIf secret value is stored as JSON in Plaintext (vs Key/value), check contents validity\" + } + if ($SpecifiedKeys -eq $True) { + foreach ($keyName in $keys) { + $variableName = $variableNameOrPrefix + if ([string]::IsNullOrWhiteSpace($variableName)) { + $variableName = \"$($name.Trim())\" + } + if ($keys.Count -gt 1) { + $variableName += \".$keyName\" + } + if ($secretKeyValues -inotcontains $keyName) { + throw \"Key '$keyName' not found in AWS Secret: $name.\" + } + $variableValue = $AwsSecretValue.$keyName + Save-OctopusVariable -Name $variableName -Value $variableValue + } + } + else { + foreach ($secretKeyValueName in $secretKeyValues) { + $variableName = $variableNameOrPrefix + if ([string]::IsNullOrWhiteSpace($variableName)) { + $variableName = \"$($name.Trim())\" + } + if ($secretKeyValues.Count -gt 1) { + $variableName += \".$secretKeyValueName\" + } + $variableValue = $AwsSecretValue.$secretKeyValueName + Save-OctopusVariable -Name $variableName -Value $variableValue + } + } +} + +Write-Host \"Created $($script:storedVariables.Count) output variables\"", + "Octopus.Action.AwsAccount.Variable": "#{AWS.SecretsManager.RetrieveSecrets.Account}", + "Octopus.Action.Aws.Region": "#{AWS.SecretsManager.RetrieveSecrets.Region}" + }, + "Parameters": [ + { + "Id": "8623cdbe-f962-4801-9470-5d14d1d7d5ed", + "Name": "AWS.SecretsManager.RetrieveSecrets.Account", + "Label": "AWS Account", + "HelpText": "An AWS account with permissions to access secrets from Secrets Manager.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "55a1d3e8-90c8-4c1a-a315-246fd8660e81", + "Name": "AWS.SecretsManager.RetrieveSecrets.Region", + "Label": "AWS Region", + "HelpText": "Specify the default region. View the [AWS Regions and Endpoints](https://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) documentation for a current list of the available region codes.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e204a03d-80a6-437a-9a8b-8812c299741c", + "Name": "AWS.SecretsManager.RetrieveSecrets.SecretNames", + "Label": "Secret names to retrieve", + "HelpText": "Specify the names of the secrets to be returned from AWS Secrets Manager, in the format: + +`SecretName SecretVersionId SecretVersionStage | KeyNames | OutputVariableName` where: + +- `SecretName` is the name of the secret to retrieve. You can specify either the `Amazon Resource Name (ARN)` or the friendly name of the secret. +- `SecretVersionId` is the unique identifier of the version of the secret that you want to retrieve. If this value isn't specified, the version with the `VersionStage` value as specified in `SecretVersionStage` will be retrieved. +- `SecretVersionStage` specifies the secret version that you want to retrieve by the staging label attached to the version. *Staging labels are used to keep track of different versions during the rotation process*. If this value isn't specified, the version with the `VersionStage` value of `AWSCURRENT` will be retrieved. +- `KeyNames` are the names of the keys stored in the secret that you wish to retrieve values for. Multiple fields can be retrieved separated by a space. Alternatively, you can specify all fields using the special keyword `all` or `*`. *See the step description for examples*. +- `OutputVariableName` is the _optional_ Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) name to store the secret's value in. If multiple fields are specified the field name will be appended to this value. *If this value isn't specified, an output name will be generated dynamically*. + +**Note:** Multiple fields can be retrieved by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "17ba53a4-bf94-498c-8905-0d37b86eaeea", + "Name": "AWS.SecretsManager.RetrieveSecrets.PrintVariableNames", + "Label": "Print output variable names", + "HelpText": "Write out the Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) names to the task log. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.AwsRunScript", + "$Meta": { + "ExportedAt": "2023-04-22T17:43:19.580Z", + "OctopusVersion": "2023.2.6614", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "aws" + } diff --git a/step-templates/aws-windows-install-tentacle.json.human b/step-templates/aws-windows-install-tentacle.json.human new file mode 100644 index 000000000..3e526deac --- /dev/null +++ b/step-templates/aws-windows-install-tentacle.json.human @@ -0,0 +1,303 @@ +{ + "Id": "234ed1c6-a5fa-47ee-8669-a187d8787057", + "Name": "AWS Win - Install Octopus Tentacle", + "Description": "This step template will install the latest tentacle on an AWS hosted, Windows virtual machine. This will also open the firewall for inbound traffic on port 10933 on the Security Group. +
+*Note: Expects the AWS CLI and Powershell to be installed on the worker running this task*", + "ActionType": "Octopus.AwsRunScript", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{awsInstallWinTentacle.AWSAccount}", + "Octopus.Action.Script.ScriptBody": "$sgName = $OctopusParameters[\"awsInstallWinTentacle.awsSGName\"] +$instanceId = $OctopusParameters[\"awsInstallWinTentacle.awsVmInstanceId\"] +$serverUri = $OctopusParameters[\"awsInstallWinTentacle.octoServerUrl\"] +$apiKey = $OctopusParameters[\"awsInstallWinTentacle.octoApiKey\"] +$rolesRaw = $OctopusParameters[\"awsInstallWinTentacle.octopusRoles\"] +$enviroRaw = $OctopusParameters[\"awsInstallWinTentacle.octopusEnvironments\"] +$octoThumb = $OctopusParameters[\"awsInstallWinTentacle.octoServerThumb\"] +$comStyle = $OctopusParameters[\"awsInstallWinTentacle.tentacleType\"] +$hostname = $OctopusParameters[\"awsInstallWinTentacle.tentacleHostName\"] +$tentacleName = $OctopusParameters[\"awsInstallWinTentacle.tentacleName\"] +$portNumber = $OctopusParameters[\"awsInstallWinTentacle.portNumber\"] + +Write-Host \"Parsing Parameters\" + +if([string]::IsNullOrEmpty($sgName)) +{ +\tthrow \"Security Group name must be provided\" +} + +if([string]::IsNullOrEmpty($instanceId)) +{ +\tthrow \"Instance Id must be provided\" +} + +if([string]::IsNullOrEmpty($apiKey)) +{ +\tthrow \"apiKey must be provided\" +} + +if([string]::IsNullOrEmpty($rolesRaw)) +{ +\tthrow \"At least one role must be defined\" +} + +if([string]::IsNullOrEmpty($enviroRaw)) +{ +\tthrow \"At least one environment must be defined\" +} + +if([string]::IsNullOrEmpty($octoThumb)) +{ +\tthrow \"octo thumbprint must be provided\" +} + +$roles = \"\" +$rolesRaw -split \"`n\" | ForEach-Object { $roles += \"--role $_ \"} +$roles = $roles.TrimEnd(' ') + +$environments = \"\" +$enviroRaw -split \"`n\" | ForEach-Object { $environments += \"--env $_ \"} +$environments = $environments.TrimEnd(' ') + +if($comStyle -eq \"TentaclePassive\") +{ +\tif([string]::IsNullOrEmpty($hostname)) + { + \t$hostname = aws ec2 describe-instances --filters \"Name=instance-id,Values=$instanceId\" --query \"Reservations[].Instances[].NetworkInterfaces[].Association.PublicIp\" --output=text + $hostname = $hostname.Trim(\"`n\") + } + + $noListen = \"--port $portNumber --noListen 'false'\" + $comStyle += \" --publicHostName='$hostname'\" + $openFirewall = 'true' +} +else +{ +\t$noListen = \"--noListen 'true'\" + $openFirewall = 'false' +} + +if([string]::IsNullOrEmpty($tentacleName)) +{ +\t$tentacleName = $hostname +} + +if($openFirewall -eq 'true') +{ +\tWrite-Host \"Checking SG...\" -NoNewline + $sgCheck = aws ec2 describe-security-groups --group-names $sgName --output json --filters Name=ip-permission.from-port,Values=$portNumber Name=ip-permission.cidr,Values='0.0.0.0/0' | convertfrom-json + + if($sgCheck.SecurityGroups.count -eq 0) + { +\t\tWrite-Host \"Creating SG Rule\" + \taws ec2 authorize-security-group-ingress --group-name $sgName --ip-permissions IpProtocol=tcp,ToPort=$portNumber,FromPort=$portNumber,IpRanges='[{CidrIp=0.0.0.0/0,Description=\"OctopusListeningTentacle\"}]' +\t} + else + { + \tWrite-Host \"Found Existing SG Rule\" + } +} +$remoteGuid = (new-guid).guid +Write-Verbose \"hostname: $hostname`nnoListen: $noListen\" + +$remoteScript = @\" +{ \"commands\": [ +\"if('$env:PROCESSOR_ARCHITECTURE' -eq \\\"x86\\\") {Invoke-WebRequest -Uri 'http://octopus.com/downloads/latest/OctopusTentacle' -OutFile `$env:TEMP/$remoteGuid.msi -UseBasicParsing} else { Invoke-WebRequest -Uri 'http://octopus.com/downloads/latest/OctopusTentacle64' -OutFile `$env:TEMP/$remoteGuid.msi -UseBasicParsing}\", +\"Start-Process `$env:TEMP/$remoteGuid.msi /quiet -Wait\", +\"Remove-Item \\\"`$env:TEMP/$remoteGuid.msi\\\"\", +\"cd 'C:/Program Files/Octopus Deploy/Tentacle'\", +\".\\\\Tentacle.exe create-instance --instance 'Tentacle' --config 'C:/Octopus/Tentacle.config' --console\", +\".\\\\Tentacle.exe new-certificate --instance 'Tentacle' --if-blank --console\", +\".\\\\Tentacle.exe configure --instance 'Tentacle' --reset-trust --console\", +\".\\\\Tentacle.exe configure --instance 'Tentacle' --home 'C:/Octopus/' --app 'C:/Octopus/Applications' $noListen --console\", +\".\\\\Tentacle.exe configure --instance 'Tentacle' --trust '$octoThumb' --console\", +\"if('$openFirewall' -eq 'true'){New-NetFirewallRule -DisplayName 'Octopus Tentacle' -Direction Inbound -LocalPort $portNumber -Protocol TCP -Action Allow}\", +\".\\\\Tentacle.exe register-with --instance 'Tentacle' --server '$serverUri' --apiKey=$apiKey $roles $environments --comms-style $comStyle --force --console\", +\".\\\\Tentacle.exe service --instance 'Tentacle' --install --start --console\" +]} +\"@ + +Write-Host \"Installing tentacle on remote machine\" +$guid = (new-guid).guid +Set-Content -Value $remoteScript.Replace('`r','') -Path \"$env:Temp/$guid.json\" + +write-verbose $remoteScript + +write-verbose \"aws ssm send-command --document-name AWS-RunPowerShellScript --instance-ids $instanceId --parameters file://$env:Temp/$guid.json\" +try { +\t$cmdResponse = aws ssm send-command --document-name \"AWS-RunPowerShellScript\" --instance-ids \"$instanceId\" --parameters \"file://$env:Temp/$guid.json\" --query \"Command\" --output json | convertfrom-json + $cmdId = $cmdResponse.CommandId + $errorResponse = aws ssm get-command-invocation --command-id \"$cmdId\" --instance-id \"$instanceId\" --output json | convertfrom-json + + while($errorResponse.Status -eq 'InProgress') + { + \twrite-verbose \"`nStatus: $($errorResponse.Status)\" + \t$errorResponse = aws ssm get-command-invocation --command-id \"$cmdId\" --instance-id \"$instanceId\" --output json | convertfrom-json + } + + write-verbose \"`nErrorResponse: $errorResponse`n\" + + if(![string]::IsNullOrEmpty($errorResponse.StandardErrorContent)) + { + \tthrow $errorResponse.StandardErrorContent + } +} +finally { +\tremove-item \"$env:Temp\\$guid.json\" +} +", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.AwsAccount.Variable": "#{awsInstallWinTentacle.AWSAccount}", + "Octopus.Action.Aws.Region": "#{awsInstallWinTentacle.awsRegion}" + }, + "Parameters": [ + { + "Id": "dea51842-271f-48fa-901e-243488049f97", + "Name": "awsInstallWinTentacle.AWSAccount", + "Label": "AWS Account", + "HelpText": "AWS account with permissions to the virtual machine in which to install the tentacle on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "6a97548c-3b97-4a19-8b9a-45aa58ac62d9", + "Name": "awsInstallWinTentacle.awsRegion", + "Label": "AWS Region", + "HelpText": "The name of the aws region. I.E. `us-east-1`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d9efb017-65c0-4010-b09f-0a6a34fc009f", + "Name": "awsInstallWinTentacle.awsSGName", + "Label": "Security Group Name", + "HelpText": "The name of the AWS security group to create an exception in", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e4358aec-2798-412a-bcd2-1c7e41a0728d", + "Name": "awsInstallWinTentacle.awsVmInstanceId", + "Label": "VM Instance ID", + "HelpText": "The instance ID of the virtual machine to target when ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8dbe0a29-4e0f-43f8-8ef2-ae23a2884e85", + "Name": "awsInstallWinTentacle.octoServerThumb", + "Label": "Server Thumbprint", + "HelpText": "The Thumbprint of the octopus server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "a0859551-6c84-45d1-af2b-c57becb4e8ef", + "Name": "awsInstallWinTentacle.octoApiKey", + "Label": "Octo User API Key", + "HelpText": "The API key used to configure the tentacle.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "dd07815d-bfb7-43b9-90f8-21ec5a2d2953", + "Name": "awsInstallWinTentacle.octopusRoles", + "Label": "Roles", + "HelpText": "Roles to assign to this tentacle installation.
+*Note: Each role should be on it's own line*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "d3ffb38e-7fcf-4547-be9a-6cd8e46f8e5e", + "Name": "awsInstallWinTentacle.octopusEnvironments", + "Label": "Environments", + "HelpText": "Environments to assign this tentacle installation to.
+*Note: Each environment should be on its own line*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "15150827-7bd3-4146-a798-66344851f602", + "Name": "instrallTentacle.octoServerUrl", + "Label": "Octo Server Url", + "HelpText": "The server url to register the tentacle with. Defaults to the base url", + "DefaultValue": "#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9bcd6bb5-2db3-453f-b4f6-6a78f3c39b59", + "Name": "awsInstallWinTentacle.tentacleType", + "Label": "Tentacle Type", + "HelpText": "Select between a listening or polling tentacle", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "TentaclePassive|Listening +TentacleActive|Polling" + } + }, + { + "Id": "6bb98570-fe3e-4ab7-adfb-79ef7ce5c2ce", + "Name": "awsInstallWinTentacle.tentacleHostName", + "Label": "Tentacle Host Name", + "HelpText": "The host name to register the listening tentacles with. Octopus deploy server uses this value to reach out to the vm.
+*Note: Leave blank to automatically use assigned public IP address.*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "386d9f39-9e90-4215-b739-53a1af5bd105", + "Name": "awsInstallWinTentacle.tentacleName", + "Label": "Tentacle Name", + "HelpText": "The name of the tentacle to use in the infrastructure area of octopus deploy. Will use the host name if not provided", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2dc806d8-bba5-44a7-b21d-4b3f041f4d49", + "Name": "awsInstallWinTentacle.portNumber", + "Label": "Port Number", + "HelpText": "Port number used when installing and registering the tentacle. This is also the port opened on the firewall", + "DefaultValue": "10933", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-08-23T12:40:10.975Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "benjimac93", + "Category": "aws" + } diff --git a/step-templates/azure-add-or-update-azure-loadbalancer-health-probe.json.human b/step-templates/azure-add-or-update-azure-loadbalancer-health-probe.json.human new file mode 100644 index 000000000..88934374c --- /dev/null +++ b/step-templates/azure-add-or-update-azure-loadbalancer-health-probe.json.human @@ -0,0 +1,192 @@ +{ + "Id": "59528692-2107-45eb-9fde-55e7329822a9", + "Name": "Add or update Azure Load Balancer Health Probe", + "Description": null, + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Azure.AccountId": "#{Azure.LoadBalancerCreateHealthProbe.Account}", + "Octopus.Action.Script.ScriptBody": " + +Write-Output \"Resource group name: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.ResourceGroupName'])\" +Write-Output \"Load balancer name : $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.LoadBalancerName'])\" +Write-Output \"Health probe name: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.HealthProbeName'])\" + +Write-Output \"Protocol: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Protocol'])\" +Write-Output \"Path: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Path'])\" +Write-Output \"Port: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Port'])\" +Write-Output \"Interval: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Interval'])\" +Write-Output \"Probe count: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.ProbeCount'])\" + + +$loadBalancer = Get-AzureRmLoadBalancer -ResourceGroupName $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.ResourceGroupName'] -name $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.LoadBalancerName'] +$healthProbe = Get-AzureRmLoadBalancerProbeConfig -name $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.HealthProbeName'] -LoadBalancer $loadBalancer -ErrorAction:SilentlyContinue + +if($healthProbe -eq $null) +{ +\t#Create healthProbe +\tWrite-Output \"Creating healt probe: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.HealthProbeName']) on load balancer: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.LoadBalancerName']) in resource group: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.ResourceGroupName'])\" +\t +\tif($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Protocol'] -eq \"http\") +\t{ +\t\t#path only used in http +\t\t$loadBalancer | Add-AzureRmLoadBalancerProbeConfig -Name $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.HealthProbeName'] ` +\t\t\t-RequestPath $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Path'] ` +\t\t\t-Protocol $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Protocol'] ` +\t\t\t-Port $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Port'] ` +\t\t\t-IntervalInSeconds $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Interval'] ` +\t\t\t-ProbeCount $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.ProbeCount'] +\t} +\telse +\t{ +\t\t# Path is not part of tcp config +\t\t$loadBalancer | Add-AzureRmLoadBalancerProbeConfig -Name $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.HealthProbeName'] ` +\t\t\t-Protocol $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Protocol'] ` +\t\t\t-Port $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Port'] ` +\t\t\t-IntervalInSeconds $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Interval'] ` +\t\t\t-ProbeCount $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.ProbeCount'] +\t} +} +else +{ +\t#Update healthProbe +\tWrite-Output \"Updating healt probe: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.HealthProbeName']) on load balancer: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.LoadBalancerName']) in resource group: $($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.ResourceGroupName'])\" +\t +\tif($OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Protocol'] -eq \"http\") +\t{ +\t\t#path only used in http +\t\t$loadBalancer | Set-AzureRmLoadBalancerProbeConfig -Name $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.HealthProbeName'] ` +\t\t\t-RequestPath $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Path'] ` +\t\t\t-Protocol $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Protocol'] ` +\t\t\t-Port $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Port'] ` +\t\t\t-IntervalInSeconds $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Interval'] ` +\t\t\t-ProbeCount $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.ProbeCount'] +\t} +\telse +\t{ +\t\t# Path is not part of tcp config +\t\t$loadBalancer | Set-AzureRmLoadBalancerProbeConfig -Name $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.HealthProbeName'] ` +\t\t\t-Protocol $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Protocol'] ` +\t\t\t-Port $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Port'] ` +\t\t\t-IntervalInSeconds $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.Interval'] ` +\t\t\t-ProbeCount $OctopusParameters['Azure.LoadBalancerCreateHealthProbe.ProbeCount'] +\t} +} + +Write-Host \"Save changes to loadbalancer\" +Set-AzureRmLoadBalancer -LoadBalancer $loadBalancer" + }, + "Parameters": [ + { + "Id": "e720d0e8-14d7-47cc-96a9-b074aa2dcf14", + "Name": "Azure.LoadBalancerCreateHealthProbe.Account", + "Label": "Account", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + }, + "Links": {} + }, + { + "Id": "87a93c04-2395-4a73-9999-6b6dacc546fe", + "Name": "Azure.LoadBalancerCreateHealthProbe.ResourceGroupName", + "Label": "Resource group name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "d155c5eb-7cd8-4f57-b3ca-d6c32c7403e1", + "Name": "Azure.LoadBalancerCreateHealthProbe.LoadBalancerName", + "Label": "Load balancer name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "79619a6d-1d28-4900-bc24-1c597c649bf4", + "Name": "Azure.LoadBalancerCreateHealthProbe.HealthProbeName", + "Label": "Health probe name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "beed45f9-6c8d-424b-a072-57c0d1657437", + "Name": "Azure.LoadBalancerCreateHealthProbe.Protocol", + "Label": "Protocol", + "HelpText": null, + "DefaultValue": "tcp", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "http|http +tcp|tcp" + }, + "Links": {} + }, + { + "Id": "403a32d1-17c5-4ee9-8095-25919369ded3", + "Name": "Azure.LoadBalancerCreateHealthProbe.Path", + "Label": "Path", + "HelpText": null, + "DefaultValue": "/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "6c7d97da-264c-402d-b207-239922c547ef", + "Name": "Azure.LoadBalancerCreateHealthProbe.Port", + "Label": "Port", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a5bfd24b-1249-4383-8204-336abc43a435", + "Name": "Azure.LoadBalancerCreateHealthProbe.Interval", + "Label": "Interval", + "HelpText": "interval in seconds", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "033d0e37-30f9-49c8-b012-c18416596624", + "Name": "Azure.LoadBalancerCreateHealthProbe.ProbeCount", + "Label": "Unhealty threshold", + "HelpText": "number of consecutive failures", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "prebenh", + "$Meta": { + "ExportedAt": "2018-05-04T12:46:43.193Z", + "OctopusVersion": "2018.4.7", + "Type": "ActionTemplate" + }, + "Logo" : "iVBORw0KGgoAAAANSUhEUgAAAlgAAAE7CAYAAAAB7v+1AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAB47SURBVHhe7d1PjF3Vgefxt+pedXpHNpGQAgFPK1I02WSZ7Fj29CbRjLqTSCy6sxi6lVmEDTaUGSQQ6UTpZGh1BD2JAh0lNGIyxNhVZYoYO2AGDIkBx2Abg43/ALYhFWxsSL/xKe5LyuVTrnrv3T/nnPv5SD+1OkxwXffo3a/eufXeACAlmxYGH5uZ/+Tnb5/7i1vDZuauu2dmfsPCJZu79t/+8M+3XX3DpkcGV1X/dQAAghBIm+ev+7vN8//p0Yv/czjJZuav27N52zU33z776eurfy0AQP9snrv+q0thFAmmaTYzf+2h8O5WeDes+qMAAMp2++yn/0sTYXX5NhzbPHvtP9zy2OBPqj8aAKAs4dmqmbnrfxmPoeYW3tEK75ZVPwYAQBnCw+qx+Glz4QF5x4YAQPY++q3ADQux4OliM/OfesmD8ABAtkLIhKCJhU6Xm5nbcDocV1Y/JgBAHj563mrD6VjgJDPPZQEAuQgf/jkzd/3ZaNSkNpEFAKQuq7gaTWQBAKnKMq5GE1kAQGqyjqvRRBYAkIoi4mo0kQUAdK2ouBpNZAEAXSkyrkYTWQBA24qOq9FEFgDQll7E1WgiCwBoWq/iajSRBQA0pZdxNZrIAgDq1uu4Gk1kAQB1EVfLJrIAgGmJq8hEFgAwKXF1hYksAGBc4modE1kAwHqJqzEmsgCAtYirCSayAIDVpB5Xdy18IfqfJzGRBQCslHpcvXL8ueHRUwdEFgCQhxziakRkAQDJyymuRkQWAJCsHONqRGQBAMnJOa5GRBYAkIwS4mpEZAEAnSsprkZEFgDQmRLjakRkAQCtKzmuRkQWANCaPsTViMgCABrXp7gaEVkAQGP6GFcjIgsAqF2f42pEZAEAtRFXfySyAICpiavLiSwAYGLianUiCwAYm7ham8gCANZNXK2fyAIA1iSuxieyAIBViavJiSwA4DLianoiCwD4A3FVn5Qj69bZPxtu2jb4SvV/dgCgKeKqfilG1m1zVw03zQ4+msgCgOaIq+akFFkzc5/8Y1yJLABojrhqXiqRdVlcjSayAKA+4qo9XUfWJUeDsYksAJieuGpfV5EVPRqMTWQBwOTEVXe6iKxoTK02kQUA4xNX3WszstY8GoxNZAHA+omrdLQRWTNzn4gH1HomsgBgbeIqPU1HVjScxpnIAoDViat0NRVZEx0NxiayAOBy4ip9dUfWVEeDsYksAPgjcZWPOiMrGknTTmQBgLjKUR2RtfRFzrFAqmMiC4A+E1f5miayaj8ajE1kAdBH4ip/k0TWuj+tvY6JLAD6RFyVY9zIavRoMDaRBUAfiKvyrDeyWjkajE1kAVAycVWutSKr1aPB2EQWACUSV+W7UmS1fjQYm8gCoCSpx9X9T98y3PPaY63u+JnXqixpzqnFE9E/u8k99Nw3L/v77exoMDaRBUAJUo+rrhZipGnhXbnYn93mOj8ajE1kAZAzcbX6+hJYSRwNxiayAMiRuLry+hBYSR0NxiayAMiJuFp7pQdWkkeDsYksAHIgrta30gMr2aPB2EQWACkTV+tfyYGV/NFgbCILgBSJq/FWamBlczQYm8gCICXiavyVGlhZHQ3GJrIASIG4mmwlBtZtc1fFoyW3iSwAuiSuJl9pgZX10WBsIguALoir6VZaYGV/NBibyAKgTeJq+pUUWMUcDcYmsgBog7iqZ6UEVnFHg7GJLACaJK7qWymBVeTRYGwiC4AmiKt6V0JgFX00GJvIAqBO4qr+5R5YvTgajE1kAVAHcdXMcg+saHz0ZSILgGmIq+aWc2D17mgwNpEFwCTEVbPLNbB6ezQYm8gCYBziqvnlGljR0OjzRBYA6yGu2lmOgeVocJWJLACuRFy1t9wCa2buE/G4sI8msgCIEVftLrfAikaFXTqRBcBy4qr95RRYjgbHmMgCIBBX3SyXwHI0OMFEFkC/iavulktgRQPC1p7IAugncdXtcgis3nyRc1MTWQD9Iq66X+qB5WiwpoksgH4QV2ks5cDyae01T2QBlE1cpbOUA8vRYAMTWQBlEldpLdXAcjTY4EQWQFnEVXpLMbAcDbYwkQVQBnGV5lIMLEeDLU1kAeRNXKW71ALL0WDLE1kAeRJXaS+lwHI02NFEFkBexFX6SymwHA12OJEFkAdxlcdSCSxHgwlMZAGkTVzlsxQCy9FgQhNZAGkSV3kthcByNJjYRBZAWsRVfus6sBwNJjqRBZAGcZXnugwsR4OJT2QBdEtc5blv/eKvluKnaUdPHRh+74kvX/bnOxrMYCILoBviKr89+sL3h4dOvjD84MMLVQK1I4TWwm/+bXjXwheGt81dFb+hW3oTWQDtElf57H8/9T+G+449PTx7frHKne6EsHv5+J7hg3vujN/QLb2JLIB2pBpX4Zmi2H/e1/3Lrq+1cgw4qeNnDgutFdt7ZNfw7oUbov+s04ksgGalGlchJBbPnYn+s74t9bBa6fBb+4Y/3H1z/Mbes4XoPLV4QmQB9EnKcRX0PbDC803Pv75j6e8iR+HoMMmwaHEhsAKRBdATqcdV0OfA+vGztw/fee/t6m8iX+E5sYef/3b85t6DjQIrEFkAhcshroI+Blbu71qtpq/vZi0PrEBkARQql7gK+hZY4VmrEt61Wk14N6tvD8GvDKxAZAEUJqe4CvoUWPc/fcvw/QvnqisvV/hYh60v3he/wRe4WGAFIgugELnFVdCXwPrZr/6p9Q8K7drOVx6O3+AL22qBFYgsgMzlGFdBHwJrx8sPVlfbvLcW3x++cebscP+JxeHOQ6cu2YvH3136Z8ffbe9dtJfe2B2/wRe0KwVWILIAMpVrXAWlB9bTr26trrR+5z/4/VJI/fve48OvbX15+NUt4+/Hzx9fCq/3zn9Q/VvrFx5+j97gC9lagRWILIDM5BxXQcmBNfvSD6qrrE8IoV+98c7wR88eiwbTpPvrn+8ffvep14fPvH5m+Ntz9R9lPvfa4/EbfAFbT2AFIgsgE7nHVVBqYD303DdrfeYqhNX/felkNI6m3d9cjKv/+sile+C5Y8PT752v/vR6bN93f/wGn/nWG1iByAJIXAlxFZQYWOGLmuv6bcFwDPiLg29PfAS4nq2Mq+Xbsu9kbceHIThL/AiHcQIrEFkAiSolroLSAut7T3x56Zqm9eHv/2P47JEzw6/PH4hGUV0LR4OxsFq+G7fsHz5x8NRS7E0rhOe9T94Uv8FnunEDKxBZAIkpKa6C0gLr0MkXqiubXHgG6q4nXosGUZ2LHQ1ead+YP7j0W4rTevPdo/Gbe6abJLACkQWQiNLiKigpsOp4qD18jELT71qNFouotRbezfrNicXqp53c7kNb4jf3DDdpYAUiC6BjJcZVUEpghaPBaZ+7Cr8d2OSzVsv33yLxNM4WDkz3dT/heawf7r45fnPPbNMEViCyADpSalwFpQTW0VMHqiuazPaX34qGUBMb92hwtYXfNJzmuawQFnds/2z85p7Rpg2sQGQBtKzkuApKCKxpP6k9fFBoLISa2FcuLhZLky58dlZ4IH9Szx6ej9/YM1odgRWILICWlB5XQe6BddfCF4Znz0/+TNLuw6ejIdTUpj0ajO2nvz5WXc34wlHhd3Z8MX5jz2R1BVYgsgAa1oe4CnIPrF0HflZdyfgOvvm7aAQ1tbqOBmN76tXT1VWNb++RXfGbeiarM7ACkQXQkL7EVZBzYE3z7lX4uIO2HmgPq/toMLYDF4NxErm/i1V3YAUiC6BmfYqrIOfAmvTdq/DJ6Jt3vBoNoabWxNHgyoWPcJj0c7JyfhericAKRBZATTbPX/u5PsVVkHNgTfru1Y+fb++h9rAmjwZX7ps7J4uNnN/FaiqwgoQj60vVyxZA2v7n7H++evP8hmOxG3mXazKuglwDK3yZ8yTCOzyxCGpqbRwNrtykR4WP7/9J/Gae+JoMrCDFyNq4bXD24v/8XPXyBZCmWx4b/MnM/HV7YjfyLtd0XAW5Btakfzc/evZYNISaWhtHgysXvlJnko9uCKGy8kaew5oOrCDJyJodHNv0yODj1csYQHpm5q55KHYT73JtxFWQY2CFh9sn+dT2kn5rcK098/pkX3id4zFhG4EVJPpO1i83Pjb40+qlDCAdt8/9xa2xm3iXayuughwD69EXvl/99OP59q7mv8B5tC6OBpdv0nexcvyOwrYCK0j0naz7qpczgDRsemRwVWoPtbcZV0GOgXXo5AvVT79++08sRkOoqXVxNLhyk7yL9ea7R6M38ZTXZmAFST74vm3wmeplDaB7M9s3fCt2A+9qbcdVn7T57FWXR4PLd8eOycIjBEtOm/ZLvieRWmRtnB1sqV7WALqV2rtX4qo54QuR2/pQ0a6PBlfu9Hvnq78F6pbcO1lbB39ZvbwBdGdm/lP/GgudLiaumtXm8WAKR4PLN81X6LC2lCJr4+xgT/XyBtCN22c/fX0sdLqYuGrev+9t54NF/zqRo8Hl++5Tr1d/CzQlqXeyvIsFdGnztmtujsVO2xNXzQu/Sff1+QPRIKpzqR0NLl/4aiCalUpkbZwdPFC9zAG0b2Z+w0IseNqcuGrHa6feiwZR3UvtaHD5nj/6TvW3QZNSiKyN2wanfS4W0InwcHsseNqcuGrPs0fORIOozqV4NLh8c/vfqv42aFoS72TNDT5fvdwBtGfz3PVfjUVPWxNX7frFwbejUVTXUj4aHO2nvz5W/W3Qhq4ja+Pc4B+rlzuA9nT5tTjiqn1NP+Ce8tHgaB50b1+XkbVxdnCoerkDaM/M/LWHYvHT9MRVN+77f0ejYVTHUj8aHG3T44eqvw3a1GVkVS93AO3p4sNFxVV3Nu94NRpH0y6Ho8HloxtdRVb1cgfQjk0Lg4/FAqjJiatuNfUJ7rGISXk+qqE7XURW9ZIH0I62P2D0+dd3VC+xdCUWR9Mul6PB5fvtuQvV3whdOPzWvmgINbXqJQ+gHTPzn/x8LISa2r/s+tpw8dyZ6iWWLtT9DlZuR4OjeQerO+E14J6dN0ZDqKlVL3kA7ejiIxpEVrfqfgYrFi85jG50EVdh1UseQDtmtl19QyyCmp7I6k6dv0WY49FgmN8i7EZXcRVWveQBtGPz3IbPxAKojYmsbtT1OVh/k2lchfkcrPZ1GVdh1UseQDu6/pockdW+uj7JPRYuucwnuber67gKq17yANoTC582J7LaVcd3EeZ6NDia7yJsTwpxFVa93AG0p6tPcl8+kdWe1069F42m9S7no8HRnj/6TvW3QZNSiauw6uUOoD0z8xsWYtHT9kRWOz78/X8Mvz5/IBpP61ksWHKbj2hoXkpxtXHb4HT1cgfQns3brrk5FjxdTGS1Y9IH3XP4Iue15gH35qUUV2EbZwcPVC93AO1p+9Pc15rIat7+E4vRgLrSSjgaDHvq1dPV3wJNSC2ulrZt8JXq5Q6gXSk8h7V8IqtZ5z/4/Vif6J7rp7XHdvq989XfAnVLMq7CFgZ/Xr3UAbRrZvuGb8VCp8uJrGb96Nlj0ZiKrYSjwbA7dhyurp66pRpXG2cHj1UvcwDta/s7Cde7LiIr/HmxnyXlHTr5QvXTr996jwlLORoMe+b18f//0tnzi9Ebd8o7fqbdkEz2nauwrYNvVC9zAN2Ymb9uT+zm3fXajqwcA2v2pR9UP/14vr3rtWhUjVbS0eA35g8u/QbluJ577fH4jTvhtRlYKcfVxtnBsU2PDD5evcQBdKOr7yVcz9qMrBwD666FLww/+PBCdQXrd/DN30XDarRSjgbDJnn3Krj3yZuiN++U11ZgpRxXS9s6+Pvq5Q2gWzNz1zwUu4GnsLYiK8fACnvl+HPVFYxntWexSjoanPTdq1OLJ+I37sTXRmClHlfh3auNjw3+tHppA+hWl1/+vJ61EVm5BtbPfvVP1RWM563F9y+Lq5KOBsMOvPm76mrHs/OVh6M379TXdGClHldL2zb4UvWyBpCGlN/FCms6snINrHBMGB7InsSPn7/0g0dLOhr85s7JYiMcuX5nxxfjN+/E12Rg5RBXG2cHe6qXM4B0bFoYfCy1z8VauSYjK9fACtt14GfVVYwnfHXM5h2vLsVVSUeDN27Zv/QO3ST2HtkVvXnnsKYCK4u42jY4venRwdXVyxlAWsKnu8/MbTgdu4mnsqYiK+fAmuZdrBAif/eoo8Eg53evwpoIrBziamlbBzdUL2MAaUr5twpHayKycg6ssEdf+H51JeMLQRILlRy3cODt6qrGl/O7V2F1B1ZGceW3BoE8bJ699h9iN/GUVndk5R5YYeG33yYVvqsvFiw57YHnjlVXM77c370KqzOwcomrjbOD+6qXLYA8zMxdd0/sJp7S6oysEgJrmnexghAosXDJYeGh9kk+kmEk93evwuoKrIziaouPZACyNDP/qX+N3chTWl2RVUJghU3y9TkjIVC27DsZDZiU9/2njyx9kfWkwv/t79j+2ehNPKfVEVjiCqAlfYmsUgLre098eeIH3keeP/pONGRSXAjCaT24587oTTy3TRtY4gqgZX2IrFICK2zS7yhc7uiZs8P/PvtKNGpSWPgohhCC08rxOwdX2zSBJa4AOlJ6ZJUUWGHTHBWO/PbcheEdOw5HA6fLhfALATitd957u4ijwdEmDSxxBdCxkiOrtMAKR4WTxuZy4bms8IXJKbybFd61mtv/1tIHpE4r/NbgA8/cGr2R57pJAktcASSi1MgqLbDC7n/6luH7F85VVzid8BB5+IypEDmx+Gl6/+fFk0vvqNXl53v/OXojz3njBpa4AkhMiZFVYmCFhS+DDu/W1CW8exRiJxZBTSx8dMSkX3uzmp2ZfpnzWhsnsMQVQKJKi6xSAyusjofeVwqhFR4y/8Ezb0TDaJp996nXl44l63zHaqSEz7tabesNLHEFkLiSIqvkwJqZ++Rw96Et1ZXWLxwf/ubE4vCnvz428RFieKfqhePv1vJ81WqOnHoleiMvZesJLHEFkIlSIqvkwBrdtB7f/5PqapsXjvXCb/qF8Hri4KlLFkIq/LPj79bzfNh6vHx8T1G/MRjbWoElrgAyU0JklRpYt81ddcnNKzzcXeczWTko6bOurrQrBZa4AshU7pFVYmCFo8HYTSx8PEFdv12YuvCuXezvoMStFljiCiBzOUdWiYEVu4mNFm644YM2SxUCssSPYrjSYoElrgAKkWtklRZYK48GYwvPJIXjs9KEh9m/s+OL0WsueSsDS1wBFCbHyCopsGbmPhG9ka22cGRYwrtZ4V2r7fvuj15jH7Y8sMQVQKFyi6ySAit2I1trdy/csPSbdrnq67tWyzcKLHEFULicIquUwFrP0eCV9sPdNw8Pv7Vv6UadgzffPTp8cM+d0Wvp20JgiSuAnsglso6feS36z3LauEeDV1rqoXVq8UTvHmJfa+EdSHEF0CM5RNa3fvFX0f88p8VuZtMuhFa4cafysQ7hXRphFV8OH6QqrgBqlkNk5bxbZ/8sekOra+HmvfXF+zp5Vyu8W7XzlYd7/4xV7hNXAA0RWc2szqPB9SyEzsGTe6v8aU54vureJ2+K/gyW18QVQMNEVr1b7dPam96zh+erDGpOOJqM/dmW18QVQEtEVn1r+mhwtQksW8/EFUDLRNb0a/tocPkElq01cQXQEZE1+bo6GhxNYNmVJq4AOiayJltXR4OjCSxbbeIKIBEia7x1eTQ4msCy2MQVQGJE1vrW9dHgaALLVk5cASRKZK29ro8GRxNYtnziCiBxImv1pXA0OJrAstHEFUAmRNblS+VocDSBZWHiCiAzIuvSpXI0OJrAMnEFkCmR9dFSOhocTWD1e+IKIHN9j6zUjgZHE1j9nbgCKESfIyu1o8HRBFY/J64ACtPHyLpt7qroTS6FCaz+TVwBFKpPkZXq0eBoAqtfE1cAhetLZKV6NDiawOrPxBVAT5QeWSkfDY4msPoxcQXQM6VGVupHg6MJrPInrgB6qsTISv1ocDSBVfbEFUDPlRRZORwNjiawyp24AmBJCZGVy9HgaAKrzIkrAC6Re2TFbnYpT2CVN3EFQFSukZXT0eBoAqusiSsArii3yMrtaHA0gVXOxBUA65JTZMVueDlMYJUxcQXAWHKIrByPBkcTWPlPXAEwkZQja2buE9GbXi4TWHlPXAEwlVQjK3bTy2kvvbG7yqDmHDy5N/pn23QTVwDUIrXIyvlo8I7tn20lrkb2HtkV/TlssokrAGqVSmTlfDR4z84bh2++e7RKn/YcP3N4ePfCDdGfydY/cQVAI1KIrNiNL4eFuFo8d6ZKnvadWjyx9DPEfjZbe+IKgEZ1GVm5fJHzyv1w983Ds+cXq9TpTgi88LPEfkZbfeIKgFZ0EVm5Hg2GoHn/wrkqcboXfhaRtf6JKwBa1WZk5fpp7anF1Uj4mRwXrj1xBUAn2oqsHI8G733ypiTjaiQcF4qs1SeuAOhU05GV49Fg+I29Lh9oX6933nt76WMjYtfQ54krAJLQVGTlejR45NQrVcKk7/Bb+6LX0NeJKwCS0kRk5Xg0+Nxrj1fpko/dh7ZEr6VvE1cAJKnOyMrxaPCBZ26tkiU/4WePXVNfJq4ASFodkZXj0WB4lik805SrPj+PJa4AyMK0keVosBvPHp6PXlvJE1cAZGXSyMrxaDB83lUJPvjwQq8+hFRcAZClcSMr198afOmN3VWi5O/gyb3Rayxt4gqArI0TWTkeDX5nxxeX3vkpRbiWcE2xay1l4gqAIqwnsnL9rsG9R3ZVaVKOcE2xay1h4gqAolwpsnI9GgxL+etwJhXexSrxNwrFFQBFWi2ycjwaDHtwz51VkpQnXFvsmnOduAKgaCsj67a5q6I3xBxWwkczrKakY0JxBUAvjCIr56PBsJw/WHQt4dpi15zbxBUAvRIiK9ejwbC7F26oUqRcuT+HJa4A6KWLN8D7YjfGHHbvkzdVGVKucI2xa89h4gqAXss1skp+wH0k1wfdxRUAXJRjZG3fd3+VIeUK1xi79pQnrgBgmdwi6/H9P6kypFy5BZa4AoCInCLr53v/ucqQcoVrjF17ihNXAHAFuUTWA8/cWmVIucI1xq49tYkrAFiHHCLrnp03VhlSrnCNsWtPaeIKAMaQQ2SVLnbNKU1cAcAEUo+ss+cXqxQpT7i22DWnMnEFAFNIObIOntxb5Uh5wrXFrjmFiSsAqEGqkbX1xfuqHClPuLbYNXc9cQUANUoxssJ39X3w4YUqScoRrinF7yEUVwDQgBQj68ipV6osKUe4pti1djlxBQANSi2ySvzKnNQ+wV1cAUALUoqsuxduKOq3CcO1pHQ8KK4AoEUpRdbOVx6u8iR/4Vpi19jFxBUAdCCVyArv+JTwLtbiuTPJvHslrgCgQ6lEVglf/pzKlzuLKwBIQCqRdWrxRJUq+Qk/e+ya2p64AoCEpBBZ9z550/D9C+eqZMlH+JnDzx67pjYnrgAgQSlE1sPPf7vKlnyEnzl2LW1OXAFAwlKIrJ0Z/VZh+Flj19DmxBUAZCCFyHrpjd1VwqQr/Iyxn73NiSsAyEjXkRU+7iDlyBJXAMBEUngnK8XjQseCAMBUUois8BB5Cr9dGH4GD7QDALVIIbLCxyB0+TlZ4c/2UQwAQK1SiKywrS/et/SVNG0Jf1b4M2M/S9sTVwBQoFQiKyw8B9Xk9xeGf3f4M2J/dhcTVwBQsJQiK/ym4fZ99w8Pntw7/ODDC1UaTS78O8K/K/w7U/nS5jBxBQA9kFJkjRaCKHzZ8svH94z1QHx4pyr8d8J/N6WoGk1cAUCPpBhZKxeC6cE9dy7t8f0/Wdrof08xplZOXAFAD+UQWblOXAFAj4ms+ieuAACRVePEFQDwByJr+okrAOAyImvyiSsAYFUia/yJKwBgTSJr/RNXAMC6iay1J64AgLGJrNUnrgCAiYmsyyeuAICpiaw/TlwBALURWeIKAGhAnyNLXAEAjeljZIkrAKBxfYoscQUAtKYPkSWuAIDWlRxZ4goA6EyJkSWuAIDOlRRZ4goASEYJkSWuAIDk5BxZ4goASFaOkSWuAIDk5RRZ4goAyMbGbYP/FQualCauAIDsXIyYv70YWmdXhk0K2zg3+MfqxwQAyMvFmPncxtnBsZWB09WWgm/b4EvVjwcAkKdNjw6uvhhZe2LB0+aq0Ptc9WMBAOQtPOu0adtgU1dHhhfj6r5Njww+Xv04AADlCJHT5gPw4UH2i2H3meqPBwAo16atg+svxs9DsSiqY0tHknODz1d/HABAf4R3tC4G0d+G2Jr2+PDif/+XF8Pt70O8Vf96AIB+W3pOa+vgL5ee1ZodPFbtpctCanZwbNk/v+Pi//uvbFoY/Hn1rwHo2GDw/wGStwqRNh9FsAAAAABJRU5ErkJggg==", + "Category": "azure" +} diff --git a/step-templates/azure-add-or-update-azure-loadbalancer-rule.json.human b/step-templates/azure-add-or-update-azure-loadbalancer-rule.json.human new file mode 100644 index 000000000..e6d06da8e --- /dev/null +++ b/step-templates/azure-add-or-update-azure-loadbalancer-rule.json.human @@ -0,0 +1,186 @@ +{ + "Id": "1df09b27-905c-4b24-9ad5-008af574508a", + "Name": "Add or update Azure Load balancer Rule", + "Description": "Create a new azure load balancer rule. With the default frontend ip configuration and default backend address pool.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 10, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "Write-Output \"Resource group name: $($OctopusParameters['Azure.LoadBalancerCreateRule.ResourceGroupName'])\" +Write-Output \"Load balancer name : $($OctopusParameters['Azure.LoadBalancerCreateRule.LoadBalancerName'])\" +Write-Output \"Rule name: $($OctopusParameters['Azure.LoadBalancerCreateRule.RuleName'])\" + +Write-Output \"Protocol: $($OctopusParameters['Azure.LoadBalancerCreateRule.Protocol'])\" +Write-Output \"Frontend port: $($OctopusParameters['Azure.LoadBalancerCreateRule.FrontendPort'])\" +Write-Output \"Backend port: $($OctopusParameters['Azure.LoadBalancerCreateRule.BackendPort'])\" +Write-Output \"Healt probe name: $($OctopusParameters['Azure.LoadBalancerCreateRule.HealthProbeName'])\" +Write-Output \"Idle timeout: $($OctopusParameters['Azure.LoadBalancerCreateRule.IdleTimeout'])\" +Write-Output \"Load distribution: $($OctopusParameters['Azure.LoadBalancerCreateRule.LoadDistribution'])\" + +$loadBalancer = Get-AzureRmLoadBalancer -ResourceGroupName $OctopusParameters['Azure.LoadBalancerCreateRule.ResourceGroupName'] -name $OctopusParameters['Azure.LoadBalancerCreateRule.LoadBalancerName'] +$rule = Get-AzureRmLoadBalancerRuleConfig -Name $OctopusParameters['Azure.LoadBalancerCreateRule.RuleName'] -LoadBalancer $loadBalancer -ErrorAction:SilentlyContinue +$healthProbe = Get-AzureRmLoadBalancerProbeConfig -name $OctopusParameters['Azure.LoadBalancerCreateRule.HealthProbeName'] -LoadBalancer $loadBalancer -ErrorAction:SilentlyContinue + +if($rule -eq $null) +{ +\t#Create rule + Write-output \"Creating load balancer rule with name: $($OctopusParameters['Azure.LoadBalancerCreateRule.RuleName']) in load balancer: $($OctopusParameters['Azure.LoadBalancerCreateRule.LoadBalancerName']) in resource group: $($OctopusParameters['Azure.LoadBalancerCreateRule.ResourceGroupName'])\" +\t + $loadBalancer | Add-AzureRmLoadBalancerRuleConfig -Name $OctopusParameters['Azure.LoadBalancerCreateRule.RuleName'] ` +\t\t-FrontendIpConfigurationId ($loadBalancer.FrontendIpConfigurations[0].Id) ` +\t\t-Protocol $OctopusParameters['Azure.LoadBalancerCreateRule.Protocol'] ` +\t\t-FrontendPort $OctopusParameters['Azure.LoadBalancerCreateRule.FrontendPort'] ` +\t\t-BackendPort $OctopusParameters['Azure.LoadBalancerCreateRule.BackendPort'] ` +\t\t-BackendAddressPoolId ($loadBalancer.BackendAddressPools[0].Id) ` +\t\t-ProbeId ($healthProbe.Id) ` +\t\t-IdleTimeoutInMinutes $OctopusParameters['Azure.LoadBalancerCreateRule.IdleTimeout'] ` +\t\t-LoadDistribution $OctopusParameters['Azure.LoadBalancerCreateRule.LoadDistribution'] +} +else +{ +\t#Update rule + Write-output \"Updating load balancer rule with name: $($OctopusParameters['Azure.LoadBalancerCreateRule.RuleName']) in load balancer: $($OctopusParameters['Azure.LoadBalancerCreateRule.LoadBalancerName']) in resource group: $($OctopusParameters['Azure.LoadBalancerCreateRule.ResourceGroupName'])\" +\t +\t$loadBalancer | Set-AzureRmLoadBalancerRuleConfig -Name $OctopusParameters['Azure.LoadBalancerCreateRule.RuleName'] ` +\t\t-FrontendIpConfigurationId ($loadBalancer.FrontendIpConfigurations[0].Id) ` +\t\t-Protocol $OctopusParameters['Azure.LoadBalancerCreateRule.Protocol'] ` +\t\t-FrontendPort $OctopusParameters['Azure.LoadBalancerCreateRule.FrontendPort'] ` +\t\t-BackendPort $OctopusParameters['Azure.LoadBalancerCreateRule.BackendPort'] ` +\t\t-BackendAddressPoolId ($loadBalancer.BackendAddressPools[0].Id) ` +\t\t-ProbeId ($healthProbe.Id) ` +\t\t-IdleTimeoutInMinutes $OctopusParameters['Azure.LoadBalancerCreateRule.IdleTimeout'] ` +\t\t-LoadDistribution $OctopusParameters['Azure.LoadBalancerCreateRule.LoadDistribution'] +} + +Write-host \"Saving loadbalancer\" +Set-AzureRmLoadBalancer -LoadBalancer $loadBalancer", + "Octopus.Action.Azure.AccountId": "#{Azure.LoadBalancerCreateRule.Account}" + }, + "Parameters": [ + { + "Id": "95e211f5-7a85-4116-9eb6-578ffa72314a", + "Name": "Azure.LoadBalancerCreateRule.Account", + "Label": "Account", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + }, + "Links": {} + }, + { + "Id": "05715167-0ecd-463e-8ede-c2dc6eaa683c", + "Name": "Azure.LoadBalancerCreateRule.ResourceGroupName", + "Label": "Resource group name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c4adbfbd-6023-420b-8582-6d79a65a28f6", + "Name": "Azure.LoadBalancerCreateRule.LoadBalancerName", + "Label": "Load balancer name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "8984e095-b895-4d2b-93c9-b62ad67b84b1", + "Name": "Azure.LoadBalancerCreateRule.RuleName", + "Label": "Rule name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "fda6f233-707e-4eb3-9d61-12f44b83d92f", + "Name": "Azure.LoadBalancerCreateRule.Protocol", + "Label": "Protocol", + "HelpText": null, + "DefaultValue": "tcp", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "tcp|tcp +udp|udp" + }, + "Links": {} + }, + { + "Id": "077dcc26-9058-4b96-95d7-b846c5548b20", + "Name": "Azure.LoadBalancerCreateRule.FrontendPort", + "Label": "Frontend port", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "8d68b615-93f3-4412-af5f-5cd2d9403cbd", + "Name": "Azure.LoadBalancerCreateRule.BackendPort", + "Label": "Backend port", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "41fdcd90-e5f1-4313-801e-033106a50afa", + "Name": "Azure.LoadBalancerCreateRule.HealthProbeName", + "Label": "Health probe name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "eef1ff50-989b-4bc7-9109-08824ac9111b", + "Name": "Azure.LoadBalancerCreateRule.IdleTimeout", + "Label": "Idle timeout", + "HelpText": "In minutes", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "62011de8-c0ec-4c38-8970-ff7a543d2b64", + "Name": "Azure.LoadBalancerCreateRule.LoadDistribution", + "Label": "Load distribution", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "default|none +SourceIP|Client IP +SourceIPProtocol|Client IPand protocol" + }, + "Links": {} + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2018-05-04T12:53:19.489Z", + "OctopusVersion": "2018.4.7", + "Type": "ActionTemplate" + }, + "Logo" : "iVBORw0KGgoAAAANSUhEUgAAAlgAAAE7CAYAAAAB7v+1AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAB47SURBVHhe7d1PjF3Vgefxt+pedXpHNpGQAgFPK1I02WSZ7Fj29CbRjLqTSCy6sxi6lVmEDTaUGSQQ6UTpZGh1BD2JAh0lNGIyxNhVZYoYO2AGDIkBx2Abg43/ALYhFWxsSL/xKe5LyuVTrnrv3T/nnPv5SD+1OkxwXffo3a/eufXeACAlmxYGH5uZ/+Tnb5/7i1vDZuauu2dmfsPCJZu79t/+8M+3XX3DpkcGV1X/dQAAghBIm+ev+7vN8//p0Yv/czjJZuav27N52zU33z776eurfy0AQP9snrv+q0thFAmmaTYzf+2h8O5WeDes+qMAAMp2++yn/0sTYXX5NhzbPHvtP9zy2OBPqj8aAKAs4dmqmbnrfxmPoeYW3tEK75ZVPwYAQBnCw+qx+Glz4QF5x4YAQPY++q3ADQux4OliM/OfesmD8ABAtkLIhKCJhU6Xm5nbcDocV1Y/JgBAHj563mrD6VjgJDPPZQEAuQgf/jkzd/3ZaNSkNpEFAKQuq7gaTWQBAKnKMq5GE1kAQGqyjqvRRBYAkIoi4mo0kQUAdK2ouBpNZAEAXSkyrkYTWQBA24qOq9FEFgDQll7E1WgiCwBoWq/iajSRBQA0pZdxNZrIAgDq1uu4Gk1kAQB1EVfLJrIAgGmJq8hEFgAwKXF1hYksAGBc4modE1kAwHqJqzEmsgCAtYirCSayAIDVpB5Xdy18IfqfJzGRBQCslHpcvXL8ueHRUwdEFgCQhxziakRkAQDJyymuRkQWAJCsHONqRGQBAMnJOa5GRBYAkIwS4mpEZAEAnSsprkZEFgDQmRLjakRkAQCtKzmuRkQWANCaPsTViMgCABrXp7gaEVkAQGP6GFcjIgsAqF2f42pEZAEAtRFXfySyAICpiavLiSwAYGLianUiCwAYm7ham8gCANZNXK2fyAIA1iSuxieyAIBViavJiSwA4DLianoiCwD4A3FVn5Qj69bZPxtu2jb4SvV/dgCgKeKqfilG1m1zVw03zQ4+msgCgOaIq+akFFkzc5/8Y1yJLABojrhqXiqRdVlcjSayAKA+4qo9XUfWJUeDsYksAJieuGpfV5EVPRqMTWQBwOTEVXe6iKxoTK02kQUA4xNX3WszstY8GoxNZAHA+omrdLQRWTNzn4gH1HomsgBgbeIqPU1HVjScxpnIAoDViat0NRVZEx0NxiayAOBy4ip9dUfWVEeDsYksAPgjcZWPOiMrGknTTmQBgLjKUR2RtfRFzrFAqmMiC4A+E1f5miayaj8ajE1kAdBH4ip/k0TWuj+tvY6JLAD6RFyVY9zIavRoMDaRBUAfiKvyrDeyWjkajE1kAVAycVWutSKr1aPB2EQWACUSV+W7UmS1fjQYm8gCoCSpx9X9T98y3PPaY63u+JnXqixpzqnFE9E/u8k99Nw3L/v77exoMDaRBUAJUo+rrhZipGnhXbnYn93mOj8ajE1kAZAzcbX6+hJYSRwNxiayAMiRuLry+hBYSR0NxiayAMiJuFp7pQdWkkeDsYksAHIgrta30gMr2aPB2EQWACkTV+tfyYGV/NFgbCILgBSJq/FWamBlczQYm8gCICXiavyVGlhZHQ3GJrIASIG4mmwlBtZtc1fFoyW3iSwAuiSuJl9pgZX10WBsIguALoir6VZaYGV/NBibyAKgTeJq+pUUWMUcDcYmsgBog7iqZ6UEVnFHg7GJLACaJK7qWymBVeTRYGwiC4AmiKt6V0JgFX00GJvIAqBO4qr+5R5YvTgajE1kAVAHcdXMcg+saHz0ZSILgGmIq+aWc2D17mgwNpEFwCTEVbPLNbB6ezQYm8gCYBziqvnlGljR0OjzRBYA6yGu2lmOgeVocJWJLACuRFy1t9wCa2buE/G4sI8msgCIEVftLrfAikaFXTqRBcBy4qr95RRYjgbHmMgCIBBX3SyXwHI0OMFEFkC/iavulktgRQPC1p7IAugncdXtcgis3nyRc1MTWQD9Iq66X+qB5WiwpoksgH4QV2ks5cDyae01T2QBlE1cpbOUA8vRYAMTWQBlEldpLdXAcjTY4EQWQFnEVXpLMbAcDbYwkQVQBnGV5lIMLEeDLU1kAeRNXKW71ALL0WDLE1kAeRJXaS+lwHI02NFEFkBexFX6SymwHA12OJEFkAdxlcdSCSxHgwlMZAGkTVzlsxQCy9FgQhNZAGkSV3kthcByNJjYRBZAWsRVfus6sBwNJjqRBZAGcZXnugwsR4OJT2QBdEtc5blv/eKvluKnaUdPHRh+74kvX/bnOxrMYCILoBviKr89+sL3h4dOvjD84MMLVQK1I4TWwm/+bXjXwheGt81dFb+hW3oTWQDtElf57H8/9T+G+449PTx7frHKne6EsHv5+J7hg3vujN/QLb2JLIB2pBpX4Zmi2H/e1/3Lrq+1cgw4qeNnDgutFdt7ZNfw7oUbov+s04ksgGalGlchJBbPnYn+s74t9bBa6fBb+4Y/3H1z/Mbes4XoPLV4QmQB9EnKcRX0PbDC803Pv75j6e8iR+HoMMmwaHEhsAKRBdATqcdV0OfA+vGztw/fee/t6m8iX+E5sYef/3b85t6DjQIrEFkAhcshroI+Blbu71qtpq/vZi0PrEBkARQql7gK+hZY4VmrEt61Wk14N6tvD8GvDKxAZAEUJqe4CvoUWPc/fcvw/QvnqisvV/hYh60v3he/wRe4WGAFIgugELnFVdCXwPrZr/6p9Q8K7drOVx6O3+AL22qBFYgsgMzlGFdBHwJrx8sPVlfbvLcW3x++cebscP+JxeHOQ6cu2YvH3136Z8ffbe9dtJfe2B2/wRe0KwVWILIAMpVrXAWlB9bTr26trrR+5z/4/VJI/fve48OvbX15+NUt4+/Hzx9fCq/3zn9Q/VvrFx5+j97gC9lagRWILIDM5BxXQcmBNfvSD6qrrE8IoV+98c7wR88eiwbTpPvrn+8ffvep14fPvH5m+Ntz9R9lPvfa4/EbfAFbT2AFIgsgE7nHVVBqYD303DdrfeYqhNX/felkNI6m3d9cjKv/+sile+C5Y8PT752v/vR6bN93f/wGn/nWG1iByAJIXAlxFZQYWOGLmuv6bcFwDPiLg29PfAS4nq2Mq+Xbsu9kbceHIThL/AiHcQIrEFkAiSolroLSAut7T3x56Zqm9eHv/2P47JEzw6/PH4hGUV0LR4OxsFq+G7fsHz5x8NRS7E0rhOe9T94Uv8FnunEDKxBZAIkpKa6C0gLr0MkXqiubXHgG6q4nXosGUZ2LHQ1ead+YP7j0W4rTevPdo/Gbe6abJLACkQWQiNLiKigpsOp4qD18jELT71qNFouotRbezfrNicXqp53c7kNb4jf3DDdpYAUiC6BjJcZVUEpghaPBaZ+7Cr8d2OSzVsv33yLxNM4WDkz3dT/heawf7r45fnPPbNMEViCyADpSalwFpQTW0VMHqiuazPaX34qGUBMb92hwtYXfNJzmuawQFnds/2z85p7Rpg2sQGQBtKzkuApKCKxpP6k9fFBoLISa2FcuLhZLky58dlZ4IH9Szx6ej9/YM1odgRWILICWlB5XQe6BddfCF4Znz0/+TNLuw6ejIdTUpj0ajO2nvz5WXc34wlHhd3Z8MX5jz2R1BVYgsgAa1oe4CnIPrF0HflZdyfgOvvm7aAQ1tbqOBmN76tXT1VWNb++RXfGbeiarM7ACkQXQkL7EVZBzYE3z7lX4uIO2HmgPq/toMLYDF4NxErm/i1V3YAUiC6BmfYqrIOfAmvTdq/DJ6Jt3vBoNoabWxNHgyoWPcJj0c7JyfhericAKRBZATTbPX/u5PsVVkHNgTfru1Y+fb++h9rAmjwZX7ps7J4uNnN/FaiqwgoQj60vVyxZA2v7n7H++evP8hmOxG3mXazKuglwDK3yZ8yTCOzyxCGpqbRwNrtykR4WP7/9J/Gae+JoMrCDFyNq4bXD24v/8XPXyBZCmWx4b/MnM/HV7YjfyLtd0XAW5Btakfzc/evZYNISaWhtHgysXvlJnko9uCKGy8kaew5oOrCDJyJodHNv0yODj1csYQHpm5q55KHYT73JtxFWQY2CFh9sn+dT2kn5rcK098/pkX3id4zFhG4EVJPpO1i83Pjb40+qlDCAdt8/9xa2xm3iXayuughwD69EXvl/99OP59q7mv8B5tC6OBpdv0nexcvyOwrYCK0j0naz7qpczgDRsemRwVWoPtbcZV0GOgXXo5AvVT79++08sRkOoqXVxNLhyk7yL9ea7R6M38ZTXZmAFST74vm3wmeplDaB7M9s3fCt2A+9qbcdVn7T57FWXR4PLd8eOycIjBEtOm/ZLvieRWmRtnB1sqV7WALqV2rtX4qo54QuR2/pQ0a6PBlfu9Hvnq78F6pbcO1lbB39ZvbwBdGdm/lP/GgudLiaumtXm8WAKR4PLN81X6LC2lCJr4+xgT/XyBtCN22c/fX0sdLqYuGrev+9t54NF/zqRo8Hl++5Tr1d/CzQlqXeyvIsFdGnztmtujsVO2xNXzQu/Sff1+QPRIKpzqR0NLl/4aiCalUpkbZwdPFC9zAG0b2Z+w0IseNqcuGrHa6feiwZR3UvtaHD5nj/6TvW3QZNSiKyN2wanfS4W0InwcHsseNqcuGrPs0fORIOozqV4NLh8c/vfqv42aFoS72TNDT5fvdwBtGfz3PVfjUVPWxNX7frFwbejUVTXUj4aHO2nvz5W/W3Qhq4ja+Pc4B+rlzuA9nT5tTjiqn1NP+Ce8tHgaB50b1+XkbVxdnCoerkDaM/M/LWHYvHT9MRVN+77f0ejYVTHUj8aHG3T44eqvw3a1GVkVS93AO3p4sNFxVV3Nu94NRpH0y6Ho8HloxtdRVb1cgfQjk0Lg4/FAqjJiatuNfUJ7rGISXk+qqE7XURW9ZIH0I62P2D0+dd3VC+xdCUWR9Mul6PB5fvtuQvV3whdOPzWvmgINbXqJQ+gHTPzn/x8LISa2r/s+tpw8dyZ6iWWLtT9DlZuR4OjeQerO+E14J6dN0ZDqKlVL3kA7ejiIxpEVrfqfgYrFi85jG50EVdh1UseQDtmtl19QyyCmp7I6k6dv0WY49FgmN8i7EZXcRVWveQBtGPz3IbPxAKojYmsbtT1OVh/k2lchfkcrPZ1GVdh1UseQDu6/pockdW+uj7JPRYuucwnuber67gKq17yANoTC582J7LaVcd3EeZ6NDia7yJsTwpxFVa93AG0p6tPcl8+kdWe1069F42m9S7no8HRnj/6TvW3QZNSiauw6uUOoD0z8xsWYtHT9kRWOz78/X8Mvz5/IBpP61ksWHKbj2hoXkpxtXHb4HT1cgfQns3brrk5FjxdTGS1Y9IH3XP4Iue15gH35qUUV2EbZwcPVC93AO1p+9Pc15rIat7+E4vRgLrSSjgaDHvq1dPV3wJNSC2ulrZt8JXq5Q6gXSk8h7V8IqtZ5z/4/Vif6J7rp7XHdvq989XfAnVLMq7CFgZ/Xr3UAbRrZvuGb8VCp8uJrGb96Nlj0ZiKrYSjwbA7dhyurp66pRpXG2cHj1UvcwDta/s7Cde7LiIr/HmxnyXlHTr5QvXTr996jwlLORoMe+b18f//0tnzi9Ebd8o7fqbdkEz2nauwrYNvVC9zAN2Ymb9uT+zm3fXajqwcA2v2pR9UP/14vr3rtWhUjVbS0eA35g8u/QbluJ577fH4jTvhtRlYKcfVxtnBsU2PDD5evcQBdKOr7yVcz9qMrBwD666FLww/+PBCdQXrd/DN30XDarRSjgbDJnn3Krj3yZuiN++U11ZgpRxXS9s6+Pvq5Q2gWzNz1zwUu4GnsLYiK8fACnvl+HPVFYxntWexSjoanPTdq1OLJ+I37sTXRmClHlfh3auNjw3+tHppA+hWl1/+vJ61EVm5BtbPfvVP1RWM563F9y+Lq5KOBsMOvPm76mrHs/OVh6M379TXdGClHldL2zb4UvWyBpCGlN/FCms6snINrHBMGB7InsSPn7/0g0dLOhr85s7JYiMcuX5nxxfjN+/E12Rg5RBXG2cHe6qXM4B0bFoYfCy1z8VauSYjK9fACtt14GfVVYwnfHXM5h2vLsVVSUeDN27Zv/QO3ST2HtkVvXnnsKYCK4u42jY4venRwdXVyxlAWsKnu8/MbTgdu4mnsqYiK+fAmuZdrBAif/eoo8Eg53evwpoIrBziamlbBzdUL2MAaUr5twpHayKycg6ssEdf+H51JeMLQRILlRy3cODt6qrGl/O7V2F1B1ZGceW3BoE8bJ699h9iN/GUVndk5R5YYeG33yYVvqsvFiw57YHnjlVXM77c370KqzOwcomrjbOD+6qXLYA8zMxdd0/sJp7S6oysEgJrmnexghAosXDJYeGh9kk+kmEk93evwuoKrIziaouPZACyNDP/qX+N3chTWl2RVUJghU3y9TkjIVC27DsZDZiU9/2njyx9kfWkwv/t79j+2ehNPKfVEVjiCqAlfYmsUgLre098eeIH3keeP/pONGRSXAjCaT24587oTTy3TRtY4gqgZX2IrFICK2zS7yhc7uiZs8P/PvtKNGpSWPgohhCC08rxOwdX2zSBJa4AOlJ6ZJUUWGHTHBWO/PbcheEdOw5HA6fLhfALATitd957u4ijwdEmDSxxBdCxkiOrtMAKR4WTxuZy4bms8IXJKbybFd61mtv/1tIHpE4r/NbgA8/cGr2R57pJAktcASSi1MgqLbDC7n/6luH7F85VVzid8BB5+IypEDmx+Gl6/+fFk0vvqNXl53v/OXojz3njBpa4AkhMiZFVYmCFhS+DDu/W1CW8exRiJxZBTSx8dMSkX3uzmp2ZfpnzWhsnsMQVQKJKi6xSAyusjofeVwqhFR4y/8Ezb0TDaJp996nXl44l63zHaqSEz7tabesNLHEFkLiSIqvkwJqZ++Rw96Et1ZXWLxwf/ubE4vCnvz428RFieKfqhePv1vJ81WqOnHoleiMvZesJLHEFkIlSIqvkwBrdtB7f/5PqapsXjvXCb/qF8Hri4KlLFkIq/LPj79bzfNh6vHx8T1G/MRjbWoElrgAyU0JklRpYt81ddcnNKzzcXeczWTko6bOurrQrBZa4AshU7pFVYmCFo8HYTSx8PEFdv12YuvCuXezvoMStFljiCiBzOUdWiYEVu4mNFm644YM2SxUCssSPYrjSYoElrgAKkWtklRZYK48GYwvPJIXjs9KEh9m/s+OL0WsueSsDS1wBFCbHyCopsGbmPhG9ka22cGRYwrtZ4V2r7fvuj15jH7Y8sMQVQKFyi6ySAit2I1trdy/csPSbdrnq67tWyzcKLHEFULicIquUwFrP0eCV9sPdNw8Pv7Vv6UadgzffPTp8cM+d0Wvp20JgiSuAnsglso6feS36z3LauEeDV1rqoXVq8UTvHmJfa+EdSHEF0CM5RNa3fvFX0f88p8VuZtMuhFa4cafysQ7hXRphFV8OH6QqrgBqlkNk5bxbZ/8sekOra+HmvfXF+zp5Vyu8W7XzlYd7/4xV7hNXAA0RWc2szqPB9SyEzsGTe6v8aU54vureJ2+K/gyW18QVQMNEVr1b7dPam96zh+erDGpOOJqM/dmW18QVQEtEVn1r+mhwtQksW8/EFUDLRNb0a/tocPkElq01cQXQEZE1+bo6GhxNYNmVJq4AOiayJltXR4OjCSxbbeIKIBEia7x1eTQ4msCy2MQVQGJE1vrW9dHgaALLVk5cASRKZK29ro8GRxNYtnziCiBxImv1pXA0OJrAstHEFUAmRNblS+VocDSBZWHiCiAzIuvSpXI0OJrAMnEFkCmR9dFSOhocTWD1e+IKIHN9j6zUjgZHE1j9nbgCKESfIyu1o8HRBFY/J64ACtPHyLpt7qroTS6FCaz+TVwBFKpPkZXq0eBoAqtfE1cAhetLZKV6NDiawOrPxBVAT5QeWSkfDY4msPoxcQXQM6VGVupHg6MJrPInrgB6qsTISv1ocDSBVfbEFUDPlRRZORwNjiawyp24AmBJCZGVy9HgaAKrzIkrAC6Re2TFbnYpT2CVN3EFQFSukZXT0eBoAqusiSsArii3yMrtaHA0gVXOxBUA65JTZMVueDlMYJUxcQXAWHKIrByPBkcTWPlPXAEwkZQja2buE9GbXi4TWHlPXAEwlVQjK3bTy2kvvbG7yqDmHDy5N/pn23QTVwDUIrXIyvlo8I7tn20lrkb2HtkV/TlssokrAGqVSmTlfDR4z84bh2++e7RKn/YcP3N4ePfCDdGfydY/cQVAI1KIrNiNL4eFuFo8d6ZKnvadWjyx9DPEfjZbe+IKgEZ1GVm5fJHzyv1w983Ds+cXq9TpTgi88LPEfkZbfeIKgFZ0EVm5Hg2GoHn/wrkqcboXfhaRtf6JKwBa1WZk5fpp7anF1Uj4mRwXrj1xBUAn2oqsHI8G733ypiTjaiQcF4qs1SeuAOhU05GV49Fg+I29Lh9oX6933nt76WMjYtfQ54krAJLQVGTlejR45NQrVcKk7/Bb+6LX0NeJKwCS0kRk5Xg0+Nxrj1fpko/dh7ZEr6VvE1cAJKnOyMrxaPCBZ26tkiU/4WePXVNfJq4ASFodkZXj0WB4lik805SrPj+PJa4AyMK0keVosBvPHp6PXlvJE1cAZGXSyMrxaDB83lUJPvjwQq8+hFRcAZClcSMr198afOmN3VWi5O/gyb3Rayxt4gqArI0TWTkeDX5nxxeX3vkpRbiWcE2xay1l4gqAIqwnsnL9rsG9R3ZVaVKOcE2xay1h4gqAolwpsnI9GgxL+etwJhXexSrxNwrFFQBFWi2ycjwaDHtwz51VkpQnXFvsmnOduAKgaCsj67a5q6I3xBxWwkczrKakY0JxBUAvjCIr56PBsJw/WHQt4dpi15zbxBUAvRIiK9ejwbC7F26oUqRcuT+HJa4A6KWLN8D7YjfGHHbvkzdVGVKucI2xa89h4gqAXss1skp+wH0k1wfdxRUAXJRjZG3fd3+VIeUK1xi79pQnrgBgmdwi6/H9P6kypFy5BZa4AoCInCLr53v/ucqQcoVrjF17ihNXAHAFuUTWA8/cWmVIucI1xq49tYkrAFiHHCLrnp03VhlSrnCNsWtPaeIKAMaQQ2SVLnbNKU1cAcAEUo+ss+cXqxQpT7i22DWnMnEFAFNIObIOntxb5Uh5wrXFrjmFiSsAqEGqkbX1xfuqHClPuLbYNXc9cQUANUoxssJ39X3w4YUqScoRrinF7yEUVwDQgBQj68ipV6osKUe4pti1djlxBQANSi2ySvzKnNQ+wV1cAUALUoqsuxduKOq3CcO1pHQ8KK4AoEUpRdbOVx6u8iR/4Vpi19jFxBUAdCCVyArv+JTwLtbiuTPJvHslrgCgQ6lEVglf/pzKlzuLKwBIQCqRdWrxRJUq+Qk/e+ya2p64AoCEpBBZ9z550/D9C+eqZMlH+JnDzx67pjYnrgAgQSlE1sPPf7vKlnyEnzl2LW1OXAFAwlKIrJ0Z/VZh+Flj19DmxBUAZCCFyHrpjd1VwqQr/Iyxn73NiSsAyEjXkRU+7iDlyBJXAMBEUngnK8XjQseCAMBUUois8BB5Cr9dGH4GD7QDALVIIbLCxyB0+TlZ4c/2UQwAQK1SiKywrS/et/SVNG0Jf1b4M2M/S9sTVwBQoFQiKyw8B9Xk9xeGf3f4M2J/dhcTVwBQsJQiK/ym4fZ99w8Pntw7/ODDC1UaTS78O8K/K/w7U/nS5jBxBQA9kFJkjRaCKHzZ8svH94z1QHx4pyr8d8J/N6WoGk1cAUCPpBhZKxeC6cE9dy7t8f0/Wdrof08xplZOXAFAD+UQWblOXAFAj4ms+ieuAACRVePEFQDwByJr+okrAOAyImvyiSsAYFUia/yJKwBgTSJr/RNXAMC6iay1J64AgLGJrNUnrgCAiYmsyyeuAICpiaw/TlwBALURWeIKAGhAnyNLXAEAjeljZIkrAKBxfYoscQUAtKYPkSWuAIDWlRxZ4goA6EyJkSWuAIDOlRRZ4goASEYJkSWuAIDk5BxZ4goASFaOkSWuAIDk5RRZ4goAyMbGbYP/FQualCauAIDsXIyYv70YWmdXhk0K2zg3+MfqxwQAyMvFmPncxtnBsZWB09WWgm/b4EvVjwcAkKdNjw6uvhhZe2LB0+aq0Ptc9WMBAOQtPOu0adtgU1dHhhfj6r5Njww+Xv04AADlCJHT5gPw4UH2i2H3meqPBwAo16atg+svxs9DsSiqY0tHknODz1d/HABAf4R3tC4G0d+G2Jr2+PDif/+XF8Pt70O8Vf96AIB+W3pOa+vgL5ee1ZodPFbtpctCanZwbNk/v+Pi//uvbFoY/Hn1rwHo2GDw/wGStwqRNh9FsAAAAABJRU5ErkJggg==", + "Category": "azure" +} diff --git a/step-templates/azure-add-service-fabric-cluster-certificate.json.human b/step-templates/azure-add-service-fabric-cluster-certificate.json.human new file mode 100644 index 000000000..8907161ad --- /dev/null +++ b/step-templates/azure-add-service-fabric-cluster-certificate.json.human @@ -0,0 +1,135 @@ +{ + "Id": "cf3fb207-05d7-4818-8c09-d2484eadc96c", + "Name": "Add Service Fabric Cluster Certificate", + "Description": "Add a secondary cluster certificate to a service fabric cluster using an existing azure key vault certificate.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 0, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Azure.AccountId": "#{Azure.AddServiceFabricClusterCertificate.Account}", + "Octopus.Action.Script.ScriptBody": "Write-Output \"Adding Service fabric cluster certificate\" +Write-Output \"Resource group name: \" $OctopusParameters[\"Azure.AddServiceFabricClusterCertificate.ResourceGroupName\"] +Write-Output \"Service fabric cluster name:\" $OctopusParameters[\"Azure.AddServiceFabricClusterCertificate.ClusterName\"] +Write-Output \"Certificate secret identifier:\" $OctopusParameters[\"Azure.AddServiceFabricClusterCertificate.SecretIdentifier\"] + +Add-AzureRmServiceFabricClusterCertificate -ResourceGroupName $OctopusParameters[\"Azure.AddServiceFabricClusterCertificate.ResourceGroupName\"] ` +\t-Name $OctopusParameters[\"Azure.AddServiceFabricClusterCertificate.ClusterName\"] ` + -SecretIdentifier $OctopusParameters[\"Azure.AddServiceFabricClusterCertificate.SecretIdentifier\"]" + }, + "Parameters": [ + { + "Id": "14ec432f-4d26-4341-8c06-5b89cfb20a6c", + "Name": "Azure.AddServiceFabricClusterCertificate.Account", + "Label": "Azure account", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + }, + "Links": {} + }, + { + "Id": "dc0b28c9-40b8-4897-be2a-a48fbbbec685", + "Name": "Azure.AddServiceFabricClusterCertificate.ResourceGroupName", + "Label": "Resource group name", + "HelpText": "The azure resource group name where the service fabric cluster is located", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "7436632f-210f-4cec-8639-c93164a6240f", + "Name": "Azure.AddServiceFabricClusterCertificate.ClusterName", + "Label": "Service fabric cluster name", + "HelpText": "The name of the service fabric cluster", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "7ee8a384-bab3-4714-af79-dcfbd3bd602f", + "Name": "Azure.AddServiceFabricClusterCertificate.SecretIdentifier", + "Label": "Secret identifier", + "HelpText": "The secret identifier of the Azure Key Value certificate. +Example +> https://{key vault name}.vault.azure.net/secrets/{certificate name}/{identifier}", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "4cb76611-ff29-4256-9475-3769fd890e0f", + "Name": "storageAccountName", + "Label": "Azure Storage Account Name", + "HelpText": "Name of the account that the files and folders will be uploaded to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cc0eb63d-4545-4d9d-aca3-7844e1e0a54e", + "Name": "storageAccountKey", + "Label": "Azure Storage Account Key", + "HelpText": "The key that is used to log into the account.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "8245c4b0-014d-467c-a95d-ab6aac230075", + "Name": "containerName", + "Label": "Azure Container Name", + "HelpText": "The name of the container the files and folder will be uploaded to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "45f6df33-d04d-44bd-8a26-1ab45c634afc", + "Name": "localFolder", + "Label": "Name of the Parent Folder", + "HelpText": "Name of the Parent Folder being uploaded", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8c559f0f-2d6e-4202-8614-65cabb29e643", + "Name": "doRecurse", + "Label": "Recursive", + "HelpText": null, + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "8a06615b-69a2-4d32-be29-981b6c5725fc", + "Name": "doForce", + "Label": "Force", + "HelpText": "Override is enabled or not", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "prebenh", + "$Meta": { + "ExportedAt": "2018-05-02T11:51:09.599Z", + "OctopusVersion": "2018.4.7", + "Type": "ActionTemplate" + }, + "Logo" : "iVBORw0KGgoAAAANSUhEUgAAAlgAAAE7CAYAAAAB7v+1AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAACxfSURBVHhe7d1NkBTnnefx5yTtxWL2AnNwLBpiQ2gdXmFmNUYgUFNVXYXksRfLM2NeNixmcCiw6EYyrDzYjCVsRsaiq19ourNET8wQwR5YeQ+EdCHswxL4wurEcNiQZAuqwba6pbWmJfmlJVvw5D5P9oPUFP+mq6srs57M/H4ifkHYYSs7n2z8/Jz11PMoAPBBOLZpSVgrF3RQ2h3WKod0rXRu3oxuOKeD7iP62IN77X837FNL3T8OAAAgf3StsDaslQ7oWvdPdK0ybQpSuOgc+/Mw7Feh7ldnw6o6aNLlLgcAAJA94TPqDh0Ut+ug9HLbClVjhlZEBasxpnCdMX/uCIfUEvfjAAAApJd9U6Vr3WM66J4SS1E7M/ApsWDdiCla03pAndB9apP78QAAANIjrFW+YorVRbEIxZHRDWKpmiumbNXNnzvCZ9Wd7kcGAADwkylWO3VQfl0sQXHGrb9aaEzRmtB96imKFgAA8I4pOYWOFKsbOfoZsUA1m6hoVdUWdzsAAACdEw6vW6ZrxZNi6UkyA0vF4rTQRAvih9Ryd3sAAADJmvk4MIHF6/MlKIplqdXYxfDmz/3uNgEAAJKhg+KwWHY6kZEHxKK02JiidYq1WQAAIHZ2x3W7OahYdDqV4fvEgtSOmJJ1gY8MAQBAbMLRrns7upB9rgx+WixH7Uq0AH5ArXHDAAAA0B6myBS8WG8lRShF7Y5dl6WrapcbDgAAgMWZOYBZKDY+ZGSdWIjiiilag25YAAAAWmOPuRGLjS9pcYPRxcSUrDMsfgcAAC3RQXmfWGp8ytA9YgmKO6ZknXLDBAAA0Bw9WnxELDS+ZZ4DnmMOe2UBAIDmzHxb0NMF7bMTdEmlJ9HoPrXJDRsAAIDM7XM1LhYa33LsfrH0JBldVVPmz5Vu+AAAAG7l3Sait8siD3huV3S/qodDaokbQgAAgE94dfxNM4l5g9GFxJSss24YAQAAZni915WUNh/w3I6wRxYAAPhYOFa5W9cq02KR8TUJbzDadKqqyw0rAADIs1Stu7qRDmww2kx0Vb3KJqQAAOScDorbxQLje4ZWiAXHi1TVQTe8AAAgb2a2ZChPigXG93R2g9HbRverafMnWzcAAJBHqTgKR8roBrHY+BRdVafdMAMAgLwIn1F3pPbtlafrr25Jn1rlhhsAAORBat9e2Xiyweh84UBoAAByJNVvr2wGloqFxrdEa7H61DI37AAAIMvCWmWnWFzSEA83GL1d2HwUAICc0LXSObG8pCEjD4hFxteYgjXhhh0AAGRVOLxumVhc0pLh+8Qi43N0n9rkhh8AAGRRWCsdEItLWuLRAc/NhsXuAABknA7Kr4vFJS0RCozviRa7D6kl7hEAAIAssYc6i6UlLfH1gOfmstk9BgAAkCWp/vagTVo2GBXCtwkBAMgoXSueFItLWjJ0j1he0hBTsC64xwAAALJE17rHxeKSlnh8wHNTYR0WAADZkvr1V0GXXFrSFdZhAQCQJXq0+IhYXNKSY/dLhSVt2e8eBwAAyIJUH+5sk5IDnm8XPaBOuMcBAACyQNe6x8TikpakcIPRxuiqOu8eBwAAyIJUnz+YsgOe54opWFPucQAAgCxI9TcIU3bA823Tp5a5RwIAANJOB91TYnlJQ1K8wegtGVLL3SMBAABpJxaXtGRohVxW0pmV7pEAAIC0E4tLWiIXlXSmqrrcIwEAAGmW6k1GRzfIRSWtoWABAJANqS5YWVp/ZUPBAgAgG8I+tVQsL2lIig94FkPBAgAgO8TykoYMLJWLSlpDwQIAIDuiw5KlAuNzsnHA881hmwYAALJDBxsnxRLjc7K0weiNULAAAMgOPdr1mlhifM7wfXJJSXOG1BL3SAAAQNrp0Yd+LJYYn5OBA55nR/erCfc4AABAFuhaZVgsMT5HKClpjilYZ9zjAAAAWaCD0m6xxPiakXViSUlzTMGquccBAACyIBxZXxCLjK/J2gajJrpPPeUeBwAAyAK7uDoMinKZ8TFZ22DUhj2wAADIHh0ULoplxscMfEouKSmN7lfT4bPqTvcoAABAVuigmI6F7hncYNQUrLPuMQAAgCwJRx96VCw0vuXY/WJJSXWq6qB7DAAAIEumqw+m49Dno5+RS0qK89Fz6i/dYwAAAFlx5fF1y8Z7uy9+MCAUGt+SsQ1GP3pehZf/mxof36ZWuscBAADS7tIThbX1PeXJ8T3l8J2DQqHxKfabjkJJSXPe+44pWNtUeGmrmnpjm9rsHgsAAEirem9xe31PZdqWK5srT5bD64FQbHxJBg94/tXjMwXrRkzRYj8sAADSqt7bfeRGsZqd3x4Rio0vydgGox8+d3O5mpUT44+xbQMAAKlhJu47xntKL0vlyuatbwvFxpcMrRCLSlrzzj6xXN3IefOslrjHBgAAfHW1t3K3XcwuFavZ+XBIKDc+RCgpaY1d3F7fLharj3Npm6r/bLta5R4fAADwTbSYvbd7SipUjXn7u0K56XRGN4hFJa2Z5+3Vx7m0VU2z+B0AAA/Vn9i4c/Zi9mbi3VusDK2/ul6d/+2VkP3ucQIAgE4b7y0OSwVqvnj3FitDBzzf2JphwdmqTrH4HQCADhr/mrrrdovZm8kHg0LR6VQGloplJW1pZu3VPDk//jdqmXvMAAAgKc0uZp8vE3/vyb5YGTrg+e09YmlaUFj8DgBAwi7vWl9odjF7M3nvOaHwJJ2MbDD6++/JhamV2MXvpmhtcY8dAADEpd5T2r3Qxezzxe7u/tGIUHqSzPB9YmFJU+zC9l/slMvSInPQPX4AANBurS5mbyaT3+7wR4UZOOC5HR8NzhkWvwMA0F5X925aMt5T/LFUjNqZ/9fJbxUKhSVNaflbgwvJVnWBxe8AALSBXcxe7ym9JhWiONKR9Vgj68TSkpa0c93VfLm0TU2w+B0AgEVo92L2ZjPdL5SgOJPiDUb/8AMVjj8ml6G4wuJ3AABaZBezS+UniVzdWw7/cFQoQnElpRuMXns+tkXtTcUUrcPu1wUAAMxnvKcwJhWfJPOrp8vhtaS+WTjwKbHA+J7J3XLxSTRb1WkWvwMAvBCOVe42E3shPLp6Y/isP5NTtJi9t3ROKjydyFvfFspQu5PSDUabPcg5kdjF71vUcvdr5AU9oNaEVdVlxmql+7cAAGkWDq9bZibuQlgrHdBBcVjXSud0rTx5y8QuZWRdqIPCRV3r/klYqxwKg407bQlz/+hYXd3ddW+9t/y6VHQ6GfvNwli3bzh2v1hgfM7U00LJ6XDs4veff1Wtcb9OsQr71CpdVVtMgTpo/jxtcl4aJym6X02bnDUF7ET03+9Tm8Ihv8ohAMCwhcoUqe2mFI3poPy6OIkvNkHRFK+N58LRwiHzrwvu0m1zuaf4SCcWszcbe5xObBuRHv2MOBH7GLuR6Fu9csHxIdHi9y1qh/u1apuoUPWpp1yZmpLGZrExpasela5+tYPCBQAdEj6j7ghrlZ327ZQ4acccHXRPRYVu5C8ecD9Sy+q7C/ukUuNbfvHN4s+vB5vaX2BTcsCzKRZX39ylLkrFxsMMul+vlpl7XmlKz6DJRONYJBEz3q+a7DLljn2/ACBuulZYq2vFk7pWmRYn6w5EB6XXwuChA2YiWOp+zKbYhck+LGZvKj0bz9n1YeH/PPTv9PG//F/SOLSUoChOrr7FfqQVDqkl9pnZHdWFQuNfZha/L3G/bk2x92hLjbnfC9I4dCrm5zll/tzsfkwAQLuYybjQqbdVzcb8fNOm+A3bjyzdjz0n3xaz3zamBLof+2Ph8S/+gzQGC04KDng2k3ut8YsQl7aqp8RS41uaXPxui5VbTxXLx3/tiit+bf8IFAByJ6xVvqJr3RfFydnj6KDrZHhstTixRYvZ93SPi2XGs9S/seEJ92PfwpSsv1r0m0TPD3i2b3Pc7d7ijW1qsylaU2Kx8Sj2Z5xr8bv9+M2UFvsx4LR0/77G/Lx18ydFCwAWym6jMPNNPmFSTknsG63oo8NZbz8u737oUZ8Xs9+I/RntLvLux55TONp176LeLA6tECfQTse+KbHbDLjbnJM9subSNlWXio1PiRa/b725LJr73OH7G6v50uxzAoDcm1m8Xjrg0xqrxcZ9s7GQlsXs9txD+5bNPZKmzHyLs8mtMGZHmDQ7GVc49rvbaopd52RKzPnGUuNpBs39rTT32fS2CmmIKVo1+zGneyQAgNmityFxbbPgQX7zw3J45Um51HiTnuKPx7+m7nKPZEHMBHeXDrqPNF2ORzeIk2WnEm0R0OI31qLF79vUiYYy413s5qjXq+qadP9pjylZE3ZjU/dIAACWmXALduuDWybhjOWDgZlz/sRy0+n0Fofd41iUmY1eK4fmfZ6eHPAcFas27bvk6+L3+nYV/uYZ+f6zltutmwOAXNFBeZ84AWc0fzw2s3GnWHI6kPqeyvTtFrO3KhzbtMRuzDrnR4cdPODZfhSo+9VgHHssvbFVbfJp8fuVHSr84JA8DllN9JGhR0dgAUCi7Horu6eVOPlmPPbombe/KxeeJNPsYvbF0sEjXzBF60c3fXzYgQ1GTbGyO5FviXvyHd+mVvqw+P1Xj6vwjz+UxyLrMSUr2rvMPRIAyIeZ423Kr8wuHXnMe/8oF58kYhezX9kpbyURl5l1WqXdOii8GK2ZESbGdiZ6UzVTqhLfEdwtfj/bWHqSytt7Zo72kcYlLzG/Y3Xz3Fe5RwIA2RYGldW61j0uFY485ndHOrD4fRGL2dvJTIL2G2125/CafeNgC1HjJLmQRP+MfnXKnpvnw8TqFr/XGstP3Hl3vzw+eYz5fbB7fLELPIBsmznqJjtbMLQrHwwmWLLatJg9LvbjO/ttMJuoKFXVQTH9ar/5M/rPJf12aqGSXPyel8XsLYSNSQFk08zmoS3slZST2DdZYiFqU6LF7E9s3OkeBxKWxOL3qafFYkFMojdZbOMAIGtmFrSn78ibpPPuIbkcLTZ2MfulJwpr3eNAh0SL37eqV6VytNhM7paLBfkkpmRNtGtLDgDwgg5KL0uFgtyatn+7sLf7YtKL2TG3OBa///LrKrz2vFwqyM0xJesCWzgAyAR79I1UJIicayPl8M2nG0pSq+kpvezDYnbcyhSjtix+N4Utt1sxtBr7zVL3GAAgncJa5StSiSC3j92MdNE7vu9+yOvF7IgWv++SStNCMv19uUSQebOgMycBwBvR2YJ8Y7Dl2GN1xOI0T6LF7L3F7e4xwHNvbFFdrS5+f+87YnEgzYftGwCkiz0mhb2uFp/3D8slaq7Ue0uTLGZPn1YWv//6m2JhIAuI2yNrpXsMAOA/HRSHpcJAFp6mzy1kMXuqucXvZxqLlBR7vuA1FrW3JaZknXWPAAD8Zndql4oCaS0fDjUUKSm7u14yE/Qd7hEgxUyBGmwsVI35LZuJtjtsQgrAf5wx2P688z2hVLnUn+g64oYeGWEXv5tMS+WK/a7aH7c/FgdDA/CXPchXKghkcbFbN/yi4VuF9Z4Si9kzTFr8Xt/OlgxxxZSsQTf0AOCXmd3aOQonrsxe8M5i9nwY36KWz178/s4+uRyQxSda8O75mZYAcoo9r+LNRyPuQGi7mP3xdUwEOREtft+qTtuC9eFzcjkg7QlvsQB4ibMG48/7z3X/K4vZ8+nf/rt6WSoFpH3hLRYA7/D2KplEH8E+Q8HKIzP5X5BKAWlvdFUddkMOAJ2na90/kQoBaX90wML2vNEDao1UBkj7Y4rshBt2AOiscHjdMqkIkHhiy6wbeuSEmfRrUhkg8UT3qU1u6AGgc8Ja6YBUBEiMOf7lP3XDj4wLn1V36qqakooAiSem0J5yww8AnaOD8utiCSDx5YWH/8ENPzLOlKstUgkg8SVa7M7GowA6iWNxOhP7jU33CJBxpmCdlkoAiT0cnwOgc3RQ3icVAJJAxjbx/7BzgI8HOxM9oE64RwAAydNB6WVx8ifx5/gX/8o9BmRU2KdWSZM/iT+6X9XdYwCA5Omge0qc/Ens0bXKsHsMyCjdp56SJn+SUIbUcvcoACA5rL/qbPQLFdZhZRzrrzoe1mEBSB7rrzzIkLrLPQ5kEOuvOhu7/5h7FACQHF3rHhMnfZJYdK2w1j0OZIw9E0+a9ElyMQX3vHscAJAcjsfpfDg2J7s4HqfzsW8Q3eMAgOREBw8Lkz5JLjroPuIeBzLGTO67pEmfJBv3OAAgGXYPJmnCJ8nGbpPhHgkyRverQWnCJ8nGPQ4ASEY4VrlbmvBJstG10jn3SJAxdqNLacInycY9DgBIhl1cLU34JNnooPSaeyTIGN2vzkgTPkk27nEAQDLM5F5onOxJ8tG17nH3SJAxpmCdlSZ8kmzc4wCAZOjR4iPShE+Sja6VJt0jQcbYLQKkCZ8kG/c4ACAZYa2yU5rwSfJxjwQZY8/CkyZ8kmzc4wCAZFCw/Il7JMgYCpYfcY8DAJKhg0e+IE32JNnwEWF28RGhH3GPAwCSYSZ3Frl7EB0UWOSeUSxy9yPucQBAMvTYF9ZJEz5JNnq0i20aMoptGvyIexwAkIzwX/76z6QJnyQbHWxko9GMYqNRP+IeBwAkg6Ny/Ige7XrJPRJkjOaoHC/iHgcAJIfDnjsfDnvOLg579iPucQBAcnRQfkWa9Ely0bWHe9zjQMaEVdUlTfgkuZiSO+UeBwAkR9e6x6RJnyQX+2UD9ziQMWGfWiZN+iS52K0y3OMAgOTooLxPmvRJghlSd7nHgQyyb1CkiZ8kE92vTrlHAQDJCUfWsxdWB6NHH2IPrIxjL6zORvepp9yjAIDkhM+oO3StMi1N/iT+6KDrpHsUyKiwqg5KEz9JKH1qlXsUAJAs/ULlp9LkTxJIsHGnewzIKBa6dy4scAfQUWGtckic/En8ObZ6uXsMyKjwWXWn7lfTUgEg8cYUrNPuMQBA8sKjqzeKkz+JNZxBmB+sw+pMTMHa5R4BAHSGrnWPSyWAxJjRwiE3/Mg4M9nvaJz8SbyJ3hr2qWXuEQBAZ/AxYQfCx4O5wceEyYePBwF4IRyr3C2WABJLOOA5fzj4OfFsdkMPAJ1lJ32pDJAYMrL279ywIyf4NmFy0f1qwr41dEMPAJ0VHvv8o2IZIG2NrpUm+R//fDIT/wWpEJD2xozzoBtyAPCDHl3PYvfYUzrghhs5YyZ/FrvHHBa3A/CSHvrP35RLAWlHol3zh9fxP/455Ra7T0jFgLQnvL0C4KVoAgg2TkrlgCw+OigOu6FGTtmz8aRiQBYf3l4B8Jo+9uBeqRyQxUXXSvZ//Je6YUZO8RYrvvD2CoDXogmg1n1RKgmk9dji6oYYOWfKwObGckAWF3vuIG+vAHjPFIJCY0EgrScqrHxzELPofnVGKgqktdiPXt3QAoDf9OiGF6WyQFrIyPqCG1YgEg6p5dGaIaEskIXFjOMFN6wA4D+7XkiPdk2JhYE0HR0UXnRDCtyEBe+Ljy2pekCtcUMKAOkQHl29MdpaQCgOZP7oFyoXw9rX+WgQczIF4ZRUHEhz4aNBAKmlg9JuqTyQ20fXypP2jEc3jIDIfauQHd5biBm3mhtGAEgn/cLD/ySVCCIneuv3wpeKbviA23Lrsdi6YQEx43WWL44ASD37MZc+/sWfSmWC3Bp9/Es9MyMHNMeuIzKlgUXvTcSMU92U0iVu6AAg3cIf7fv3+oUKZxXOE338C8fckAELoqtqi1QoyCeJSmifWuWGDACyITz+xf8U7UguFAsSLWr/aRiGbrSAhTMl67BULMjH2eyGCgCyJTz2+UfDoCgWjDxHjz40Ho5t4mMLLJopWaeFYkH61X43RACQTbZk6aCbPbJc9GjxFc4ZRDvpflUTCkZuw3YMAHIj+riQNVn2GJyx8Bl1hxsWoG10Ve3K+8L36IzBqupyQwIA+RAtfM/xtwv1C//16ZmRAOJhy0VUMoTykfWY+37V/LnSDQUA5Itd1G2/OScVkKxG1zZNhf+8veSGAIiV2ycrV5uRRuvQ2IoBAFS043sevmGog/Lr9uNRd9tAIqId33Oy+N2UyUF32wAASx/9j2v0yPrMLn439/aS+X/Vd7nbBRIXVtVBqZRkIXa9mSmRW9ytAgBms6/19ej641JBSWvsuYJ65MFt7haBjnK7vmfqI0NzP2fsR6HuFgEAc9Ejf/FAtH2BUFjSFB10H2F/K/jIFJP99q1PY1lJU8zPX9d9apO7JQBAs8Lh1X+rR9enbjuH6OPAo5/lG0zwWtinlukBdUIqLz7HFKsJu7cVBzYDwCJFm5OOPvSvUpnxKTroOhkeW81HFUiVqGj1q0Hf32i5N1YUKwBot/DoZ7+sg8KLUrnpVD4aKYfTfYX/TbFC2v3bN9WD735LhX/8oVxwOhVTrM6aP3e4HxMAEJfw+Jf/VNce7tEjG16TSk/cuR6Uw98dKYdvHyiH43tMejaecz8akFqXt6n9JqHNm7tU+P4BFV57Xi49cce+rYq+9cjidQDoDPM/xiv10VXf0KMbXtTBxkmpELUjHw6Vw/cPm1L13XJ4da8rVi71ntL0+GMceYN0M8Xq7I2CNTsT31ChfbM1/X25DLUjdsd5u1dX9BFgn1rlfiQAgC/s4nIdFLf/5gelyd/3lcMPBuXCNFfsR34fDMyUqXcPlcO3vn1roZJyuaf4iPsRgNQx/wdhyaWtaloqWI2xheudfaZ07Z8pXQv9SPHD51T4+++p8KPDasT86x0UKgBIkfHe4rBUhCb+fqY0zc4vmihQ88Zcz10aSJ1L29QWqUwtNPXtMwVsduzHjY3/OXO9urs0ACBNLveWC2IRiin1ntJr7tJA6pjSc6KxBMUcjrQBgDSya6LqeyrTUhmKK1d28k1CpNOlrWpKKEGx5Y2tbBAKAKk13lP8sVSE4kq9t7LTXRpIjTe2qC6pBMUVu9bL/B8g9rICgLSq7ynvk4pQbNnd9ZK7NJAapvQcbCxBsWarOu0uDQBIo6u7u+4Vi1BMqfd2T7lLA6lhCs8FsQjFlEtb2DQUAFKvvqd7XCpDceXyrvUFd2nAe+Nb1HKpBMUZe013eQBAWo3v7jopFaHY8mTlkLs04L1LW9UuqQTFlq3qgrs0ACDNLu+pfEUsQnGlp/iKuzTgPVN6ztxSguLNQXdpAECajX9N3ZX0dg1X925a4i4PeMt+k6/Z3dvblZ9/Va1xlwcApN14b+mcVITiSv2JjWzXAO/ZvaikEhRXLm1TE+7SAIAsGO8pHJKKUGzpLZ50lwa8ZUpPrbEExZwT7tIAgCy49GRltViEYkq9tzTpLg14y54HKJSg2GLPO3SXBgBkhd2jSipDceWNb6z9nLs04J2fbVerpBIUV9zu7axNBICssR/bSUUotvSWDrhLA94xpSfZ3du3qbPu0gCALLHnBIpFKK70ls65SwPesYWnoQDFmktb1VPu0gCALLny+LplYhGKKdHWEI+pO9zlAW/Yj+qkEhRn2L0dADJsvLf7olSG4srl3Q896i4NeMOeBSiVoLhiF9O7SwMAsqje231EKkKxpbc47C4NeOPyVnVKKkIxZtBdGgCQRZd7ywWxCMUUe9C0uzTgBbd7+5RQgmLLG1tUl7s8ACCL7JqopLdruNpbudtdHug4W3akEhRXbJmzpc5dHgCQVeM9pZelIhRX6rsL+9ylgY4zheewVIRiy1Z1yl0aAJBl9Z7SbqkIxRZT6NylgY4zBetVsQjFFLug3l0aAJBl9iM7sQjFFFPo2K4BXrBbJUglKM6M/41a5i4PAMg6u/hcKkNxxS6ud5cGOsZu9imVoBhz3l0aAJAHdvsEqQjFFbs9hLs00DGm8CS6e7vJQXdpAEAe2A1ApSIUW3q7L7pLAx3htmeYFkpQbLEHSrvLAwDyINquwR5lI5WhmGKP6nGXBxL3xja1WSpBceXSNjXhLg0AyBN7GLNUhOKKPWzaXRpInCk9JxpLUMw54S4NAMgTU7AOSEUotvQWT7pLA4mzb5SEEhRb7Bszd2kAQJ5cerKyWixCMcXuIO8uDSTq519Va6QSFFfsWi92bweAHKv3lialMhRXbKlzlwYSY0rPwcYSFHPOuEsDAPLIfmwnFaHY8mTlkLs0kBhTeM43FKBYY/fbcpcGAORRvbe4XSxCcaW3dM5dGkiE3UldKkFxxu4Y7y4PAMijq3s3LRGLUIyx13SXV+Gz6s6wqrpMHgpr5cLHGVhh/72ucIiJCrKwTy1zvztyhlT0e2bPApRKUFyxZx1GPyAAIN9M6XmlsQS1O299uxy+e6gcvn+4HH50rPR/9eiGSVOkwqYSdIXhyLpQ10rndFB4MRxZ//3w6Ge/bCdYdwvIKFuSdJ/aZArTQT2gTuh+ddZkOuxXYbO5fkT9Yfr7Knz/gArf/ZYKJ76hwvp2uRy1I6ZgHXY/PgAgz+y6KKkULSa2UL33j+XwgwGhMLUrQTHUIxte06Prj+vh/7L1xhsLpJd9hqYUbTYlatDkQmNZamc+OGQK134VTu6Wi1KreWOL6nK3AwDIs8u71hekkrTQvPm0e0M1IpShJGILl33Ddezzj7pbQ0rYt1TuDdWC3k61K9een3nD9eYuuTQ1m0tbFVuRAAA+YfeokkrTfLnyZDl853vl8A9HhcLTwehg46QeWXs0HFL/wd0iPGM/4tVVddiUqrpUejqVP/5QhVNPq3D8MblE3TZb1Sl3ewAAKDXeU3pZKlBz5eremY8AO/a2qtlEb7W6ToZjlbvdraLDomI18xFgR95WNZtrz6vwve+o8MoOoUjNEbug3t0mAAD2DVZlp1SkGhMVq+fK4TXfi5UQPdr1Unj0syvdLSNhUbEaUCekMuNzrlebL1rjj7EOEAAwy9Xeyt1SoZqdt7+bgjdW80TXStN65MHn7fYQ7taRAFNU9vv+xmq+XHtehb/+plysXM662wUA4BP1ntJrUrH6xd5yON0vF5a0RgeFcT1038Pu1hETPaDW6Kp6VSosaY399uEvvy4WrP3utgEA+MR4b3G4sVzZt1bXA7mkZCGmaI2Fta/zNisGdgG7VFCyEPux4a/33lywfrZdrXK3DgDAJy73lm/arsEuYpdKSdaij37u/7BpaftEm4NW1WmpmGQtdmsHu2nppW2q7m4fAICbjT+m7qjvqUzbrRd+d0QuI5nM4KfDaKuAPt5ALJYpV8vNWMa6QahvsbvEX/1b9c9uCAAAuNWb3yr99MMhoYRkNUHx44nSLsLWVbXFDQUWyJ7/Z8Zvanb5yEuu96lJCjoAQKT/ZduD14PK78UiktWMrLt1wqyqg25I0CRTrHbdMo45iyuXm92QAABgJsigtFvXKtNiCclyjv35XJPlac42nJ/d7kL3q5o0hrkNBR0AYOmRwj6xfOQhQyvkSdLElKzz7Jd1e1ERFcYu7zGlc9ANEQAgj8J/+uuSWDzykoFPiRPkjdidx91QoYF9UyONGfk4HJsDAHkUntyxQtc2TYnFIw8Z3SBNirdE96mn3JDBMeOyuXGcyM2JvjQxoNa4IQMA5EH4P/7uT0y5el0sHnnJHOuvpJiStckNXe7Zb8vZ8iCNE7k5Zpwm7NYVbugAAFkWhqHSx7/wE7F05ClHPyNOilLcN8Ryf1B0dGCz3TNMGCMix4zXBdbyAUAO6Be+2CcWjrxlYKk4Ic4VU7JezfM3C903Bs9KY0NuHzNup9wwAgCySNce3i6Wjbxl1gajC4mZKM+4ocwdc+9sx7C4cBg0AGRROLz2c7nc60rKyAPSBNhUdFUddkOaG+aec7+RaDvCWj4AyCAdlF8Ry0YeM3yfOAE2E20XeOfoWJRo3VVOj8Bpd8zvTp31WACQIfr4l3rEopHXDH5anACbjZkoz7qhzTy7F5g0BqS15PENKABkUji8blmu97uSIkx8LSTzG0naA5yF+yaLSN7egAJAZunRDS+KJSOvkQ54biFmopzI+sc95h4vSPdOFhddVefdEAMA0igcvHeVWDLynAVsMDpfsrzLu7k/dmuPMSx4B4AU00FxWCwZec7QPeKE10qy/BaLt1fxRlfVaTfUAIA0mVl7xbYMt2SeA54Xmiy+xTL3xdurJMJaLABIH95eCQm65IluEbFvsdyQZ4ZdIyTdK2lveIsFACkTHWsSdPPNwcYcu1+c6BabLK2nMfezsvH+SIzpU8vc0AMAfKeDIkfiSFnAAc8Lic7QWXN2nybpHkls4QgdAEgLU7B+LBaMvGeRG4zOFVOwprNyEHS0cF+4RxJPTKF91Q09AMBnLG6fIy0e8LyApH7jUTYW7VBY7A4A/tMjG54QC0be06YNRueKzsDxORyL05nYj2XdIwAA+EqPdr0kFoy8p40bjEqJPiZM+Z5Y5h7q0r2ReGPG/YJ7BAAAX+nRIt8elDK0Qpzc2pqq6nKPIXXCIbVcvCeSTDKyhg8AMikMKqvFckHavsGomKo66B5F6piff8ct90OSzGb3KAAAvtFBeZ9YLvKe0Q3ShNb2pHkdFuuvOhvzuzPoHgUAwDc6KLwoFoy8J+b1Vzdy7XkVXt6Wznz4nHxPJJnY3fPdX2MAgG90rfuiWDDynpg2GJVyZYdcYHzP9ap8PySZ6AweuQQAmcHxOHNkYKk4qcWRyd1ygfE5v/y6fC8k4bDQHQD8YzcYFctF3hP/BqM35dd75RLjc2wplO6FJBs9oNa4v84AAF/o0eIjYsHIe0YeECezuPLed+QS43Pe2SffC0k8qT8NAAAyJww27hQLRt4zfJ80kcWW3zwjlxif8+635HshCSfF23wAQGaFtQoFS0pMBzzPld9/Ty4xPuf9A/K9kIRDwQIA/+iRAntgSZEmshgz/X25xPgc+9ZNuheSbNgLCwA8FNYqh8SCkefEfMCzlA8OySXG5/yWguVF7Gav7q8zAMAXFCwhCW0wOjt//KFcYnyOfesm3QtJNhQsAPAQBUvI0D3iRBZnKFik1VCwAMBDFCwhSRzw3BA+IiSthoIFAB7ioOeGBF3iJBZ3WOROWg2L3AHAQ2zT0JBj94uTWNxhmwbSctimAQD8Q8FqSIIHPM8OG42SlkPBAgD/6FphrVg08pqENxi9EY7KIa1GV9UW99cZAOALDnuelYQPeJ4dDnsmLadPrXJ/nQEAPtFB95RYOPKWDmwweiNv7pJLjM+5skO+F5JwhtQS91cZAOATHZRfEQtH3tKBDUZvxJYVqcT4nutV+X5IMtH9asL9NQYA+EbXiifFwpG3DK0QJ7G4c+15ubykIR8+J98TSSamYJ1xf40BAL7hm4QuHdhg1OZ3z8rlJQ2xi/OleyIJhW8QAoC/wrHK3WLhyFNGN8gTWAKx38aTyksa8lavfE8koVRVl/trDADwka51j4vFIy/p4PqrXz0ul5c0ZPwx+Z5I/NH9ajp8Vt3p/goDAHyU+3VYHdpgNM3rr26EdVidiSlYZ91fXwCAr3K/DmtgqTiJxZ00r7+6EdZhdSisvwIA/4Vjm5boWmVaLB9ZTwc3GH17j1xa0hS7h5d0byT2rHR/fQEAPtO18o/EApL1jDwgTV6xx348WN8ul5a05Q8/kO+RxBNdVefdX1sAgO/0aPERsYBkPcP3iZNY3NH9quaGPvXM/exvvD8SX0zB2uWGHgCQBrpWnhRLSJbToQOe9YBa44Y99cI+tUy6R9L+RN8e5HgcAEgXHZT3iSUkyxEmsbhjJskLbsgzQ1fVaeleSXtjfncG3ZADANIifEbdkavF7p074HmzG/LMCPvUKuE+SbszpJa7IQcApIkOisNiGcliOrDBaBbfXt3AW6x4Y8fXDTUAIG3C4XXLcvMWa+gecSKLOZl7e3UDb7FijhlfN9QAgDTKzVushA94zvLbqxvMPZ6R7p0sLry9AoAMmFmLlfFvFAZd4kQWa3JwOK99y2JK1rR4/6SlROPZp5a5IQYApFlYq3xFLCZZybH7xcksrphJ8pQb2swz9zoojQFpLbpPPeWGFgCQBToovSyWkywkwQOe8/YGInxW3WnueUIaC7KwmHHM/MfKAJA74Vjlbh10T4kFJe1JcIPRPO68be57c+M4kIXFFvMsbUgLAJjFlJHCLeUk7UnwgGczQZ5wQ5k7YVUdlMaENJ0dbigBAFmUuR3eE9pgVFfVeftxmRvGXGJvrNZi17G5IQQAZJmudY+JZSWNSWCDUTNB1vnm18frsS5IY0TkmPE644YPAJB10dYNQfkVsbCkLUMrxImtXWHtzM3s8S66qqaksSI3JyrmHOYMAPkys8t797hYWtKUmDcYNWViixsyOHYPMFs8pfEiM3EldKUbMgBAnoRBZXWqj9IZ3SBObu2KmSQPu6FCAzM2u6QxIzPRfWqTGyoAQB6lehPSGNdfmQLBcSbz0GxCKobNRAEAEVNWCqncIyumDUZtcXBDg3nYMiGNYR4TrdfjI2UAwGzhaNe9Oii/LhYZXzOwVJzoWo2bIHO3kehimbHbbMYt1wvfze/OBF+GAACIwrFNS3St+ydimfEtbd5g1BYEJsjWmTFcaUpGvXFc8xBz3xfYxgMAMC8dFIfFUuNTRh4QJ7tWEk2QQ2q5u320yG5JYMbyrDTGWY2531N534AWALAApmRt9/obhsP3iRPeQsME2X5mTHOx+J3F7ACAluhaYa2ulSfFgtPptOOA56o66G4VbWbXspmilcm9stx6s83uVgEAWLhoXVbQfUQsOZ2MMPE1G/cxFptAxiza9b1fnWkc/zTHHvjNeisAQNu4bxn6cbxOiwc8m8l+QvM1+sTZTTfN2Kd6Abz5vXmVL0EAAGJjNybVte6LYvFJKgvcYNQWK/txIOfCdU50UHSfeip6FsIz8jWuGO5wtwEAQLxmilbpnFiA4s7QPeJk2JjojZXdCJNF7F4xz2aH72+0zM93wfxJsQIAdMbMQvjiyUR3gp/ngGczOdp1PzsoVn6zH9eanG58fp2K+b2ZNjllP9J0PyIAAJ0VPqPuCGuVnbFvVBp0zTU52jci+9nPKn3sovHo48OqerXxuSYRc93zJrv4CBkA4DX7zcPoI8SgONz29VrH7p+ZFO3Hf/Ztg50Y+UZgZtiCbJ7nDvttPVeabylEi40tcuafXTN/buEbgQCA1JopXPZA6dJuU7wO2bVb8557aI/CGVkX6pH1U3p0g/nPF8bsfzccWV8wkySFKieiwlVVXeaZ7zelaNDkrElTC+VtQYv+81V1OFqLZ/85FCogAUr9f4vlBHw2OuW1AAAAAElFTkSuQmCC", + "Category": "azure" +} diff --git a/step-templates/azure-administer-webjob.json.human b/step-templates/azure-administer-webjob.json.human new file mode 100644 index 000000000..017d68d6f --- /dev/null +++ b/step-templates/azure-administer-webjob.json.human @@ -0,0 +1,92 @@ +{ + "Id": "ac7868f9-dc5c-42b8-ab8c-0c1e801f6957", + "Name": "Azure Administer WebJob", + "Description": "Start, Stop, or Delete a WebJob from the Azure Web App", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$httpAction = 'POST'\r +\r +if ($WebJobAction -eq 'delete') {\r + $httpAction = 'DELETE'\r +}\r +\r +$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $WebJobUserName,$WebJobPassword)))\r +$apiUrl = \"https://$WebJobWebApp.scm.azurewebsites.net/api/$WebJobType/$WebJobName/$WebJobAction\"\r +Invoke-RestMethod -Uri $apiUrl -Headers @{Authorization=(\"Basic {0}\" -f $base64AuthInfo)} -Method $httpAction -ContentType \"Application/Json\"" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "WebJobWebApp", + "Label": "Web App", + "HelpText": "The Web App the Azure WebJob is hosted under.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "WebJobUserName", + "Label": "User Name", + "HelpText": "The Username of the authentication to the Kudu Api. + +See https://github.com/projectkudu/kudu/wiki/Deployment-credentials", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "WebJobPassword", + "Label": "Password", + "HelpText": "The Password of the authentication to the Kudu Api. + +See https://github.com/projectkudu/kudu/wiki/Deployment-credentials", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "WebJobAction", + "Label": "Action", + "HelpText": "The action to perform. Start, Stop, or Delete.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "start|Start +stop|Stop +delete|Delete" + } + }, + { + "Name": "WebJobType", + "Label": "Job Type", + "HelpText": "The type of job, Continuous or Triggered", + "DefaultValue": "continuouswebjobs", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "continuouswebjobs|Continuous +triggeredwebjobs|Triggered" + } + }, + { + "Name": "WebJobName", + "Label": "Job Name", + "HelpText": "The name of the Job to act upon.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "dustinchilson", + "$Meta": { + "ExportedAt": "2015-11-23T20:15:06.113+00:00", + "OctopusVersion": "3.2.4", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-app-config-kv-retrieve-values.json.human b/step-templates/azure-app-config-kv-retrieve-values.json.human new file mode 100644 index 000000000..947a5f6ce --- /dev/null +++ b/step-templates/azure-app-config-kv-retrieve-values.json.human @@ -0,0 +1,522 @@ +{ + "Id": "5c4fbed9-dbba-4139-8440-d8e27318772e", + "Name": "Azure AppConfig KV - Retrieve Values", + "Description": "This step retrieves one or more key/values from an Azure App Configuration store and creates [output variables](https://octopus.com/docs/projects/variables/output-variables) for each value retrieved. These values can be used in other deployment or runbook process steps. + +You can retrieve individual keys that match a specific name and you can choose a custom output variable name for each key. + +Wildcard search is also supported using the `*` notation in the **Key Names** parameter. Note: Combining a wildcard search with custom output variable names is not supported. + +Authentication is performed using an Azure Service Principal. + +--- + +**Required:** +- An Azure account with permissions to retrieve key/values from the Azure App Config store. +- The `az` CLI on the target or worker. If the CLI can't be found, the step will fail. + +Notes: + +- Tested on Octopus `2024.1` using az version `2.38.0` +- Tested with both Windows PowerShell and PowerShell Core (on Linux). + +", + "ActionType": "Octopus.AzurePowerShell", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Azure.AccountId": "#{Azure.AppConfig.KV.RetrieveValues.AzureAccount}", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +# Variables +$global:AzureAppConfigStoreName = $OctopusParameters[\"Azure.AppConfig.KV.RetrieveValues.ConfigStoreName\"] +$global:AzureAppConfigStoreEndpoint = $OctopusParameters[\"Azure.AppConfig.KV.RetrieveValues.ConfigStoreEndpoint\"] +$global:AzureAppConfigRetrievalMethod = $OctopusParameters[\"Azure.AppConfig.KV.RetrieveValues.RetrievalMethod\"] +$ConfigStoreKeyNames = $OctopusParameters[\"Azure.AppConfig.KV.RetrieveValues.KeyNames\"] +$global:ConfigStoreLabels = $OctopusParameters[\"Azure.AppConfig.KV.RetrieveValues.Labels\"] +$PrintVariableNames = $OctopusParameters[\"Azure.AppConfig.KV.RetrieveValues.PrintVariableNames\"] +$SaveValuesAsSensitiveVariables = $OctopusParameters[\"Azure.AppConfig.KV.RetrieveValues.SaveAsSensitiveVariables\"] -ieq \"True\" +$global:SuppressWarnings = $OctopusParameters[\"Azure.AppConfig.KV.RetrieveValues.SuppressWarnings\"] -ieq \"True\" +$global:TreatWarningsAsErrors = $OctopusParameters[\"Azure.AppConfig.KV.RetrieveValues.TreatWarningsAsErrors\"] -ieq \"True\" +$global:CreateAppSettingsJson = $OctopusParameters[\"Azure.AppConfig.KV.RetrieveValues.CreateAppSettingsJson\"] -ieq \"True\" + +# Validation +if ([string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreName) -and [string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreEndpoint)) { + throw \"Either parameter ConfigStoreName or ConfigStoreEndpoint not specified\" +} + +if ([string]::IsNullOrWhiteSpace($global:AzureAppConfigRetrievalMethod)) { + throw \"Required parameter Azure.AppConfig.KV.RetrieveValues.RetrievalMethod not specified\" +} + +if ([string]::IsNullOrWhiteSpace($ConfigStoreKeyNames) -and [string]::IsNullOrWhiteSpace($global:ConfigStoreLabels)) { + throw \"Either Azure.AppConfig.KV.RetrieveValues.KeyNames or Azure.AppConfig.KV.RetrieveValues.Labels not specified\" +} + +$RetrieveAllKeys = $global:AzureAppConfigRetrievalMethod -ieq \"all\" +$global:ConfigStoreParameters = \"\" +if (-not [string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreName)) { + $global:ConfigStoreParameters += \" --name \"\"$global:AzureAppConfigStoreName\"\"\" +} +if (-not [string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreEndpoint)) { + $global:ConfigStoreParameters += \" --endpoint \"\"$global:AzureAppConfigStoreEndpoint\"\"\" +} + +### Helper functions +function Test-ForAzCLI() { + $oldPreference = $ErrorActionPreference + $ErrorActionPreference = \"Stop\" + try { + if (Get-Command \"az\") { + return $True + } + } + catch { + return $false + } + finally { + $ErrorActionPreference = $oldPreference + } +} + +function Write-OctopusWarning( + [string] $Message +) { + if ($global:TreatWarningsAsErrors) { + throw \"Error: $($Message)\" + } + else { + if ($global:SuppressWarnings -eq $False) { + Write-Warning -Message $Message + } + else { + Write-Verbose -Message $Message + } + } +} + + +function Save-OctopusVariable( + [string]$variableName, + [string]$variableValue) { + + $VariableParams = @{name = $variableName; Value = $variableValue } + + if ($SaveValuesAsSensitiveVariables) { + $VariableParams.Sensitive = $True + } + + Set-OctopusVariable @VariableParams + + $global:VariablesCreated += 1 + + if ($global:CreateAppSettingsJson) { + $global:AppSettingsVariables += [PsCustomObject]@{name = $variableName; value = $variableValue; slotSetting = $false } + } + + if ($PrintVariableNames) { + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$variableName}\" + } +} + +function Find-AzureAppConfigMatchesFromKey( + [string]$KeyName, + [bool]$IsWildCard, + [string]$VariableName, + [PsCustomObject]$AppConfigValues) { + + if ($IsWildCard -eq $False) { + Write-Verbose \"Finding exact match for: $($keyName)\" + $matchingAppConfigKeys = $appConfigValues | Where-Object { $_.key -ieq $keyName } + if ($null -eq $matchingAppConfigKeys -or $matchingAppConfigKeys.Count -eq 0) { + Write-OctopusWarning \"Unable to find a matching key in Azure App Config for: $($keyName)\" + } + else { + if ($matchingAppConfigKeys.Count -gt 1) { + Write-OctopusWarning \"Found multiple matching keys ($($matchingAppConfigKeys.Count)) in Azure App Config for: $($keyName). This is usually due to multiple values with labels\" + + foreach ($matchingAppConfigKey in $matchingAppConfigKeys) { + Write-Verbose \"Found match for $($keyName) $(if(![string]::IsNullOrWhiteSpace($matchingAppConfigKey.label)) {\"(label: $($matchingAppConfigKey.label))\"})\" + $variableValue = $matchingAppConfigKey.value + + if ([string]::IsNullOrWhiteSpace($variableName)) { + $variableName = $keyName.Trim() + } + + if (![string]::IsNullOrWhiteSpace($matchingAppConfigKey.label)) { + $variableName = \"$($keyName.Trim())-$($matchingAppConfigKey.label)\" + Write-Verbose \"Appending label to variable name to avoid duplicate output name: $variableName\" + } + + Save-OctopusVariable -variableName $variableName -variableValue $variableValue + } + } + else { + $matchingAppConfigKey = $matchingAppConfigKeys | Select-Object -First 1 + + Write-Verbose \"Found match for $($keyName)\" + $variableValue = $matchingAppConfigKey.value + + if ([string]::IsNullOrWhiteSpace($variableName)) { + $variableName = \"$($keyName.Trim())\" + } + + Save-OctopusVariable -variableName $variableName -variableValue $variableValue + } + } + } + else { + Write-Verbose \"Finding wildcard match for: $($keyName)\" + $matchingAppConfigKeys = @($appConfigValues | Where-Object { $_.key -ilike $keyName }) + if ($matchingAppConfigKeys.Count -eq 0) { + Write-OctopusWarning \"Unable to find any matching keys in Azure App Config for wildcard: $($keyName)\" + } + else { + foreach ($match in $matchingAppConfigKeys) { + # Have to explicitly set variable Name here as its a wildcard match + $variableName = $match.key + $variableValue = $match.value + Write-Verbose \"Found wildcard match '$variableName' $(if(![string]::IsNullOrWhiteSpace($matchingAppConfigKey.content_type)) {\"($($matchingAppConfigKey.content_type))\"})\" + Save-OctopusVariable -variableName $variableName -variableValue $variableValue + } + } + } +} + +function Find-AzureAppConfigMatchesFromLabels() { + + Write-Verbose \"Retrieving values matching labels: $($global:ConfigStoreLabels)\" + $command = \"az appconfig kv list $($global:ConfigStoreParameters) --label \"\"$global:ConfigStoreLabels\"\" --auth-mode login\" + + Write-Verbose \"Invoking expression: $command\" + $appConfigResponse = Invoke-Expression -Command $command + $ExitCode = $LastExitCode + Write-Verbose \"az exit code: $ExitCode\" + if ($ExitCode -ne 0) { + throw \"Error retrieving appsettings. ExitCode: $ExitCode\" + } + + if ([string]::IsNullOrWhiteSpace($appConfigResponse)) { + Write-OctopusWarning \"Null or empty response received from Azure App Configuration service\" + } + else { + $appConfigValues = $appConfigResponse | ConvertFrom-Json + if ($appConfigValues.Count -eq 0) { + Write-OctopusWarning \"Unable to find any matching keys in Azure App Config for labels: $($global:ConfigStoreLabels)\" + } + else { + Write-Verbose \"Finding match(es) for labels: $($global:ConfigStoreLabels)\" + foreach ($appConfigValue in $appConfigValues) { + # Have to explicitly set variable Name here as its a match based on label alone + $variableName = $appConfigValue.key + Write-Verbose \"Found label match '$($appConfigValue.key)' $(if(![string]::IsNullOrWhiteSpace($appConfigValue.content_type)) {\"($($appConfigValue.content_type))\"})\" + if (![string]::IsNullOrWhiteSpace($appConfigValue.label)) { + $variableName = \"$($variableName)-$($appConfigValue.label)\" + Write-Verbose \"Appending label to variable name to avoid duplicate output name: $variableName\" + } + $variableValue = $appConfigValue.value + + Save-OctopusVariable -variableName $variableName -variableValue $variableValue + } + } + } +} + +# Check if Az cli is installed. +$azCliAvailable = Test-ForAzCLI +if ($azCliAvailable -eq $False) { + throw \"Cannot find the Azure CLI (az) on the machine. This must be available to continue.\"\t +} + +$Keys = @() +$global:VariablesCreated = 0 +$global:AppSettingsVariables = @() +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +# Extract key names+optional custom variable name +@(($ConfigStoreKeyNames -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + Write-Verbose \"Working on: '$_'\" + $keyDefinition = ($_ -Split \"\\|\") + $keyName = $keyDefinition[0].Trim() + $KeyIsWildcard = $keyName.EndsWith(\"*\") + $variableName = $null + if ($keyDefinition.Count -gt 1) { + if ($KeyIsWildcard) { + throw \"Key definition: '$_' evaluated as a wildcard with a custom variable name. This is not supported.\" + } + $variableName = $keyDefinition[1].Trim() + } + + if ([string]::IsNullOrWhiteSpace($keyName)) { + throw \"Unable to establish key name from: '$($_)'\" + } + + $key = [PsCustomObject]@{ + KeyName = $keyName + KeyIsWildcard = $KeyIsWildcard + VariableName = if (![string]::IsNullOrWhiteSpace($variableName)) { $variableName } else { \"\" } + } + $Keys += $key + } +} + +$LabelsArray = $global:ConfigStoreLabels -Split \",\" | Where-Object { [string]::IsNullOrWhiteSpace($_) -eq $False } + +Write-Verbose \"Azure AppConfig Retrieval Method: $global:AzureAppConfigRetrievalMethod\" +if (![string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreName)) { + Write-Verbose \"Azure AppConfig Store Name: $global:AzureAppConfigStoreName\" +} +if (![string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreEndpoint)) { + Write-Verbose \"Azure AppConfig Store Endpoint: $global:AzureAppConfigStoreEndpoint\" +} +Write-Verbose \"Save sensitive variables: $SaveValuesAsSensitiveVariables\" +Write-Verbose \"Treat warnings as errors: $global:TreatWarningsAsErrors\" +Write-Verbose \"Suppress warnings: $global:SuppressWarnings\" +Write-Verbose \"Print variables: $PrintVariableNames\" +Write-Verbose \"Keys to retrieve: $($Keys.Count)\" +Write-Verbose \"Labels to retrieve: $($LabelsArray.Count)\" + +$appConfigResponse = $null + +# Retrieving all keys should be more performant, but may have a larger payload response. +if ($RetrieveAllKeys) { + + if ($Keys.Count -gt 0) { + Write-Host \"Retrieving ALL config values from store\" + $command = \"az appconfig kv list $($global:ConfigStoreParameters) --all --auth-mode login\" + + if (![string]::IsNullOrWhiteSpace($global:ConfigStoreLabels)) { + $command += \" --label \"\"$($global:ConfigStoreLabels)\"\" \" + } + Write-Verbose \"Invoking expression: $command\" + $appConfigResponse = Invoke-Expression -Command $command + $ExitCode = $LastExitCode + Write-Verbose \"az exit code: $ExitCode\" + if ($ExitCode -ne 0) { + throw \"Error retrieving appsettings. ExitCode: $ExitCode\" + } + + if ([string]::IsNullOrWhiteSpace($appConfigResponse)) { + Write-OctopusWarning \"Null or empty response received from Azure App Configuration service\" + } + else { + $appConfigValues = $appConfigResponse | ConvertFrom-Json + } + + foreach ($key in $Keys) { + $keyName = $key.KeyName + $KeyIsWildcard = $key.KeyIsWildcard + $variableName = $key.VariableName + + Find-AzureAppConfigMatchesFromKey -KeyName $keyName -IsWildcard $KeyIsWildcard -VariableName $variableName -AppConfigValues $appConfigValues + } + } + # Possible that ONLY labels have been provided + elseif ($LabelsArray.Count -gt 0) { + Find-AzureAppConfigMatchesFromLabels + } +} +# Loop through and get keys based on the supplied names +else { + + Write-Host \"Retrieving keys based on supplied names...\" + if ($Keys.Count -gt 0) { + foreach ($key in $Keys) { + $keyName = $key.KeyName + $KeyIsWildcard = $key.KeyIsWildcard + $variableName = $key.VariableName + + if ([string]::IsNullOrWhiteSpace($variableName)) { + $variableName = \"$($keyName.Trim())\" + } + + Write-Verbose \"Retrieving values matching key: $($keyName) from store\" + $command = \"az appconfig kv list $($global:ConfigStoreParameters) --key \"\"$keyName\"\" --auth-mode login\" + + if (![string]::IsNullOrWhiteSpace($global:ConfigStoreLabels)) { + $command += \" --label \"\"$($global:ConfigStoreLabels)\"\" \" + } + Write-Verbose \"Invoking expression: $command\" + + $appConfigResponse = Invoke-Expression -Command $command + $ExitCode = $LastExitCode + Write-Verbose \"az exit code: $ExitCode\" + if ($ExitCode -ne 0) { + throw \"Error retrieving appsettings. ExitCode: $ExitCode\" + } + + if ([string]::IsNullOrWhiteSpace($appConfigResponse)) { + Write-OctopusWarning \"Null or empty response received from Azure App Configuration service\" + } + else { + $appConfigValues = $appConfigResponse | ConvertFrom-Json + if ($appConfigValues.Count -eq 0) { + Write-OctopusWarning \"Unable to find a matching key in Azure App Config for: $($keyName)\" + } + else { + Write-Verbose \"Finding match(es) for: $($keyName)\" + Find-AzureAppConfigMatchesFromKey -KeyName $keyName -IsWildcard $KeyIsWildcard -VariableName $variableName -AppConfigValues $appConfigValues + } + } + } + } +} + +if ($global:AppSettingsVariables.Count -gt 0 -and $global:CreateAppSettingsJson) { + Write-Verbose \"Creating AppSettings JSON output variable\" + $AppSettingsJson = ($global:AppSettingsVariables | Sort-Object -Property * -Unique) | ConvertTo-Json -Compress -Depth 10 + if ($SaveValuesAsSensitiveVariables) { + Set-OctopusVariable -Name \"AppSettingsJson\" -Value $AppSettingsJson -Sensitive + } + else { + Set-OctopusVariable -Name \"AppSettingsJson\" -Value $AppSettingsJson + } + $global:VariablesCreated += 1 + if ($PrintVariableNames) { + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.AppSettingsJson}\" + } +} + +Write-Host \"Created $global:VariablesCreated output variable(s)\"" + }, + "Parameters": [ + { + "Id": "4bc809d3-71d5-4f05-ba0e-994ef6649db0", + "Name": "Azure.AppConfig.KV.RetrieveValues.AzureAccount", + "Label": "Azure Account", + "HelpText": "An Azure account with permissions to retrieve values from the Azure App Config store", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "2b53fffb-cef1-4c3b-a070-aa6448fe84b0", + "Name": "Azure.AppConfig.KV.RetrieveValues.ConfigStoreName", + "Label": "Config Store Name", + "HelpText": "The name of the Azure App Configuration store. Provide this or the **Config store endpoint**.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "41f5fa14-c1ce-4a27-9e63-06419e96f095", + "Name": "Azure.AppConfig.KV.RetrieveValues.ConfigStoreEndpoint", + "Label": "Config Store Endpoint", + "HelpText": "The endpoint for the Azure App Configuration Store. Provide this or the **Config store name**.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "39d76051-4ede-47dd-8139-57f3801d215e", + "Name": "Azure.AppConfig.KV.RetrieveValues.RetrievalMethod", + "Label": "Retrieval Method", + "HelpText": "Choose how the step calls the az cli. Either: +- `All`: Retrieve all configuration values in one call +- `Individually`: Retrieve configuration values in multiple calls; one for each key specified in the **Key Names** parameter. + +Default: `All`", + "DefaultValue": "all", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "all|All +individual|Individually" + } + }, + { + "Id": "02d67ef3-d990-499c-bdbd-226d583ccdc0", + "Name": "Azure.AppConfig.KV.RetrieveValues.KeyNames", + "Label": "Key Names", + "HelpText": "Specify the names of the keys to be returned from Azure App Configuration in the format `KeyName | OutputVariableName` where: + +- `KeyName` is the key to retrieve. Wildcards are supported by adding `*` at the end of the key name. +- `OutputVariableName` is the _optional_ Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) name to store the key's value in. *If this value isn't specified, an output name will be generated dynamically*. + +**Note:** Multiple keys can be retrieved by entering each one on a new line. Note: Combining a wildcard search with custom output variable names is not supported.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "b97ff62d-6b14-4baa-abc2-9065632f5118", + "Name": "Azure.AppConfig.KV.RetrieveValues.Labels", + "Label": "Labels", + "HelpText": "Labels are an attribute on keys. Provide one or more labels in the format `label1,label2` to retrieve only selected keys that are tagged with those labels. + +**Note:** You can include both label values *and* specify key names if you want.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a5dfafc4-743e-4a56-8c89-cd001dc73af3", + "Name": "Azure.AppConfig.KV.RetrieveValues.SaveAsSensitiveVariables", + "Label": "Save sensitive output variables", + "HelpText": "Set the Octopus [output variables](https://octopus.com/docs/projects/variables/output-variables) to Sensitive values. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "c93c377c-e557-4b8a-a918-ee0c47f80c1b", + "Name": "Azure.AppConfig.KV.RetrieveValues.SuppressWarnings", + "Label": "Suppress warnings", + "HelpText": "Suppress warnings from being written to the task log. For example, when a supplied key can't be found in the Azure App Configuration store.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "c51f515f-c749-4c00-90b3-fdc09b2cd426", + "Name": "Azure.AppConfig.KV.RetrieveValues.TreatWarningsAsErrors", + "Label": "Treat Warnings as Errors", + "HelpText": "Treats warnings as errors. If enabled, the **Suppress Warnings** parameter is ignored. ", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "166f88c8-2fb0-4a3c-a0dc-c4087a5ebc76", + "Name": "Azure.AppConfig.KV.RetrieveValues.CreateAppSettingsJson", + "Label": "Create AppSettings JSON", + "HelpText": "Create an Azure App Service AppSettings JSON output variable called `AppSettingsJson`. This can be useful when using the Octopus **Azure App Service** step and you want to dynamically create the JSON app settings.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "159858de-5a33-4111-a059-de261f687b08", + "Name": "Azure.AppConfig.KV.RetrieveValues.PrintVariableNames", + "Label": "Print output variable names", + "HelpText": "Write out the Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) names to the task log. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.AzurePowerShell", + "$Meta": { + "ExportedAt": "2023-11-30T15:10:08.828Z", + "OctopusVersion": "2024.1.2702", + "Type": "ActionTemplate" + }, + "LastModifiedAt": "2023-11-30T15:10:08.828Z", + "LastModifiedBy": "harrisonmeister", + "Category": "azure" +} diff --git a/step-templates/azure-blob-storage-set-cors.json.human b/step-templates/azure-blob-storage-set-cors.json.human new file mode 100644 index 000000000..8a14277ac --- /dev/null +++ b/step-templates/azure-blob-storage-set-cors.json.human @@ -0,0 +1,125 @@ +{ + "Id": "745fa985-7022-4b19-a788-2fd77aa5b365", + "Name": "Azure Blob Storage set CORS Rule", + "Description": "Set a given CORS rule on the specified Azure storage blob container", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "try +{ + Import-Module Azure -ErrorAction Stop +} +catch +{ + throw \"Windows Azure Powershell not found! Please make sure to install them from http://www.windowsazure.com/en-us/downloads/#cmd-line-tools\" +} + +Import-AzurePublishSettingsFile $PublishSettingsFile + +$context = New-AzureStorageContext ` + -StorageAccountName $StorageAccount ` + -StorageAccountKey $StorageAccountKey + +$container = Get-AzureStorageContainer -Context $context | + Where-Object { $_.Name -like $StorageContainer } + +if (-not $container) +{ + throw \"Azure storage container ($StorageAccount) not found\" +} + +$corsRules = (@{ + AllowedHeaders=@($AllowedHeaders); + AllowedOrigins=@($AllowedOrigins); + MaxAgeInSeconds=$MaxAgeInSeconds; + AllowedMethods=@($AllowedMethods)}) + +Set-AzureStorageCORSRule -Context $context -ServiceType Blob -CorsRules $corsRules + +Write-Host \"Added CORS rule to container: $StorageContainer\"" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "PublishSettingsFile", + "Label": "Publish Settings File", + "HelpText": "Absolute path on the tentacle to the Azure publishsettings file to use. Eg. C:\\Azure\\Azure.publishsettings", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "StorageAccount", + "Label": "Storage Account", + "HelpText": "The Azure Storage Account to use.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "StorageAccountKey", + "Label": "Storage Account Key", + "HelpText": "The primary or secondary key for the Azure Storage Account.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "StorageContainer", + "Label": "Storage Container", + "HelpText": "The storage container to use.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AllowedHeaders", + "Label": "Allowed Headers", + "HelpText": "The allowed headers for the CORS rule", + "DefaultValue": "\"x-ms-blob-content-type\",\"x-ms-blob-content-disposition\"", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AllowedOrigins", + "Label": "Allowed Origins", + "HelpText": "The allowed origins for the CORS rule", + "DefaultValue": "*", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MaxAgeInSeconds", + "Label": "Max Age In Seconds", + "HelpText": "The max age in seconds that a pre-flight response can be cached on the client for.", + "DefaultValue": "1800", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AllowedMethods", + "Label": "Allowed Methods", + "HelpText": "The allowed HTTP methods for the CORS rule", + "DefaultValue": "Get", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2016-01-22T04:49:27.549+00:00", + "OctopusVersion": "3.1.3", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-blob-storage-upload.json.human b/step-templates/azure-blob-storage-upload.json.human new file mode 100644 index 000000000..4c5fed9a6 --- /dev/null +++ b/step-templates/azure-blob-storage-upload.json.human @@ -0,0 +1,196 @@ +{ + "Id": "0b167d34-832e-4c96-8a8f-2ea0a6c0fe0c", + "Name": "Azure Blob Storage Upload", + "Description": "Upload files in a directory to a specified Azure Storage blob container.", + "ActionType": "Octopus.Script", + "Version": 14, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function Find-InstallLocations { + $result = @() + $OctopusParameters.Keys | foreach { + if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) { + $result += $OctopusParameters[$_] + } + } + return $result +} + +function Find-InstallLocation($stepName) { + $result = $OctopusParameters.Keys | where { + $_.Equals(\"Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath\", [System.StringComparison]::OrdinalIgnoreCase) + } | select -first 1 + + if ($result) { + return $OctopusParameters[$result] + } + + throw \"No install location found for step: $stepName\" +} + +function Find-SingleInstallLocation { + $all = @(Find-InstallLocations) + if ($all.Length -eq 1) { + return $all[0] + } + if ($all.Length -eq 0) { + throw \"No package steps found\" + } + throw \"Multiple package steps have run; please specify a single step\" +} + +# Check if Windows Azure Powershell is avaiable +try{ + Import-Module Azure -ErrorAction Stop +}catch{ + throw \"Windows Azure Powershell not found! Please make sure to install them from http://www.windowsazure.com/en-us/downloads/#cmd-line-tools\" +} + +Import-AzurePublishSettingsFile $PublishSettingsFile + +# The script has been tested on Powershell 3.0 +Set-StrictMode -Version 3 + +$stepPath = \"\" +if (-not [string]::IsNullOrEmpty($NugetPackageStepName)) { + Write-Host \"Finding path to package step: $NugetPackageStepName\" + $stepPath = Find-InstallLocation $NugetPackageStepName +} else { + $stepPath = Find-SingleInstallLocation +} +Write-Host \"Package was installed to: $stepPath\" + +$fullPath = \"$stepPath\\$CopyDirectory\" +Write-Host \"Copying Files in: $fullPath\" + +# Get a list of files from the project folder +$files = @(ls -Path $fullPath -File -Recurse) + +$fileCount = $files.Count +Write-Host \"Found $fileCount Files: $files\" + +$context = New-AzureStorageContext ` + -StorageAccountName $StorageAccount ` + -StorageAccountKey $StorageAccountKey + +if ($files -ne $null -and $files.Count -gt 0) +{ + # Create the storage container. + $existingContainer = Get-AzureStorageContainer -Context $context | + Where-Object { $_.Name -like $StorageContainer } + + if (-not $existingContainer) + { + $newContainer = New-AzureStorageContainer ` + -Context $context ` + -Name $StorageContainer ` + -Permission Blob + \"Storage container '\" + $newContainer.Name + \"' created.\" + } + + # Upload the files to storage container. + $fileCount = $files.Count + $time = [DateTime]::UtcNow + if ($files.Count -gt 0) + { + foreach ($file in $files) + { + $blobFileName = $file.FullName.Replace($fullPath, '').TrimStart('\\') + $contentType = switch ([System.IO.Path]::GetExtension($file)) +\t { +\t \".css\" {\"text/css\"} +\t \".js\" {\"text/javascript\"} +\t \".json\" {\"application/json\"} +\t \".html\" {\"text/html\"} +\t \".png\" {\"image/png\"} +\t \".svg\" {\"image/svg+xml\"} +\t default {\"application/octet-stream\"} +\t } + + Set-AzureStorageBlobContent ` + -Container $StorageContainer ` + -Context $context ` + -File $file.FullName ` + -Blob $blobFileName ` + -Properties @{ContentType=$contentType} ` + -Force + } + } + + $duration = [DateTime]::UtcNow - $time + + \"Uploaded \" + $files.Count + \" files to blob container '\" + $StorageContainer + \"'.\" + \"Total upload time: \" + $duration.TotalMinutes + \" minutes.\" +} +else +{ + Write-Warning \"No files found.\" +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "CopyDirectory", + "Label": "Copy Directory", + "HelpText": "Replicates files and directory under the Copy Directory. +Eg. `Content/CDN` that is located at the root of the nuget package.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "PublishSettingsFile", + "Label": "Publish Settings File", + "HelpText": "Absolute path on the tentacle to the Azure publishsettings file to use. +Eg. `C:\\Azure\\Azure.publishsettings`", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "StorageAccount", + "Label": "Storage Account", + "HelpText": "The Azure Storage Account to use.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "StorageAccountKey", + "Label": "Storage Account Key", + "HelpText": "The primary or secondary key for the Azure Storage Account.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "StorageContainer", + "Label": "Storage Container", + "HelpText": "The storage container to use.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NugetPackageStepName", + "Label": "Nuget Package Step Name", + "HelpText": "Name of the previously-deployed package step that contains the Copy Directory.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "shawnmclean", + "$Meta": { + "ExportedAt": "2016-01-21T07:21:36.182+00:00", + "OctopusVersion": "3.1.3", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-capture-virtualmachine-image.json.human b/step-templates/azure-capture-virtualmachine-image.json.human new file mode 100644 index 000000000..0d59551c4 --- /dev/null +++ b/step-templates/azure-capture-virtualmachine-image.json.human @@ -0,0 +1,412 @@ +{ + "Id": "dd2b147c-3f20-42e1-a94c-17b157a0f0a4", + "Name": "Azure - Capture AzureRM Virtual Machine Image", + "Description": "Prepares an AzureRM Virtual Machine (Managed Disk or Storage Account based) and captures a [Managed Image](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/capture-image-resource) or [Image VHD](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/capture-image): +1. Runs Sysprep +2. Deallocates & Generalizes VM +3. Creates Managed Image or Image VHD +4. Removes virtual machine resource group", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "Properties": { + "Octopus.Action.Azure.AccountId": "#{StepTemplate_Account}", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "<# + ----- Capture AzureRM Virtual Machine Image ----- + Paul Marston @paulmarsy (paul@marston.me) +Links + https://github.com/OctopusDeploy/Library/commits/master/step-templates/azure-capture-virtualmachine-image.json + +The sequence of steps performed by the step template: + 1) Virtual Machine prep + a) PowerState/running - Custom script extension is used to sysprep & shutdown + b) PowerState/stopped - only when the VM is shutdown by the OS, if Azure stops the VM it is automatically deallocated + c) PowerState/deallocated + d) OSState/generalized + 2) Image capture + - Managed VM & Managed Image - New image with VM as source + - Managed VM & Unmanaged VHD - Access to the underlying blob is granted, and the VHD copied into the specified storage account + - Unmanaged VM & Managed Image - New image with VM as source + - Unmanaged VM & Unmanaged VHD - VM image is saved, a SAS token is generated and it is copied from the VM's storage account into the specified storage account + 3) Virtual machine cleanup. + Once a VM has been marked as 'generalized' Azure will no longer allow it to be started up, making the VM unusable + If the delete option is selected, and the image just created has been moved outside the VM's resource group + +----- Advanced Configuration Settings ----- +Variable names can use either of the following two formats: + Octopus.Action. - will apply to all steps in the deployment, e.g. + Octopus.Action.DebugLogging + Octopus.Action[Step Name]. - will apply to 'step name' alone, e.g. + Octopus.Action[Capture Web VM Image].StorageAccountKey + +Available Settings: + VhdDestContainer - overrides the default container that an unmanaged VHD image is copied to, default is 'images' + StorageAccountKey - allows copying to a storage account in a different subscription by using the providing the key, default is null +#> +#Requires -Modules AzureRM.Resources +#Requires -Modules AzureRM.Compute +#Requires -Modules AzureRM.Storage +#Requires -Modules Azure.Storage + +$ErrorActionPreference = 'Stop' + +<#---------- SysPrep Script - Begin ----------#> +<# + Sysprep marker file: C:\\WindowsAzure\\sysprep + 1) If marker file exists, sysprep has already been run so exit script + 2) Start a new powershell process and exit with code 0, this allows the custom script extension to report back as having run successfully to Azure + a) In the child script wait until the successful exit code has been logged + b) Create the marker file + c) Run sysprep +#> +$SysPrepScript = @' +if (Test-Path \"${env:SystemDrive}\\WindowsAzure\\sysprep\") { return } + +Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NonInteractive','-NoProfile',('-EncodedCommand {0}' -f ([System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes({ + do { + Start-Sleep -Seconds 1 + $status = Get-ChildItem \"${env:SystemDrive}\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\*\\Status\\\" -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content | ConvertFrom-Json + } while ($status[0].status.code -ne 0) + New-Item -ItemType File -Path \"${env:SystemDrive}\\WindowsAzure\\sysprep\" -Force | Out-Null + & (Join-Path -Resolve ([System.Environment]::SystemDirectory) 'sysprep\\sysprep.exe') /oobe /generalize /quiet /shutdown +}.ToString()))))) + +exit 0 +'@ +<#---------- SysPrep Script - End ----------#> + +function Get-OctopusSetting { + param([Parameter(Position = 0, Mandatory)][string]$Name, [Parameter(Position = 1)]$DefaultValue) + $formattedName = 'Octopus.Action.{0}' -f $Name + if ($OctopusParameters.ContainsKey($formattedName)) { + $value = $OctopusParameters[$formattedName] + if ($DefaultValue -is [int]) { return ([int]::Parse($value)) } + if ($DefaultValue -is [bool]) { return ([System.Convert]::ToBoolean($value)) } + if ($DefaultValue -is [array] -or $DefaultValue -is [hashtable] -or $DefaultValue -is [pscustomobject]) { return (ConvertFrom-Json -InputObject $value) } + return $value + } + else { return $DefaultValue } +} +function Test-String { + param([Parameter(Position=0)]$InputObject,[switch]$ForAbsence) + + $hasNoValue = [System.String]::IsNullOrWhiteSpace($InputObject) + if ($ForAbsence) { $hasNoValue } + else { -not $hasNoValue } +} +filter Out-Verbose { + Write-Verbose ($_ | Out-String) +} +function Split-BlobUri { + param($Uri) + $uriRegex = [regex]::Match($Uri, '(?>https:\\/\\/)(?[a-z0-9]{3,24})\\.blob\\.core\\.windows\\.net\\/(?[-a-z0-9]{3,63})\\/(?.+)') + if (!$uriRegex.Success) { + throw \"Unable to parse blob uri: $Uri\" + } + [pscustomobject]@{ + Account = $uriRegex.Groups['Account'].Value + Container = $uriRegex.Groups['Container'].Value + Blob = $uriRegex.Groups['Blob'].Value + } +} +function Get-AzureRmAccessToken { + # https://github.com/paulmarsy/AzureRest/blob/master/Internals/Get-AzureRmAccessToken.ps1 + $accessToken = Invoke-RestMethod -UseBasicParsing -Uri ('https://login.microsoftonline.com/{0}/oauth2/token?api-version=1.0' -f $OctopusAzureADTenantId) -Method Post -Body @{\"grant_type\" = \"client_credentials\"; \"resource\" = \"https://management.core.windows.net/\"; \"client_id\" = $OctopusAzureADClientId; \"client_secret\" = $OctopusAzureADPassword } + [System.Net.Http.Headers.AuthenticationHeaderValue]::new('Bearer', $accessToken.access_token).ToString() +} +function Get-TemporarySasBlob { + param($BlobName) + # https://github.com/paulmarsy/AzureRest/blob/master/Exported/New-AzureBlob.ps1 + $sasToken = Invoke-RestMethod -UseBasicParsing -Uri 'https://mscompute2.iaas.ext.azure.com/api/Compute/VmExtensions/GetTemporarySas/' -Headers @{ + [Microsoft.WindowsAzure.Commands.Common.ApiConstants]::AuthorizationHeaderName = (Get-AzureRmAccessToken) + } + $containerSas = [uri]::new($sasToken) + $container = [Microsoft.WindowsAzure.Storage.Blob.CloudBlobContainer]::new($containerSas) + $blobRef = $container.GetBlockBlobReference($BlobName) + + [psobject]@{ + Blob = $blobRef + Uri = [uri]::new($blobRef.Uri.AbsoluteUri + $containerSas.Query) + } +} + +'Checking AzureRM Modules...' | Out-Verbose +Get-Module | ? Name -like 'AzureRM.*' | Format-Table -AutoSize -Property Name,Version | Out-String | Out-Verbose +if ((Get-Module AzureRM.Compute | % Version) -lt '2.6.0') { + $bundledErrorMessage = if ([System.Convert]::ToBoolean($OctopusUseBundledAzureModules)) { + 'The Azure PowerShell Modules bundled with Octopus have been loaded. To use the version installed on the server create a variable named \"Octopus.Action.Azure.UseBundledAzurePowerShellModules\" and set its value to \"False\".' + } + throw \"${bundledErrorMessage}Please ensure version 2.6.0 or newer of the AzureRM.Compute module has been installed. The module can be installed with the PowerShell command: Install-Module AzureRM.Compute -MinimumVersion 2.6.0\" +} + +$vm = Get-AzureRmVM -ResourceGroupName $StepTemplate_ResourceGroupName -Name $StepTemplate_VMName -WarningAction SilentlyContinue +if ($null -eq $vm) { + throw \"Unable to find virtual machine '$StepTemplate_VMName' in resource group '$StepTemplate_ResourceGroupName'\" +} +Write-Host \"Image will be captured from Virtual Machine '$($vm.Name)' in resource group '$($vm.ResourceGroupName)'\" +if (Test-String $StepTemplate_ImageDest -ForAbsence) { + throw \"The Image Destination parameter is required\" +} +$StepTemplate_ImageStorageContext = if ($StepTemplate_ImageType -eq 'unmanaged') { + $storageAccountKey = Get-OctopusSetting StorageAccountKey $null + if (Test-String $storageAccountKey) { + Write-Host \"Image will be copied to storage account context '$StepTemplate_ImageDest' using provided key\" + New-AzureStorageContext -StorageAccountName $StepTemplate_ImageDest -StorageAccountKey $storageAccountKey + } else { + $storageAccountResource = Find-AzureRmResource -ResourceNameEquals $StepTemplate_ImageDest -ResourceType Microsoft.Storage/storageAccounts + if ($storageAccountResource) { + Write-Host \"Image will be copied to storage account '$($storageAccountResource.Name)' found in resource group '$($storageAccountResource.ResourceGroupName)'\" + } else { + throw \"Unable to find storage account '$StepTemplate_ImageDest'\" + } + Get-AzureRmStorageAccount -ResourceGroupName $storageAccountResource.ResourceGroupName -Name $storageAccountResource.Name | % Context + } +} +$StepTemplate_ImageResourceGroupName = switch ($StepTemplate_ImageType) { + 'managed' { + $resourceGroup = Get-AzureRmResourceGroup -Name $StepTemplate_ImageDest | % ResourceGroupName + Write-Host \"Managed Image will be created in resource group '$resourceGroup'\" + $resourceGroup + } + 'unmanaged' { Find-AzureRmResource -ResourceNameEquals $StepTemplate_ImageDest -ResourceType Microsoft.Storage/storageAccounts | % ResourceGroupName } +} +if ($StepTemplate_ImageResourceGroupName -ieq $StepTemplate_ResourceGroupName -and $StepTemplate_DeleteVMResourceGroup -ieq 'True') { + throw \"You have chosen to delete the virtual machine and it's resource group ($StepTemplate_ResourceGroupName), however this resource group is also where the captured image will be created!\" +} + +Write-Host ('-'*80) +Write-Host \"Preparing virtual machine $($vm.Name) for image capture...\" + +$sysprepRun = $false +while ($true) { + $statusCode = Get-AzureRmVM -ResourceGroupName $StepTemplate_ResourceGroupName -Name $StepTemplate_VMName -Status -WarningAction SilentlyContinue | % Statuses | % Code + if ($statusCode -contains 'OSState/generalized') { + Write-Host 'VM is deallocated & generalized, proceeding to image capture...' + break + } + if ($statusCode -contains 'PowerState/deallocated') { + Write-Host 'VM has been deallocated, setting state to generalized... ' + Set-AzureRmVM -ResourceGroupName $StepTemplate_ResourceGroupName -Name $StepTemplate_VMName -Generalized | Out-Verbose + continue + } + if ($statusCode -contains 'PowerState/deallocating') { + Write-Host 'VM is deallocating, waiting...' + Start-Sleep 30 + continue + } + if ($statusCode -contains 'PowerState/stopped') { + Write-Host 'VM has been shutdown, starting deallocation...' + Stop-AzureRmVm -ResourceGroupName $StepTemplate_ResourceGroupName -Name $StepTemplate_VMName -Force | Out-Verbose + continue + } + if ($statusCode -contains 'PowerState/stopping') { + Write-Host 'VM is stopping, waiting...' + Start-Sleep 30 + continue + } + if ($statusCode -contains 'PowerState/running' -and $sysprepRun) { + Write-Host 'VM is running, but sysprep already deployed, waiting...' + Start-Sleep 30 + continue + } + if ($statusCode -contains 'PowerState/running') { + Write-Host 'VM is running, performing sysprep...' + $existingCustomScriptExtensionName = $vm.Extensions | ? VirtualMachineExtensionType -eq 'CustomScriptExtension' | % Name + if ($existingCustomScriptExtensionName) { + Write-Warning \"Removing existing CustomScriptExtension ($existingCustomScriptExtensionName)...\" + Remove-AzureRmVMCustomScriptExtension -ResourceGroupName $StepTemplate_ResourceGroupName -VMName $StepTemplate_VMName -Name $existingCustomScriptExtensionName -Force | Out-Verbose + } + + Write-Host 'Uploading sysprep script to blob storage...' + $sysprepScriptFileName = 'Sysprep.ps1' + $sysprepScriptBlob = Get-TemporarySasBlob $sysprepScriptFileName + $sysprepScriptBlob.Blob.UploadText($SysPrepScript) + + Write-Host 'Deploying sysprep custom script extension...' + Set-AzureRmVMCustomScriptExtension -ResourceGroupName $StepTemplate_ResourceGroupName -VMName $StepTemplate_VMName -Name 'Sysprep' -Location $vm.Location -FileUri $sysprepScriptBlob.Uri -Run $sysprepScriptFileName -ForceRerun (Get-Date).Ticks | Out-Verbose + $sysprepRun = $true + continue + } + Write-Warning \"VM is in an unknown state. Current status codes: $($statusCode -join ', '). Waiting...\" + Start-Sleep -Seconds 30 +} + +Write-Host ('-'*80) + +Write-Host 'Retrieving virtual machine disk configuration...' +$vm = Get-AzureRmVM -ResourceGroupName $StepTemplate_ResourceGroupName -Name $StepTemplate_VMName -WarningAction SilentlyContinue +$isManagedVm = $null -ne $vm.StorageProfile.OsDisk.ManagedDisk +if ($isManagedVm) { Write-Host \"Virtual machine $($vm.Name) is using Managed Disks\" } +$isUnmanagedVm = $null -ne $vm.StorageProfile.OsDisk.Vhd +if ($isUnmanagedVm) { Write-Host \"Virtual machine $($vm.Name) is using unmanaged storage account VHDs\" } + +if ($StepTemplate_ImageType -eq 'managed') { + Write-Host \"Creating Managed Image of $($vm.Name)...\" + $image = New-AzureRmImageConfig -Location $vm.Location -SourceVirtualMachineId $vm.Id + New-AzureRmImage -Image $image -ImageName $StepTemplate_ImageName -ResourceGroupName $StepTemplate_ImageResourceGroupName | Out-Verbose + Write-Host 'Image created:' + Get-AzureRmImage -ImageName $StepTemplate_ImageName -ResourceGroupName $StepTemplate_ImageResourceGroupName | Out-Host +} + +if ($StepTemplate_ImageType -eq 'unmanaged') { + if ($isManagedVm) { + Write-Host \"Granting access to os disk ($($vm.StorageProfile.OsDisk.Name)) blob...\" + $manageDisk = Grant-AzureRmDiskAccess -ResourceGroupName $StepTemplate_ResourceGroupName -DiskName $vm.StorageProfile.OsDisk.Name -DurationInSecond 3600 -Access Read + $vhdSasUri = $manageDisk.AccessSAS + } + if ($isUnmanagedVm) { + Write-Host \"Saving Unmanaged Image of $($vm.Name)...\" + $armTemplatePath = [System.IO.Path]::GetTempFileName() + $vhdDestContainer = Get-OctopusSetting VhdDestContainer 'images' + Save-AzureRmVMImage -ResourceGroupName $StepTemplate_ResourceGroupName -Name $StepTemplate_VMName -DestinationContainerName $vhdDestContainer -VHDNamePrefix $StepTemplate_ImageName -Overwrite -Path $armTemplatePath | Out-Verbose + $armTemplate = Get-Content -Path $armTemplatePath + \"VM Image ARM Template:`n$armTemplate\" | Out-Verbose + Remove-Item $armTemplatePath -Force + $osDiskUri = ($armTemplate | ConvertFrom-Json).resources.properties.storageprofile.osdisk.image.uri + \"OS Disk Image URI: $osDiskUri\" | Out-Verbose + $unmanagedVhd = Split-BlobUri $osDiskUri + + Write-Host \"Granting access to vhd image ($($unmanagedVhd.Blob))...\" + $unmanagedVhdStorageResource = Find-AzureRmResource -ResourceNameEquals $unmanagedVhd.Account -ResourceType Microsoft.Storage/storageAccounts + $unmanagedVhdStorageResource | Out-Verbose + $unmanagedVhdStorageContext = Get-AzureRmStorageAccount -ResourceGroupName $unmanagedVhdStorageResource.ResourceGroupName -Name $unmanagedVhdStorageResource.Name | % Context + $vhdSasUri = New-AzureStorageBlobSASToken -Container $unmanagedVhd.Container -Blob $unmanagedVhd.Blob -Permission r -ExpiryTime (Get-Date).AddHours(1) -FullUri -Context $unmanagedVhdStorageContext + } + Write-Host \"Source image SAS token created: $vhdSasUri\" + + Write-Host 'Copying image to storage account...' + $destContainerName = Get-OctopusSetting VhdDestContainer 'images' + $destContainer = Get-AzureStorageContainer -Name $destContainerName -Context $StepTemplate_ImageStorageContext -ErrorAction SilentlyContinue + if ($destContainer) { + Write-Host \"Using container '$destContainerName' in storage account $StepTemplate_ImageDest...\" + } else { + Write-Host \"Creating container '$destContainerName' in storage account $StepTemplate_ImageDest...\" + $destContainer = New-AzureStorageContainer -Name $destContainerName -Context $StepTemplate_ImageStorageContext -Permission Off + } + + $copyBlob = Start-AzureStorageBlobCopy -AbsoluteUri $vhdSasUri -DestContainer $destContainerName -DestContext $StepTemplate_ImageStorageContext -DestBlob $StepTemplate_ImageName -Force + $copyBlob | Out-Verbose + do { + if ($copyState.Status -eq 'Pending') { + Start-Sleep -Seconds 60 + } + $copyState = $copyBlob | Get-AzureStorageBlobCopyState + $copyState | Out-Verbose + $percent = ($copyState.BytesCopied / $copyState.TotalBytes) * 100 + Write-Host \"Blob transfer $($copyState.Status.ToString().ToLower())... $('{0:N2}' -f $percent)% @ $([System.Math]::Round($copyState.BytesCopied/1GB, 2))GB / $([System.Math]::Round($copyState.TotalBytes/1GB, 2))GB\" + } while ($copyState.Status -eq 'Pending') + Write-Host \"Final image transfer status: $($copyState.Status)\" + + if ($isManagedVm) { + Write-Host 'Revoking access to os disk blob...' + Revoke-AzureRmDiskAccess -ResourceGroupName $StepTemplate_ResourceGroupName -DiskName $vm.StorageProfile.OsDisk.Name | Out-Verbose + } +} + +Write-Host \"Image of $($vm.Name) captured successfully!\" + +if ($StepTemplate_DeleteVMResourceGroup -ieq 'True') { + Write-Host ('-'*80) + Write-Host \"Removing $($vm.Name) VM's resource group $StepTemplate_ResourceGroupName, the following resources will be deleted...\" + Find-AzureRmResource -ResourceGroupNameEquals $StepTemplate_ResourceGroupName | Sort-Object -Property ResourceId -Descending | Select-Object -Property ResourceGroupName,ResourceType,ResourceName | Format-Table -AutoSize | Out-Host + Remove-AzureRmResourceGroup -Name $StepTemplate_ResourceGroupName -Force | Out-Verbose +}", + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "911668fe-9653-4f08-892c-0e103e72cad0", + "Name": "StepTemplate_Account", + "Label": "Octopus Azure Account", + "HelpText": "Select the [account id](#/accounts) to use for the connection.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b838a80e-3e49-4788-8302-6b64bf0159ea", + "Name": "StepTemplate_ResourceGroupName", + "Label": "Resource Group Name", + "HelpText": "Name of the Azure Resource Group containing the Virtual Machine.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "8be775a8-53e4-4740-ae0f-10d72f9fdc67", + "Name": "StepTemplate_VMName", + "Label": "Virtual Machine Name", + "HelpText": "The name of the AzureRM Virtual Machine to capture. This VM will be shut down & generalized.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "73dae502-7a82-461c-a82b-8c40f519611f", + "Name": "StepTemplate_ImageType", + "Label": "Image Type", + "HelpText": "Desired type of image to capture from the Virtual Machine.", + "DefaultValue": "managed", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "managed|Managed Image +unmanaged|Storage Account VHD" + }, + "Links": {} + }, + { + "Id": "111b827b-a2c2-4d57-895e-b18daf9c6344", + "Name": "StepTemplate_ImageDest", + "Label": "Image Destination", + "HelpText": "Where the image should be created. + +**Managed Images** should enter a _Resource Group_ name + +**Storage Account VHDs** should enter a _Storage Account_ name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f461eac4-565c-47e6-8bf0-923f9647e3b2", + "Name": "StepTemplate_ImageName", + "Label": "Image Name", + "HelpText": "Name to use when creating the image.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "ba0ea0f4-710c-4732-8fb4-9f9b4faf325b", + "Name": "StepTemplate_DeleteVMResourceGroup", + "Label": "Delete VM Resource Group?", + "HelpText": "Delete the virtual machine resource group after an image has been captured. + +**Once a Virtual Machine is marked as generalized Azure will prevent it from being started or modified.**", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "LastModifiedBy": "paulmarsy", + "$Meta": { + "ExportedAt": "2017-05-29T18:58:54.733Z", + "OctopusVersion": "3.13.7", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-check-vmss-provision-status.json.human b/step-templates/azure-check-vmss-provision-status.json.human new file mode 100644 index 000000000..100a5b361 --- /dev/null +++ b/step-templates/azure-check-vmss-provision-status.json.human @@ -0,0 +1,697 @@ +{ + "Id": "e04c5cd8-0982-44b8-9cae-0a4b43676adc", + "Name": "Check VMSS Provision Status (Deployment Targets)", + "Description": "Use this step when leveraging Azure Virtual Machines Scale Sets (VMSS) with Octopus Deploy Deployment Targets. + +**Please run this on a worker or the Octopus Server.** + +This step specifically targets Deployment Targets. It will: +- Pause a runbook run or deployment until the VMSS has been provisioned +- Pause a runbook run or deployment until all the VMs in a VMss have been provisioned +- Reconcile the list of VMs in the VMSS with the list of VMs in Octopus Deploy. Any VMs in Octopus Deploy (based on role) not in the VMSS will be removed. +- Set output variables of all the deployment targets found in the VMSS. + +This step will set the following output variables: +- `VMSSHasServersToDeployTo`: Indicates the VMSS has servers to deploy to. +- `VMSSDeploymentTargetIds`: A comma-separated list of deployment target Ids you can use in later steps. +- `VMSSDeploymentTargetNames`: A comma-separated list of deployment target names you can use in later steps. + +**Please Note**: Setting the parameter `Exclude Pre-Existing Servers from Output` to `Yes` will remove any servers prior to a scale event from being returned in the output variables. + +This step makes the following assumptions: +- The name of the machine registration in Octopus matches the computer name. Like matching is supported, for example, a match will be made in the case of Octopus has `p-app-server-01` and the computer name is `p-app-server-01.mydomain.com.` +- You have the Azure Az PowerShell modules pre-installed on a worker or your Octopus Server. +- This step is running in the same space/environment/tenant (optional) as the deployment targets.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "OctopusUseBundledTooling": "False", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{VMSS.Azure.Account}", + "Octopus.Action.Script.ScriptBody": "$vmssScaleSetName = $OctopusParameters[\"VMSS.ScaleSet.Name\"] +$vmssScaleSetResourceGroup = $OctopusParameters[\"VMSS.ResourceGroup.Name\"] +$roleToSearchFor = $OctopusParameters[\"VMSS.DeploymentTarget.Roles\"] +$apiKey = $OctopusParameters[\"VMSS.Octopus.ApiKey\"] +$octopusUrl = $OctopusParameters[\"VMSS.Octopus.Url\"] +$timeoutInMinutes = $OctopusParameters[\"VMSS.Timeout.Value\"] +$timeoutErrorHandle = $OctopusParameters[\"VMSS.Timeout.ErrorHandle\"] +$duplicateRunDetectionInMinutes = $OctopusParameters[\"VMSS.Duplicate.TimeInMinutes\"] +$duplicateRunHandle = $OctopusParameters[\"VMSS.Duplicate.Handle\"] +$excludeOldServers = $OctopusParameters[\"VMSS.OldServers.ExcludeFromOutput\"] + +$octopusSpaceId = $OctopusParameters[\"Octopus.Space.Id\"] +$octopusEnvironmentId = $OctopusParameters[\"Octopus.Environment.Id\"] +$octopusTenantId = $OctopusParameters[\"Octopus.Deployment.Tenant.Id\"] +$octopusDeploymentId = $OctopusParameters[\"Octopus.Deployment.Id\"] +$octopusTriggerId = $OctopusParameters[\"Octopus.Deployment.Trigger.Id\"] +$octopusTaskId = $OctopusParameters[\"Octopus.Task.Id\"] +$octopusRunbookRunId = $OctopusParameters[\"Octopus.RunbookRun.Id\"] + +function Invoke-OctopusApi +{ + param + ( + $octopusUrl, + $endPoint, + $spaceId, + $apiKey, + $method, + $item + ) + + if ([string]::IsNullOrWhiteSpace($SpaceId)) + { + $url = \"$OctopusUrl/api/$EndPoint\" + } + else + { + $url = \"$OctopusUrl/api/$spaceId/$EndPoint\" + } + + try + { + if ($null -ne $item) + { + $body = $item | ConvertTo-Json -Depth 10 + Write-Verbose $body + + Write-Host \"Invoking $method $url\" + return Invoke-RestMethod -Method $method -Uri $url -Headers @{\"X-Octopus-ApiKey\" = \"$ApiKey\" } -Body $body -ContentType 'application/json; charset=utf-8' + } + +\t\tWrite-Verbose \"No data to post or put, calling bog standard invoke-restmethod for $url\" + $result = Invoke-RestMethod -Method $method -Uri $url -Headers @{\"X-Octopus-ApiKey\" = \"$ApiKey\" } -ContentType 'application/json; charset=utf-8' + + return $result + + + } + catch + { + if ($null -ne $_.Exception.Response) + { + if ($_.Exception.Response.StatusCode -eq 401) + { + Write-Error \"Unauthorized error returned from $url, please verify API key and try again\" + } + elseif ($_.Exception.Response.statusCode -eq 403) + { + Write-Error \"Forbidden error returned from $url, please verify API key and try again\" + } + else + { + Write-Verbose -Message \"Error calling $url $($_.Exception.Message) StatusCode: $($_.Exception.Response.StatusCode )\" + } + } + else + { + Write-Verbose $_.Exception + } + } + + Throw \"There was an error calling the Octopus API.\" +} + +function Get-QueuedEventInfo +{ + param ( + $octopusRunbookRunId, + $octopusDeploymentId, + $octopusSpaceId, + $octopusUrl, + $apiKey + ) + + if ([string]::IsNullOrWhiteSpace($octopusRunbookRunId)) + { + $queuedListRaw = Invoke-OctopusApi -octopusUrl $octopusUrl -endPoint \"events?regardingAny=$($OctopusDeploymentId)&spaces=$($octopusSpaceId)&documentTypes=Deployments&eventCategories=DeploymentQueued\" -spaceId $null -apiKey $apiKey -method \"GET\" + } + else + { + $queuedListRaw = Invoke-OctopusApi -octopusUrl $octopusUrl -endPoint \"events?regardingAny=$($octopusRunbookRunId)&spaces=$($octopusSpaceId)&documentTypes=RunbookRuns&eventCategories=RunbookRunQueued\" -spaceId $null -apiKey $apiKey -method \"GET\" + } + + $queuedArray = @($queuedListRaw.Items) + + return @{ + CurrentDeploymentQueued = [DateTime]$queuedArray[0].Occurred + NumberOfQueuedEvents = $queuedArray.Length + } +} + +function Get-CompletedEventInfo +{ + param ( + $octopusRunbookRunId, + $octopusDeploymentId, + $octopusSpaceId, + $octopusUrl, + $apiKey + ) + + if ([string]::IsNullOrWhiteSpace($octopusRunbookRunId)) + { + $finishedEventListRaw = Invoke-OctopusApi -octopusUrl $octopusUrl -endPoint \"events?regardingAny=$($OctopusDeploymentId)&spaces=$($octopusSpaceId)&documentTypes=Deployments&eventCategories=DeploymentSucceeded,DeploymentFailed&skip=0&take=1\" -spaceId $null -apiKey $apiKey -method \"GET\" + } + else + { + $finishedEventListRaw = Invoke-OctopusApi -octopusUrl $octopusUrl -endPoint \"events?regardingAny=$($OctopusDeploymentId)&spaces=$($octopusSpaceId)&documentTypes=RunbookRuns&eventCategories=RunbookRunSucceeded,RunbookRunFailed&skip=0&take=1\" -spaceId $null -apiKey $apiKey -method \"GET\" + } + + $finishedEventArray = @($finishedEventListRaw.Items) + + return [DateTime]$finishedEventArray[0].Occurred +} + +function Test-ForDuplicateRun +{ + param ( + $octopusRunbookRunId, + $octopusDeploymentId, + $octopusSpaceId, + $queuedEventInfo, + $duplicateRunDetectionInMinutes, + $duplicateRunHandle, + $octopusTaskId, + $octopusUrl, + $apiKey + ) + + Write-Host \"Checking to see if this current run is a duplicate because of deployment target triggers\" + $duplicateRun = $false + + if ([string]::IsNullOrWhiteSpace($octopusTriggerId) -eq $false) + { + Write-Highlight \"This run was triggered by a trigger.\" + + Write-Host \"The number of items in the queued array is: $($queuedEventInfo.NumberOfQueuedEvents)\" + if ($queuedEventInfo.NumberOfQueuedEvents -gt 1) + { + Write-Host \"This task has been run before\" + + $previousDeploymentFinished = Get-CompletedEventInfo -octopusRunbookRunId $octopusRunbookRunId -octopusDeploymentId $octopusDeploymentId -octopusSpaceId $octopusSpaceId -octopusUrl $octopusUrl -apiKey $apiKey + Write-Host \"The current deployment was queued $($queuedEventInfo.CurrentDeploymentQueued) while the previous deployment was finished $previousDeploymentFinished\" + + $queuedCompletedDifference = $queuedEventInfo.CurrentDeploymentQueued - $previousDeploymentFinished + Write-Host \"The difference in minutes is $($queuedCompletedDifference.TotalMinutes)\" + + if ($queuedCompletedDifference.TotalMinutes -le $duplicateRunDetectionInMinutes) + { + Write-Highlight \"The previous deployment finished in the last $($queuedCompletedDifference.TotalMinutes) minutes before this was trigger, that is extremely fast. This is a duplicate run.\" + $duplicateRun = $true + + if ($duplicateRunHandle.ToLower().Trim() -eq \"cancel\") + { + Write-Highlight \"The duplicate run handle is set to cancel, cancelling current deployment.\" + Invoke-OctopusApi -octopusUrl $octopusUrl -apiKey $apiKey -spaceId $OctopusSpaceId -method \"POST\" -endPoint \"tasks/$($octopusTaskId)/cancel\" + exit 0 + } + else + { + Write-Highlight \"The duplicate run handle is set to proceed.\" + } + } + else + { + Write-Highlight \"The last deployment finished and this one was queued after $($queuedCompletedDifference.TotalMinutes) minutes has passed which is outside the window of $duplicateRunDetectionInMinutes minutes. Not a duplicate.\" + } + }\t + else + { + Write-Highlight \"This is the first time this release has been deployed to this environment. This is not a duplicate run.\" + } + } + + return $duplicateRun +} + +function Start-WaitForVMSSToFinishProvisioning +{ + param + ( + $vmssScaleSetResourceGroup, + $vmssScaleSetName, + $timeoutInMinutes + ) + + $vmssState = \"Provisioning\" + $startTime = Get-Date + + Write-Host \"Will now wait until the VMSS has finished provisioning.\" + + do + { + try + { + $vmssInfo = Get-AzVmss -ResourceGroupName $vmssScaleSetResourceGroup -VMScaleSetName $vmssScaleSetName + } + catch + { + Write-Highlight \"Unable to access the scale set $vmssScaleSetName. Exiting step.\" + Write-Host $_.Exception + exit 0 + } + + Write-Verbose \"VMSSInfo: \" + Write-Verbose ($vmssInfo | ConvertTo-JSON -Depth 10) + + $vmssInstanceCount = $vmssInfo.Sku.Capacity + $vmssState = $vmssInfo.ProvisioningState + + if($vmssState.ToLower().Trim() -ne \"provisioning\") + { + Write-Highlight \"The VMSS $vmssScaleSetName capacity is current set to $vmssInstanceCount with a provisioning state of $vmssState\" + } + else + { + Write-Host \"The VMSS is still provisioning, sleeping for 10 seconds then checking again.\" + Start-Sleep -Seconds 10 + } + + $currentTime = Get-Date + $dateDifference = $currentTime - $startTime + + if ($dateDifference.TotalMinutes -ge $timeoutInMinutes) + { + Write-Highlight \"We have been waiting $($dateDifference.TotalMinutes) for the VMSS to finish provisioning. Timeout reached, exiting.\" + exit 1 + } + + } While ($vmssState.ToLower().Trim() -eq \"provisioning\") +} + +function Start-WaitForVMsInVMSSToFinishProvisioning +{ + param + ( + $vmssScaleSetResourceGroup, + $vmssScaleSetName, + $timeoutInMinutes, + $timeoutErrorHandle + ) + + $vmssVmsAreProvisioning = $false + $startTime = Get-Date + $numberOfWaits = 0 + $printVmssVmList = $true + + Write-Highlight \"Checking the state of all VMs in the scale set.\" + + do + { + $numberOfWaits += 1 + $vmssVmList = Get-AzVmssVM -ResourceGroupName $vmssScaleSetResourceGroup -VMScaleSetName $vmssScaleSetName + + if ($printVmssVmList -eq $true) + { + Write-Host ($vmssVmList | ConvertTo-Json -Depth 10) + $printVmssVmList = $false + } + + $vmssVmsAreProvisioning = $false + foreach ($vmInfo in $vmssVmList) + { + if ($vmInfo.ProvisioningState.ToLower().Trim() -eq \"creating\") + { + $vmssVmsAreProvisioning = $true + break + } + } + + if ($vmssVmsAreProvisioning -eq $true) + { + $currentTime = Get-Date + $dateDifference = $currentTime - $startTime + + if ($dateDifference.TotalMinutes -ge $timeoutInMinutes) + { + $vmssVmsAreProvisioning = $false + if ($timeoutErrorHandle.ToLower().Trim() -eq \"error\") + { + Write-Highlight \"The VMs in the scale have been provisioning for over $timeoutInMinutes. Error handle is set to error out, exiting with an exit code of 1.\" + exit 1 + } + + Write-Highlight \"The VMs in the scale have been provisioning for over $timeoutInMinutes. Going to move on and continue with the deployment for any VMs that have finished provisioning.\" + } + else + { + if ($numberofWaits -ge 10) + { + Write-Highlight \"The VMs are still currently provisioning, waiting...\" + $numberOfWaits = 0 + } + else + { + Write-Host \"The VMs are still currently provisioning, sleeping for 10 seconds then checking again.\" + } + Start-Sleep -Seconds 10 + } + } + else + { + Write-Highlight \"All the VMs in the VM Scale Set have been provisioned, reconciling them with the list in Octopus.\" + } + } while ($vmssVmsAreProvisioning -eq $true) +} + + +Write-Host \"ScaleSet Name: $vmssScaleSetName\" +Write-Host \"Resource Group Name: $vmssScaleSetResourceGroup\" +Write-Host \"Deployment Target Role to Search For: $roleToSearchFor\" +Write-Host \"Octopus Url: $octopusUrl\" +Write-Host \"Timeout In Minutes: $timeoutInMinutes\" +Write-Host \"Timeout Error Handle: $timeoutErrorHandle\" +Write-Host \"Duplicate Run Detection in Minutes: $duplicateRunDetectionInMinutes\" +Write-Host \"Duplicate Run Handle: $duplicateRunHandle\" +Write-host \"Exclude Old Servers: $excludeOldServers\" + +Write-Host \"Space Id: $octopusSpaceId\" +Write-Host \"Environment Id: $octopusEnvironmentId\" +Write-Host \"Tenant Id: $octopusTenantId\" +Write-Host \"Deployment Id: $octopusDeploymentId\" +Write-Host \"Trigger Id: $octopusTriggerId\" +Write-Host \"Task Id: $octopusTaskId\" +Write-Host \"Runbook Run Id: $octopusRunbookRunId\" + +if ([string]::IsNullOrWhiteSpace($vmssScaleSetName)) { Write-Error \"Scale Set Name is required.\" } +if ([string]::IsNullOrWhiteSpace($vmssScaleSetResourceGroup)) { Write-Error \"Resource Group Name is required.\" } +if ([string]::IsNullOrWhiteSpace($roleToSearchFor)) { Write-Error \"Scale Set Name is required.\" } +if ([string]::IsNullOrWhiteSpace($octopusUrl)) { Write-Error \"Octopus Url is required.\" } +if ([string]::IsNullOrWhiteSpace($apiKey)) { Write-Error \"Octopus Api Key is required.\" } +if ([string]::IsNullOrWhiteSpace($timeoutInMinutes)) { Write-Error \"Timeout in minutes is required.\" } +if ([string]::IsNullOrWhiteSpace($timeoutErrorHandle)) { Write-Error \"Timeout error handle is required.\" } +if ([string]::IsNullOrWhiteSpace($duplicateRunDetectionInMinutes)) { Write-Error \"Duplicate run detection in minutes is required.\" } +if ([string]::IsNullOrWhiteSpace($duplicateRunHandle)) { Write-Error \"Duplicate run handle is required.\" } +if ([string]::IsNullOrWhiteSpace($excludeOldServers)) { Write-Error \"Exclude old servers is required.\" } + +$queuedEventInfo = Get-QueuedEventInfo -octopusRunbookRunId $octopusRunbookRunId -octopusDeploymentId $octopusDeploymentId -octopusSpaceId $octopusSpaceId -octopusUrl $octopusUrl -apiKey $apiKey + +Write-Host \"The current deployment was queued at: $($queuedEventInfo.CurrentDeploymentQueued)\" + +$duplicateRun = Test-ForDuplicateRun -octopusRunbookRunId $octopusRunbookRunId -octopusDeploymentId $octopusDeploymentId -octopusSpaceId $octopusSpaceId -duplicateRunDetectionInMinutes $duplicateRunDetectionInMinutes -duplicateRunHandle $duplicateRunHandle -queuedEventInfo $queuedEventInfo -octopusTaskId $octopusTaskId -octopusUrl $octopusUrl -apiKey $apiKey + +Start-WaitForVMSSToFinishProvisioning -vmssScaleSetResourceGroup $vmssScaleSetResourceGroup -vmssScaleSetName $vmssScaleSetName -timeoutInMinutes $timeoutInMinutes +Start-WaitForVMsInVMSSToFinishProvisioning -vmssScaleSetResourceGroup $vmssScaleSetResourceGroup -vmssScaleSetName $vmssScaleSetName -timeoutInMinutes $timeoutInMinutes -timeoutErrorHandle $timeoutErrorHandle + +$vmssVmList = Get-AzVmssVM -ResourceGroupName $vmssScaleSetResourceGroup -VMScaleSetName $vmssScaleSetName +$vmListToReconcile = @() + +foreach ($vmInfo in $vmssVmList) +{ +\tif ($vmInfo.ProvisioningState.ToLower().Trim() -ne \"failed\") + { + \t$vmListToReconcile += $vmInfo.OsProfile.ComputerName + } +} + +$octopusDeployTargets = Invoke-OctopusApi -octopusUrl $octopusUrl -endPoint \"machines?environmentIds=$($octopusEnvironmentId)&roles=$($roleToSearchFor)&skip=0&take=1000\" -spaceId $octopusSpaceId -apiKey $apiKey -method \"GET\" +$octopusDeployTargetIds = @() +$octopusDeployTargetNames = @() +$roleList = $roleToSearchFor.Split(\",\") +foreach ($deploymentTarget in $octopusDeployTargets.Items) +{ +\t$matchingRole = $true + foreach ($role in $roleList) + { + \tif ($deploymentTarget.Roles -notContains ($role.Trim())) + { + \tWrite-Host \"The target $($deploymentTarget.Name) does not contain the role $role. To be considered part of the scale set it has to be assigned to all the roles $roleToSearchFor. Excluding from reconcilation logic.\" + $matchingRole = $false + break + } + } + + if ($matchingRole -eq $false) + { + \tcontinue + } + + if ([string]::IsNullOrWhiteSpace($octopusTenantId) -eq $false -and $deploymentTarget.TenantIds -notcontains $octopusTenantId) + { + \tWrite-Host \"The target $($deploymentTarget.Name) is not assigned to $octopusTenantId. But the current run is running under the context of that tenant. Excluding from reconcilation logic.\" + continue + } + + $hasMatchingName = $false + $deploymentTargetNameLowerTrim = $deploymentTarget.Name.ToLower().Trim() + + Write-Host \"Attempting to do a match on name\" +\tforeach ($vmssVM in $vmListToReconcile) + { + \t$vmssVMLowerTrim = $vmssVM.ToLower().Trim() + + Write-Host \"Checking to see if $($deploymentTarget.Name) is like $vmssVM\" + if ($deploymentTargetNameLowerTrim -eq $vmssVMLowerTrim) + { + Write-Host \"The vmss vm name $vmssVM is equal to to the deployment target name $($deploymentTarget.Name), set matching to true\" + $hasMatchingName = $true + break + } + if ($deploymentTargetNameLowerTrim -like \"*$vmssVMLowerTrim*\") + { + Write-Host \"The deployment target name $($deploymentTarget.Name) contains the vmss vm name $vmssVM, set matching to true\" + $hasMatchingName = $true + break + } + elseif ($vmssVMLowerTrim -like \"*$deploymentTargetNameLowerTrim*\") + { + Write-Host \"The vmss vm name $vmssVM contains the deployment target name $($deploymentTarget.Name), set matching to true\" + $hasMatchingName = $true + break + } + } + + if ($hasMatchingName -eq $false) + { + \tWrite-Highlight \"The deployment target $($deploymentTarget.Name) is not in the list of VMs assigned to the scale set, deleting it.\" + Invoke-OctopusApi -octopusUrl $octopusUrl -endPoint \"machines/$($deploymentTarget.Id)\" -spaceId $octopusSpaceId -apiKey $apiKey -method \"DELETE\" + } + else + { + \tWrite-Highlight \"The deployment target $($deploymentTarget.Name) is in the list of VMS assigned to the scale set, leaving it alone.\" + + $addToOutputArray = $true + if ($excludeOldServers.ToLower().Trim() -eq \"yes\") + { + \tWrite-Host \"Pulling back the creation event for $($deploymentTarget.Name)\" + \t$creationTasks = Invoke-OctopusApi -octopusUrl $octopusUrl -endPoint \"events?regarding=$($deploymentTarget.Id)&spaces=$($octopusSpaceId)&includeSystem=true&eventCategories=Created\" -spaceId $null -apiKey $apiKey -method \"GET\" + + $creationDate = @($creationTasks.Items)[0].Occurred + Write-Host \"The deployment target $($deploymentTarget.Name) was created on $creationDate\" + $differenceInCreationTime = [DateTime]$queuedEventInfo.CurrentDeploymentQueued - [DateTime]$creationDate + + Write-Host \"The difference in minutes between creation date and current task queued time is $($differenceInCreationTime.TotalMinutes) minutes\" + if ($differenceInCreationTime.TotalMinutes -gt 3) + { + \tWrite-Host \"The deployment target $($deploymentTarget.Name) existed for more than 3 minutes before this was ran and the excludeOldServers was set to yes, removing this from the output\" + \t$addToOutputArray = $false + } + } + + if ($addToOutputArray -eq $true) + { +\t $octopusDeployTargetIds += $deploymentTarget.Id + \t $octopusDeployTargetNames += $deploymentTarget.Name + } + } +} + +Write-Highlight \"The Azure VM Scale Set $vmssScaleSetName and Octopus Deploy target list have been successfully reconciled.\" + +$vmssHasServersToDeployTo = $octopusDeployTargetIds.Count -gt 0 +if ($duplicateRun -eq $true) +{ +\tWrite-Highlight \"Duplicate run detected, therefore there are no new servers to deploy to.\" + $vmssHasServersToDeployTo = $false +} +elseif ($vmssHasServersToDeployTo -eq $false) +{ +\tWrite-Highlight \"There are no servers to deploy to. Exclude old servers was set to '$excludeOldServers'. This likely means this was a scale in event or all the servers existed prior to this run.\" +} + +Write-Highlight \"Setting the output variable 'VMSSHasServersToDeployTo' to $vmssHasServersToDeployTo.\" +Set-OctopusVariable -Name \"VMSSHasServersToDeployTo\" -Value $vmssHasServersToDeployTo + +Write-Highlight \"Setting the output variable 'VMSSDeploymentTargetIds' to $($octopusDeployTargetIds -join \",\").\" +Set-OctopusVariable -Name \"VMSSDeploymentTargetIds\" -Value ($octopusDeployTargetIds -join \",\") + +Write-Highlight \"Setting the output variable 'VMSSDeploymentTargetNames' to $($octopusDeployTargetNames -join \",\").\" +Set-OctopusVariable -Name \"VMSSDeploymentTargetNames\" -Value ($octopusDeployTargetNames -join \",\")" + }, + "Parameters": [ + { + "Id": "9f9bc9fb-fa96-41ab-aa06-ad0dd68ff038", + "Name": "VMSS.ScaleSet.Name", + "Label": "VMSS Name", + "HelpText": "**Required** + +The name of the Azure Virtual Machine Scale Set", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "32939873-a965-46ae-915f-f94d93f94c0b", + "Name": "VMSS.ResourceGroup.Name", + "Label": "VMSS Resource Group Name", + "HelpText": "**Required** + +The name of the resource group where the VMSS is located.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "571df393-6353-4f12-96cd-c92f3a5e1452", + "Name": "VMSS.Azure.Account", + "Label": "Azure Account", + "HelpText": "**Required** + +The Azure Account to use when querying the VMSS.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "b03d1bf3-76f8-4a3c-917a-60d5e19a15f4", + "Name": "VMSS.DeploymentTarget.Roles", + "Label": "Deployment Target Roles", + "HelpText": "**Required** + +A comma-separated list of deployment target roles to filter your deployment targets by. These roles are how this step will determine which machines to reconcile. + +If you supply multiple roles, for example `todo-web-server,todo-virtual-machine-scale-set` the deployment targets have to be assigned `todo-web-server` AND `todo-virtual-machine-scaleset` roles for the target to be considered.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "757ca61b-d15b-4dbb-911a-b3f4dc0258b5", + "Name": "VMSS.Octopus.Url", + "Label": "Octopus URL", + "HelpText": "**Required** + +The URL of the Octopus Server to query against. Example: `https://samples.octopus.app`.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7b070182-e79b-48a0-966a-39d332802b60", + "Name": "VMSS.Octopus.ApiKey", + "Label": "Octopus API Key", + "HelpText": "**Required** + +API Key of a service account that has permissions to: +- Query deployment targets, environments, and tenants. +- Query events (audit history) +- Delete deployment targets + +Assigning the service account to a team with `Project Viewer` and `Environment Manager` roles will work.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "6e0f8635-70ba-4b8a-b5b4-909dba3f9fd8", + "Name": "VMSS.Timeout.Value", + "Label": "Timeout (In Minutes)", + "HelpText": "**Required** + +How long this step will wait (in minutes) for the VMSS and VMs in the VMSS to finish being created.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0392f153-73b0-44d4-9181-4f366c9012f7", + "Name": "VMSS.Timeout.ErrorHandle", + "Label": "Timeout Error", + "HelpText": "**Required** + +What will happen when a timeout occurs. + +- `Proceed`: The script will reconcile the VMs it can and then finish. No error thrown. +- `Error`: Will throw an error and it will stop the deployment from proceeding. + +The default is `Proceed`", + "DefaultValue": "Proceed", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Proceed|Proceed +Error|Error" + } + }, + { + "Id": "80ef4fb9-c773-4d0a-82af-efdfb302e58f", + "Name": "VMSS.Duplicate.TimeInMinutes", + "Label": "Duplicate Run Time Allowance (in Minutes)", + "HelpText": "**Required** + +This step is designed to wait for VMSS to finish scaling out. However, Octopus isn't aware of VMSS and may attempt to run the same deployment multiple times as new targets are \"discovered.\" + +Typically, when this happens, a deployment is queued within a few minutes of the previous one finishing. When that happens this step will treat that as a duplicate run. + +This setting indicates the number of minutes that must pass before a duplicate run is found. The default is `3` minutes.", + "DefaultValue": "3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0eb3bed8-9259-440e-bff0-cb86af7da52e", + "Name": "VMSS.Duplicate.Handle", + "Label": "Duplicate Run Adjustment", + "HelpText": "**Required** + +What the step will do when a duplicate run is found. The two options are: + +- `Cancel`: cancel the current runbook run or deployment. +- `Proceed`: continue on with the current runbook run or deployment. + +The default is `Proceed`.", + "DefaultValue": "Proceed", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Cancel|Cancel +Proceed|Proceed" + } + }, + { + "Id": "adaf6a8c-453f-4b69-9ba6-f6a54b4f5bb9", + "Name": "VMSS.OldServers.ExcludeFromOutput", + "Label": "Exclude Pre-Existing Servers from Output", + "HelpText": "**Required** + +Old servers are any servers that existed prior to scaling out the VMSS. + +If this step is run in a deployment target trigger it will pull back all the machines in a scale set and return them in an output variable. Depending on how you use that list, this could result in redeployment. + +You can exclude pre-existing servers by setting this value to `Yes`. The default is `No`.", + "DefaultValue": "No", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "No|No +Yes|Yes" + } + } + ], + "$Meta": { + "ExportedAt": "2021-07-26T19:15:02.718Z", + "OctopusVersion": "2021.2.6259", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "BobJWalker", + "Category": "azure" + } diff --git a/step-templates/azure-create-containerapp-environment.json.human b/step-templates/azure-create-containerapp-environment.json.human new file mode 100644 index 000000000..c915d0fa9 --- /dev/null +++ b/step-templates/azure-create-containerapp-environment.json.human @@ -0,0 +1,253 @@ +{ + "Id": "9b4b9fdc-2f97-4507-8df5-a0c1dd7464a5", + "Name": "Azure - Create Container App Environment", + "Description": "Creates a Container App Environment if it doesn't exist. An output variable called `ManagedEnvironmentId` is created which holds the Id.", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Write-Host \"Saving module $PowerShellModuleName to temporary folder ...\" + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force + Write-Host \"Save successful!\" +} + +# Check to see if $IsWindows is available +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) +{ + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +# Check to see if it's running on Windows +if ($IsWindows) +{ +\t# Disable the progress bar so downloading files via Invoke-WebRequest are faster + $ProgressPreference = 'SilentlyContinue' +} + +if ($PSEdition -eq \"Core\") { + $PSStyle.OutputRendering = \"PlainText\" +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PWD/Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([IO.Path]::PathSeparator)$env:PSModulePath\" +$azureModule = \"Az.App\" + +# Get variables +$templateAzureAccountClient = $OctopusParameters['Template.Azure.Account.ClientId'] +$templateAzureAccountPassword = $OctopusParameters['Template.Azure.Account.Password'] +$templateAzureAccountTenantId = $OctopusParameters['Template.Azure.Account.TenantId'] +$templateAzureResourceGroup = $OctopusParameters['Template.Azure.ResourceGroup.Name'] +$templateAzureSubscriptionId = $OctopusParameters['Template.Azure.Account.SubscriptionId'] +$templateEnvironmentName = $OctopusParameters['Template.ContainerApp.Environment.Name'] +$templateAzureLocation = $OctopusParameters['Template.Azure.Location.Name'] + +# Check for required PowerShell module +Write-Host \"Checking for module $azureModule ...\" + +if ((Get-ModuleInstalled -PowerShellModuleName $azureModule) -eq $false) +{ +\t# Install the module + Install-PowerShellModule -PowerShellModuleName $azureModule -LocalModulesPath $LocalModules +} + +# Import the necessary module +Write-Host \"Importing module $azureModule ...\" +Import-Module $azureModule + +# Check to see if the account was specified +if (![string]::IsNullOrWhitespace($templateAzureAccountClient)) +{ +\t# Login using the provided account + Write-Host \"Logging in as specified account ...\" + +\t# Create credential object for az module +\t$securePassword = ConvertTo-SecureString $templateAzureAccountPassword -AsPlainText -Force +\t$azureCredentials = New-Object System.Management.Automation.PSCredential ($templateAzureAccountClient, $securePassword) + + Connect-AzAccount -Credential $azureCredentials -ServicePrincipal -Tenant $templateAzureAccountTenantId | Out-Null + + Write-Host \"Login successful!\" +} +else +{ +\tWrite-Host \"Using machine Managed Identity ...\" + Connect-AzAccount -Identity | Out-Null + + # Get Identity context + $identityContext = Get-AzContext + + # Set variables + $templateAzureSubscriptionId = $identityContext.Subscription + + if ([string]::IsNullOrWhitespace($templateAzureAccountTenantId)) + { + \t$templateAzureAccountTenantId = $identityContext.Tenant + } + + Set-AzContext -Tenant $templateAzureAccountTenantId | Out-Null + +\tWrite-Host \"Successfully set context for Managed Identity!\" +} + +# Check to see if Container App Environment already exists +Write-Host \"Getting list of existing environments ...\" +$existingEnvironments = Get-AzContainerAppManagedEnv -ResourceGroupName $templateAzureResourceGroup -SubscriptionId $templateAzureSubscriptionId +$managedEnvironment = $null + +if (($null -ne $existingEnvironments) -and ($null -ne ($existingEnvironments | Where-Object {$_.Name -eq $templateEnvironmentName}))) +{ +\tWrite-Host \"Environment $templateEnvironmentName already exists.\" + $managedEnvironment = $existingEnvironments | Where-Object {$_.Name -eq $templateEnvironmentName} +} +else +{ +\tWrite-Host \"Environment $templateEnvironmentName not found, creating ...\" + $managedEnvironment = New-AzContainerAppManagedEnv -EnvName $templateEnvironmentName -ResourceGroupName $templateAzureResourceGroup -Location $templateAzureLocation -AppLogConfigurationDestination \"\" # Empty AppLogConfigurationDestination is workaround for properties issue caused by marking this as required +} + +# Set output variable +Write-Host \"Setting output variable ManagedEnvironmentId to $($managedEnvironment.Id)\" +Set-OctopusVariable -name \"ManagedEnvironmentId\" -value \"$($managedEnvironment.Id)\"" + }, + "Parameters": [ + { + "Id": "4e3ee370-7f62-4d00-a8c2-bb8717b5d681", + "Name": "Template.Azure.ResourceGroup.Name", + "Label": "Azure Resource Group Name", + "HelpText": "Provide the resource group name to create the environment in.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "11ccbfe4-f170-4d25-bc55-e52327680613", + "Name": "Template.Azure.Account.SubscriptionId", + "Label": "Azure Account Subscription Id", + "HelpText": "The subscription ID of the Azure account to use. This value can be retrieved from an Azure Account variable type. Add an Azure Account to your project , then assign the `SubscriptionNumber` property to for this entry. Leave blank to use the Managed Identity. + +For example, if your Azure Account variable is called MyAccount, the value for this input would be `#{MyAccount.SubscriptionNumber}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e1cf9fa6-e8b9-475a-9800-ef688f1e7ad5", + "Name": "Template.Azure.Account.ClientId", + "Label": "Azure Account Client Id", + "HelpText": "The subscription ID of the Azure account to use. This value can be retrieved from an Azure Account variable type. Add an Azure Account to your project , then assign the `Client` property to for this entry. Leave blank to use the Managed Identity. + +For example, if your Azure Account variable is called MyAccount, the value for this input would be `#{MyAccount.Client}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4dbcd5a4-6d41-4ae0-ac69-76e27fc6bd28", + "Name": "Template.Azure.Account.TenantId", + "Label": "Azure Account Tenant Id", + "HelpText": "The subscription ID of the Azure account to use. This value can be retrieved from an Azure Account variable type. Add an Azure Account to your project , then assign the `TenantId` property to for this entry. If blank, it will use the Managed Identity tenant. + +For example, if your Azure Account variable is called MyAccount, the value for this input would be `#{MyAccount.TenantId}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "99a141f5-2fa1-40b9-b04e-8d37fe259a27", + "Name": "Template.Azure.Account.Password", + "Label": "Azure Account Password", + "HelpText": "The subscription ID of the Azure account to use. This value can be retrieved from an Azure Account variable type. Add an Azure Account to your project , then assign the `Password` property to for this entry. Leave blank to use the Managed Identity. + +For example, if your Azure Account variable is called MyAccount, the value for this input would be `#{MyAccount.Password}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "66f5ee93-3ba9-44e7-8a04-50535b1907cb", + "Name": "Template.ContainerApp.Environment.Name", + "Label": "Container App Environment Name", + "HelpText": "The name of the container app environment to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "92daa091-2b90-4615-9a2e-ffc52275ddb4", + "Name": "Template.Azure.Location.Name", + "Label": "Azure Location", + "HelpText": "The location in which to create the container app environment.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-07-05T15:56:04.248Z", + "OctopusVersion": "2023.3.4541", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "azure" +} diff --git a/step-templates/azure-create-new-resourcegroup.json.human b/step-templates/azure-create-new-resourcegroup.json.human new file mode 100644 index 000000000..e34773001 --- /dev/null +++ b/step-templates/azure-create-new-resourcegroup.json.human @@ -0,0 +1,85 @@ +{ + "Id": "7ae25451-366d-49cc-a49d-eba03d147db0", + "Name": "Create Azure Resources - RG", + "Description": "The New-AzureRmResourceGroup cmdlet creates an Azure resource group", + "ActionType": "Octopus.AzurePowerShell", + "Version": 28, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Azure.AccountId": "#{CreateResourceGroup.AzureAccount}", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$ResourceGroupName =$OctopusParameters[\"CreateResourceGroup.ResourceGroupName\"] +$Location =$OctopusParameters[\"CreateResourceGroup.Location\"] + + Write-Output \"Variables:\" + Write-Output \"ResourceGroupName: $ResourceGroupName\" + Write-Output \"Location: $Location\" + +Write-Output '###############################################' +Write-Output '##Step1: Create Resource Group ' +$AzureResourceGroup = Get-AzureRmResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue +if ( $null -eq $AzureResourceGroup) +{ +Write-Output \"Resource Group $ResourceGroupName does not exist, creating one ...\" +$AzureResourceGroup =New-AzureRmResourceGroup -Name $ResourceGroupName -Location $Location +} +else{ +Write-Output \"Resource Group $ResourceGroupName already exists ...\" +} + +Write-Output '###############################################' +Write-Output '##Step2: Validate Resource Group ' + +if ($null -eq $AzureResourceGroup ){ +Throw \"Failed to create resource group $AzureResourceGroupName\" +} +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "a5398248-3866-4e86-bfcd-6f6091199839", + "Name": "CreateResourceGroup.ResourceGroupName", + "Label": "ResourceGroupName", + "HelpText": "Required: Specifies a name for the resource group. This parameter is required. The resource name must be unique in the subscription.You can use -Name or its alias, -ResourceGroupName.If a resource group with that name already exists, the command prompts you for confirmation before replacing the existing resource group. To suppress the confirmation prompt, use the Force parameter.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "ddbea94f-8053-4da7-aaea-c71e31be38a5", + "Name": "CreateResourceGroup.Location", + "Label": "Location", + "HelpText": "Required: Specifies the location of the resource group. This parameter is required. Enter an Azure data center location, such as \"West US\" or \"Southeast Asia\".You can place a resource group in any location. The resource group does not have to be in the same location your Azure subscription or the same location as its resources. Resource groups can contain resources from different locations. To determine which location support each resource type, use the Get-AzureRmResourceProvider with the ProviderNamespace parameter cmdlet.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "18d18732-972c-446a-a6da-912866332c02", + "Name": "CreateResourceGroup.AzureAccount", + "Label": "AzureAccount", + "HelpText": "Enter the SPN used to connect to Azure", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2018-10-03T13:00:00.000+01:00", + "LastModifiedBy": "Jens-H-Eriksen", + "$Meta": { + "ExportedAt": "2018-10-03T11:37:04.316Z", + "OctopusVersion": "2018.8.6", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-create-new-webapp.json.human b/step-templates/azure-create-new-webapp.json.human new file mode 100644 index 000000000..cf655ef14 --- /dev/null +++ b/step-templates/azure-create-new-webapp.json.human @@ -0,0 +1,156 @@ +{ + "Id": "c9a3122f-6723-4753-8461-f9fb3e73a513", + "Name": "Create Azure Resources - WA", + "Description": "The New-AzureRmAppServicePlan cmdlet creates a new app service plan. +The New-AzureRmWebApp cmdlet creates a new web app.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Azure.AccountId": "#{AzureAccount}", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "###############################################\r +##Step 1: Get Variables\r +$ResourceGroupName = $OctopusParameters[\"ResourceGroupName\"] \r +$DeploymentLocation = $OctopusParameters[\"Location\"] \r +$AppServicePlanName = $OctopusParameters[\"AppServicePlanName\"] \r +$AppServicePlanTier = $OctopusParameters[\"AppServicePlanTier\"]\r +$WebAppName = $OctopusParameters[\"WebAppName\"]\r +$TimeStamp = Get-Date -Format ddMMyyyy_hhmmss\r +$PublishProfilePath = Join-Path -Path $ENV:Temp -ChildPath \"publishprofile$TimeStamp.xml\"\r +$AppServiceUse32BitWorkerProcess= $OctopusParameters[\"AppServiceUse32BitWorkerProcess\"] \r +###############################################\r +\r +###############################################\r +##Step 2: Check and Create Service Plan\r +try{\r + $ServicePlan= Get-AzureRmAppServicePlan -ResourceGroupName $ResourceGroupName -Name $AppServicePlanName -ErrorAction SilentlyContinue \r + if ($null -eq $ServicePlan)\r + {\r + Write-Output \"Creating Service Plan\"\r + $ServicePlan=New-AzureRmAppServicePlan -Name $AppServicePlanName -Location $Location -ResourceGroupName $ResourceGroupName -Tier $AppServicePlanTier\r + }\r + else{\r + Write-Output \"Service Plan already set up\"\r + }\r + $WebApp = Get-AzureRmWebApp -ResourceGroupName $ResourceGroupName -Name $WebAppName -ErrorAction SilentlyContinue\r + if($null -eq $WebApp)\r + {\r + Write-Output \"Creating WebApp\"\r + $webApp = New-AzureRmWebApp -Name $WebAppName -AppServicePlan $AppServicePlanName -ResourceGroupName $ResourceGroupName -Location $DeploymentLocation\r + }\r + else {\r + Write-Output \"WebApp already created\"\r + }\r + \r + Write-Output \"setting app to use $AppServiceUse32BitWorkerProcess\" \r + Set-AzureRmWebApp -ResourceGroupName $ResourceGroupName -Name $WebAppName -Use32BitWorkerProcess ([bool]$AppServiceUse32BitWorkerProcess)\r + $null = Get-AzureRmWebAppPublishingProfile -OutputFile $PublishProfilePath -ResourceGroupName $ResourceGroupName -Name $WebAppName -Format WebDeploy -Verbose\r + \r + Write-output \"profile: $(get-content $PublishProfilePath)\"\r + if (!(Test-Path -Path $PublishProfilePath)){\r + throw [System.IO.FileNotFoundException] \"$PublishProfilePath not found.\"\r + }\r +\r + get-childitem $psscriptroot\r +}\r +catch{\r + Write-Output \"Cannot add serviceplan/webapp : $AzureAppServicePlanName / $AzureWebAppName\"\r + Write-Output $_\r +\r +}\r +", + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "f6595af0-9cb5-4281-8fce-69f046c53e96", + "Name": "ResourceGroupName", + "Label": "ResourceGroupName", + "HelpText": "Enter the name of the resource group you are deploying this Web App into", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f8d7d8c4-e862-436d-bea3-aa3f9a6caec3", + "Name": "Location", + "Label": "DeploymentLocation", + "HelpText": "Enter the location (Region) where the service plan is located", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "ef49ded7-ec59-482b-83bd-e890d36103b9", + "Name": "AppServicePlanName", + "Label": "AppServicePlanName", + "HelpText": "Enter the name of the app service plan", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "554a3a1c-9235-45e9-b198-d77cd8dae52d", + "Name": "AppServicePlanTier", + "Label": "AppServicePlanTier", + "HelpText": "Enter the tier of the app service plan", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "eee220dc-1af4-44ce-9bfc-950e87e9587d", + "Name": "WebAppName", + "Label": "WebAppName", + "HelpText": "Enter the name of your web app", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1f60c02e-77d1-4f2f-9874-87da815fc157", + "Name": "AppServiceUse32BitWorkerProcess", + "Label": "AppServiceUse32BitWorkerProcess", + "HelpText": "Sets the app pool for 32-bit processes or 64-bit processes. +Use 1 for 32-bit +Use 0 for 64-bit", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "7dda30f1-94e4-4a2a-beb6-c8663924bdee", + "Name": "AzureAccount", + "Label": "AzureAccount", + "HelpText": "Enter the SPN used to connect to Azure", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2018-01-08T11:00:00.085+00:00", + "LastModifiedBy": "IanMoroney", + "$Meta": { + "ExportedAt": "2018-01-08T10:36:38.951Z", + "OctopusVersion": "3.16.2", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-create-resource-group-az-module.json.human b/step-templates/azure-create-resource-group-az-module.json.human new file mode 100644 index 000000000..ef44f9c53 --- /dev/null +++ b/step-templates/azure-create-resource-group-az-module.json.human @@ -0,0 +1,123 @@ +{ + "Id": "c2b850f7-2d9a-4cda-ab3c-ff3d229eca8e", + "Name": "Create Resource Group If Not Exists (AZ Module)", + "Description": "This step uses the new `az` modules to create a resource group if it doesn't exist. + +Requires `Octopus Deploy 2020.1` or later. Requires a worker with the `az` module installed on it. That module is not bundled with Octopus Deploy.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "Author": "octobob", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Azure.AccountId": "#{CreateResourceGroup.Azure.Account}", + "Octopus.Action.Script.ScriptBody": "$resourceGroupName = $OctopusParameters[\"CreateResourceGroup.ResourceGroup.Name\"] +$resourceGroupLocationAbbr = $OctopusParameters[\"CreateResourceGroup.ResourceGroup.Location.Abbr\"] + +$existingResourceGroups = (az group list --query \"[?location=='$resourceGroupLocationAbbr']\") | ConvertFrom-JSON + +$createResourceGroup = $true +foreach ($resourceGroupFound in $existingResourceGroups) +{\t +\tWrite-Host \"Checking if current resource group $($resourceGroupFound.name) matches $resourceGroupName\" + if ($resourceGroupFound.name -eq $resourceGroupName) + { + \t$createResourceGroup = $false + \tWrite-Highlight \"Resource group already exists, skipping creation\" + \tbreak + } +} + +if ($createResourceGroup) +{ +\tWrite-Host \"Creating the $resourceGroupName because it was not found in $resourceGroupLocationAbbr\" +\taz group create -l $resourceGroupLocationAbbr -n $resourceGroupName +}" + }, + "Parameters": [ + { + "Id": "b854aba4-4acb-428d-b981-79d1f2f50537", + "Name": "CreateResourceGroup.Azure.Account", + "Label": "Azure Account", + "HelpText": "The Azure Account with the necessary permissions to create resource groups.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "c7e43d70-f9ac-43cb-95a8-6a0e75ad40c6", + "Name": "CreateResourceGroup.ResourceGroup.Name", + "Label": "Resource Group Name", + "HelpText": "The name of the resource group to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e1bd7e57-5ad0-445b-9d11-6642acc55b88", + "Name": "CreateResourceGroup.ResourceGroup.Location.Abbr", + "Label": "The Resource Group Abbreviated Location", + "HelpText": "The abbreviated resource group location, for example: centralus", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "centralus|Americas - Central US +eastus|Americas - East US +eastus2|Americas - East US 2 +northcentralus|Americas - North Central US +southcentralus|Americas - South Central US +westus|Americas - West US +westus2|Americas - West US 2 +westcentralus|Americas - West Central US +canadacentral|Americas - Canada Central +canadaeast|Americas - Canada East +brazilsouth|Americas - Brazil South +eastasia|Asia Pacific - East Asia +southeastasia|Asia Pacific - Southeast Asia +australiacentral|Asia Pacific - Australia Central +australiacentral2|Asia Pacific - Australia Central 2 +australiaeast|Asia Pacific - Australia East +australiasoutheast|Asia Pacific - Australia Southeast +chinaeast|Asia Pacific - China East +chinaeast2|Asia Pacific - China East 2 +chinanorth|Asia Pacific - China North +chinanorth2|Asia Pacific - China North 2 +centralindia|Asia Pacific - Central India +southindia|Asia Pacific - South India +westindia|Asia Pacific - West India +japaneast|Asia Pacific - Japan East +japanwest|Asia Pacific - Japan West +koreacentral|Asia Pacific - Korea Central +koreasouth|Asia Pacific - Korea South +northeurope|Europe - North Europe +westeurope|Europe - West Europe +francecentral|Europe - France Central +francesouth|Europe - France South +germanynorth|Europe - Germany North +germanywestcentral|Europe - Germany West Central +norwayeast|Europe - Norway East +norwaywest|Europe - Norway West +spaincentral|Europe - Spain Central +switzerlandnorth|Europe - Switzerland North +switzerlandwest|Europe - Switzerland West +uksouth|Europe - UK South +ukwest|Europe - UK West +southafricanorth|Middle East and Africa - South Africa North +southafricawest|Middle East and Africa - South Africa West +uaecentral|Middle East and Africa - UAE Central +uaenorth|Middle East and Africa - UAE North" + } + } + ], + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2020-04-13T15:37:29.866Z", + "OctopusVersion": "2020.1.10", + "Type": "ActionTemplate" + }, + "Category": "azure" + } diff --git a/step-templates/azure-create-staging-deployment-slot.json.human b/step-templates/azure-create-staging-deployment-slot.json.human new file mode 100644 index 000000000..79b471e5c --- /dev/null +++ b/step-templates/azure-create-staging-deployment-slot.json.human @@ -0,0 +1,182 @@ +{ + "Id": "10f6021e-27bd-47c5-9f10-4a1599182d8a", + "Name": "Create Azure Staging Deployment Slot", + "Description": "This template will create an azure deployment slot. This step template should be placed before the \"Deploy an Azure App\" Octopus Deploy template and be used with its sister step \"Switch Azure RM Deployment Slot\" + +This should be used for green-blue deployments, as referenced in this document: https://octopus.com/docs/deploying-applications/deploying-to-azure/deploying-a-package-to-an-azure-web-app/using-deployment-slots-with-azure-web-apps + +NB: This step will promote your web app service plan to standard if it is currently using free, shared or basic tier", + "ActionType": "Octopus.AzurePowerShell", + "Version": 2, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Azure.AccountId": "#{AzureAccount}", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "###############################################\r +# Create Azure RM Staging Deployment Slot\r +###############################################\r +##Step1: Get Variables\r +$ResourceGroupName = $OctopusParameters[\"ResourceGroupName\"] \r +$AppName = $OctopusParameters[\"AppName\"] \r +$stagingSlotName = $OctopusParameters[\"SlotName\"]\r +$AppServicePlanName = $OctopusParameters[\"AppServicePlanName\"] \r +###############################################\r +###############################################\r +Function Add-DeploymentSlotFunctionaility\r +{\r + [cmdletbinding()]\r + param\r + ( \r + [string]$ResourceGroupName,\r + [string]$AppName,\r + [string]$AppServicePlanName\r + )\r + try \r + {\r + write-output \"Will make sure the service plan can support deployment slots\"\r + $servicePlan = Get-AzureRmAppServicePlan -ResourceGroupName $ResourceGroupName -Name $AppServicePlanName\r + \r + if(($servicePlan.Sku.Tier.ToLower() -eq \"free\" ) -or ($servicePlan.Sku.Tier.ToLower() -eq \"shared\" ) -or ($servicePlan.Sku.Tier.ToLower() -eq \"basic\" ))\r + {\r + Write-Warning \"Service plan does not currently support deployment slots, will now scale to standard tier\"\r + $planUpdate = Set-AzureRmAppServicePlan -ResourceGroupName $ResourceGroupName -Name $AppServicePlanName -Tier \"Standard\"\r + Write-Output \"Plan updated\"\r + $planUpdate | Out-String | Write-Verbose\r + write-output \"Plan Tier now set to:\"\r + $planUpdate.Sku | Out-String | Write-Output\r + }\r + else \r + {\r + Write-Output \"Service plan already supports deployment slots\" \r + } \r + }\r + catch \r + {\r + throw \"Error adding Deployment Slot functionailty. $_\" \r + }\r +}\r +\r +Function Invoke-RequiredVariablesCheck\r +{\r + if([string]::IsNullOrEmpty($ResourceGroupName))\r + {\r + Write-Error \"ResourceGroupName variable is not set\"\r + }\r +\r + if([string]::IsNullOrEmpty($AppName))\r + {\r + write-error \"AppName variable is not set\"\r + }\r +\r + if([string]::IsNullOrEmpty($stagingSlotName))\r + {\r + write-error \"stagingSlotName variable is not set\"\r + }\r +\r + if([string]::IsNullOrEmpty($AppServicePlanName))\r + {\r + write-error \"AppServicePlanName variable is not set\"\r + }\r + Write-Verbose \"Variables in use are:\"\r + write-verbose \"ResourceGroupName:$ResourceGroupName\"\r + write-verbose \"AppName:$AppName\"\r + write-verbose \"stagingSlotName:$stagingSlotName\"\r + write-verbose \"AppServicePlanName:$AppServicePlanName\"\r +}\r +\r +$ErrorActionPreference = \"Stop\"\r +\r +try \r +{\r + Invoke-RequiredVariablesCheck\r + Add-DeploymentSlotFunctionaility -ResourceGroupName $ResourceGroupName -AppName $AppName -AppServicePlanName $AppServicePlanName\r + Write-output \"Preparing Deployment Staging slot\"\r + $deploymentSlot = Get-AzureRmWebAppSlot -ResourceGroupName $ResourceGroupName -Name $AppName -Slot $stagingSlotName -ErrorAction SilentlyContinue\r + if($deploymentSlot.Id -eq $null)\r + {\r + Write-output \"No current deployment slot created, will create one now\"\r + New-AzureRmWebAppSlot -ResourceGroupName $ResourceGroupName -Name $AppName -Slot $stagingSlotName\r + }\r + else \r + { \r + Write-Verbose \"Current slot exists, will remove to speed up deployment\"\r + Remove-AzureRmWebAppSlot -ResourceGroupName $ResourceGroupName -Name $AppName -Slot $stagingSlotName -Force\r + Write-Verbose \"Slot removed\"\r + New-AzureRmWebAppSlot -ResourceGroupName $ResourceGroupName -Name $AppName -Slot $stagingSlotName \r + }\r + Write-Output \"Deployment slot $stagingSlotName created\"\r +}\r +catch \r +{\r + Write-Error \"Error in Create Azure RM Staging Deployment Slot step. $_\" \r +}", + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "37e14646-c52f-497a-a17b-2f17e9b1a629", + "Name": "ResourceGroupName", + "Label": "ResourceGroupName", + "HelpText": "Enter the name of the resource group you are deploying this Web App into", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "46bf87cd-342d-4095-bd2f-78d6a495faf5", + "Name": "AppServicePlanName", + "Label": "AppServicePlanName", + "HelpText": "Enter the name of the app service plan", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "54b04906-e716-4b1d-93c9-965f3a412c28", + "Name": "AppName", + "Label": "AppName", + "HelpText": "Enter the name of your web/api/etc app", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "15176529-6bc9-4e0f-82ab-b2e004959d9e", + "Name": "AzureAccount", + "Label": "AzureAccount", + "HelpText": "Enter the SPN used to connect to Azure", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "30da9c4b-1fb4-425e-a5f4-2338d24ca23b", + "Name": "SlotName", + "Label": "SlotName", + "HelpText": "Enter the name you wish to call your deployment slot", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2018-02-27T11:23:00.000+00:00", + "LastModifiedBy": "MarkDordoy", + "$Meta": { + "ExportedAt": "2018-02-27T11:24:00.844Z", + "OctopusVersion": "3.16.2", + "Type": "ActionTemplate" + }, + "Category": "azure" + } diff --git a/step-templates/azure-database-execute-sql-cmd.json.human b/step-templates/azure-database-execute-sql-cmd.json.human new file mode 100644 index 000000000..67fa65491 --- /dev/null +++ b/step-templates/azure-database-execute-sql-cmd.json.human @@ -0,0 +1,227 @@ +{ + "Id": "d09f55f6-5b32-441f-b6b2-1ee6c3e53182", + "Name": "Azure DB - Execute SQL ", + "Description": "Runs a sql command against an Azure SQL Server database. + +Adds a firewall rule to allow the machine executing the step to access the database; the rule is then removed. + +*Depends on az cli* + +*Depends on sqlserver powershell module *", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{azDbSqlCmd.azAccount}", + "Octopus.Action.Script.ScriptBody": "# insure module installed. Designed to run on cloud infrastructure where owners doesn't have controll over the workers. + +if (Get-Module -ListAvailable -Name sqlserver) +{ +\tWrite-Verbose \"sqlserver module installed and available\" + Import-Module sqlserver +} + +else +{ +\tWrite-Warning \"installing module for the current user.`nIf worker is owned, consider installing the powershell module 'sqlserver' globally to speed up deployments\" +\tInstall-Module -Name sqlserver -Scope CurrentUser -AllowClobber -Force +} + +# parse parameters + +$resourceGroup = $OctopusParameters[\"azDbSqlCmd.resourceGroupName\"] +$sqlServerName = $OctopusParameters[\"azDbSqlCmd.ServerName\"] +$dbName = $OctopusParameters[\"azDbSqlCmd.dbName\"] +$userId = $OctopusParameters[\"azDbSqlCmd.userId\"] +$userPwd = $OctopusParameters[\"azDbSqlCmd.userPwd\"] +$authType = $OctopusParameters[\"azDbSqlCmd.AuthType\"] +$connTimeout = $OctopusParameters[\"azDbSqlCmd.connectionTimeout\"] -as [int] +$resultsOutput = $OctopusParameters[\"azDbSqlCmd.resultsOutput\"] + +$sqlCmd = $OctopusParameters[\"azDbSqlCmd.sqlCmd\"] + +# get current IP address +Write-Host \"Getting worker IP address...\" -NoNewLine +$workerPublicIp = (Invoke-WebRequest -uri \"http://ifconfig.me/ip\" -UseBasicParsing).Content +Write-Host \"Done. IP is: $workerPublicIp\" + +# create Connection string +switch ($authType) +{ +\t\"sql\" + { + \t$connectionString = \"Server=tcp:$sqlServerName.database.windows.net;Initial Catalog=$dbName;Persist Security Info=False;User ID=$userId;Password=$userPwd;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;\" + } + \"adPwd\" + { + \t$connectionString = \"Server=tcp:$sqlServerName.database.windows.net;Initial Catalog=$dbName;Persist Security Info=False;User ID=$userId;Password=$userPwd;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Authentication=`\"Active Directory Password`\";\" + } + \"ad\" + { + \t$connectionString = \"Server=tcp:$sqlServerName.database.windows.net;Initial Catalog=$dbName;Persist Security Info=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Authentication=`\"Active Directory Integrated`\";\" + } +} + +# open firewall port +write-host \"opening firewall on server $sqlServerName for ip: $workerPublicIp\" +Invoke-Expression \"az sql server firewall-rule create -g $resourceGroup -n tempOctoSqlCmd -s $sqlServerName --start-ip-address $workerPublicIp --end-ip-address $workerPublicIp\" + +# invoke sql cmd +try +{ +\t$id = New-Guid + $resultFilePath = \"$env:temp/$id.txt\" +\tWrite-Host \"running sql statement: ``$sqlCmd``\" + + switch ($resultsOutput) + { + 'none' + { + \tInvoke-SqlCmd -ConnectionString $connectionString -Query $sqlCmd -QueryTimeout $connTimeout + } + + 'variable' + { + \tInvoke-SqlCmd -ConnectionString $connectionString -Query $sqlCmd -QueryTimeout $connTimeout | ConvertTo-CSV | Out-File -FilePath \"$resultFilePath\" + $outputContent = Get-Content -Path $resultFilePath | ConvertFrom-CSV + Set-OctopusVariable -name \"azDbSqlCmd.results\" + } + + 'artifact' + { + \tInvoke-SqlCmd -ConnectionString $connectionString -Query $sqlCmd -QueryTimeout $connTimeout | ConvertTo-CSV | Out-File -FilePath \"$resultFilePath\" + New-OctopusArtifact -Path $resultFilePath -Name azDbSqlCmd.results.csv + } + } +} +catch +{ +\tthrow +} +finally +{ + # close firewall port + write-host \"closing firewall on server $sqlServerName for ip: $workerPublicIp\" + Invoke-Expression \"az sql server firewall-rule delete -g $resourceGroup -n tempOctoSqlCmd -s $sqlServerName\" +} +" + }, + "Parameters": [ + { + "Id": "4f94e536-a48f-4d9a-854d-c02ca56d6ef2", + "Name": "azDbSqlCmd.azAccount", + "Label": "Azure Account", + "HelpText": "An Azure account with permissions to the subscription and the sql server being targeted", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "068237eb-7329-426e-8dee-72ed96eb3e32", + "Name": "azDbSqlCmd.resourceGroupName", + "Label": "Resource Group Name", + "HelpText": "The name of the resource group hosting the sql server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "12590219-613f-4556-908a-cd87c509449f", + "Name": "azDbSqlCmd.ServerName", + "Label": "SQL Server Name", + "HelpText": "The name of the sql server. The FQDN (`database.windows.net`) will automatically be appended to this when needed.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3fe35f76-f140-48df-be0c-2a927069bc8b", + "Name": "azDbSqlCmd.dbName", + "Label": "Database Name", + "HelpText": "The name of the database to execute the sql command against", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bbe9a203-89db-4286-a684-c95748543805", + "Name": "azDbSqlCmd.AuthType", + "Label": "SQL Server Authentication Type", + "HelpText": "The type of authentication to use when connecting.", + "DefaultValue": "sql", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "sql|SQL Authentication +adPwd|Active Directory Password +ad|Active Directory Integration" + } + }, + { + "Id": "539885ef-8ff9-407e-8e33-3bf211d58df6", + "Name": "azDbSqlCmd.userId", + "Label": "User Id", + "HelpText": "The user id to use when authenticating with the sql server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "60af2045-ec59-4f4f-baf4-79e0ae8bd60d", + "Name": "azDbSqlCmd.userPwd", + "Label": "User Password", + "HelpText": "Used with SQL or Active directory Password authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e631537b-c109-4678-a7e5-bbc8166cca78", + "Name": "azDbSqlCmd.connectionTimeout", + "Label": "Connection Timeout", + "HelpText": "The timeout for the query measured in seconds between 0 and 65534. 0 indicates no timeout", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9f787938-012a-45e9-ae58-5a06b1f4f012", + "Name": "azDbSqlCmd.sqlCmd", + "Label": "SQL Command", + "HelpText": "The sql command to execute", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "273c678e-008e-4598-978e-9d7abaadbf6c", + "Name": "azDbSqlCmd.resultsOutput", + "Label": "Results Output", + "HelpText": "How should the results of the sql statement be retained. Results are only provided with select statements.", + "DefaultValue": "none", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "none|None +variable|Output Variable +artifact|Process Artifact" + } + } + ], + "$Meta": { + "ExportedAt": "2020-07-22T16:38:39.012Z", + "OctopusVersion": "2020.2.10", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "xtreampb", + "Category": "azure" +} diff --git a/step-templates/azure-database-export.json.human b/step-templates/azure-database-export.json.human new file mode 100644 index 000000000..c961f2bed --- /dev/null +++ b/step-templates/azure-database-export.json.human @@ -0,0 +1,176 @@ +{ + "Id": "c1d6e994-16c8-4896-b6f2-57f9558184ea", + "Name": "Azure Database - Export", + "Description": "Exports a database to a bacpac + +*Depends on az cli* + +*Source database requires 'Allow Azure services and resources to access this server' option turn on in the SQL server firewall*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{azDbExport.AzAcct}", + "Octopus.Action.Script.ScriptBody": "$rgname = $OctopusParameters[\"azDbExport.rgName\"] +$svrName = $OctopusParameters[\"azDbExport.sqlSvrName\"] +$dbName = $OctopusParameters[\"azDbExport.dbName\"] +$adminName = $OctopusParameters[\"azDbExport.adminName\"] +$adminPwd = $OctopusParameters[\"azDbExport.adminPwd\"] +$accessKey = $OctopusParameters[\"azDbExport.blobAccessKey\"] +$accessKeyType = $OctopusParameters[\"azDbExport.accessKeyType\"] +$containerUri = $OctopusParameters[\"azDbExport.ContainerUri\"] +$backupName = $OctopusParameters[\"azDbExport.backupName\"] + +$backupUri = \"$containerUri/$backupName.bacpac\" + +if([string]::IsNullOrEmpty($rgname)) +{ +\tthrow \"resource group name is not provided\" +} + +if([string]::IsNullOrEmpty($svrName)) +{ +\tthrow \"sql server name is not provided\" +} + +if([string]::IsNullOrEmpty($dbName)) +{ +\tthrow \"database name not provided\" +} +# admin name, password and access key will not be validated in favor of security + +if([string]::IsNullOrEmpty($accessKeyType)) +{ +\tthrow \"access key type not provided\" +} + +if([string]::IsNullOrEmpty($containerUri)) +{ +\tthrow \"containerUri not provided\" +} + +if([string]::IsNullOrEmpty($backupName)) +{ +\tthrow \"backup name not provided\" +} + +write-host \"starting db export\" +az sql db export --resource-group $rgname --server $svrName --name $dbName --admin-password $adminPwd --admin-user $adminName --storage-key $accessKey --storage-key-type $accessKeyType --storage-uri $backupUri +" + }, + "Parameters": [ + { + "Id": "0bf888db-667e-4303-a4bd-c721bf1de0ad", + "Name": "azDbExport.AzAcct", + "Label": "Azure Account", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "d5885f12-d921-4ce8-afc2-384f0b82daae", + "Name": "azDbExport.rgName", + "Label": "Resource Group Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "03b281f5-e1e2-42a8-bb43-317bc4981393", + "Name": "azDbExport.sqlSvrName", + "Label": "SQL Server Name", + "HelpText": "The name of the SQL Server to connect to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e67ef4ab-3084-45e7-81df-7cda76baf2fc", + "Name": "azDbExport.dbName", + "Label": "Database Name", + "HelpText": "The name of the database you wish to export", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6da5679d-1c84-4fb4-8f91-22fd91b50baa", + "Name": "azDbExport.adminName", + "Label": "SQL Server Admin Name", + "HelpText": "The admin name of the SQL server containing the database you wish to export", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "44e1a2a3-6b9b-44a5-893d-d26f14166b52", + "Name": "azDbExport.adminPwd", + "Label": "Admin Password", + "HelpText": "The admin password of the SQL server containing the database you wish to export", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "40715e88-4892-48c6-83f2-603c0193439b", + "Name": "azDbExport.blobAccessKey", + "Label": "Azure Blob Access Key", + "HelpText": "the access key (Shared Access Key or Storage Access Key) to grant access to the storage account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "fcb94050-5f81-410e-9ed9-c5f52653f9c6", + "Name": "azDbExport.accessKeyType", + "Label": "Access Key Type", + "HelpText": "The type of key the access key represents", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "SharedAccessKey|Shared Access Key +StorageAccessKey|Storage Access Key" + } + }, + { + "Id": "a8631db7-556e-4532-aacd-595266c4347f", + "Name": "azDbExport.ContainerUri", + "Label": "Container Uri", + "HelpText": "The URI of the container to save the exported database in. Format is: `https://{StorageAccountName}.blob.core.windows.net/{ContainerName}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "85686923-5e57-4b1d-8dd7-6b5ca263ba42", + "Name": "azDbExport.backupName", + "Label": "Backup Name", + "HelpText": "The name of the file being saved. The bacpac extension (`.bacpac`) will be appended automatically. Defaults to DB Name", + "DefaultValue": "#{azDbExport.dbName}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-23T21:41:09.205Z", + "OctopusVersion": "2020.2.10", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "Category": "azure" +} diff --git a/step-templates/azure-database-import-create-new-DTU.json.human b/step-templates/azure-database-import-create-new-DTU.json.human new file mode 100644 index 000000000..c9d6a10b8 --- /dev/null +++ b/step-templates/azure-database-import-create-new-DTU.json.human @@ -0,0 +1,318 @@ +{ + "Id": "cc3b7dd9-f107-4477-acc4-4d1ffdf9e820", + "Name": "Azure Database - Import Create New DTU", + "Description": "restores a bacpac to a new database + +*Depends on az cli* + +*Source database requires 'Allow Azure services and resources to access this server' option turn on in the SQL server firewall*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#get variables into easy to use format +# Create DB Variables +$databaseName = $OctopusParameters[\"azDbImportNewDTU.dbName\"] +$sqlServer = $OctopusParameters[\"azDbImportNewDTU.server\"] +$rgName = $OctopusParameters[\"azDbImportNewDTU.resourceGroup\"] +$elasticPool = $OctopusParameters[\"azDbImportNewDTU.elasticPool\"] +$readScaleTruthy = $OctopusParameters[\"azDbImportNewDTU.readScale\"] +$serviceObjective = $OctopusParameters[\"azDbImportNewDTU.serviceObjective\"] +$tags = $OctopusParameters[\"azDbImportNewDTU.tags\"] +$zoneRedundant = $OctopusParameters[\"azDbImportNewDTU.zoneRedundant\"] +$maxSize = $OctopusParameters[\"azDbImportNewDTU.maxSize\"] + +# Import bacpac variables +$adminName = $OctopusParameters[\"azDbImportNewDTU.adminName\"] +$adminPwd = $OctopusParameters[\"azDbImportNewDTU.adminPwd\"] +$accessKey = $OctopusParameters[\"azDbImportNewDTU.blobAccessKey\"] +$accessKeyType = $OctopusParameters[\"azDbImportNewDTU.accessKeyType\"] +$containerUri = $OctopusParameters[\"azDbImportNewDTU.ContainerUri\"] +$backupName = $OctopusParameters[\"azDbImportNewDTU.backupName\"] +$backupUri = \"$containerUri/$backupName.bacpac\" + +$readScaleValue = \"Disabled\" + +if($readScaleTruthy -eq \"true\") { $readScalevalue = \"Enabled\" } + +$ServiceObjectiveSizes = @{Basic = 2GB; S0 = 250GB; S1 = 250GB; S2 = 250GB; S3 = 1TB; S4 = 1TB; S6 = 1TB; S7 = 1TB; S9 = 1TB; S12 = 1TB; P1 = 1TB; P2 = 1TB; P4 = 1TB; P6 = 1TB; P11 = 4TB; P15 = 4TB} + +if($null -eq (az sql server list --query \"[?Name==$sqlServer]\" | ConvertFrom-Json)) +{ + throw \"$sqlServer doesn't exist or the selected azure account doesn't have access to it.\" +} + +if($null -ne (az sql db list --resource-group $rgName --server $sqlServer --query \"[?Name==$databaseName]\" | ConvertFrom-Json)) +{ + throw \"$databaseName already exists\" +} + +#validate parameters + +if(($maxSize / 1GB) -gt ($ServiceObjectiveSizes[$serviceObjective] / 1GB)) +{ + Write-Warning \"Desired max size of $($maxSize / 1GB)GB exceeds max size of $($ServiceObjectiveSizes[$serviceObjective] / 1GB)GB for selected service objective: $serviceObjective\" + Write-Warning \"Setting max size to $($ServiceObjectiveSizes[$serviceObjective] / 1GB)GB\" + $maxSize = \"$($ServiceObjectiveSizes[$serviceObjective] / 1GB)GB\" +} + +if([string]::IsNullOrEmpty($rgname)) +{ +\tthrow \"resource group name is not provided\" +} + +if([string]::IsNullOrEmpty($sqlServer)) +{ +\tthrow \"sql server name is not provided\" +} + +if([string]::IsNullOrEmpty($databaseName)) +{ +\tthrow \"database name not provided\" +} + +# admin name, password and access key will not be validated in favor of security + +if([string]::IsNullOrEmpty($accessKeyType)) +{ +\tthrow \"access key type not provided\" +} + +if([string]::IsNullOrEmpty($containerUri)) +{ +\tthrow \"containerUri not provided\" +} + +if([string]::IsNullOrEmpty($backupName)) +{ +\tthrow \"backup name not provided\" +} + +# validate premium SKU settings +if(!$serviceObjective.Contains('P')) +{ + if($readScaleValue -eq \"Enabled\") + { + Write-Warning \"Read Scaling only available for premium SKUs. Setting database read scale to disabled\" + $readScaleValue = \"Disabled\" + } + if($zoneRedundant -eq \"true\") + { + Write-Warning \"Zone redundant only available for premium SKUs. Setting database zone redundant to false\" + $zoneRedundant = \"false\" + } +} + +$cliArgs = \"--name $databaseName --resource-group $rgName --server $sqlServer\" + +if($elasticPool) {$cliArgs += \" --elastic-pool $elasticPool\"} +else {$cliArgs += \" --max-size $maxSize --service-objective $serviceObjective --zone-redundant $zoneRedundant\"} + +if($tags) {$cliArgs += \" --tags $tags\"} +if($readScale) {$cliArgs += \" --read-scale $readScaleValue\"} + + +$cmd = \"az sql db create $cliArgs\" + +write-verbose \"cmd is: $cmd\" + +Write-Host \"Creating Database\" +Invoke-Expression $cmd + +write-host \"starting db import\" +write-verbose \"import cmd az sql db import --resource-group $rgname --server $sqlServer --name $databaseName --admin-password $adminPwd --admin-user $adminName --storage-key $accessKey --storage-key-type $accessKeyType --storage-uri $backupUri\" + +az sql db import --resource-group $rgname --server $sqlServer --name $databaseName --admin-password $adminPwd --admin-user $adminName --storage-key $accessKey --storage-key-type $accessKeyType --storage-uri $backupUri +", + "Octopus.Action.Azure.AccountId": "azureserviceprincipal-chris-azure-account-spaces-1" + }, + "Parameters": [ + { + "Id": "918c8c99-4afe-4d8b-8c3d-3f7d5443a951", + "Name": "azDbImportNewDTU.azAccount", + "Label": "Azure Account", + "HelpText": "Azure account with permissions to the subscription and resource group", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "dcf78060-568c-4717-87d5-d1d2d1044418", + "Name": "azDbImportNewDTU.resourceGroup", + "Label": "Resource Group", + "HelpText": "Resource group name housing the target SQL server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2a35cc98-c991-4bbf-bf80-b6fb74b5d066", + "Name": "azDbImportNewDTU.server", + "Label": "SQL Server", + "HelpText": "Name of the Azure SQL server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "aad2ddb7-ea11-4e7b-b90a-8ccb354e4a60", + "Name": "azDbImportNewDTU.dbName", + "Label": "Database Name", + "HelpText": "Name of the database that will be created", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1c794a0d-d7f2-4757-875a-e2e0148e8fd1", + "Name": "azDbImportNewDTU.serviceObjective", + "Label": "Service Objective", + "HelpText": "The service objective for the new database", + "DefaultValue": "Basic", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Basic|Basic +S0|S0 +S1|S1 +S3|S3 +S4|S4 +S6|S6 +S7|S7 +S9|S9 +S12|S12 +P1|P1 +P2|P2 +P4|P4 +P6|P6 +P11|P11 +P15|P15" + } + }, + { + "Id": "81cef05c-98d2-4e82-bc78-f1bcb6536291", + "Name": "azDbImportNewDTU.elasticPool", + "Label": "Elastic Pool", + "HelpText": "The name or resource id of the elastic pool to create the database in", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "48128da8-94e8-4560-8fa7-7c30e68d1849", + "Name": "azDbImportNewDTU.readScale", + "Label": "Read Scale", + "HelpText": "If enabled, connections that have application intent set to readonly in their connection string may be routed to a readonly secondary replica. This property is only settable for Premium and Business Critical databases", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "4a52b908-7515-4089-91ad-0736e6bb687c", + "Name": "azDbImportNewDTU.zoneRedundant", + "Label": "Zone Redundant", + "HelpText": "Specifies whether to enable zone redundancy", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "948d5fa8-7d9a-4d51-bcd3-6ac6eaf37d3b", + "Name": "azDbImportNewDTU.maxsize", + "Label": "Max Storage Size", + "HelpText": "The max storage size. If no unit is specified, defaults to bytes (B)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4bc52675-3963-4bdd-9ca4-b2611dc968eb", + "Name": "azDbImportNewDTU.tags", + "Label": "Tags", + "HelpText": "Space-separated tags. `key[=value] key[=value]`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "40715e88-4892-48c6-83f2-603c0193439b", + "Name": "azDbImportNewDTU.blobAccessKey", + "Label": "Azure Blob Access Key", + "HelpText": "the access key (Shared Access Key or Storage Access Key) to grant access to the storage account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "fcb94050-5f81-410e-9ed9-c5f52653f9c6", + "Name": "azDbImportNewDTU.accessKeyType", + "Label": "Access Key Type", + "HelpText": "The type of key the access key represents", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "SharedAccessKey|Shared Access Key +StorageAccessKey|Storage Access Key" + } + }, + { + "Id": "a8631db7-556e-4532-aacd-595266c4347f", + "Name": "azDbImportNewDTU.ContainerUri", + "Label": "Container Uri", + "HelpText": "The URI of the container to save the exported database in. Format is: `https://{StorageAccountName}.blob.core.windows.net/{ContainerName}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "85686923-5e57-4b1d-8dd7-6b5ca263ba42", + "Name": "azDbImportNewDTU.backupName", + "Label": "Backup Name", + "HelpText": "The name of the file being saved. Defaults to DB Name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6da5679d-1c84-4fb4-8f91-22fd91b50baa", + "Name": "azDbImportNewDTU.adminName", + "Label": "SQL Server Admin Name", + "HelpText": "The admin name of the SQL server containing the database you wish to import", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "44e1a2a3-6b9b-44a5-893d-d26f14166b52", + "Name": "azDbImportNewDTU.adminPwd", + "Label": "Admin Password", + "HelpText": "The admin password of the SQL server containing the database you wish to import", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-23T20:12:56.412Z", + "OctopusVersion": "2020.2.10", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "xtreampb", + "Category": "azure" +} diff --git a/step-templates/azure-database-import-create-new-vcpu.json.human b/step-templates/azure-database-import-create-new-vcpu.json.human new file mode 100644 index 000000000..02870676f --- /dev/null +++ b/step-templates/azure-database-import-create-new-vcpu.json.human @@ -0,0 +1,477 @@ +{ + "Id": "4f03ce31-a9ce-4aff-8c7b-5144af5401a1", + "Name": "Azure Database - Import Create New vcpu", + "Description": "Restores a bacpac to a new database + +*Depends on az cli* + +*Source database requires 'Allow Azure services and resources to access this server' option turn on in the SQL server firewall*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#get variables into easy to use format + +# vCPU vars +$family = $OctopusParameters[\"azDbImportNewVCPU.family\"] # Gen4 is being phased out. +$computeModel = $OctopusParameters[\"azDbImportNewVCPU.computeModel\"] +$readReplicaCount = $OctopusParameters[\"azDbImportNewVCPU.readReplicaCount\"] +$edition = $OctopusParameters[\"azDbImportNewVCPU.edition\"] +$capacity = $OctopusParameters[\"azDbImportNewVCPU.coreCount\"] -as [int] + +# Create DB Variables +$databaseName = $OctopusParameters[\"azDbImportNewVCPU.dbName\"] +$sqlServer = $OctopusParameters[\"azDbImportNewVCPU.server\"] +$rgName = $OctopusParameters[\"azDbImportNewVCPU.resourceGroup\"] +$elasticPool = $OctopusParameters[\"azDbImportNewVCPU.elasticPool\"] +$readScaleTruthy = $OctopusParameters[\"azDbImportNewVCPU.readScale\"] + +$tags = $OctopusParameters[\"azDbImportNewVCPU.tags\"] +$zoneRedundant = $OctopusParameters[\"azDbImportNewVCPU.zoneRedundant\"] +$maxSize = $OctopusParameters[\"azDbImportNewVCPU.maxSize\"] + +# Import bacpac variables +$adminName = $OctopusParameters[\"azDbImportNewVCPU.adminName\"] +$adminPwd = $OctopusParameters[\"azDbImportNewVCPU.adminPwd\"] +$accessKey = $OctopusParameters[\"azDbImportNewVCPU.blobAccessKey\"] +$accessKeyType = $OctopusParameters[\"azDbImportNewVCPU.accessKeyType\"] +$containerUri = $OctopusParameters[\"azDbImportNewVCPU.ContainerUri\"] +$backupName = $OctopusParameters[\"azDbImportNewVCPU.backupName\"] +$backupUri = \"$containerUri/$backupName.bacpac\" + +$readScaleValue = \"Disabled\" + +if($readScaleTruthy -eq \"true\") { $readScalevalue = \"Enabled\" } +$maxAvailableSize = 1TB + +$gen5VcpuCount = 2,4,6,8,10,12,14,16,18,20,24,32,40,80 +$gen5VcpuCountSvrless = 1,2,4,6,8,10,12,14,16,18,20,24,32,40 + +#validate parameters + +if($null -eq (az sql server list --query \"[?Name==$sqlServer]\" | ConvertFrom-Json)) +{ + throw \"$sqlServer doesn't exist or the selected azure account doesn't have access to it.\" +} + +if($null -ne (az sql db list --resource-group $rgName --server $sqlServer --query \"[?Name==$databaseName]\" | ConvertFrom-Json)) +{ + throw \"$database already exists\" +} + +# max size for all databases (except GP serverless) is 1TB + +if([string]::IsNullOrEmpty($rgName)) +{ +\tthrow \"resource group name is not provided\" +} + +if([string]::IsNullOrEmpty($sqlServer)) +{ +\tthrow \"sql server name is not provided\" +} + +if([string]::IsNullOrEmpty($databaseName)) +{ +\tthrow \"database name not provided\" +} + +# admin name, password and access key will not be validated in favor of security + +if([string]::IsNullOrEmpty($accessKeyType)) +{ +\tthrow \"access key type not provided\" +} + +if([string]::IsNullOrEmpty($containerUri)) +{ +\tthrow \"containerUri not provided\" +} + +if([string]::IsNullOrEmpty($backupName)) +{ +\tthrow \"backup name not provided\" +} + +switch($edition) +{ + \"GeneralPurpose\" + { + switch($computeModel) + { + \"Provisioned\" + { + switch($family) + { + \"Gen5\" + { + \twrite-verbose \"Capacity set to: $capacity\" + if($capacity -lt 2) + { + \tWrite-Warning \"Minimum vCPU for provisioned is 2\" + Write-Warning \"setting vCPU to 2\" + $capacity = 2 + } + if(!$gen5VcpuCount.Contains($capacity)) + { + throw \"Invalid max vCPU count entered valid values for Gen5 hardware is: $gen5VcpuCount\" + } + $maxAvailableSize = 1TB + } + \"FSv2\" + { + $capacity = 72 + $maxAvailableSize = 4TB + } + \"Default\" + { + throw \"Invalid hardware family selected for General purpose\" + } + } + } + \"Serverless\" + { + if($capacity -gt 40) + { + Write-Warning \"Max vCPUs for serverless is 40\" + Write-Warning \"Setting max vCPU to 40\" + $capacity = 40 + } + + if($family -ne \"Gen5\") {throw \"Only Gen5 hardware family available for serverless\"} + + if(!$gen5VcpuCountSvrless.Contains($capacity)) + { + throw \"Invalid max vCPU count entered valid values for serverless Gen5 hardware is: $gen5VcpuCountSvrless\" + } + $maxAvailableSize = 512GB + } + } + } + \"Hyperscale\" + { + if($family -ne \"Gen5\") {throw \"Only Gen5 hardware family available for Hyperscale\"} + + if($capacity -lt 2) + { + Write-Warning \"Minimum vCPU for provisioned is 2\" + Write-Warning \"setting vCPU to 2\" + $capacity = 2 + } + + if(!$gen5VcpuCount.Contains($capacity)) + { + throw \"Invalid max vCPU count entered valid values for Gen5 hardware is: $gen5VcpuCount\" + } + } + \"BusinessCritical\" + { + switch ($family) + { + \"Gen5\" + { + if($capacity -lt 2) + { + Write-Warning \"Minimum vCPU for provisioned is 2\" + Write-Warning \"setting vCPU to 2\" + $capacity = 2 + } + + if(!$gen5VcpuCount.Contains($capacity)) + { + throw \"Invalid max vCPU count entered valid values for Gen5 hardware is: $gen5VcpuCount\" + } + $maxAvailableSize = 1TB + } + \"M\" + { + $capacity = 128 + $maxAvailableSize = 4TB + if($zoneRedundant -eq \"true\") + { + Write-Warning \"Zone redundant not available for M-Series hardware configuration\" + Write-Warning \"Setting zone redundant to false\" + $zoneRedundant = \"false\" + } + } + } + } +} + +if(($maxSize / 1GB) -gt ($maxAvailableSize / 1GB)) +{ + Write-Warning \"Desired max size of $($maxSize / 1GB)GB exceeds available max size of $($maxAvailableSize / 1GB)GB\" + Write-Warning \"Setting max size to $($maxAvailableSize / 1GB)GB\" + $maxSize = $maxAvailableSize +} + +$cliArgs = \"--name $databaseName --resource-group $rgName --server $sqlServer\" + +if($elasticPool) {$cliArgs += \" --elastic-pool $elasticPool\"} +else {$cliArgs += \" --edition $edition --family $family --capacity $capacity\"} + +if((!$edition -eq \"Hyperscale\") -and $maxSize) {$cliArgs += \" --max-size $maxSize\"} +if($edition -eq \"GeneralPurpose\") {$cliArgs += \" --compute-model $computeModel\"} +if($edition -eq \"Hyperscale\") {cliArgs += \" --read-replicas $readReplicaCount\"} +if($tags) {$cliArgs += \" --tags $tags\"} +if($edition -eq \"BusinessCritical\") {$cliArgs += \" --read-scale $readScaleValue --zone-redundant $zoneRedundant\"} +if($elasticPool) {$cliArgs += \" --elastic-pool $elasticPool\"} + +$cmd = \"az sql db create $cliArgs\" + +write-verbose \"cmd is: $cmd\" + +Write-Host \"Creating Database\" +invoke-expression \"$cmd\" + +write-host \"starting db import\" +write-verbose \"import cmd az sql db import --resource-group $rgname --server $sqlServer --name $databaseName --admin-password $adminPwd --admin-user $adminName --storage-key $accessKey --storage-key-type $accessKeyType --storage-uri $backupUri\" + +az sql db import --resource-group $rgname --server $sqlServer --name $databaseName --admin-password $adminPwd --admin-user $adminName --storage-key $accessKey --storage-key-type $accessKeyType --storage-uri $backupUri +", + "Octopus.Action.Azure.AccountId": "#{azDbImportNewVCPU.azAccount}" + }, + "Parameters": [ + { + "Id": "a7ee5061-2e7a-4b1a-876c-21bee392e455", + "Name": "azDbImportNewVCPU.azAccount", + "Label": "Azure Account", + "HelpText": "Azure account with permissions to the subscription and resource group", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "0f280e95-7f8b-4864-bd1d-6af9930257ec", + "Name": "azDbImportNewVCPU.resourceGroup", + "Label": "Resource Group", + "HelpText": "Resource group name housing the target SQL server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "12c084b7-1a60-4076-91e4-6471a57f8978", + "Name": "azDbImportNewVCPU.server", + "Label": "SQL Server", + "HelpText": "Name of the Azure SQL server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7e8e96ee-63d6-4b6b-ab69-da69bf35a798", + "Name": "azDbImportNewVCPU.dbName", + "Label": "Database Name", + "HelpText": "Name of the database that will be created", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a7db94c4-7954-4bbb-af9f-b25f0d282978", + "Name": "azDbImportNewVCPU.edition", + "Label": "Edition", + "HelpText": "The edition component of the sku", + "DefaultValue": "GeneralPurpose", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "GeneralPurpose|General Purpose +BusinessCritical|Business Critical +Hyperscale|Hyperscale" + } + }, + { + "Id": "20568782-31c5-496a-8885-f69f4240a9b4", + "Name": "azDbImportNewVCPU.computeModel", + "Label": "Compute Model", + "HelpText": "The compute model of the database. Only applicable for general purpose instances", + "DefaultValue": "Provisioned", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Provisioned|Provisioned +Serverless|Serverless" + } + }, + { + "Id": "c99d1edc-62c4-4d3a-9896-8a7647a42783", + "Name": "azDbImportNewVCPU.family", + "Label": "Hardware Configuration", + "HelpText": "Select Between Gen5, Fsv2 , or M-Series. Please note the documented vCPU overrides if listed.", + "DefaultValue": "Gen5", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Gen5|Gen5 +Fsv2|Fsv2 (General Purpose, Compute Optimized 72 vCPUs) +M|M-Series (Business Critical, Memory Opimized 128 vCPUs)" + } + }, + { + "Id": "d869775f-4bf4-4497-a1eb-c83fddf4c215", + "Name": "azDbImportNewVCPU.coreCount", + "Label": "vCPU Core Count", + "HelpText": "the number of vCores to use. +*General purpose FSv2 hardware is set to 72*", + "DefaultValue": "2", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "1|1 (Only available for general purpose, serverless instances) +2|2 +4|4 +6|6 +8|8 +10|10 +12|12 +14|14 +16|16 +18|18 +20|20 +24|24 +32|32 +40|40 +80|80 (Not available for general purpose, serverless instances)" + } + }, + { + "Id": "fd690b8e-8467-4872-a8b9-b6b046f8c91d", + "Name": "azDbImportNewVCPU.maxsize", + "Label": "Max Storage Size", + "HelpText": "The max storage size. If no unit is specified, defaults to bytes (B)", + "DefaultValue": "1GB", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ff278593-3b7a-4923-b15e-3dc958c87496", + "Name": "azDbImportNewVCPU.elasticPool", + "Label": "Elastic Pool", + "HelpText": "The name or resource id of the elastic pool to create the database in", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "26414a8b-d167-4b94-9142-fa9bf1c984fe", + "Name": "azDbImportNewVCPU.zoneRedundant", + "Label": "Zone Redundant", + "HelpText": "Specifies whether to enable zone redundancy. This property is only applied to Business Critical databases.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "a2e4e51d-8a3b-4447-92f4-39e83f82339e", + "Name": "azDbImportNewVCPU.readScale", + "Label": "Read Scale", + "HelpText": "If enabled, connections that have application intent set to read only in their connection string may be routed to a read only secondary replica. This property is only applied to Business Critical databases.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "61ef6953-b59c-44f4-80a4-a005feba912c", + "Name": "azDbImportNewVCPU.readReplicaCount", + "Label": "Read Reaplica Count", + "HelpText": "The number of readonly replicas to provision for the database. This property is only applied to hyperscale databases.", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "0|0 +1|1 +2|2 +3|3 +4|4" + } + }, + { + "Id": "713515d2-9dd1-4d63-8a66-14cfd08a7065", + "Name": "azDbImportNewVCPU.tags", + "Label": "Tags", + "HelpText": "Space-separated tags. `key[=value] key[=value]`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "53913bf4-b70e-455f-8dbe-7002e3584ef0", + "Name": "azDbImportNewVCPU.blobAccessKey", + "Label": "Azure Blob Access Key", + "HelpText": "the access key (Shared Access Key or Storage Access Key) to grant access to the storage account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "9c7cdde5-e5a1-4566-80f2-f9f5391c398b", + "Name": "azDbImportNewVCPU.accessKeyType", + "Label": "Access Key Type", + "HelpText": "The type of key the access key represents", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "SharedAccessKey|Shared Access Key +StorageAccessKey|Storage Access Key" + } + }, + { + "Id": "fda41fbc-36cc-47b2-864f-2670144ad3e7", + "Name": "azDbImportNewVCPU.ContainerUri", + "Label": "Container Uri", + "HelpText": "The URI of the container to save the exported database in. Format is: `https://{StorageAccountName}.blob.core.windows.net/{ContainerName}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0a0da7eb-afee-4595-a073-993a9e951838", + "Name": "azDbImportNewVCPU.backupName", + "Label": "Backup Name", + "HelpText": "The name of the file being saved. Defaults to DB Name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8a601118-6e0d-4340-badd-c66d5c8101dd", + "Name": "azDbImportNewVCPU.adminName", + "Label": "SQL Server Admin Name", + "HelpText": "The admin name of the SQL server containing the database you wish to import", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f3f441ba-e92e-4cf2-bab9-0477591bc907", + "Name": "azDbImportNewVCPU.adminPwd", + "Label": "SQL Server Admin Password", + "HelpText": "The admin password of the SQL server containing the database you wish to import", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-23T20:13:26.129Z", + "OctopusVersion": "2020.2.10", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "Your GitHub Username", + "Category": "other" + } diff --git a/step-templates/azure-database-import.json.human b/step-templates/azure-database-import.json.human new file mode 100644 index 000000000..51b417992 --- /dev/null +++ b/step-templates/azure-database-import.json.human @@ -0,0 +1,177 @@ +{ + "Id": "5b4544ed-987b-4532-9d6d-7c7a2ce5bd00", + "Name": "Azure Database - Import", + "Description": "Imports a bacpac file into an existing database. + +*depends on az cli* + +*Target database requires 'Allow Azure services and resources to access this server' option turn on in the SQL server firewall*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{azDbImport.AzAcct}", + "Octopus.Action.Script.ScriptBody": "$rgname = $OctopusParameters[\"azDbImport.rgName\"] +$svrName = $OctopusParameters[\"azDbImport.sqlSvrName\"] +$dbName = $OctopusParameters[\"azDbImport.dbName\"] +$adminName = $OctopusParameters[\"azDbImport.adminName\"] +$adminPwd = $OctopusParameters[\"azDbImport.adminPwd\"] +$accessKey = $OctopusParameters[\"azDbImport.blobAccessKey\"] +$accessKeyType = $OctopusParameters[\"azDbImport.accessKeyType\"] +$containerUri = $OctopusParameters[\"azDbImport.ContainerUri\"] +$backupName = $OctopusParameters[\"azDbImport.backupName\"] + +$backupUri = \"$containerUri/$backupName.bacpac\" + +if([string]::IsNullOrEmpty($rgname)) +{ +\tthrow \"resource group name is not provided\" +} + +if([string]::IsNullOrEmpty($svrName)) +{ +\tthrow \"sql server name is not provided\" +} + +if([string]::IsNullOrEmpty($dbName)) +{ +\tthrow \"database name not provided\" +} + +# admin name, password and access key will not be validated in favor of security + +if([string]::IsNullOrEmpty($accessKeyType)) +{ +\tthrow \"access key type not provided\" +} + +if([string]::IsNullOrEmpty($containerUri)) +{ +\tthrow \"containerUri not provided\" +} + +if([string]::IsNullOrEmpty($backupName)) +{ +\tthrow \"backup name not provided\" +} + +write-host \"starting db import\" +az sql db import --resource-group $rgname --server $svrName --name $dbName --admin-password $adminPwd --admin-user $adminName --storage-key $accessKey --storage-key-type $accessKeyType --storage-uri $backupUri +" + }, + "Parameters": [ + { + "Id": "0bf888db-667e-4303-a4bd-c721bf1de0ad", + "Name": "azDbImport.AzAcct", + "Label": "Azure Account", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "d5885f12-d921-4ce8-afc2-384f0b82daae", + "Name": "azDbImport.rgName", + "Label": "Resource Group Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "03b281f5-e1e2-42a8-bb43-317bc4981393", + "Name": "azDbImport.sqlSvrName", + "Label": "SQL Server Name", + "HelpText": "The name of the SQL Server to connect to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "85686923-5e57-4b1d-8dd7-6b5ca263ba42", + "Name": "azDbImport.backupName", + "Label": "Backup Name", + "HelpText": "The name of the file being saved. Defaults to DB Name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6da5679d-1c84-4fb4-8f91-22fd91b50baa", + "Name": "azDbImport.adminName", + "Label": "SQL Server Admin Name", + "HelpText": "The admin name of the SQL server containing the database you wish to import", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "44e1a2a3-6b9b-44a5-893d-d26f14166b52", + "Name": "azDbImport.adminPwd", + "Label": "Admin Password", + "HelpText": "The admin password of the SQL server containing the database you wish to import", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "40715e88-4892-48c6-83f2-603c0193439b", + "Name": "azDbImport.blobAccessKey", + "Label": "Azure Blob Access Key", + "HelpText": "the access key (Shared Access Key or Storage Access Key) to grant access to the storage account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "fcb94050-5f81-410e-9ed9-c5f52653f9c6", + "Name": "azDbImport.accessKeyType", + "Label": "Access Key Type", + "HelpText": "The type of key the access key represents", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "SharedAccessKey|Shared Access Key +StorageAccessKey|Storage Access Key" + } + }, + { + "Id": "a8631db7-556e-4532-aacd-595266c4347f", + "Name": "azDbImport.ContainerUri", + "Label": "Container Uri", + "HelpText": "The URI of the container to save the exported database in. Format is: `https://{StorageAccountName}.blob.core.windows.net/{ContainerName}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e67ef4ab-3084-45e7-81df-7cda76baf2fc", + "Name": "azDbImport.dbName", + "Label": "Database Name", + "HelpText": "The name of the database you wish to import", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-16T14:33:48.670Z", + "OctopusVersion": "2020.2.10", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "Category": "azure" +} diff --git a/step-templates/azure-delete-from-webapp.json.human b/step-templates/azure-delete-from-webapp.json.human new file mode 100644 index 000000000..158c1cb73 --- /dev/null +++ b/step-templates/azure-delete-from-webapp.json.human @@ -0,0 +1,159 @@ +{ + "Id": "7a3caa63-8312-4e6e-aeb9-674022e5da2f", + "Name": "Azure Web App - Delete Files", + "Description": "Provides the ability to delete files and folders from an Azure Web App through the kudu API.", + "Category": "azure", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "Write-Host \"Resource Group Name: $($ResourceGroupName)\" +Write-Host \"Web App Name: $($WebAppName)\" +Write-Host \"Slot Name: $($SlotName)\" + +function Get-AzureRmWebAppPublishingCredentials($ResourceGroupName, $WebAppName, $SlotName = $null){\t +\t +\tif ([string]::IsNullOrWhiteSpace($SlotName)) { +\t\t$resourceType = \"Microsoft.Web/sites/config\" +\t\t$resourceName = \"$WebAppName/publishingcredentials\" +\t} else { +\t\t$resourceType = \"Microsoft.Web/sites/slots/config\" +\t\t$resourceName = \"$WebAppName/$SlotName/publishingcredentials\" +\t} + +\t$publishingCredentials = Invoke-AzureRmResourceAction -ResourceGroupName $ResourceGroupName -ResourceType $resourceType -ResourceName $resourceName -Action list -ApiVersion 2015-08-01 -Force + + return $publishingCredentials + +} + +function Get-KuduApiAuthorisationHeaderValue($ResourceGroupName, $WebAppName, $SlotName = $null) { + + $publishingCredentials = Get-AzureRmWebAppPublishingCredentials $ResourceGroupName $WebAppName $SlotName + + return (\"Basic {0}\" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $publishingCredentials.Properties.PublishingUserName, $publishingCredentials.Properties.PublishingPassword)))) + +} + +function Delete-PathFromWebApp($ResourceGroupName, $WebAppName, $SlotName = $null, $kuduPath) { +\t + $kuduApiAuthorisationToken = Get-KuduApiAuthorisationHeaderValue $ResourceGroupName $WebAppName $SlotName + + Write-Host \"Kudu Auth Token\": + Write-Host $kuduApiAuthorisationToken + + if ([string]::IsNullOrWhiteSpace($SlotName)) { + $kuduApiUrl = \"https://$WebAppName.scm.azurewebsites.net/api/vfs\" + } else { + $kuduApiUrl = \"https://$WebAppName`-$SlotName.scm.azurewebsites.net/api/vfs\" + } + + Write-Host \"API Url: $($kuduApiUrl)\" + Write-Host \"File Path: $($kuduPath)\" + + Invoke-RestMethod -Uri \"$kuduApiUrl/site/wwwroot/$kuduPath\" ` + -Headers @{\"Authorization\"=$kuduApiAuthorisationToken;\"If-Match\"=\"*\"} ` + -Method DELETE +} + +function Delete-FilesAndFoldersFromWebApp($ResourceGroupName, $WebAppName, $SlotName, $FilesList, $RetryAttempts = 3) { + + $list = $FilesList.Split([Environment]::NewLine) + + foreach($item in $list) { + if(![string]::IsNullOrWhiteSpace($item)) { + + $retryCount = $RetryAttempts + $retry = $true + + while ($retryCount -gt 0 -and $retry) { + try { + $retryCount = $retryCount -1 + + Delete-PathFromWebApp $ResourceGroupName $WebAppName $SlotName $item + + $retry = $false + } catch { + $retry = $true + if($retryCount -eq 0) { + throw (\"Exceeded retry attempts \" + $RetryAttempts + \" for \" + $item) + } + } + } + } + } +} + +Delete-FilesAndFoldersFromWebApp $ResourceGroupName $WebAppName $SlotName $FilesList $RetryAttempts", + "Octopus.Action.Azure.AccountId": "#{AzureAccount}" + }, + "Parameters": [ + { + "Id": "a575296c-babf-4c73-9023-d5a4c9cc84bf", + "Name": "FilesList", + "Label": "List of Files & Folders to Delete", + "HelpText": "List each file/folder on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "7643ed88-ed04-46d2-ae74-1d65b94f03f9", + "Name": "AzureAccount", + "Label": "Azure Account", + "HelpText": "Must exactly match the name of a configured Azure account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ac5e4147-8b41-4aa5-82d0-ad5cf5e23499", + "Name": "ResourceGroupName", + "Label": "Resource Group Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6a833494-e3e6-4b8d-8639-c03f37d6d347", + "Name": "WebAppName", + "Label": "Web App Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0186edda-110a-4cb7-a270-bdc92e55ea1e", + "Name": "SlotName", + "Label": "Slot Name", + "HelpText": "Optional. If you want to target a specific slot, enter it's name.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ffeb3047-63d5-4ce7-9c90-81f117106a93", + "Name": "RetryAttempts", + "Label": "Retry Attempts", + "HelpText": "Number of delete attempts before failing. This should be set higher than 1, as the Azure Kudu API often fails on the first request.", + "DefaultValue": "3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "matt-byrne", + "$Meta": { + "ExportedAt": "2018-08-17T02:46:48.536Z", + "OctopusVersion": "2018.6.14", + "Type": "ActionTemplate" + } +} diff --git a/step-templates/azure-delete-resource-group-az-module.json.human b/step-templates/azure-delete-resource-group-az-module.json.human new file mode 100644 index 000000000..a6b8f83a0 --- /dev/null +++ b/step-templates/azure-delete-resource-group-az-module.json.human @@ -0,0 +1,125 @@ +{ + "Id": "3d61fa51-3352-4af6-b532-7ae1f48218b7", + "Name": "Delete Resource Group If Exists (AZ Module)", + "Description": "This step uses the new `az` modules to delete a resource group if it exist. + +Requires `Octopus Deploy 2020.1` or later. + +Requires a worker with the `az` module installed on it. That module is not bundled with Octopus Deploy.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "Author": "octobob", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Azure.AccountId": "#{DeleteResourceGroup.Azure.Account}", + "Octopus.Action.Script.ScriptBody": "$resourceGroupName = $OctopusParameters[\"DeleteResourceGroup.ResourceGroup.Name\"] +$resourceGroupLocationAbbr = $OctopusParameters[\"DeleteResourceGroup.ResourceGroup.Location.Abbr\"] + +$existingResourceGroups = (az group list --query \"[?location=='$resourceGroupLocationAbbr']\") | ConvertFrom-JSON + +$deleteResourceGroup = $false +foreach ($resourceGroupFound in $existingResourceGroups) +{\t +\tWrite-Host \"Checking if current resource group $($resourceGroupFound.name) matches $resourceGroupName\" + if ($resourceGroupFound.name -eq $resourceGroupName) + { + \t$deleteResourceGroup = $true + \tWrite-Highlight \"Resource group found, deleting\" + \tbreak + } +} + +if ($deleteResourceGroup) +{ +\tWrite-Host \"Deleting the $resourceGroupName because it was found\" +\taz group delete -n $resourceGroupName -y +}" + }, + "Parameters": [ + { + "Id": "b854aba4-4acb-428d-b981-79d1f2f50537", + "Name": "DeleteResourceGroup.Azure.Account", + "Label": "Azure Account", + "HelpText": "The Azure Account with the necessary permissions to create resource groups.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "c7e43d70-f9ac-43cb-95a8-6a0e75ad40c6", + "Name": "DeleteResourceGroup.ResourceGroup.Name", + "Label": "Resource Group Name", + "HelpText": "The name of the resource group to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e1bd7e57-5ad0-445b-9d11-6642acc55b88", + "Name": "DeleteResourceGroup.ResourceGroup.Location.Abbr", + "Label": "The Resource Group Abbreviated Location", + "HelpText": "The abbreviated resource group location, for example: centralus", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "centralus|Americas - Central US +eastus|Americas - East US +eastus2|Americas - East US 2 +northcentralus|Americas - North Central US +southcentralus|Americas - South Central US +westus|Americas - West US +westus2|Americas - West US 2 +westcentralus|Americas - West Central US +canadacentral|Americas - Canada Central +canadaeast|Americas - Canada East +brazilsouth|Americas - Brazil South +eastasia|Asia Pacific - East Asia +southeastasia|Asia Pacific - Southeast Asia +australiacentral|Asia Pacific - Australia Central +australiacentral2|Asia Pacific - Australia Central 2 +australiaeast|Asia Pacific - Australia East +australiasoutheast|Asia Pacific - Australia Southeast +chinaeast|Asia Pacific - China East +chinaeast2|Asia Pacific - China East 2 +chinanorth|Asia Pacific - China North +chinanorth2|Asia Pacific - China North 2 +centralindia|Asia Pacific - Central India +southindia|Asia Pacific - South India +westindia|Asia Pacific - West India +japaneast|Asia Pacific - Japan East +japanwest|Asia Pacific - Japan West +koreacentral|Asia Pacific - Korea Central +koreasouth|Asia Pacific - Korea South +northeurope|Europe - North Europe +westeurope|Europe - West Europe +francecentral|Europe - France Central +francesouth|Europe - France South +germanynorth|Europe - Germany North +germanywestcentral|Europe - Germany West Central +norwayeast|Europe - Norway East +norwaywest|Europe - Norway West +spaincentral|Europe - Spain Central +switzerlandnorth|Europe - Switzerland North +switzerlandwest|Europe - Switzerland West +uksouth|Europe - UK South +ukwest|Europe - UK West +southafricanorth|Middle East and Africa - South Africa North +southafricawest|Middle East and Africa - South Africa West +uaecentral|Middle East and Africa - UAE Central +uaenorth|Middle East and Africa - UAE North" + } + } + ], + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2020-04-13T15:37:29.866Z", + "OctopusVersion": "2020.1.10", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-delete-resource-group.json.human b/step-templates/azure-delete-resource-group.json.human new file mode 100644 index 000000000..34ba78ff4 --- /dev/null +++ b/step-templates/azure-delete-resource-group.json.human @@ -0,0 +1,129 @@ +{ + "Id": "18ed1352-b0be-4669-aa9a-35309a669aff", + "Name": "Delete Azure Resource Group - AzureRM", + "Description": "Step to delete an Azure Resource Group using the bundled AzureRM cmdlets with Octopus Deploy. It will first check to see if the resource group exists, and if it does exist, it will delete it.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "Author": "octobob", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{DeleteResourceGroup.Azure.Account}", + "Octopus.Action.Script.ScriptBody": "$resourceGroupName = $OctopusParameters[\"DeleteResourceGroup.ResourceGroup.Name\"] +$resourceGroupLocation = $OctopusParameters[\"DeleteResourceGroup.ResourceGroup.Location\"] + +$deleteResourceGroup = $false +Try { +\tWrite-Host \"Getting list of existing resource groups\" +\t$resourceGroupList = Get-AzureRmResourceGroup -Location \"$resourceGroupLocation\" + + Write-Host \"Looping through resource group list\" + foreach ($resourceGroupItem in $resourceGroupList) + { + \tWrite-Host \"Checking if current resource group $($resourceGroupItem.ResourceGroupName) matches $resourceGroupName\" + \tif ($resourceGroupItem.ResourceGroupName -eq $resourceGroupName) + { + \t\t$deleteResourceGroup = $true + Write-Highlight \"Found resource group to delete\" + break + } + } + +} Catch { +\t$deleteResourceGroup = $true +} + +if ($deleteResourceGroup -eq $true){ +\tWrite-Host \"Resource group exists, deleting it\" + Remove-AzureRMResourceGroup -Name $resourceGroupName -Force\t +} + + +" + }, + "Parameters": [ + { + "Id": "d0ff0ef9-90c8-4646-85ac-99e3aa8aa051", + "Name": "DeleteResourceGroup.Azure.Account", + "Label": "Azure Account", + "HelpText": "The Azure Account to authenticate with to delete the resource group", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "a2a3bc84-1be8-46c1-8d9b-2669edd33721", + "Name": "DeleteResourceGroup.ResourceGroup.Name", + "Label": "Resource Group Name", + "HelpText": "The name of the resource group to delete", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f0ca9228-92a6-4392-808e-5f767c62e2c7", + "Name": "DeleteResourceGroup.ResourceGroup.Location", + "Label": "Resource Group Location", + "HelpText": "The location where this resource group is located", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Central US|Americas - Central US +East US|Americas - East US +East US 2|Americas - East US 2 +North Central US|Americas - North Central US +South Central US|Americas - South Central US +West US|Americas - West US +West US 2|Americas - West US 2 +West Central US|Americas - West Central US +Canada Central|Americas - Canada Central +Canada East|Americas - Canada East +Brazil South|Americas - Brazil South +East Asia|Asia Pacific - East Asia +Southeast Asia|Asia Pacific - Southeast Asia +Australia Central|Asia Pacific - Australia Central +Australia Central 2|Asia Pacific - Australia Central 2 +Australia East|Asia Pacific - Australia East +Australia Southeast|Asia Pacific - Australia Southeast +China East|Asia Pacific - China East +China East 2|Asia Pacific - China East 2 +China North|Asia Pacific - China North +China North 2|Asia Pacific - China North 2 +Central India|Asia Pacific - Central India +South India|Asia Pacific - South India +West India|Asia Pacific - West India +Japan East|Asia Pacific - Japan East +Japan West|Asia Pacific - Japan West +Korea Central|Asia Pacific - Korea Central +Korea South|Asia Pacific - Korea South +North Europe|Europe - North Europe +West Europe|Europe - West Europe +France Central|Europe - France Central +France South|Europe - France South +Germany North|Europe - Germany North +Germany West Central|Europe - Germany West Central +Norway East|Europe - Norway East +Norway West|Europe - Norway West +Spain Central|Europe - Spain Central +Switzerland North|Europe - Switzerland North +Switzerland West|Europe - Switzerland West +UK South|Europe - UK South +UK West|Europe - UK West +South Africa North|Middle East and Africa - South Africa North +South Africa West|Middle East and Africa - South Africa West +UAE Central|Middle East and Africa - UAE Central +UAE North|Middle East and Africa - UAE North" + } + } + ], + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2020-04-13T15:27:10.581Z", + "OctopusVersion": "2020.1.10", + "Type": "ActionTemplate" + }, + "Category": "azure" + } diff --git a/step-templates/azure-deploy-containerapp.json.human b/step-templates/azure-deploy-containerapp.json.human new file mode 100644 index 000000000..58707362b --- /dev/null +++ b/step-templates/azure-deploy-containerapp.json.human @@ -0,0 +1,523 @@ +{ + "Id": "db701b9a-5dbe-477e-b820-07f9e354f634", + "Name": "Azure - Deploy Container App", + "Description": "Deploys a container to an Azure Container App Environment", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "636b2191-09f6-4f86-a59c-97e1891475db", + "Name": "Template.Azure.Container.Image", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "NotAcquired", + "Properties": { + "Extract": "False", + "SelectionMode": "deferred", + "PackageParameterName": "Template.Azure.Container.Image", + "Purpose": "" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Write-Host \"Saving module $PowerShellModuleName to temporary folder ...\" + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force + Write-Host \"Save successful!\" +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) +{ + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +# Check to see if it's running on Windows +if ($IsWindows) +{ +\t# Disable the progress bar so downloading files via Invoke-WebRequest are faster + $ProgressPreference = 'SilentlyContinue' +} + +if ($PSEdition -eq \"Core\") { + $PSStyle.OutputRendering = \"PlainText\" +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PWD/modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([IO.Path]::PathSeparator)$env:PSModulePath\" +$azureModule = \"Az.App\" + +# Get variables +$templateAzureAccountClient = $OctopusParameters['Template.Azure.Account.ClientId'] +$templateAzureAccountPassword = $OctopusParameters['Template.Azure.Account.Password'] +$templateAzureAccountTenantId = $OctopusParameters['Template.Azure.Account.TenantId'] +$templateAzureResourceGroup = $OctopusParameters['Template.Azure.ResourceGroup.Name'] +$templateAzureSubscriptionId = $OctopusParameters['Template.Azure.Account.SubscriptionId'] +$templateEnvironmentName = $OctopusParameters['Template.ContainerApp.Environment.Name'] +$templateAzureLocation = $OctopusParameters['Template.Azure.Location.Name'] +$templateAzureContainer = $OctopusParameters['Template.Azure.Container.Image'] +$templateAzureContainerIngressPort = $OctopusParameters['Template.Azure.Container.Ingress.Port'] +$templateAzureContainerIngressExternal = $OctopusParameters['Template.Azure.Container.ExternalIngress'] +$vmMetaData = $null +$secretRef = @() +$templateAzureContainerSecrets = $null + +if (![string]::IsNullOrWhitespace($OctopusParameters['Template.Azure.Container.Variables'])) +{ + $templateAzureContainerEnvVars = ($OctopusParameters['Template.Azure.Container.Variables'] | ConvertFrom-JSON) +} +else +{ +\t$templateAzureContainerEnvVars = $null +} + +if (![string]::IsNullOrWhitespace($OctopusParameters['Template.Azure.Container.Secrets'])) +{ + $templateAzureContainerSecrets = ($OctopusParameters['Template.Azure.Container.Secrets'] | ConvertFrom-JSON) +} +else +{ +\t$templateAzureContainerSecrets = $null +} + +$templateAzureContainerCPU = $OctopusParameters['Template.Azure.Container.Cpu'] +$templateAzureContainerMemory = $OctopusParameters['Template.Azure.Container.Memory'] + +# Check for required PowerShell module +Write-Host \"Checking for module $azureModule ...\" + +if ((Get-ModuleInstalled -PowerShellModuleName $azureModule) -eq $false) +{ +\t# Install the module + Install-PowerShellModule -PowerShellModuleName $azureModule -LocalModulesPath $LocalModules +} + +# Import the necessary module +Write-Host \"Importing module $azureModule ...\" +Import-Module $azureModule + +# Check to see if the account was specified +if (![string]::IsNullOrWhitespace($templateAzureAccountClient)) +{ +\t# Login using the provided account + Write-Host \"Logging in as specified account ...\" + +\t# Create credential object for az module +\t$securePassword = ConvertTo-SecureString $templateAzureAccountPassword -AsPlainText -Force +\t$azureCredentials = New-Object System.Management.Automation.PSCredential ($templateAzureAccountClient, $securePassword) + + Connect-AzAccount -Credential $azureCredentials -ServicePrincipal -Tenant $templateAzureAccountTenantId | Out-Null + + Write-Host \"Login successful!\" +} +else +{ +\tWrite-Host \"Using machine Managed Identity ...\" + $vmMetaData = Invoke-RestMethod -Headers @{\"Metadata\"=\"true\"} -Method GET -Uri \"http://169.254.169.254/metadata/instance?api-version=2021-02-01\" + + Connect-AzAccount -Identity + + # Get Identity context + $identityContext = Get-AzContext + + # Set variables + $templateAzureSubscriptionId = $vmMetaData.compute.subscriptionId + + if ([string]::IsNullOrWhitespace($templateAzureAccountTenantId)) + { + \t$templateAzureAccountTenantId = $identityContext.Tenant + } + + Set-AzContext -Tenant $templateAzureAccountTenantId | Out-Null +\tWrite-Host \"Successfully set context for Managed Identity!\" +} + +# Check to see if the environment name is a / in it +if ($templateEnvironmentName.Contains(\"/\") -ne $true) +{ +\t# Lookup environment id by name + Write-Host \"Looking up Managed Environment by Name ...\" + $templateEnvironmentName = (Get-AzContainerAppManagedEnv -ResourceGroupName $templateAzureResourceGroup -EnvName $templateEnvironmentName -SubscriptionId $templateAzureSubscriptionId).Id +} + +# Build parameter list to pass to New-AzContainerAppTemplateObject +$PSBoundParameters.Add(\"Image\", $OctopusParameters[\"Octopus.Action.Package[Template.Azure.Container.Image].Image\"]) +$PSBoundParameters.Add(\"Name\", $OctopusParameters[\"Template.Azure.Container.Name\"]) + +if (![string]::IsNullOrWhitespace($templateAzureContainerCPU)) +{ + $PSBoundParameters.Add(\"ResourceCpu\", \"$templateAzureContainerCPU\") +} + +if (![string]::IsNullOrWhitespace($templateAzureContainerMemory)) +{ + $PSBoundParameters.Add(\"ResourceMemory\", \"$templateAzureContainerMemory\") +} + +if ($null -ne $templateAzureContainerEnvVars) +{ + # Loop through list + $envVars = @() + foreach ($envVar in $templateAzureContainerEnvVars) + { + \t$envEntry = @{} + $envEntry.Add(\"Name\", $envVar.Name) + + # Check for specific property + if ($envVar.SecretRef) + { + \t$envEntry.Add(\"SecretRef\", $envVar.SecretRef) + } + else + { + \t$envEntry.Add(\"Value\", $envVar.Value) + } + + # Add to collection + $envVars += $envEntry + } + + $PSBoundParameters.Add(\"Env\", $envVars) +} + +if ($null -ne $templateAzureContainerSecrets) +{ +\t# Loop through list + foreach ($secret in $templateAzureContainerSecrets) + { + # Create new secret object and add to array + $secretRef += New-AzContainerAppSecretObject -Name $secret.Name -Value $secret.Value + } +} + +# Create new container app +$containerDefinition = New-AzContainerAppTemplateObject @PSBoundParameters +$PSBoundParameters.Clear() + +# Define ingress components +if (![string]::IsNullOrWhitespace($templateAzureContainerIngressPort)) +{ +\t$PSBoundParameters.Add(\"IngressExternal\", [System.Convert]::ToBoolean($templateAzureContainerIngressExternal)) + $PSBoundParameters.Add(\"IngressTargetPort\", $templateAzureContainerIngressPort) +} + +# Check the image +if ($OctopusParameters[\"Octopus.Action.Package[Template.Azure.Container.Image].Image\"].Contains(\"azurecr.io\")) +{ +\t# Define local parameters + $registryCredentials = @{} + $registrySecret = @{} + + # Accessing an ACR repository, configure credentials + if (![string]::IsNullOrWhitespace($templateAzureAccountClient)) + { + +\t\t# Use configured client, name must be lower case + $registryCredentials.Add(\"Username\", $templateAzureAccountClient) + $registryCredentials.Add(\"PasswordSecretRef\", \"clientpassword\") + +\t\t$secretRef += New-AzContainerAppSecretObject -Name \"clientpassword\" -Value $templateAzureAccountPassword + } + else + { + \t# Using Managed Identity + $registryCredentials.Add(\"Identity\", \"system\") + + } + + $registryServer = $OctopusParameters[\"Octopus.Action.Package[Template.Azure.Container.Image].Image\"] + $registryServer = $registryServer.Substring(0, $registryServer.IndexOf(\"/\")) + $registryCredentials.Add(\"Server\", $registryServer) + + # Add credentials + $PSBoundParameters.Add(\"Registry\", $registryCredentials) +} + +# Define secrets component +if ($secretRef.Count -gt 0) +{ +\t# Add to parameters + $PSBoundParameters.Add(\"Secret\", $secretRef) +} + +# Create new configuration object +Write-Host \"Creating new Configuration Object ...\" +$configurationObject = New-AzContainerAppConfigurationObject @PSBoundParameters +$PSBoundParameters.Clear() + +# Define parameters +$PSBoundParameters.Add(\"Name\", $OctopusParameters[\"Template.Azure.Container.Name\"]) +$PSBoundParameters.Add(\"TemplateContainer\", $containerDefinition) +$PSBoundParameters.Add(\"ResourceGroupName\", $templateAzureResourceGroup) +$PSBoundParameters.Add(\"Configuration\", $configurationObject) + + +# Check to see if the container app already exists +$containerApp = Get-AzContainerApp -Name $OctopusParameters[\"Template.Azure.Container.Name\"] -ResourceGroupName $templateAzureResourceGroup +if ($null -eq $containerApp) +{ +\t# Add parameters required for creating container app +\t$PSBoundParameters.Add(\"EnvironmentId\", $templateEnvironmentName) +\t$PSBoundParameters.Add(\"Location\", $templateAzureLocation) +\t +\t# Deploy container + Write-Host \"Creating new container app ...\" +\tNew-AzContainerApp @PSBoundParameters +} +else +{ +\tWrite-Host \"Updating existing container app ...\" + Update-AzContainerApp @PSBoundParameters +} +" + }, + "Parameters": [ + { + "Id": "a1ae1b6c-99d0-4ee5-9c3c-08c345966842", + "Name": "Template.Azure.ResourceGroup.Name", + "Label": "Azure Resource Group Name", + "HelpText": "Provide the resource group name to create the environment in.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fb92b5d7-5e48-485d-9222-dc2c97b58535", + "Name": "Template.Azure.Account.SubscriptionId", + "Label": "Azure Account Subscription Id", + "HelpText": "The subscription ID of the Azure account to use. This value can be retrieved from an Azure Account variable type. Add an Azure Account to your project , then assign the `SubscriptionNumber` property to for this entry. Leave blank to use the Managed Identity. + +For example, if your Azure Account variable is called MyAccount, the value for this input would be `#{MyAccount.SubscriptionNumber}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e1ce647f-956e-48d1-8002-44d958f1c8a4", + "Name": "Template.Azure.Account.ClientId", + "Label": "Azure Account Client Id", + "HelpText": "The subscription ID of the Azure account to use. This value can be retrieved from an Azure Account variable type. Add an Azure Account to your project , then assign the `Client` property to for this entry. Leave blank to use the Managed Identity. + +For example, if your Azure Account variable is called MyAccount, the value for this input would be `#{MyAccount.Client}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a02e3fd0-9d91-4106-84a4-ff0d68e5de7c", + "Name": "Template.Azure.Account.TenantId", + "Label": "Azure Account Tenant Id", + "HelpText": "The subscription ID of the Azure account to use. This value can be retrieved from an Azure Account variable type. Add an Azure Account to your project , then assign the `TenantId` property to for this entry. If blank, it will use the Managed Identity tenant. + +For example, if your Azure Account variable is called MyAccount, the value for this input would be `#{MyAccount.TenantId}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7bd73320-1889-41eb-a1c0-c8788e5eed9d", + "Name": "Template.Azure.Account.Password", + "Label": "Azure Account Password", + "HelpText": "The subscription ID of the Azure account to use. This value can be retrieved from an Azure Account variable type. Add an Azure Account to your project , then assign the `Password` property to for this entry. Leave blank to use the Managed Identity. + +For example, if your Azure Account variable is called MyAccount, the value for this input would be `#{MyAccount.Password}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "f7fafc7e-2658-4a6f-ac30-7d9f738b9f52", + "Name": "Template.ContainerApp.Environment.Name", + "Label": "Container App Environment Name", + "HelpText": "The name or ID of the container app environment to deploy to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "31f02b7e-4822-49de-97dc-ee981b12268c", + "Name": "Template.Azure.Location.Name", + "Label": "Azure Location", + "HelpText": "The location in which to create the container app environment.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d74123b0-4104-45b9-ae12-b1f808b8b948", + "Name": "Template.Azure.Container.Name", + "Label": "Container Name", + "HelpText": "The name of the container to create/update. If you want to use the image name, specify `#{Octopus.Action.Package[Template.Azure.Container.Image].PackageId}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1bdb6d31-4b97-4eab-aca3-5a60891b3455", + "Name": "Template.Azure.Container.Image", + "Label": "Container Image", + "HelpText": "Select the container image to deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "9e7aa5cf-9bab-4303-a851-1b8757f2f486", + "Name": "Template.Azure.Container.Variables", + "Label": "Environment variables", + "HelpText": "JSON formatted key/value pair of environment variables to pass to the container. This supports use of OctoStache. + +``` +[ + { + \"name\": \"ConnectionStrings__CatalogConnection\", + \"value\": \"Server=#{Project.SQL.DNS},1433;Integrated Security=true;Initial Catalog=#{Project.Catalog.Database.Name};User Id=#{Project.SQL.Admin.Username};Password=#{Project.SQL.Admin.Password};Trusted_Connection=false;Trust Server Certificate=True;\" + }, + { + \"name\": \"MyPasswordFromSecret\", + \"secretref\": \"Name of my secret\" + }, + ... +] +```", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "498f308d-a94c-4364-b0be-8d739ac63f4e", + "Name": "Template.Azure.Container.Secrets", + "Label": "Secrets", + "HelpText": "JSON formatted key/value pair of secrets to create/update. This supports use of OctoStache. + +**Note:** The name of the secret must be lowercase. + +``` +[ + { + \"name\": \"mysecret\", + \"value\": \"#{Project.SQL.Admin.Password}\" + }, + ... +] +```", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "20e3e428-62b9-4eef-9cdc-780a0eedee79", + "Name": "Template.Azure.Container.Cpu", + "Label": "Resource CPU", + "HelpText": "The amount of CPU to allocate to the container app. + +Example: 0.5", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d804f749-625e-4ca5-9bef-13fc2c2a463c", + "Name": "Template.Azure.Container.Memory", + "Label": "Resource Memory", + "HelpText": "The amount of memory to allocate to the container app. + +Examples: 250Mb or 4.0Gi", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a506b328-4e10-49fe-947b-6623d4d392c1", + "Name": "Template.Azure.Container.Ingress.Port", + "Label": "Container Ingress Port", + "HelpText": "The port to allow traffic to the container.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "682d68a4-f1eb-469c-9557-9577ed9f1505", + "Name": "Template.Azure.Container.ExternalIngress", + "Label": "External Ingress", + "HelpText": "Whether the ingress is externally accessible or not.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-07-05T15:57:10.891Z", + "OctopusVersion": "2023.3.4541", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "azure" +} diff --git a/step-templates/azure-devops-build-pipeline.json.human b/step-templates/azure-devops-build-pipeline.json.human new file mode 100644 index 000000000..e0755f90c --- /dev/null +++ b/step-templates/azure-devops-build-pipeline.json.human @@ -0,0 +1,240 @@ +{ + "Id": "405e97d7-e7ad-4d62-81cd-5d5fc751ef42", + "Name": "Run Azure DevOps Build Pipeline", + "Description": "Will trigger a build pipeline in Azure DevOps for a specified branch using the [Azure DevOps REST API](https://docs.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-6.1).", + "ActionType": "Octopus.Script", + "Version": 2, + "Author": "adamoctoclose", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$AzureDevOpsAccessKey = $AzureDevOpsAccessKey +$AzureDevOpsOrganizationName = $AzureDevOpsOrganizationName +$AzureDevOpsProjectName = $AzureDevOpsProjectName +$AzureDevOpsPipelineName = $AzureDevOpsPipelineName +$AzureDevOpsBranch = $AzureDevOpsBranch +$AzureDevOpsWaitUntilCompletion = $AzureDevOpsWaitUntilCompletion +$AzureDevOpsVariables = $AzureDevOpsVariables + +function Test-RequiredValues +{ +\tparam ( + \t[PSVariable]$variableToCheck + ) + if ([string]::IsNullOrWhiteSpace($variableToCheck.Value) -eq $true) + { + \tWrite-Host \"$($variableToCheck.Name) is required.\" + return $false + } + return $true +} +function Get-BuildPipelineId +{ + param ( + $defaultUrl, + $pipeline + ) + try { + Write-Host \"Getting list of available pipelines\" + $url = \"$defaultUrl/_apis/pipelines?api-version=6.0-preview.1\" + $pipelines = Invoke-RestMethod -Uri $url -Method GET -Headers $azureDevOpsAuthenticationHeader + if([string]::IsNullOrWhiteSpace($pipelines) -eq $true) + { + Write-Error \"Couldn't find any pipelines in $AzureDevOpsPipelineName\" + Exit 1 + } + $pipelineId = ($pipelines.value | Where-Object {$_.name -eq $pipeline}).id + if([string]::IsNullOrWhiteSpace($pipelineId) -eq $true) + { + Write-Error \"Found ${$pipelines.count} pipelines in project $AzureDevOpsProjectName but couldn't find $AzureDevOpsPipelineName\" + Exit 1 + } + return $pipelineId + } + catch + { + Write-Error \"An error occurred while getting the pipelines:`n$($_.Exception.Message)\" + Exit 1 + } +} +function Invoke-BuildPipeline +{ + param ( + $defaultUrl, + $pipelineId, + $body + ) + try { + $url = \"$defaultUrl/_apis/pipelines/$pipelineId/runs?api-version=6.0-preview.1\" + $pipeline = Invoke-RestMethod -Uri $url -Body $body -ContentType \"application/json\" -Method POST -Headers $azureDevOpsAuthenticationHeader + return $pipeline + } + catch { + Write-Error \"An error occurred while invoking the pipeline:`n$($_.Exception.Message)\" + Exit 1 + } +} +function Get-BuildPipelineStatus +{ + param ( + $defaultUrl, + $pipelineId, + $runId + ) + try { + $url = \"$defaultUrl/_apis/pipelines/$pipelineId/runs/$($runId)?api-version=6.0-preview.1\" + return Invoke-RestMethod -Uri $url -Method GET -Headers $azureDevOpsAuthenticationHeader + } + catch { + Write-Error \"An error occurred while getting the pipeline status:`n$($_.Exception.Message)\" + Exit 1 + } +} +$verificationPassed = @() +$verificationPassed += Test-RequiredValues -variableToCheck (Get-Variable AzureDevOpsAccessKey) +$verificationPassed += Test-RequiredValues -variableToCheck (Get-Variable AzureDevOpsOrganizationName) +$verificationPassed += Test-RequiredValues -variableToCheck (Get-Variable AzureDevOpsProjectName) +$verificationPassed += Test-RequiredValues -variableToCheck (Get-Variable AzureDevOpsPipelineName) +$verificationPassed += Test-RequiredValues -variableToCheck (Get-Variable AzureDevOpsBranch) + +if ($verificationPassed -contains $false) +{ +\tWrite-Error \"Required values missing. Please see output for further details.\" +\tExit 1 +} + +$azureDevOpsAuthenticationHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(\":$($AzureDevOpsAccessKey)\")) } + +Write-Host \"Azure DevOps Organization Name: $AzureDevOpsOrganizationName\" +Write-Host \"Azure DevOps Project Name: $AzureDevOpsProjectName\" +Write-Host \"Azure DevOps Pipeline Name: $AzureDevOpsPipelineName\" +Write-Host \"Azure DevOps Branch: $AzureDevOpsBranch\" +Write-Host \"Azure DevOps Wait Until Completion: $AzureDevOpsWaitUntilCompletion\" + +$defaultUrl = \"https://dev.azure.com/$AzureDevOpsOrganizationName/$AzureDevOpsProjectName\" + +$buildPipelineId = Get-BuildPipelineId -defaultUrl $defaultUrl -pipeline $AzureDevOpsPipelineName + +$body = @\" +{ + \"resources\": { + \"repositories\": { + \"self\": { + \"refName\": \"refs/heads/$AzureDevOpsBranch\" + } + } + }, + \"variables\": { + $AzureDevOpsVariables + } + } +\"@ + +$run = Invoke-BuildPipeline -defaultUrl $defaultUrl -pipelineId $buildPipelineId -body $body + +Write-Highlight \"The pipeline was successfully invoked, you can access the pipeline [here]($($run._links.web.href)).\" + +if ($run.state -ne \"completed\" -and $AzureDevOpsWaitUntilCompletion -eq $true) +{ + do + { + Write-Host \"Waiting for pipeline completion...\" + Start-Sleep 30 + $status = Get-BuildPipelineStatus -defaultUrl $defaultUrl -pipelineId $buildPipelineId -runId $run.id + Write-Host \"Current Status: $($status.state)\" + } + while ($status.state -ne \"completed\") + if ($status.result -ne \"succeeded\") + { + Write-Error \"The Azure DevOps pipeline failed to complete successfully\" + Exit 1 + } +} +else +{ + Write-Host \"Azure DevOps pipeline status unknown. Update process to wait until completion for status updates.\" +}" + }, + "Parameters": [ + { + "Id": "5f4bdb73-ce24-4d5d-8541-82efae04b9f5", + "Name": "AzureDevOpsAccessKey", + "Label": "Azure DevOps Access Key", + "HelpText": "A [personal access token (PAT)](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page) is used as an alternate password to authenticate into Azure DevOps. Learn how to create, use, modify, and revoke PATs for Azure DevOps.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "63dd21d5-3d38-4bc2-9b09-9e2e4efa5974", + "Name": "AzureDevOpsOrganizationName", + "Label": "Azure DevOps Organization Name", + "HelpText": "The name of the Azure DevOps [organization](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/organization-management?view=azure-devops).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0d641c0c-bd5f-408a-9d9a-6ac710bd134c", + "Name": "AzureDevOpsProjectName", + "Label": "Azure DevOps Project Name", + "HelpText": "Project ID or project name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b5e6ea31-5679-48b6-a5ec-4c7274dbb398", + "Name": "AzureDevOpsPipelineName", + "Label": "Azure DevOps Pipeline Name", + "HelpText": "The name of the pipeline you want to run in your Azure DevOps project + +Example: helloword-ci", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "62f3eab8-9785-4b96-9c52-a0a12f794e12", + "Name": "AzureDevOpsBranch", + "Label": "Azure DevOps Branch", + "HelpText": "Source branch to build from", + "DefaultValue": "Main", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ea425cd0-110e-45de-b38f-1c07b82cb390", + "Name": "AzureDevOpsWaitUntilCompletion", + "Label": "Azure DevOps Wait Until Completion", + "HelpText": "Wait for the pipeline to finish. If selected then the step will wait until the build pipeline is completed before finishing the step. If the build pipeline fails then the step will also fail.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "a3d3830b-87ca-40ab-aca0-e243f0080dda", + "Name": "AzureDevOpsVariables", + "Label": "AzureDevOpsVariables", + "HelpText": "Azure DevOps pipeline variables should be passed in as JSON. Example... \"TestEnv\": {\"isSecret\": false,\"value\": \"true\"},\"ProdEnv\": {\"isSecret\": false,\"value\": \"false\"}", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-03-25T13:12:50.932Z", + "OctopusVersion": "2020.5.5", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "adamoctoclose", + "Category": "azure-devops" + } diff --git a/step-templates/azure-function-deployment.json.human b/step-templates/azure-function-deployment.json.human new file mode 100644 index 000000000..e6190efde --- /dev/null +++ b/step-templates/azure-function-deployment.json.human @@ -0,0 +1,175 @@ +{ + "Id": "03bb1a08-52be-43ad-bdfd-117eb562b414", + "Name": "Azure Functions Deployment", + "Description": "Deploys Azure Functions via the Kudu API to an Azure Function App. + +DO NOT RUN THIS STEP ON AN AZURE WEB APP TARGET. USE A PLAIN TENTACLE TARGET ONLY. + +Process: +1. Deploys the package to a tentacle. +2. Zips up the deployment artefacts and uploads the resulting zip file. + +This process enables the following: +- Config and variable transform +- Package type agnostic (i.e. will work with .nupkg as well as .zip files) +- Retention policies can be applied to the target in step 1 + +**Notes:** + +This deployment step requires an available tentacle to deploy the package to before the final package is deployed to Azure. This tentacle does not need to be within the target deployment environment. It is advised to install the tentacle on the Octopus Deploy server, and deploy to that tentacle. + +The target Function App will not be purged before deployment. This is by design of the Kudu API. An advantage of this is that multiple deployment packages can be deployed to a single Function App, as long as each function to be deployed has a unique name. However, you should be careful not to expect functions that have been removed from the deployment package to also be removed from the Functions App. + +The package to deploy should mirror exactly the file and folder structure that the Functions App expects. A quick and easy way to get this up and running is to download an existing Function App's content from the Azure portal, and upload the downloaded zip file to Octopus Deploy via the Library > Packages page.", + "ActionType": "Octopus.TentaclePackage", + "Version": 4, + "Properties": { + "Octopus.Action.Package.AutomaticallyRunConfigurationTransformationFiles": "True", + "Octopus.Action.Package.AutomaticallyUpdateAppSettingsAndConnectionStrings": "True", + "Octopus.Action.EnabledFeatures": "Octopus.Features.CustomScripts,Octopus.Features.JsonConfigurationVariables", + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.Package.FeedId": "feeds-builtin", + "Octopus.Action.Package.JsonConfigurationVariablesEnabled": "True", + "Octopus.Action.CustomScripts.PostDeploy.ps1": "$installationPath = $OctopusParameters[\"Octopus.Action.Package.InstallationDirectoryPath\"] +$packageId = $OctopusParameters[\"Octopus.Action.Package.PackageId\"] +$packageVersion = $OctopusParameters[\"Octopus.Action.Package.PackageVersion\"] + +Write-Host \"Installation Path: $($installationPath)\" +Write-Host \"Package ID: $($packageId)\" +Write-Host \"Package Version: $($packageVersion)\" + +$zipFilePath = \"$($installationPath)\\$($packageId).$($packageVersion).zip\" + +Write-Host \"Zip File Path: $($zipFilePath)\" + +Compress-Archive -Path \"$($installationPath)\\*\" -DestinationPath $zipFilePath + +Write-Host \"Deployment zip file created\" + +$username = $OctopusParameters[\"Azf.Username\"] +$password = $OctopusParameters[\"Azf.Password\"] +$appName = $OctopusParameters[\"Azf.ApplicationName\"] + +if(!$username){ + Write-Error \"No Username has been supplied. You can do this from the Step Details page of this step.\" + + exit 1; +} + + +if(!$password){ + Write-Error \"No Password has been supplied. You can do this from the Step Details page of this step.\" + + exit 1; +} + + +if(!$appName){ + Write-Error \"No Application Name has been supplied. You can do this from the Step Details page of this step.\" + + exit 1; +} + +$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $username,$password))) + +$apiUrl = \"https://$($appName).scm.azurewebsites.net/api/zipdeploy\" + +Write-Host \"Uploading deployment zip file to $($apiUrl)\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +Invoke-RestMethod -Uri $apiUrl -Headers @{Authorization=(\"Basic {0}\" -f $authHeader)} -Method POST -InFile $zipFilePath -ContentType \"multipart/form-data\" + +Write-Host \"Upload complete\" +", + "Octopus.Action.Package.PackageId": "#{Azf.PackageName}", + "Octopus.Action.Package.JsonConfigurationVariablesTargets": "#{Azf.JsonConfigFiles}", + "Octopus.Action.CustomScripts.PreDeploy.ps1": "", + "Octopus.Action.CustomScripts.Deploy.ps1": "" + }, + "Parameters": [ + { + "Id": "538b02c5-ca4c-4bb6-aadc-e46906e69537", + "Name": "Azf.Username", + "Label": "Username", + "HelpText": "See [Kudu Deployment Credentials](https://github.com/projectkudu/kudu/wiki/Deployment-credentials#user-level-credentials-aka-deployment-credentials) for more information.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "5510ce13-6016-471b-a65d-051597c0c972", + "Name": "Azf.Password", + "Label": "Password", + "HelpText": "See [Kudu Deployment Credentials](https://github.com/projectkudu/kudu/wiki/Deployment-credentials#user-level-credentials-aka-deployment-credentials) for more information.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "2bcc2c06-26c7-48c1-9408-1892ecd15910", + "Name": "Azf.ApplicationName", + "Label": "Application Name", + "HelpText": "This value can be determined from the URL of your application. i.e.: https://{ApplicationName}.azurewebsites.net", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f6ac1609-2cdf-4ba9-a598-6c32df68db85", + "Name": "Azf.PackageName", + "Label": "Package Name", + "HelpText": "The name of the package to deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "0d71cd13-9fb7-4b7f-b352-004a96d6a741", + "Name": "Azf.JsonConfigFiles", + "Label": "Json Configuration Files", + "HelpText": "A comma- or newline-separated list of file names to replace settings in, relative to the package contents. Extended wildcard syntax is supported. E.g., appsettings.json, Config\\*.json, **\\specific-folder\\*.json.. Configuration values can be replaced using a `:` separated key, starting from the root of the hierarchy. + +Consider the following appsettings.json file: + + { + \"Data\": { + \"DefaultConnection\": { + \"ConnectionString\": \"Server=(localdb)\\\\SQLEXPRESS;Database=OctoFX;Trusted_Connection=True\" + } + } + } + +To replace the value of `ConnectionString` you would add a variable name `Data:DefaultConnection:ConnectionString` to your project variables / library variable set. The JSON also supports extended template syntax. + +The 'Target files' field supports extended template syntax. Conditional `if` and `unless`: + + #{if MyVar}...#{/if} + +Iteration over variable sets or comma-separated values with `each`: + + #{each mv in MyVar}...#{mv}...#{/each}", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2020-03-15T18:13:57.632Z", + "OctopusVersion": "2019.13.7", + "Type": "ActionTemplate" + }, + "Category": "azureFunctions" +} diff --git a/step-templates/azure-function-set-appsettings-from-azconfig.json.human b/step-templates/azure-function-set-appsettings-from-azconfig.json.human new file mode 100644 index 000000000..4aa9029eb --- /dev/null +++ b/step-templates/azure-function-set-appsettings-from-azconfig.json.human @@ -0,0 +1,620 @@ +{ + "Id": "67fcc93c-509c-4c13-bc24-645eff53c5c2", + "Name": "Azure Function - Set AppSettings from Azure AppConfig", + "Description": "This step retrieves one or more key/values from an Azure App Configuration store and adds them to an Azure App Function's AppSettings. + +You can retrieve individual keys that match a specific name, and you can choose a custom setting name for each key. + +Wildcard search is also supported using the `*` notation in the **Key Names** parameter. Note: Combining a wildcard search with custom setting names is not supported. + +You can also combine retrieved values with additional parameters passed into the step using the `Additional AppSettings` parameter. + +Authentication is performed using an Azure Service Principal. + +--- + +**Required:** +- An Azure account with permission to both retrieve values from the Azure App Config store and publish to the App Function. +- The `az` CLI on the target or worker. If the CLI can't be found, the step will fail. + +Notes: + +- Tested on Octopus `2024.1` using az version `2.38.0` +- Tested with both Windows PowerShell and PowerShell Core (on Linux). +- Slot Settings are not currently supported.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +# KV Variables +$global:AzureAppConfigStoreName = $OctopusParameters[\"AzFunction.SetAppSettings.FromAzAppConfig.ConfigStoreName\"] +$global:AzureAppConfigStoreEndpoint = $OctopusParameters[\"AzFunction.SetAppSettings.FromAzAppConfig.ConfigStoreEndpoint\"] +$global:AzureAppConfigRetrievalMethod = $OctopusParameters[\"AzFunction.SetAppSettings.FromAzAppConfig.RetrievalMethod\"] +$ConfigStoreKeyNames = $OctopusParameters[\"AzFunction.SetAppSettings.FromAzAppConfig.KeyNames\"] +$global:ConfigStoreLabels = $OctopusParameters[\"AzFunction.SetAppSettings.FromAzAppConfig.Labels\"] +$global:SuppressWarnings = $OctopusParameters[\"AzFunction.SetAppSettings.FromAzAppConfig.SuppressWarnings\"] -ieq \"True\" +$global:TreatWarningsAsErrors = $OctopusParameters[\"AzFunction.SetAppSettings.FromAzAppConfig.TreatWarningsAsErrors\"] -ieq \"True\" + +# Function Variables +$FunctionName = $OctopusParameters[\"AzFunction.SetAppSettings.FromAzAppConfig.FunctionName\"] +$ResourceGroup = $OctopusParameters[\"AzFunction.SetAppSettings.FromAzAppConfig.ResourceGroup\"] +$AdditionalSettingsValues = $OctopusParameters[\"AzFunction.SetAppSettings.FromAzAppConfig.AdditionalSettingsValues\"] +$Slot = $OctopusParameters[\"AzFunction.SetAppSettings.FromAzAppConfig.Slot\"] + +# KV params validation +if ([string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreName) -and [string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreEndpoint)) { + throw \"Either parameter ConfigStoreName or ConfigStoreEndpoint not specified\" +} + +if ([string]::IsNullOrWhiteSpace($global:AzureAppConfigRetrievalMethod)) { + throw \"Required parameter AzFunction.SetAppSettings.FromAzAppConfig.RetrievalMethod not specified\" +} + +if ([string]::IsNullOrWhiteSpace($ConfigStoreKeyNames) -and [string]::IsNullOrWhiteSpace($global:ConfigStoreLabels)) { + throw \"Either AzFunction.SetAppSettings.FromAzAppConfig.KeyNames or AzFunction.SetAppSettings.FromAzAppConfig.Labels not specified\" +} + +# Function params validation +if ([string]::IsNullOrWhiteSpace($FunctionName)) { + throw \"Required parameter AzureFunction.ConfigureAppSettings.FunctionName not specified\" +} + +if ([string]::IsNullOrWhiteSpace($ResourceGroup)) { + throw \"Required parameter AzureFunction.ConfigureAppSettings.ResourceGroup not specified\" +} + +$RetrieveAllKeys = $global:AzureAppConfigRetrievalMethod -ieq \"all\" +$global:ConfigStoreParameters = \"\" +if (-not [string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreName)) { + $global:ConfigStoreParameters += \" --name \"\"$global:AzureAppConfigStoreName\"\"\" +} +if (-not [string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreEndpoint)) { + $global:ConfigStoreParameters += \" --endpoint \"\"$global:AzureAppConfigStoreEndpoint\"\"\" +} + +### Helper functions +function Test-ForAzCLI() { + $oldPreference = $ErrorActionPreference + $ErrorActionPreference = \"Stop\" + try { + if (Get-Command \"az\") { + return $True + } + } + catch { + return $false + } + finally { + $ErrorActionPreference = $oldPreference + } +} + + +function Write-OctopusWarning( + [string] $Message +) { + if ($global:TreatWarningsAsErrors) { + throw \"Error: $Message\" + } + else { + if ($global:SuppressWarnings -eq $False) { + Write-Warning -Message $Message + } + else { + Write-Verbose -Message $Message + } + } +} + +function Save-AppSetting( + [string]$settingName, + [string]$settingValue) { + + $global:Settings += [PsCustomObject]@{name = $settingName; value = $settingValue; slotSetting = $false } +} + +function Find-AzureAppConfigMatchesFromKey( + [string]$KeyName, + [bool]$IsWildCard, + [string]$settingName, + [PsCustomObject]$AppConfigValues) { + + if ($IsWildCard -eq $False) { + Write-Verbose \"Finding exact match for: $keyName\" + $matchingAppConfigKeys = $appConfigValues | Where-Object { $_.key -ieq $keyName } + if ($null -eq $matchingAppConfigKeys -or $matchingAppConfigKeys.Count -eq 0) { + Write-OctopusWarning \"Unable to find a matching key in Azure App Config for: $keyName\" + } + else { + + if ($matchingAppConfigKeys.Count -gt 1) { + Write-OctopusWarning \"Found multiple matching keys ($($matchingAppConfigKeys.Count)) in Azure App Config for: $keyName. This is usually due to multiple values with labels\" + + foreach ($matchingAppConfigKey in $matchingAppConfigKeys) { + Write-Verbose \"Found match for $keyName $(if(![string]::IsNullOrWhiteSpace($matchingAppConfigKey.content_type)) {\"($($matchingAppConfigKey.content_type))\"})\" + $settingValue = $matchingAppConfigKey.value + + if ([string]::IsNullOrWhiteSpace($settingName)) { + $settingName = $keyName.Trim() + } + if (![string]::IsNullOrWhiteSpace($matchingAppConfigKey.label)) { + $settingName = \"$($keyName.Trim())-$($matchingAppConfigKey.label)\" + Write-Verbose \"Appending label to setting name to avoid duplicate setting: $settingName\" + } + + Save-AppSetting -settingName $settingName -settingValue $settingValue + } + } + else { + $matchingAppConfigKey = $matchingAppConfigKeys | Select-Object -First 1 + Write-Verbose \"Found match for $keyName $(if(![string]::IsNullOrWhiteSpace($matchingAppConfigKey.content_type)) {\"($($matchingAppConfigKey.content_type))\"})\" + $settingValue = $matchingAppConfigKey.value + + if ([string]::IsNullOrWhiteSpace($settingName)) { + $settingName = $keyName.Trim() + } + + Save-AppSetting -settingName $settingName -settingValue $settingValue + } + } + } + else { + Write-Verbose \"Finding wildcard match for: $keyName\" + $matchingAppConfigKeys = @($appConfigValues | Where-Object { $_.key -ilike $keyName }) + if ($matchingAppConfigKeys.Count -eq 0) { + Write-OctopusWarning \"Unable to find any matching keys in Azure App Config for wildcard: $keyName\" + } + else { + foreach ($match in $matchingAppConfigKeys) { + # Have to explicitly set settings as they are wildcard matches + $settingName = $match.key + $settingValue = $match.value + Write-Verbose \"Found wildcard match '$settingName' $(if(![string]::IsNullOrWhiteSpace($matchingAppConfigKey.content_type)) {\"($($matchingAppConfigKey.content_type))\"})\" + Save-AppSetting -settingName $settingName -settingValue $settingValue + } + } + } +} + +function Find-AzureAppConfigMatchesFromLabels() { + + Write-Verbose \"Retrieving values matching labels: $global:ConfigStoreLabels\" + $command = \"az appconfig kv list $global:ConfigStoreParameters --label \"\"$global:ConfigStoreLabels\"\" --auth-mode login\" + + Write-Verbose \"Invoking expression: $command\" + $appConfigResponse = Invoke-Expression -Command $command + $ExitCode = $LastExitCode + Write-Verbose \"az exit code: $ExitCode\" + if ($ExitCode -ne 0) { + throw \"Error retrieving appsettings. ExitCode: $ExitCode\" + } + + if ([string]::IsNullOrWhiteSpace($appConfigResponse)) { + Write-OctopusWarning \"Null or empty response received from Azure App Configuration service\" + } + else { + $appConfigValues = $appConfigResponse | ConvertFrom-Json + if ($appConfigValues.Count -eq 0) { + Write-OctopusWarning \"Unable to find any matching keys in Azure App Config for labels: $global:ConfigStoreLabels\" + } + else { + Write-Verbose \"Finding match(es) for labels: $global:ConfigStoreLabels\" + foreach ($appConfigValue in $appConfigValues) { + # Have to explicitly set setting Name here as its a match based on label alone + $settingName = $appConfigValue.key + Write-Verbose \"Found label match '$($appConfigValue.key)' $(if(![string]::IsNullOrWhiteSpace($appConfigValue.content_type)) {\"($($appConfigValue.content_type))\"})\" + if (![string]::IsNullOrWhiteSpace($appConfigValue.label)) { + $settingName = \"$($settingName)-$($appConfigValue.label)\" + Write-Verbose \"Appending label to setting to avoid duplicate name: $settingName\" + } + $settingValue = $appConfigValue.value + + Save-AppSetting -settingName $settingName -settingValue $settingValue + } + } + } +} + +# Check if Az cli is installed. +$azCliAvailable = Test-ForAzCLI +if ($azCliAvailable -eq $False) { + throw \"Cannot find the Azure CLI (az) on the machine. This must be available to continue.\"\t +} + +# Begin KV Retrieval +$Keys = @() +$global:Settings = @() + +# Extract key names+optional custom setting name +@(($ConfigStoreKeyNames -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + Write-Verbose \"Working on: '$_'\" + $keyDefinition = ($_ -Split \"\\|\") + $keyName = $keyDefinition[0].Trim() + $KeyIsWildcard = $keyName.EndsWith(\"*\") + $settingName = $null + if ($keyDefinition.Count -gt 1) { + if ($KeyIsWildcard) { + throw \"Key definition: '$_' evaluated as a wildcard with a custom setting name. This is not supported.\" + } + $settingName = $keyDefinition[1].Trim() + } + + if ([string]::IsNullOrWhiteSpace($keyName)) { + throw \"Unable to establish key name from: '$_'\" + } + + $key = [PsCustomObject]@{ + KeyName = $keyName + KeyIsWildcard = $KeyIsWildcard + SettingName = if (![string]::IsNullOrWhiteSpace($settingName)) { $settingName } else { \"\" } + } + $Keys += $key + } +} + +$LabelsArray = $global:ConfigStoreLabels -Split \",\" | Where-Object { [string]::IsNullOrWhiteSpace($_) -eq $False } + +Write-Verbose \"Azure AppConfig Retrieval Method: $global:AzureAppConfigRetrievalMethod\" +if (![string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreName)) { + Write-Verbose \"Azure AppConfig Store Name: $global:AzureAppConfigStoreName\" +} +if (![string]::IsNullOrWhiteSpace($global:AzureAppConfigStoreEndpoint)) { + Write-Verbose \"Azure AppConfig Store Endpoint: $global:AzureAppConfigStoreEndpoint\" +} +Write-Verbose \"Suppress warnings: $global:SuppressWarnings\" +Write-Verbose \"Treat warnings as errors: $global:TreatWarningsAsErrors\" +Write-Verbose \"Keys to retrieve: $($Keys.Count)\" +Write-Verbose \"Labels to retrieve: $($LabelsArray.Count)\" + +$appConfigResponse = $null + +# Retrieving all keys should be more performant, but may have a larger payload response. +if ($RetrieveAllKeys) { + + if ($Keys.Count -gt 0) { + Write-Host \"Retrieving ALL config values from store\" + $command = \"az appconfig kv list $global:ConfigStoreParameters --all --auth-mode login\" + + if (![string]::IsNullOrWhiteSpace($global:ConfigStoreLabels)) { + $command += \" --label \"\"$global:ConfigStoreLabels\"\" \" + } + Write-Verbose \"Invoking expression: $command\" + $appConfigResponse = Invoke-Expression -Command $command + $ExitCode = $LastExitCode + Write-Verbose \"az exit code: $ExitCode\" + if ($ExitCode -ne 0) { + throw \"Error retrieving appsettings. ExitCode: $ExitCode\" + } + + if ([string]::IsNullOrWhiteSpace($appConfigResponse)) { + Write-OctopusWarning \"Null or empty response received from Azure App Configuration service\" + } + else { + $appConfigValues = $appConfigResponse | ConvertFrom-Json + } + + foreach ($key in $Keys) { + $keyName = $key.KeyName + $KeyIsWildcard = $key.KeyIsWildcard + $SettingName = $key.SettingName + + Find-AzureAppConfigMatchesFromKey -KeyName $keyName -IsWildcard $KeyIsWildcard -SettingName $SettingName -AppConfigValues $appConfigValues + } + } + # Possible that ONLY labels have been provided + elseif ($LabelsArray.Count -gt 0) { + Find-AzureAppConfigMatchesFromLabels + } +} +# Loop through and get keys based on the supplied names +else { + + Write-Host \"Retrieving keys based on supplied names...\" + if ($Keys.Count -gt 0) { + foreach ($key in $Keys) { + $keyName = $key.KeyName + $KeyIsWildcard = $key.KeyIsWildcard + $settingName = $key.SettingName + + if ([string]::IsNullOrWhiteSpace($settingName)) { + $settingName = \"$($keyName.Trim())\" + } + + Write-Verbose \"Retrieving values matching key: $keyName from store\" + $command = \"az appconfig kv list $global:ConfigStoreParameters --key \"\"$keyName\"\" --auth-mode login\" + + if (![string]::IsNullOrWhiteSpace($global:ConfigStoreLabels)) { + $command += \" --label \"\"$global:ConfigStoreLabels\"\" \" + } + Write-Verbose \"Invoking expression: $command\" + + $appConfigResponse = Invoke-Expression -Command $command + $ExitCode = $LastExitCode + Write-Verbose \"az exit code: $ExitCode\" + if ($ExitCode -ne 0) { + throw \"Error retrieving appsettings. ExitCode: $ExitCode\" + } + + if ([string]::IsNullOrWhiteSpace($appConfigResponse)) { + Write-OctopusWarning \"Null or empty response received from Azure App Configuration service\" + } + else { + $appConfigValues = $appConfigResponse | ConvertFrom-Json + if ($appConfigValues.Count -eq 0) { + Write-OctopusWarning \"Unable to find a matching key in Azure App Config for: $keyName\" + } + else { + Write-Verbose \"Finding match(es) for: $keyName\" + Find-AzureAppConfigMatchesFromKey -KeyName $keyName -IsWildcard $KeyIsWildcard -SettingName $settingName -AppConfigValues $appConfigValues + } + } + } + } +} +# End KV Retrieval + +# Begin AZ Function set + +$AdditionalSettings = @() +#$SlotSettings = @() + +# Extract additional settings values +if (-not [string]::IsNullOrWhiteSpace($AdditionalSettingsValues)) { + @(($AdditionalSettingsValues -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + Write-Verbose \"Working on: '$_'\" + if (-not $_.Contains(\"|\")) { + throw \"Setting '$_' doesnt contain the '|' delimiter. Multi-line values aren't supported.\" + } + $settingDefinition = ($_ -Split \"\\|\") + $settingName = $settingDefinition[0].Trim() + $settingValue = \"\" + if ($settingDefinition.Count -gt 1) { + $settingValue = $settingDefinition[1].Trim() + } + if ([string]::IsNullOrWhiteSpace($settingName)) { + throw \"Unable to establish additional setting name from: '$_'\" + } + $setting = [PsCustomObject]@{ + name = $settingName + value = $settingValue + slotSetting = $false + } + $AdditionalSettings += $setting + } + } +} + + +if ($Settings.Count -gt 0 -or $AdditionalSettings.Count -gt 0) { + Write-Host \"Settings found to publish to App Function: $FunctionName\" + + Write-Verbose \"Function Name: $FunctionName\" + Write-Verbose \"Resource Group: $ResourceGroup\" + Write-Verbose \"Slot: $Slot\" + Write-Verbose \"Settings: $($Settings.Count)\" + Write-Verbose \"Additional Settings: $($AdditionalSettings.Count)\" + if ($AdditionalSettings.Count -gt 0) { + Write-Verbose \"Combining additional settings with settings retrieved from Azure App Config\" + $Settings = $Settings + $AdditionalSettings + } + + $settingsFile = $null + + try { + + $command = \"az functionapp config appsettings set --name=\"\"$Functionname\"\" --resource-group \"\"$ResourceGroup\"\" \" + if (-not([string]::IsNullOrWhiteSpace($Slot))) { + $command += \" --slot \"\"$Slot\"\" \" + } + + if ($Settings.Count -ge 1) { + $settingsFile = [System.IO.Path]::GetRandomFileName() + $ConvertToJsonParameters = @{} + if ($PSVersionTable.PSVersion.Major -ge 6) { + $ConvertToJsonParameters.Add(\"AsArray\", $True) + } + $Settings | ConvertTo-Json @ConvertToJsonParameters | Set-Content -Path $settingsFile + $command += \" --settings '@$settingsFile'\" + } + + Write-Verbose \"Invoking expression: $command\" + Write-Host \"##octopus[stderr-progress]\" + $settingsUpdateResponse = Invoke-Expression -Command $command + $ExitCode = $LastExitCode + Write-Verbose \"FunctionApp update ExitCode: $ExitCode\" + if ($ExitCode -ne 0) { + throw \"Error configuring appsettings for function app '$FunctionName'. ExitCode: $ExitCode\" + } + Write-Host \"##octopus[stderr-default]\" + if ($null -ne $settingsUpdateResponse) { + Write-Host \"Update of function '$FunctionName' was successful\" + try { + $functionSettings = $settingsUpdateResponse | ConvertFrom-Json + if ($null -ne $functionSettings) { + $settingsCount = @($functionSettings | Where-Object { $_.slotSetting -eq $False }).Count + $slotSettingsCount = @($functionSettings | Where-Object { $_.slotSetting -eq $True }).Count + Write-Verbose \"Function '$FunctionName' has $settingsCount setting(s) and $slotSettingsCount slot setting(s).\" + } + } + catch {} + } + + } + catch { throw } + finally { + if (-not([string]::IsNullOrWhiteSpace($settingsFile))) { + Write-Verbose \"Removing temporary settings file $settingsFile\" + Remove-Item -Path $settingsFile -Force -ErrorAction Ignore + } + if (-not([string]::IsNullOrWhiteSpace($slotSettingsFile))) { + Write-Verbose \"Removing temporary slot settings file $slotSettingsFile\" + Remove-Item -Path $slotSettingsFile -Force -ErrorAction Ignore + } + } +} +else { + Write-Host \"No settings found to publish to the Azure App function\" +}", + "Octopus.Action.Azure.AccountId": "#{AzFunction.SetAppSettings.FromAzAppConfig.AzureAccount}" + }, + "Parameters": [ + { + "Id": "b4c7f413-9341-4602-b5f4-5d69365e6cff", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.AzureAccount", + "Label": "Azure Account", + "HelpText": "An Azure account with permission to both retrieve values from the Azure App Config store and publish to the App Function", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "fcf6f1fd-1b3c-47d2-8037-802224c76452", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.ConfigStoreName", + "Label": "Config Store Name", + "HelpText": "The name of the Azure App Configuration store. Provide this or the **Config store endpoint**.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7d6b9332-9c13-42b1-83b5-a8e84fc593f0", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.ConfigStoreEndpoint", + "Label": "Config Store Endpoint", + "HelpText": "The endpoint for the Azure App Configuration Store. Provide this or the **Config store name**.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c25b6875-2007-4d55-97a1-7ec7b061b06b", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.RetrievalMethod", + "Label": "Retrieval Method", + "HelpText": "Choose how the step calls the az cli. Either: +- `All`: Retrieve all configuration values in one call +- `Individually`: Retrieve configuration values in multiple calls; one for each key specified in the **Key Names** parameter. + +Default: `All`", + "DefaultValue": "all", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "all|All +individual|Individually" + } + }, + { + "Id": "cf069635-3027-47a4-8965-d229c8e64afd", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.KeyNames", + "Label": "Key Names", + "HelpText": "Specify the names of the keys to be returned from Azure App Configuration in the format `KeyName | CustomSettingName` where: + +- `KeyName` is the key to retrieve. Wildcards are supported by adding `*` at the end of the key name. +- `CustomSettingName` is the _optional_ name to set for the AppSetting value in the App Function. *If this value isn't specified, the original key will be used.*. + +**Note:** Multiple keys can be retrieved by entering each one on a new line. Note: Combining a wildcard search with custom setting names is not supported.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "13007305-0fa2-468c-b7de-3c5317bd8611", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.Labels", + "Label": "Labels", + "HelpText": "Labels are an attribute on keys. Provide one or more labels in the format `label1,label2` to retrieve only selected keys tagged with those labels. + +**Note:** You can include both label values *and* specify key names if you want.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a7e79ba3-5ea3-40ab-9759-aa281e3c02f9", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.SuppressWarnings", + "Label": "Suppress warnings", + "HelpText": "Suppress warnings from being written to the task log. For example, when a supplied key can't be found in the Azure App Configuration store.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "62d8f95f-8a57-4908-b789-c3ecabd7c6dd", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.TreatWarningsAsErrors", + "Label": "Treat Warnings as Errors", + "HelpText": "Treats warnings as errors. If enabled the **Suppress Warnings** parameter is ignored. ", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "c8c1c14d-a515-47e7-b117-6a69644b78af", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.FunctionName", + "Label": "Azure Function App Name", + "HelpText": "The name of the Azure App function.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "07fcb750-13a1-4272-b6b6-a5cdd120a15f", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.ResourceGroup", + "Label": "Resource Group", + "HelpText": "The name of the resource group where the Function App is located.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fbd1b5bf-e1ce-415c-b7db-9c9d49e9f8e3", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.Slot", + "Label": "Slot", + "HelpText": " +The name of the slot. Defaults to the production slot if not specified.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "21e9c8b3-50ce-4006-83a7-86d7803c31ee", + "Name": "AzFunction.SetAppSettings.FromAzAppConfig.AdditionalSettingsValues", + "Label": "Additional AppSettings", + "HelpText": "Specify the name and values of any **additional** settings to be applied to the Azure App Function in the format `KEY | VALUE` where: + +- `KEY` is the name of the app setting to add +- `VALUE` is the value to be used. [Octopus variables](https://octopus.com/docs/projects/variables) can be used here. + + +**Note:** Multiple settings can be added by entering each one on a new line. As a result, values that span multiple lines will result in an error.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.AzurePowerShell", + "$Meta": { + "ExportedAt": "2023-11-30T16:25:08.828Z", + "OctopusVersion": "2024.1.2558", + "Type": "ActionTemplate" + }, + "LastModifiedAt": "2023-11-30T16:25:08.828Z", + "LastModifiedBy": "harrisonmeister", + "Category": "azure" +} diff --git a/step-templates/azure-install-windows-tentacle.json.human b/step-templates/azure-install-windows-tentacle.json.human new file mode 100644 index 000000000..0cad9c2dc --- /dev/null +++ b/step-templates/azure-install-windows-tentacle.json.human @@ -0,0 +1,286 @@ +{ + "Id": "07b23f27-e76a-4b4b-acb1-017006a83269", + "Name": "Azure Windows - Install Octopus Tentacle", + "Description": "This step template will install the latest tentacle on an Azure hosted, Windows virtual machine. This will also open the firewall for inbound traffic on port 10933 on both the NSG and the the vm. +
+*note: expects the Azure CLI to be installed on the worker running this task*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{installWinTentacle.AzureAccount}", + "Octopus.Action.Script.ScriptBody": "$nsgName = $OctopusParameters[\"installWinTentacle.azNsgName\"] +$resourceGroup = $OctopusParameters[\"installWinTentacle.azRgName\"] +$nsgRulePriority = $OctopusParameters[\"installWinTentacle.azNsgRulePriority\"] +$vmName = $OctopusParameters[\"installWinTentacle.azVmName\"] +$serverUri = $OctopusParameters[\"instrallTentacle.octoServerUrl\"] +$apiKey = $OctopusParameters[\"installWinTentacle.octoApiKey\"] +$rolesRaw = $OctopusParameters[\"installWinTentacle.octopusRoles\"] +$enviroRaw = $OctopusParameters[\"installWinTentacle.octopusEnvironments\"] +$octoThumb = $OctopusParameters[\"installWinTentacle.octoServerThumb\"] +$comStyle = $OctopusParameters[\"installWinTentacle.tentacleType\"] +$hostname = $OctopusParameters[\"installWinTentacle.tentacleHostName\"] +$portNumber = $OctopusParameters[\"installWinTentacle.portNumber\"] + +Write-Host \"Parsing Parameters\" + +if([string]::IsNullOrEmpty($rolesRaw)) +{ +\tthrow \"At least one role must be defined\" +} + +if([string]::IsNullOrEmpty($enviroRaw)) +{ +\tthrow \"At least one environment must be defined\" +} + +$roles = \"\" +$rolesRaw -split \"`n\" | ForEach-Object { $roles += \"--role $_ \"} +$roles = $roles.TrimEnd(' ') + +$environments = \"\" +$enviroRaw -split \"`n\" | ForEach-Object { $environments += \"--environment $_ \"} +$environments = $environments.TrimEnd(' ') + +if($comStyle -eq \"TentaclePassive\") +{ +\tif([string]::IsNullOrEmpty($hostname)) + { + \t$hostname = az vm show -d -g $resourceGroup -n $vmName --query publicIps -o tsv + $hostname = $hostname.Trim(\"`n\") + } + + $noListen = \"--port $portNumber --noListen `\"false`\"\" + $comStyle += \" --publicHostName=`\"$hostname`\"\" + $openFirewall = 'true' +} +else +{ +\t$noListen = \"--noListen `\"true`\"\" + $openFirewall = 'false' +} + +if($openFirewall -eq 'true') +{ +\tWrite-Host \"Creating NSG Rule\" +\taz network nsg rule create --name \"OctopusTentacle\" --nsg-name $nsgName --priority $nsgRulePriority --resource-group $resourceGroup --direction Inbound --destination-port-ranges $portNumber +} + +$remoteScript = @\" +`$msiLocation = \"`$env:TEMP\\`$(new-guid).msi\" + +if(`$env:PROCESSOR_ARCHITECTURE -eq \"x86\") +{ + `$downloadPath = \"http://octopus.com/downloads/latest/OctopusTentacle\" +} +else +{ + `$downloadPath = \"http://octopus.com/downloads/latest/OctopusTentacle64\" +} + +Invoke-WebRequest -Uri `$downloadPath -OutFile `$msiLocation -UseBasicParsing + +Start-Process `$msiLocation /quiet -Wait + +Remove-Item `$msiLocation + +\"@ + +Write-Host \"Installing tentacle on remote machine\" + +Set-Content -Value $remoteScript -Path \".\\script.ps1\" + +$result = az vm run-command invoke --command-id RunPowerShellScript --name $vmName -g $resourceGroup --scripts \"@script.ps1\" + +$result + +$msg = (($result | convertfrom-json).value | where {$_.code -eq \"componentstatus/stderr/succeeded\"}).message + +if(![string]::IsNullOrEmpty($msg)) +{ +\tthrow $msg +} + +Write-Verbose \"hostname: $hostname`noListen: $noListen\" + +$remoteScript = @\" + +cd \"C:\\Program Files\\Octopus Deploy\\Tentacle\" + +.\\Tentacle.exe create-instance --instance \"Tentacle\" --config \"C:\\Octopus\\Tentacle.config\" --console +.\\Tentacle.exe new-certificate --instance \"Tentacle\" --if-blank --console +.\\Tentacle.exe configure --instance \"Tentacle\" --reset-trust --console +.\\Tentacle.exe configure --instance \"Tentacle\" --home \"C:\\Octopus\" --app \"C:\\Octopus\\Applications\" $noListen --console +.\\Tentacle.exe configure --instance \"Tentacle\" --trust \"$octoThumb\" --console +if('$openFirewall' -eq 'true'){ +\tNew-NetFirewallRule -DisplayName \"Octopus Tentacle\" -Direction Inbound -LocalPort $portNumber -Protocol TCP -Action Allow +} +.\\Tentacle.exe register-with --instance \"Tentacle\" --server \"$serverUri\" --apiKey=$apiKey $roles $environments --comms-style $comStyle --force --console +.\\Tentacle.exe service --instance \"Tentacle\" --install --start --console + +\"@ + +Write-Host \"Configuring tentacle on remote machine\" + +Set-Content -Value $remoteScript -Path \".\\script.ps1\" + +$result = az vm run-command invoke --command-id RunPowerShellScript --name $vmName -g $resourceGroup --scripts \"@script.ps1\" + +$result + +$msg = (($result | convertfrom-json).value | where {$_.code -eq \"componentstatus/stderr/succeeded\"}).message + +if(![string]::IsNullOrEmpty($msg)) +{ +\tthrow $msg +} +" + }, + "Parameters": [ + { + "Id": "dea51842-271f-48fa-901e-243488049f97", + "Name": "installWinTentacle.AzureAccount", + "Label": "Azure Account", + "HelpText": "Azure account with permissions to the virtual machine in which to install the tentacle on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "6a97548c-3b97-4a19-8b9a-45aa58ac62d9", + "Name": "installWinTentacle.azRgName", + "Label": "Azure Resource Group", + "HelpText": "The name of the resource group housing the NSG and VM", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d9efb017-65c0-4010-b09f-0a6a34fc009f", + "Name": "installWinTentacle.azNsgName", + "Label": "NSG Name", + "HelpText": "The name of the azure network security group to create ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "36c6d441-9fe8-4ea2-957a-2135aa9d1687", + "Name": "installWinTentacle.azNsgRulePriority", + "Label": "Azure NSG Rule Priority", + "HelpText": "the priority to assign to the NSG rule created for the octopus tentacle. Defaults to 400", + "DefaultValue": "400", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e4358aec-2798-412a-bcd2-1c7e41a0728d", + "Name": "installWinTentacle.azVmName", + "Label": "VM Name", + "HelpText": "The name of the virtual machine to target when ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8dbe0a29-4e0f-43f8-8ef2-ae23a2884e85", + "Name": "installWinTentacle.octoServerThumb", + "Label": "Server Thumbprint", + "HelpText": "The Thumbprint of the octopus server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "a0859551-6c84-45d1-af2b-c57becb4e8ef", + "Name": "installWinTentacle.octoApiKey", + "Label": "API Key", + "HelpText": "The API key used to configure the tentacle.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "dd07815d-bfb7-43b9-90f8-21ec5a2d2953", + "Name": "installWinTentacle.octopusRoles", + "Label": "Roles", + "HelpText": "Roles to assign to this tentacle installation.
+*Note: Each role should be on it's own line*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "d3ffb38e-7fcf-4547-be9a-6cd8e46f8e5e", + "Name": "installWinTentacle.octopusEnvironments", + "Label": "Environments", + "HelpText": "Environments to assign this tentacle installation to.
+*Note: Each environment should be on its own line*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "15150827-7bd3-4146-a798-66344851f602", + "Name": "instrallTentacle.octoServerUrl", + "Label": "Server Url", + "HelpText": "The server url to register the tentacle with. Defaults to the base url", + "DefaultValue": "#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9bcd6bb5-2db3-453f-b4f6-6a78f3c39b59", + "Name": "installWinTentacle.tentacleType", + "Label": "Tentacle Type", + "HelpText": "Select between a listing or polling tentacle", + "DefaultValue": "TentaclePassive", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "TentaclePassive|Listening +TentacleActive|Polling" + } + }, + { + "Id": "6bb98570-fe3e-4ab7-adfb-79ef7ce5c2ce", + "Name": "installWinTentacle.tentacleHostName", + "Label": "Tentacle Host Name", + "HelpText": "The host name to register the listening tentacles with. Octopus deploy server uses this value to reach out to the vm.
+*Note: Leave blank to automatically use assigned public IP address.*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7cbbafc4-5eed-41bd-ad9d-c1e67567a469", + "Name": "installWinTentacle.portNumber", + "Label": "Port Number", + "HelpText": "Port number used when installing and registering the tentacle. This port is also opened when installing a listening tentacle", + "DefaultValue": "10933", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2022-02-09T15:56:03.173Z", + "OctopusVersion": "2021.3.12155", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "azure" +} diff --git a/step-templates/azure-keyvault-retrieve-secrets.json.human b/step-templates/azure-keyvault-retrieve-secrets.json.human new file mode 100644 index 000000000..9be1efd78 --- /dev/null +++ b/step-templates/azure-keyvault-retrieve-secrets.json.human @@ -0,0 +1,252 @@ +{ + "Id": "6f59f8aa-b2db-4f7a-b02d-a72c13d386f0", + "Name": "Azure Key Vault - Retrieve Secrets", + "Description": "This step retrieves one or more secrets from an Azure Key Vault and creates [sensitive output variables](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) for each value retrieved. These values can be used in other steps in your deployment or runbook process. + +You can retrieve secrets with a specific version, and you can choose a custom output variable name for each secret. + +--- + +**Required:** +- An azure account with permissions to retrieve secrets from the Azure Key Vault. +- The`Az.KeyVault` PowerShell module installed on the target or worker. If the module can't be found, the step will fail. *The `Az` module(s) can be installed from the [PowerShell gallery](https://www.powershellgallery.com/packages/Az)* + +Notes: + +- Tested on Octopus `2021.1`. +- Tested with both Windows PowerShell and PowerShell Core on Linux. + +", + "ActionType": "Octopus.AzurePowerShell", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ErrorActionPreference = 'Stop' + +# Variables +$AzVaultModuleName = \"Az.KeyVault\" +$AzureKeyVaultName = $OctopusParameters[\"Azure.KeyVault.RetrieveSecrets.VaultName\"] +$VaultSecretNames = $OctopusParameters[\"Azure.KeyVault.RetrieveSecrets.VaultSecrets\"] +$AzVaultModuleSpecificVersion = $OctopusParameters[\"Azure.KeyVault.RetrieveSecrets.AzModule.SpecificVersion\"] +$AzVaultModuleCustomInstallLocation = $OctopusParameters[\"Azure.KeyVault.RetrieveSecrets.AzModule.CustomInstallLocation\"] +$PrintVariableNames = $OctopusParameters[\"Azure.KeyVault.RetrieveSecrets.PrintVariableNames\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($AzureKeyVaultName)) { + throw \"Required parameter Azure.KeyVault.RetrieveSecrets.VaultName not specified\" +} +if ([string]::IsNullOrWhiteSpace($VaultSecretNames)) { + throw \"Required parameter Azure.KeyVault.RetrieveSecrets.VaultSecrets not specified\" +} + +if ([string]::IsNullOrWhiteSpace($AzVaultModuleSpecificVersion) -eq $False) { + $requiredVersion = [Version]$AzVaultModuleSpecificVersion +} + +# Cross-platform bits +$WindowsPowerShell = $True +if ($PSEdition -eq \"Core\") { + $WindowsPowerShell = $False +} + +### Helper functions +function Get-Module-CrossPlatform { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [string] $Name + ) + + $module = Get-Module -Name $Name -ListAvailable + if($WindowsPowerShell -eq $True -and $null -eq $module) { + $module = Get-InstalledModule -Name $Name + } + + return $module +} + +$PowerShellModuleName = $AzVaultModuleName + +# Check for Custom install location specified for AzVaultModule +if ([string]::IsNullOrWhiteSpace($AzVaultModuleCustomInstallLocation) -eq $false) { + if ((Test-Path $AzVaultModuleCustomInstallLocation -IsValid) -eq $false) { + throw \"The path $AzVaultModuleCustomInstallLocation is not valid, please use a relative or absolute path.\" + } + + $AzVaultModulesFolder = [System.IO.Path]::GetFullPath($AzVaultModuleCustomInstallLocation) + $LocalModules = (New-Item \"$AzVaultModulesFolder\" -ItemType Directory -Force).FullName + $env:PSModulePath = $LocalModules + [System.IO.Path]::PathSeparator + $env:PSModulePath + + # Check to see if there + if ((Test-Path -Path \"$LocalModules/$AzVaultModuleName\") -eq $true) + { + # Use specific location + $PowerShellModuleName = \"$LocalModules/$PowerShellModuleName\" + } +} + +# Import module +if([string]::IsNullOrWhiteSpace($AzVaultModuleSpecificVersion)) { + Write-Host \"Importing module $PowerShellModuleName ...\" + Import-Module -Name $PowerShellModuleName +} +else { + Write-Host \"Importing module $PowerShellModuleName ($AzVaultModuleSpecificVersion)...\" + Import-Module -Name $PowerShellModuleName -RequiredVersion $requiredVersion +} + +# Check if Az.Vault Module is installed. +$azVaultModule = Get-Module-CrossPlatform -Name $AzVaultModuleName\t +if ($null -eq $azVaultModule) { + throw \"Cannot find the '$AzVaultModuleName' module on the machine. If you think it is installed, try restarting the Tentacle service for it to be detected.\"\t +} + +$Secrets = @() +$VariablesCreated = 0 +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +# Extract secret names+versions +@(($VaultSecretNames -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + Write-Verbose \"Working on: '$_'\" + $secretDefinition = ($_ -Split \"\\|\") + $secretName = $secretDefinition[0].Trim() + $secretNameAndVersion = ($secretName -Split \" \") + $secretVersion = $null + if($secretNameAndVersion.Count -gt 1) { + \t$secretName = $secretNameAndVersion[0].Trim() + $secretVersion = $secretNameAndVersion[1].Trim() + } + if([string]::IsNullOrWhiteSpace($secretName)) { + throw \"Unable to establish secret name from: '$($_)'\" + } + $secret = [PsCustomObject]@{ + Name = $secretName + SecretVersion= $secretVersion + VariableName = if (![string]::IsNullOrWhiteSpace($secretDefinition[1])) { $secretDefinition[1].Trim() } else { \"\" } + } + $Secrets += $secret + } +} + +Write-Verbose \"Vault Name: $AzureKeyVaultName\" +Write-Verbose \"Print variables: $PrintVariableNames\" +Write-Verbose \"Secrets to retrieve: $($Secrets.Count)\" +Write-Verbose \"Az Version specified: $AzVaultModuleSpecificVersion\" +Write-Verbose \"Az Custom Install Dir: $AzVaultModuleCustomInstallLocation\" + +# Retrieve Secrets +foreach($secret in $secrets) { + $name = $secret.Name + $secretVersion = $secret.SecretVersion + $variableName = $secret.VariableName + if ([string]::IsNullOrWhiteSpace($variableName)) { + $variableName = \"$($AzureKeyVaultName.Trim()).$($name.Trim())\" + } + + if ([string]::IsNullOrWhiteSpace($secretVersion)) { + \t$azSecretValue = Get-AzKeyVaultSecret -VaultName $AzureKeyVaultName -Name $name -AsPlainText + } + else { + \t$azSecretValue = Get-AzKeyVaultSecret -VaultName $AzureKeyVaultName -Name $name -Version $secretVersion -AsPlainText + } + + Set-OctopusVariable -Name $variableName -Value $azSecretValue -Sensitive + + if($PrintVariableNames -eq $True) { + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$variableName}\" + } + $VariablesCreated += 1 +} + +Write-Host \"Created $variablesCreated output variables\"", + "Octopus.Action.Azure.AccountId": "#{Azure.KeyVault.RetrieveSecrets.Account}" + }, + "Parameters": [ + { + "Id": "5b05337d-a62d-44f4-a702-95b45a400160", + "Name": "Azure.KeyVault.RetrieveSecrets.Account", + "Label": "Azure Account", + "HelpText": "An Azure account with permissions to retrieve secrets from the Azure Key Vault.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "9b09b2b3-3c4d-4cbb-a065-8955f62448ad", + "Name": "Azure.KeyVault.RetrieveSecrets.VaultName", + "Label": "Vault Name", + "HelpText": "The name of the Azure Key Vault to retrieve secrets from.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "43949afd-1049-41fc-bd01-e878476f5952", + "Name": "Azure.KeyVault.RetrieveSecrets.VaultSecrets", + "Label": "Vault Secrets to retrieve", + "HelpText": "Specify the names of the Secrets to be returned from Azure Key Vault, in the format `SecretName SecretVersion | OutputVariableName` where: + +- `SecretName` is the name of the Secret to retrieve. +- `SecretVersion` is the _optional_ version of the Secret to retrieve. *If this value isn't specified, the latest version will be retrieved*. +- `OutputVariableName` is the _optional_ Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) name to store the secret's value in. *If this value isn't specified, an output name will be generated dynamically*. + +**Note:** Multiple fields can be retrieved by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "46842344-405e-4633-a63f-115baaff7774", + "Name": "Azure.KeyVault.RetrieveSecrets.PrintVariableNames", + "Label": "Print output variable names", + "HelpText": "Write out the Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) names to the task log. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "0c20c004-c792-4acc-81c5-62ecceecf6ac", + "Name": "Azure.KeyVault.RetrieveSecrets.AzModule.SpecificVersion", + "Label": "Az PowerShell Module version (optional)", + "HelpText": "If you wish to use a specific version of the `Az` PowerShell module (rather than the default), enter the version number here. e.g. `5.9.0`. + +**Note:** The version specified must exist on the machine. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d4d93a9a-72e1-48be-9ed6-78986bbfaa06", + "Name": "Azure.KeyVault.RetrieveSecrets.AzModule.CustomInstallLocation", + "Label": "Az PowerShell Install Location (optional)", + "HelpText": "If you wish to provide a custom path to the `Az` PowerShell module (rather than the default), enter the value here. + +**Note:** The Module must exist at the specified location on the machine. This step template will not download the Module. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedAt": "2021-06-02T08:34:02.548Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2021-06-02T08:34:02.548Z", + "OctopusVersion": "2021.1.7236", + "Type": "ActionTemplate" + }, + "Category": "azure-keyvault" + } diff --git a/step-templates/azure-linux-install-octopus-tentacle.json.human b/step-templates/azure-linux-install-octopus-tentacle.json.human new file mode 100644 index 000000000..c18e17608 --- /dev/null +++ b/step-templates/azure-linux-install-octopus-tentacle.json.human @@ -0,0 +1,274 @@ +{ + "Id": "2d21abec-491a-4d85-9210-2784bc3dfeb8", + "Name": "Azure Linux - Install Octopus Tentacle", + "Description": "This step template will install the latest tentacle on an Azure hosted, Linux virtual machine. This will also open the firewall for inbound traffic on port 10933 on the NSG. +
+*Note: Expects the Azure CLI and Powershell to be installed on the worker running this task*
+*Note: Requires dotnet core to be pre-installed on the target machine*
+*Note: Firewall ports will not be opened on the remote machine*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{installLinuxTentacle.AzureAccount}", + "Octopus.Action.Script.ScriptBody": "$nsgName = $OctopusParameters[\"installLinuxTentacle.azNsgName\"] +$resourceGroup = $OctopusParameters[\"installLinuxTentacle.azRgName\"] +$nsgRulePriority = $OctopusParameters[\"installLinuxTentacle.azNsgRulePriority\"] +$vmName = $OctopusParameters[\"installLinuxTentacle.azVmName\"] +$serverUri = $OctopusParameters[\"instrallTentacle.octoServerUrl\"] +$apiKey = $OctopusParameters[\"installLinuxTentacle.octoApiKey\"] +$rolesRaw = $OctopusParameters[\"installLinuxTentacle.octopusRoles\"] +$enviroRaw = $OctopusParameters[\"installLinuxTentacle.octopusEnvironments\"] +$octoThumb = $OctopusParameters[\"installLinuxTentacle.octoServerThumb\"] +$comStyle = $OctopusParameters[\"installLinuxTentacle.tentacleType\"] +$hostname = $OctopusParameters[\"installLinuxTentacle.tentacleHostName\"] +$portNumber = $OctopusParameters[\"installLinuxTentacle.portNumber\"] + +Write-Host \"Parsing Parameters\" + +if([string]::IsNullOrEmpty($rolesRaw)) +{ +\tthrow \"At least one role must be defined\" +} + +if([string]::IsNullOrEmpty($enviroRaw)) +{ +\tthrow \"At least one environment must be defined\" +} + +$roles = \"\" +$rolesRaw -split \"`n\" | ForEach-Object { $roles += \"--role $_ \"} +$roles = $roles.TrimEnd(' ') + +$environments = \"\" +$enviroRaw -split \"`n\" | ForEach-Object { $environments += \"--env $_ \"} +$environments = $environments.TrimEnd(' ') + +if($comStyle -eq \"TentaclePassive\") +{ +\tif([string]::IsNullOrEmpty($hostname)) + { + \t$hostname = az vm show -d -g $resourceGroup -n $vmName --query publicIps -o tsv + $hostname = $hostname.Trim(\"`n\") + } + + $noListen = \"--port $portNumber --noListen `\"false`\"\" + $comStyle += \" --publicHostName=`\"$hostname`\"\" + $openFirewall = 'true' +} +else +{ +\t$noListen = \"--noListen `\"true`\"\" + $openFirewall = 'false' +} + +if($openFirewall -eq 'true') +{ +\tWrite-Host \"Creating NSG Rule\" +\taz network nsg rule create --name \"OctopusTentacle\" --nsg-name $nsgName --priority $nsgRulePriority --resource-group $resourceGroup --direction Inbound --destination-port-ranges $portNumber +} + +Write-Verbose \"hostname: $hostname`noListen: $noListen\" + +$remoteScript = @\" + +printf '%s\ +' \"Test case x failed\" >&2 +exit 1 + +configFilePath=\"/etc/octopus/default/tentacle-default.config\" +appPath=\"/home/Octopus/Applications\" + +# try curl +{ + curl -L https://octopus.com/downloads/latest/Linux_x64TarGz/OctopusTentacle --output /tmp/tentacle-linux_x64.tar.gz -fsS +} || { + wget https://octopus.com/downloads/latest/Linux_x64TarGz/OctopusTentacle -O /tmp/tentacle-linux_x64.tar.gz -fsS +} + +if [ ! -d \"/opt/octopus\" ]; then + mkdir /opt/octopus +fi + +tar xvzf /tmp/tentacle-linux_x64.tar.gz -C /opt/octopus +rm /tmp/tentacle-linux_x64.tar.gz + +cd /opt/octopus/tentacle + +sudo /opt/octopus/tentacle/Tentacle create-instance --config \"`$configFilePath\" +sudo chmod a+rwx `$configFilePath +/opt/octopus/tentacle/Tentacle new-certificate --if-blank +/opt/octopus/tentacle/Tentacle configure --port $portNumber --noListen False --reset-trust --app \"`$appPath\" +/opt/octopus/tentacle/Tentacle configure --trust $octoThumb +echo \"Registering the Tentacle $name with server $serverUri in environment $environments with role $roles\" +/opt/octopus/tentacle/Tentacle register-with --server \"$serverUri\" --apiKey \"$apikey\" --name \"$name\" $environments $roles --comms-style $comStyle --force +sudo /opt/octopus/tentacle/Tentacle service --install --start + +\"@ + +Write-Host \"Installing tentacle on remote machine\" +$scriptGuid = (new-guid).guid +Set-Content -Value $remoteScript -Path \".\\$scriptGuid.ps1\" + +$result = az vm run-command invoke --command-id RunShellScript --name $vmName -g $resourceGroup --scripts \"@script.ps1\" + +$result + +$msg = ($result | convertfrom-json).value[0].message + +if($msg -match \"(?<=\\[stderr\\]).+\") +{ +\tthrow $msg +} + +remove-item \".\\$scriptGuid.ps1\" +" + }, + "Parameters": [ + { + "Id": "dea51842-271f-48fa-901e-243488049f97", + "Name": "installLinuxTentacle.AzureAccount", + "Label": "Azure Account", + "HelpText": "Azure account with permissions to the virtual machine in which to install the tentacle on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "6a97548c-3b97-4a19-8b9a-45aa58ac62d9", + "Name": "installLinuxTentacle.azRgName", + "Label": "Azure Resource Group", + "HelpText": "The name of the resource group housing the NSG and VM", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d9efb017-65c0-4010-b09f-0a6a34fc009f", + "Name": "installLinuxTentacle.azNsgName", + "Label": "NSG Name", + "HelpText": "The name of the azure network security group to create ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "36c6d441-9fe8-4ea2-957a-2135aa9d1687", + "Name": "installLinuxTentacle.azNsgRulePriority", + "Label": "Azure NSG Rule Priority", + "HelpText": "the priority to assign to the NSG rule created for the octopus tentacle. Defaults to 400", + "DefaultValue": "400", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e4358aec-2798-412a-bcd2-1c7e41a0728d", + "Name": "installLinuxTentacle.azVmName", + "Label": "VM Name", + "HelpText": "The name of the virtual machine to target when ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8dbe0a29-4e0f-43f8-8ef2-ae23a2884e85", + "Name": "installLinuxTentacle.octoServerThumb", + "Label": "Server Thumbprint", + "HelpText": "The Thumbprint of the octopus server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "a0859551-6c84-45d1-af2b-c57becb4e8ef", + "Name": "installLinuxTentacle.octoApiKey", + "Label": "API Key", + "HelpText": "The API key used to configure the tentacle.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "dd07815d-bfb7-43b9-90f8-21ec5a2d2953", + "Name": "installLinuxTentacle.octopusRoles", + "Label": "Roles", + "HelpText": "Roles to assign to this tentacle installation.
+*Note: Each role should be on it's own line*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "d3ffb38e-7fcf-4547-be9a-6cd8e46f8e5e", + "Name": "installLinuxTentacle.octopusEnvironments", + "Label": "Environments", + "HelpText": "Environments to assign this tentacle installation to.
+*Note: Each environment should be on its own line*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "15150827-7bd3-4146-a798-66344851f602", + "Name": "installLinuxTentacle.octoServerUrl", + "Label": "Server Url", + "HelpText": "The server url to register the tentacle with. Defaults to the base url", + "DefaultValue": "#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9bcd6bb5-2db3-453f-b4f6-6a78f3c39b59", + "Name": "installLinuxTentacle.tentacleType", + "Label": "Tentacle Type", + "HelpText": "Select between a listening or polling tentacle", + "DefaultValue": "TentaclePassive", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "TentaclePassive|Listening +TentacleActive|Polling" + } + }, + { + "Id": "6bb98570-fe3e-4ab7-adfb-79ef7ce5c2ce", + "Name": "installLinuxTentacle.tentacleHostName", + "Label": "Tentacle Host Name", + "HelpText": "The host name to register the listening tentacles with. Octopus deploy server uses this value to reach out to the vm.
+*Note: Leave blank to automatically use assigned public IP address.*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1ae37ac0-fc37-447c-941f-7b72ed32a647", + "Name": "installLinuxTentacle.portNumber", + "Label": "Port Number", + "HelpText": "Port number used when installing and registering the tentacle. This port is also opened when installing a listening tentacle", + "DefaultValue": "10933", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2022-02-09T15:56:03.173Z", + "OctopusVersion": "2021.3.12155", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "azure" +} diff --git a/step-templates/azure-load-appsettings-from-file.json.human b/step-templates/azure-load-appsettings-from-file.json.human new file mode 100644 index 000000000..8c65c1aef --- /dev/null +++ b/step-templates/azure-load-appsettings-from-file.json.human @@ -0,0 +1,171 @@ +{ + "Id": "140d22e8-abe9-4a32-aab7-20af667c6255", + "Name": "Azure Website - Load App Settings From File (Geta)", + "Description": "Loads app settings from a json file (e.g. local.settings.json) which is also json-transformed to inject environment-specific values.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 7, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "31f79f978a2e46f9b3add02f695e672b", + "Name": "LoadAppSettingsFromFile.Package", + "PackageId": "#{Parameters.PackageId}", + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Azure.AccountId": "#{Azure.Subscription.Name}", + "Octopus.Action.Package.JsonConfigurationVariablesEnabled": "True", + "Octopus.Action.EnabledFeatures": "Octopus.Features.JsonConfigurationVariables", + "Octopus.Action.Package.JsonConfigurationVariablesTargets": "#{Parameters.SettingsFile.Path}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.PackageId": "#{Utilities.PackageId}", + "Octopus.Action.Package.FeedId": "feeds-builtin", + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Function Get-Parameter($Name, $Required, $Default, [switch]$FailOnValidate) { + $result = $null + $errMessage = [string]::Empty + + If ($null -ne $OctopusParameters) { + $result = $OctopusParameters[$Name] + Write-Host (\"Octopus parameter value for \" + $Name + \": \" + $result) + } + + If ($null -eq $result) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($null -ne $variable) { + $result = $variable.Value + } + } + + If ($null -eq $result) { + If ($Required) { + $errMessage = \"Mandatory parameter '$Name' not specified\" + } + Else { + $result = $Default + } + } + + If (-Not [string]::IsNullOrEmpty($errMessage)) { + If ($FailOnValidate) { + Throw $errMessage + } + Else { + Write-Warning $errMessage + } + } + + return $result +} + +Function Main( + [Parameter(Mandatory = $true)][string] $azureResourceGroupName, + [Parameter(Mandatory = $true)][string] $azureWebAppName, + [Parameter(Mandatory = $true)][string] $azureSettingsFilePath, + [Parameter(Mandatory = $false)][string] $azureDeploySlotName = $null +) { + Write-Host \"Start AzureLoadAppSettingsFromFile\" + + If ((Test-Path $azureSettingsFilePath) -ne $true) { + Write-Warning \"Settings file '$azureSettingsFilePath' not found!\" + Exit 0 + } + + $settingsJson = Get-Content -Raw -Path $azureSettingsFilePath | ConvertFrom-Json + + If (($settingsJson -eq $null) -or ($settingsJson.Values -eq $null)) { + Write-Warning \"Settings file '$azureSettingsFilePath' doesn't contain Values object. Unable to load app settings!\" + Exit 0 + } + + # Parse app settings into a hashtable object + + $settingsValues = $settingsJson.Values + + $appSettings = @{} + $settingsValues.psobject.properties | Foreach { $appSettings[$_.Name] = $_.Value } + + # Set app settings for either slot or a webapp + + If ([string]::IsNullOrEmpty($azureDeploySlotName)) { + Set-AzureRmWebApp -Name $azureWebAppName -ResourceGroupName $azureResourceGroupName -AppSettings $appSettings + } Else { + Set-AzureRmWebAppSlot -Name $azureWebAppName -ResourceGroupName $azureResourceGroupName -AppSettings $appSettings -Slot $azureDeploySlotName + } + + Write-Host \"End AzureLoadAppSettingsFromFile\" +} + +& Main ` + -azureResourceGroupName (Get-Parameter \"Parameters.ResourceGroup.Name\" $true \"\" $true) ` + -azureWebAppName (Get-Parameter \"Parameters.WebApp.Name\" $true \"\" $true) ` + -azureSettingsFilePath (Get-Parameter \"Parameters.SettingsFile.Path\" $true \"\" $true) ` + -azureDeploySlotName (Get-Parameter \"Parameters.DeploySlot.Name\" $false \"\" $true)" + }, + "Parameters": [ + { + "Id": "e21c4265-7e92-4893-a359-3d8c4b0223c2", + "Name": "Parameters.ResourceGroup.Name", + "Label": "Resource group name", + "HelpText": "Name of the target resource group.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4485ab2e-2003-4fab-814f-90ce9a338cce", + "Name": "Parameters.WebApp.Name", + "Label": "Webapp / function name", + "HelpText": "Name of the target webapp.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f589143a-3f46-4a2b-b14f-02469ed80c19", + "Name": "Parameters.SettingsFile.Path", + "Label": "Settings file path", + "HelpText": "Path of the JSON settings file relative to script directory. Or in most of the cases easiest is to use Octopus Action variable for the extract path: `#{Octopus.Action.Package[LoadAppSettingsFromFile.Package].ExtractedPath}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fb46e50c-283a-4d7d-b4ec-2a171f76d8c4", + "Name": "Parameters.DeploySlot.Name", + "Label": "Deploy slot name", + "HelpText": "Name of the deploy slot. Production slot will be updated if left blank.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "81b491a9-e1f5-4b22-b9d2-22df2bb60686", + "Name": "Parameters.PackageId", + "Label": "Package Id", + "HelpText": "Id of the referenced package which when extracted might contain the settings file.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "sarbis", + "$Meta": { + "ExportedAt": "2019-01-22T09:58:08.811Z", + "OctopusVersion": "2018.10.0", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-manage-webjob.json.human b/step-templates/azure-manage-webjob.json.human new file mode 100644 index 000000000..529b6efa0 --- /dev/null +++ b/step-templates/azure-manage-webjob.json.human @@ -0,0 +1,103 @@ +{ + "Id": "923532f4-1ee1-49db-b3b3-a8cab0d8986b", + "Name": "Azure Manage WebJob", + "Description": "This template can start, stop, or delete a web job", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{Octopus.Action.Azure.AccountId}", + "Octopus.Action.Script.ScriptBody": "$httpAction = 'POST' + +if ($WebJobAction -eq 'delete') { + $httpAction = 'DELETE' +} + +$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $WebJobUserName,$WebJobPassword))) +$apiUrl = \"https://$WebJobWebApp.scm.azurewebsites.net/api/$WebJobType/$WebJobName/$WebJobAction\" +Invoke-RestMethod -Uri $apiUrl -Headers @{Authorization=(\"Basic {0}\" -f $base64AuthInfo)} -Method $httpAction -ContentType \"Application/Json\"", + "OctopusUseBundledTooling": "False" + }, + "Parameters": [ + { + "Id": "c0de82c1-748a-4741-8380-e2e4660b80df", + "Name": "WebJobWebApp", + "Label": "Web App", + "HelpText": "The Web App the Azure WebJob is hosted under.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bae34daf-b130-4b5e-99df-a35ec1ab4a13", + "Name": "WebJobUserName", + "Label": "User Name", + "HelpText": "The Username of the authentication to the Kudu Api. + +See https://github.com/projectkudu/kudu/wiki/Deployment-credentials", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "689302ce-9989-4aa8-8c5b-221d899b41b0", + "Name": "WebJobPassword", + "Label": "Password", + "HelpText": "The Password of the authentication to the Kudu Api. + +See https://github.com/projectkudu/kudu/wiki/Deployment-credentials", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "b87406da-59c7-400c-8d45-6e174b87660e", + "Name": "WebJobAction", + "Label": "Action", + "HelpText": "The action to perform. Start, Stop, or Delete.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "start|Start +stop|Stop +delete|Delete" + } + }, + { + "Id": "f6b72bf8-fe1a-404d-98f2-3ea12d09e1b2", + "Name": "WebJobType", + "Label": "Job Type", + "HelpText": "The type of job, Continuous or Triggered", + "DefaultValue": "continuouswebjobs", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "continuouswebjobs|Continuous +triggeredwebjobs|Triggered" + } + }, + { + "Id": "727d450b-f3aa-4f80-9ae4-e3397e339bab", + "Name": "WebJobName", + "Label": "Job Name", + "HelpText": "The name of the Job to act upon.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.AzurePowerShell", + "$Meta": { + "ExportedAt": "2024-06-24T10:30:22.248Z", + "OctopusVersion": "2023.3.13118", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "Your GitHub Username", + "Category": "other" +} diff --git a/step-templates/azure-powershell-version-update.json.human b/step-templates/azure-powershell-version-update.json.human new file mode 100644 index 000000000..796859501 --- /dev/null +++ b/step-templates/azure-powershell-version-update.json.human @@ -0,0 +1,50 @@ +{ + "Id": "fdb57a35-9061-48e2-a650-2e6231200456", + "Name": "Update Azure PowerShell Module", + "Description": "This Step Template will help you to update the Azure PowerShell Module version on any targeted tentacle. [Note: Windows PowerShell 5.0 installed as pre-requisites for this step template to work]", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "Get-ChildItem -Path \"C:\\Program Files\\WindowsPowerShell\\Modules\\\" -Filter Azure* -Recurse -Force | Remove-Item -Force -Recurse -Verbose + +Install-Module -Name $ModuleName -RequiredVersion $AzurePSModuleVersion -Force -Verbose", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "ac77d527-18e1-4c47-904b-1e1ad9d7b32c", + "Name": "AzurePSModuleVersion", + "Label": "Azure PowerShell Version", + "HelpText": "example : 2.2.0 +Refer to find the version number : https://www.powershellgallery.com/packages/Azure", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3ea5675f-ceb9-43e2-b31a-fa0c2da7bc39", + "Name": "ModuleName", + "Label": "PowerShell Module Name", + "HelpText": "AzureRM +Refer for Module Name : https://www.powershellgallery.com/packages/AzureRM", + "DefaultValue": "AzureRM", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2016-10-20T04:20:39.295+00:00", + "LastModifiedBy": "mani0070", + "$Meta": { + "ExportedAt": "2016-10-20T04:20:39.295+00:00", + "OctopusVersion": "3.4.12", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-remove-resource-group-deployments.json.human b/step-templates/azure-remove-resource-group-deployments.json.human new file mode 100644 index 000000000..b1f6e3664 --- /dev/null +++ b/step-templates/azure-remove-resource-group-deployments.json.human @@ -0,0 +1,165 @@ +{ + "Id": "7351a3e7-df59-4e0c-863a-dca6f33ad2e1", + "Name": "Azure - Remove Resource Group Deployments", + "Description": "There is a cap in Azure that prevents having more than 800 deployments in the history at any given time: link to microsoft docs + +This script helps alleviate this issue by limiting how many deployments are allowed exist, keeps the latest specified number of deployments, and will remove the rest. + +What it does: Logs into Azure, selects the resource group of the app. Based on how many deployments it wants to keep, it will keep the latest X deployments and remove the rest. If there are less deployments than X to keep, the script will skip.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Azure.AccountId": "#{AzureSubscriptionAccount}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($null -ne $OctopusParameters) { + $result = $OctopusParameters[$Name] + } + + if ($null -eq $result) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($null -ne $variable) { + $result = $variable.Value + } + } + + if ($null -eq $result) { + if ($Required) { + throw \"Missing parameter value $Name\" + } + else { + $result = $Default + } + } + + return $result +} + +Function Get-Deployments { + Param( + [Parameter(Mandatory = $true)] + [string]$resourceGroupName + ) + $listOfDeployments = Get-AzureRmResourceGroupDeployment -ResourceGroupName $resourceGroupName + $azureDeploymentNameAndDate = @() + $listOfDeployments | ForEach-Object { $azureDeploymentNameAndDate += [PSCustomObject]@{DeploymentName = $_.DeploymentName; Time = $_.Timestamp } } + + return $azureDeploymentNameAndDate +} + +Function Remove-AzureRmResourceDeployments { + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$resourceGroupName, + + [ValidateRange(0, 800)] + [int]$numberOfDeploymentsToKeep, + + [int]$numberOfDaysToKeep + ) + + if ($null -ne $($numberOfDaysToKeep) -and $($numberOfDaysToKeep) -gt 0) { + $azureDeploymentNameAndDate = Get-Deployments $resourceGroupName + + Write-Output \"Found $($azureDeploymentNameAndDate.Count) deployments from the $resourceGroupName resource group\" + + $itemsToRemove = $azureDeploymentNameAndDate | Where-Object { $_.Time -lt ((get-date).AddDays( - $($numberOfDaysToKeep))) } + $numberOfItemsToRemove = $itemsToRemove | Measure-Object + + if ($numberOfitemsToRemove.Count -eq 0) { + Write-Output \"There are no deployments older than $($numberOfDaysToKeep) days old in $($resourceGroupName)... skipping\" + } + else { + Write-Output \"Deleting $($numberOfitemsToRemove.Count) deployment(s) from $($resourceGroupName) as they are more than $($numberOfDaysToKeep) days old.\" + $itemsToRemove | ForEach-Object { Write-Output \"Deleting $($_.DeploymentName)\"; Remove-AzureRmResourceGroupDeployment -ResourceGroupName $resourceGroupName -name $_.DeploymentName } + } + } + + if ($null -ne $($numberOfDeploymentsToKeep) -and $($numberOfDeploymentsToKeep) -gt 0) { + $azureDeploymentNameAndDate = Get-Deployments $resourceGroupName + + Write-Output \"Found $($azureDeploymentNameAndDate.Count) deployments from the $resourceGroupName resource group\" + + $itemsToRemove = $azureDeploymentNameAndDate | Sort-Object Time -Descending | select-object -skip $numberOfDeploymentsToKeep + $numberOfItemsToRemove = $itemsToRemove | Measure-Object + + if ($numberOfitemsToRemove.Count -eq 0) { + Write-Output \"Max number of deployments set to keep is $numberOfDeploymentsToKeep... skipping\" + } + else { + Write-Output \"Maximum number of deployments exceeded. Deleting $($numberOfitemsToRemove.Count) deployment(s) from $($resourceGroupName)\" + $itemsToRemove | ForEach-Object { Write-Output \"Deleting $($_.DeploymentName)\"; Remove-AzureRmResourceGroupDeployment -ResourceGroupName $resourceGroupName -name $_.DeploymentName } + } + } +} + +## -------------------------------------------------------------------------------------- +## Input +## -------------------------------------------------------------------------------------- + +$resourceGroupName = Get-Param 'Azure.RemoveResourceGroupDeployments.ResourceGroupName' -Required +$numberOfDeploymentsToKeep = Get-Param 'Azure.RemoveResourceGroupDeployments.NumberOfDeploymentsToKeep' -Default 0 +$numberOfDaysToKeep = Get-Param 'Azure.RemoveResourceGroupDeployments.NumberOfDaysToKeep' -Default 0 + +Remove-AzureRmResourceDeployments -resourceGroupName $resourceGroupName -numberOfDeploymentsToKeep $numberOfDeploymentsToKeep -numberOfDaysToKeep $numberOfDaysToKeep -Verbose +" + }, + "Parameters": [ + { + "Id": "30a798bd-db11-44d3-82bd-28c3b13e77a5", + "Name": "AzureSubscriptionAccount", + "Label": "Azure Subscription Account", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "27c68d1f-7fdc-4e10-aa4a-77a0ff55eb29", + "Name": "Azure.RemoveResourceGroupDeployments.ResourceGroupName", + "Label": "Resource Group Name", + "HelpText": "Enter the name of the resource group.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2adcf1a9-d319-4ed3-80e7-4c533ddbe5cd", + "Name": "Azure.RemoveResourceGroupDeployments.NumberOfDeploymentsToKeep", + "Label": "Number Of Deployments To Keep", + "HelpText": "Number Of Deployments To Keep. Defaults to empty and will not be used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c5ce930b-ea04-42b8-8ade-d167a26dab2a", + "Name": "Azure.RemoveResourceGroupDeployments.NumberOfDaysToKeep", + "Label": "Number Of Days To Keep.", + "HelpText": "Number Of Days To Keep. Defaults to empty and will not be used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "Category": "Azure", + "SpaceId": "Spaces-1", + "LastModifiedBy": "bmdixon", + "$Meta": { + "ExportedAt": "2019-04-25T13:33:59.856Z", + "OctopusVersion": "2019.4.2", + "Type": "ActionTemplate" + } +} diff --git a/step-templates/azure-site-extensions-install.json.human b/step-templates/azure-site-extensions-install.json.human new file mode 100644 index 000000000..126ee98ee --- /dev/null +++ b/step-templates/azure-site-extensions-install.json.human @@ -0,0 +1,134 @@ +{ + "Id": "7518eaa0-677c-4562-82d5-a131f29e1744", + "Name": "Azure Site Extensions - Install", + "Description": "Installs an Azure [site extension](https://www.siteextensions.net) in your Azure web app.", + "ActionType": "Octopus.AzureResourceGroup", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Azure.ResourceGroupName": "#{AzSiteExt_ResourceGroupName}", + "Octopus.Action.Azure.TemplateSource": "Inline", + "Octopus.Action.Azure.ResourceGroupTemplateParameters": "{\"siteName\":{\"value\":\"#{AzSiteExt_SiteName}\"},\"extensionName\":{\"value\":\"#{AzSiteExt_ExtensionName}\"},\"appServicePlanName\":{\"value\":\"#{AzSiteExt_AppServicePlanName}\"}}", + "Octopus.Action.Azure.ResourceGroupDeploymentMode": "Incremental", + "Octopus.Action.Azure.ResourceGroupTemplate": "{\r + \"$schema\": \"http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#\",\r + \"contentVersion\": \"1.0.0.0\",\r + \"parameters\": { \r + \"siteName\": {\r + \"type\": \"string\",\r + \"metadata\": {\r + \"description\": \"Web site name\"\r + }\r + },\r + \"appServicePlanName\": {\r + \"type\": \"string\",\r + \"metadata\": {\r + \"description\": \"App service plan name\"\r + }\r + },\r + \"extensionName\": {\r + \"type\": \"string\",\r + \"metadata\": {\r + \"description\": \"Extension name\"\r + }\r + } \r + },\r + \"variables\": { \r + \"extensionApi\": \"2015-06-01\"\r + },\r + \"resources\": [\r + {\r + \"apiVersion\": \"[variables('extensionApi')]\",\r + \"name\": \"[parameters('siteName')]\",\r + \"type\": \"Microsoft.Web/sites\",\r + \"location\": \"[resourceGroup().location]\",\r + \"dependsOn\": [],\r + \"properties\": {\r + \"name\": \"[parameters('siteName')]\",\r + \"serverFarmId\": \"[parameters('appServicePlanName')]\"\r + },\r + \"resources\": [\r + {\r + \"apiVersion\": \"[variables('extensionApi')]\",\r + \"name\": \"[parameters('extensionName')]\",\r + \"type\": \"siteextensions\",\r + \"dependsOn\": [\r + \"[resourceId('Microsoft.Web/Sites', parameters('siteName'))]\"\r + ],\r + \"properties\": {}\r + }\r + ]\r + }\r + ]\r +}", + "Octopus.Action.Azure.AccountId": "#{AzSiteExt_AzureAccountId}" + }, + "Parameters": [ + { + "Id": "d1494374-4db4-4bed-8136-eec378de7c9d", + "Name": "AzSiteExt_ResourceGroupName", + "Label": "Resource group name", + "HelpText": "The Resource Group of your Azure Web App.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1d188d2a-915e-425f-a4d8-5780421ebb00", + "Name": "AzSiteExt_SiteName", + "Label": "Site name", + "HelpText": "The name of your Azure Web App.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "00edff48-a947-424b-b46e-4f58826568c3", + "Name": "AzSiteExt_AppServicePlanName", + "Label": "App service plan name", + "HelpText": "The App Service Plan of your Azure Web App.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2d2cfc5f-1f41-4d7b-a4df-3dbb652553fb", + "Name": "AzSiteExt_ExtensionName", + "Label": "Site extension name", + "HelpText": "The package name of the extension you want to install. + +The extensions available to be installed can be found on the [siteextensions.net](https://www.siteextensions.net/packages) website. The name of the package can be derived from the URL of the extension (e.g. for the Application Insights extension the URL is `https://www.siteextensions.net/packages/Microsoft.ApplicationInsights.AzureWebSites/`, so the package name is `Microsoft.ApplicationInsights.AzureWebSites`).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "3629e43b-1af0-4df5-a6ce-6f36ead4dea0", + "Name": "AzSiteExt_AzureAccountId", + "Label": "Azure account ID", + "HelpText": "The Azure account to use for the connection. + +**NOTE:** a **service principal** account is required, a **management certificate** account will not work.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "alfhenrik", + "$Meta": { + "ExportedAt": "2017-09-06T20:42:23.783Z", + "OctopusVersion": "3.16.7", + "Type": "ActionTemplate" + }, + "Category": "azure-site-extensions" +} diff --git a/step-templates/azure-switch-staging-deployment-slot.json.human b/step-templates/azure-switch-staging-deployment-slot.json.human new file mode 100644 index 000000000..fd86b4b95 --- /dev/null +++ b/step-templates/azure-switch-staging-deployment-slot.json.human @@ -0,0 +1,194 @@ +{ + "Id": "7c39a530-6d16-4294-8ff5-74663ea13131", + "Name": "Switch Azure Staging Deployment Slot", + "Description": "This template will warm up your deployment slot then swap it with production. This step template should be placed after \"\"Deploy an Azure Web App\" Octopus Deploy template and be used with its sister step \"Create Azure RM Deployment Slot\" + +This should be used for green-blue deployments, as referenced in this document: https://octopus.com/docs/deploying-applications/deploying-to-azure/deploying-a-package-to-an-azure-web-app/using-deployment-slots-with-azure-web-apps", + "ActionType": "Octopus.AzurePowerShell", + "Version": 3, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Azure.AccountId": "#{AzureAccount}", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "###############################################\r +# Switch Azure RM Staging Deployment Slot\r +###############################################\r +###############################################\r +##Step1: Get Variables\r +$ResourceGroupName = $OctopusParameters[\"ResourceGroupName\"] \r +$AppName = $OctopusParameters[\"AppName\"] \r +$StagingSlotName = $OctopusParameters[\"SlotName\"]\r +$SmokeTestResponseCode = $OctopusParameters[\"SmokeTestResponseCode\"]\r +$smokeTestTimeoutSecs = $OctopusParameters[\"smokeTestTimeoutSecs\"]\r +###############################################\r +###############################################\r +$ErrorActionPreference = \"Stop\"\r +\r +Function Invoke-RequiredVariablesCheck\r +{\r + if([string]::IsNullOrEmpty($ResourceGroupName))\r + {\r + Write-Error \"ResourceGroupName variable is not set\"\r + }\r +\r + if([string]::IsNullOrEmpty($AppName))\r + {\r + write-error \"AppName variable is not set\"\r + }\r +\r + if([string]::IsNullOrEmpty($stagingSlotName))\r + {\r + write-error \"stagingSlotName variable is not set\"\r + }\r +\r + if([string]::IsNullOrEmpty($smokeTestTimeoutSecs))\r + {\r + Write-Output \"Smoke test timeout not set, will use default of 180 seconds\"\r + $smokeTestTimeoutSecs = 180\r + }\r +\r + if([string]::IsNullOrEmpty($SmokeTestResponseCode))\r + {\r + Write-Output \"Smoke test respose code not specfied will detail to 200\"\r + $SmokeTestResponseCode = \"200\"\r + }\r +\r + Write-Verbose \"Variables in use are:\"\r + write-verbose \"ResourceGroupName:$ResourceGroupName\"\r + write-verbose \"AppName:$AppName\"\r + write-verbose \"stagingSlotName:$stagingSlotName\"\r + Write-Verbose \"smokeTestTimeoutSecs: $smokeTestTimeoutSecs\"\r + Write-Verbose \"SmokeTestResponseCode: $SmokeTestResponseCode\"\r +}\r +\r +Function Invoke-SlotWarmup\r +{\r + [cmdletbinding()]\r + param\r + (\r + [parameter(Mandatory=$true)]\r + [string]$httpEndpoint,\r + [parameter(Mandatory=$true)]\r + [int32]$timeout\r + )\r + try \r + {\r + $response = (Invoke-WebRequest -UseBasicParsing -Uri $httpEndpoint -TimeoutSec $timeout).statusCode\r + }\r + catch \r + {\r + $response = $_.Exception.Response.StatusCode.Value__\r + }\r + return $response\r +}\r +\r +try \r +{\r + Invoke-RequiredVariablesCheck\r + Write-Output \"Will attempt to warm up staging slot\"\r + $slotDetails = Get-AzureRmWebAppSlot -ResourceGroupName $ResourceGroupName -Name $AppName -Slot $StagingSlotName\r + \r + $hostname = $slotDetails.EnabledHostNames | select-object -First 1\r +\r + Write-Output \"Performing default smoke test to warm up deployment slot\"\r + \r + $returnStatusCode = Invoke-SlotWarmup -httpEndpoint \"https://$hostname\" -timeout $smokeTestTimeoutSecs\r +\r + if($returnStatusCode -ne $SmokeTestResponseCode)\r + {\r + Write-Error \"Response code to https://$hostname was $returnStatusCode and did not match the expected response code of $SmokeTestResponseCode. Deployment canceled\"\r + }\r + else \r + {\r + Write-Output \"Staging slot (https://$hostname) warmed up and responding ok\"\r + }\r +\r + Write-Output \"Will now switch staging slot to production\"\r + Switch-AzureRmWebAppSlot -ResourceGroupName $ResourceGroupName -Name $AppName -SourceSlotName $StagingSlotName -DestinationSlotName \"Production\"\r + Write-Output \"Deployment slot switch complete\"\r +}\r +catch \r +{\r + Write-Error \"Error in Switch Azure RM Staging Deployment Slot Script. $_\" \r +}", + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "4eb022c3-2d12-4f5b-acb0-80eaec2ce26c", + "Name": "ResourceGroupName", + "Label": "ResourceGroupName", + "HelpText": "Enter the name of the resource group you are deploying this Web App into", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "e44dbd73-6c0a-4c9f-b190-47396ea2534e", + "Name": "AppName", + "Label": "AppName", + "HelpText": "Enter the name of your app", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c66cc42c-88e6-4008-8289-a45913dc36df", + "Name": "AzureAccount", + "Label": "AzureAccount", + "HelpText": "Enter the SPN used to connect to Azure", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "e9559ca6-751a-4939-888a-cbf76ce5c91d", + "Name": "SlotName", + "Label": "SlotName", + "HelpText": "Enter the name you wish to call your deployment slot", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "ed1fd1cf-479a-4aa6-beb5-404565a51ca7", + "Name": "SmokeTestResponseCode", + "Label": "SmokeTestResponseCode(Optional)", + "HelpText": "This is the response code that comes back from your web app. The default value is set to 200, if your app will respond with a different status code such as a 401 then please update the value.", + "DefaultValue": "200", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c906639e-f40d-4ab0-903c-d46462d8a8ae", + "Name": "smokeTestTimeoutSecs", + "Label": "smokeTestTimeoutSecs(Optional)", + "HelpText": "The timeout setting for waking up your web app. The default value is 180 seconds.", + "DefaultValue": "180", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2018-03-14T16:21:25.000+00:00", + "LastModifiedBy": "MarkDordoy", + "$Meta": { + "ExportedAt": "2018-03-14T16:21:25.214Z", + "OctopusVersion": "3.16.2", + "Type": "ActionTemplate" + }, + "Category": "azure" + } diff --git a/step-templates/azure-sync-instancecount-from-prod-to-staging.json.human b/step-templates/azure-sync-instancecount-from-prod-to-staging.json.human new file mode 100644 index 000000000..1b4f3baad --- /dev/null +++ b/step-templates/azure-sync-instancecount-from-prod-to-staging.json.human @@ -0,0 +1,39 @@ +{ + "Id": "8a0a75da-4960-40ca-b641-5ed2305fa655", + "Name": "Azure - Sync Instance Count", + "Description": "This step template is useful when you want to have the instance count matched just before the VIP swap. It takes the source slot (usually Production slot) and match with the current deployment slot (normally Staging). Note: This will be helpful in scenario when 15 instances in Production and 4 instance in staging. This step template should only be used when a production slot already exists.", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Current Cloud Service name \r +$ServiceName =$octopusparameters[\"Octopus.Action.Azure.CloudServiceName\"]\r +\r +$deployment = Get-AzureDeployment -slot $sourceslot -serviceName $serviceName\r +# Obtain the instance count and role name.\r +$SourceInstanceCount =$deployment.RolesConfiguration.values.InstanceCount\r +$rolenameService = $deployment.RolesConfiguration.values.Name\r +#Set the Current deployment slot instance count to match production count\r +Set-AzureRole -ServiceName $serviceName -Slot $octopusparameters[\"Octopus.Action.Azure.Slot\"] -RoleName $rolenameService -Count $SourceInstanceCount " + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "sourceslot", + "Label": "Specify the Slot to Get Instance Count", + "HelpText": "This is usually Production Slot to obtain the Count and match to staging before swap.", + "DefaultValue": "Production", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-06-18T13:44:19.041+00:00", + "LastModifiedBy": "mani0070", + "$Meta": { + "ExportedAt": "2015-06-18T13:48:39.548+00:00", + "OctopusVersion": "3.0.0.1614", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-upload-files.json.human b/step-templates/azure-upload-files.json.human new file mode 100644 index 000000000..4c65db401 --- /dev/null +++ b/step-templates/azure-upload-files.json.human @@ -0,0 +1,134 @@ +{ + "Id": "bc18b460-06a7-412f-850f-44098f1b497a", + "Name": "Azure - Upload Files to Azure", + "Description": "Uploads files and folders to an Azure container from a specified location. + +**IMPORTANT:** Azure PowerShell must be installed on the tentacle server for this step to work. This can be downloaded from http://bit.ly/AzurePowershellDownload", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#Sets the Permissions to public if the selection is true +if ([boolean]::Parse($doRecurse)) +{ + +\t$recurse = \"-Recurse\" + +} + +if ([boolean]::Parse($doForce)) +{ + +\t$force = \"-Force\" + +} + +#-------------------------------------------------------------------- +#Checking to see if Azure is installed on the computer +$name = 'Azure' + +Write-Output \"Checking if Azure Powershell is installed\" + +if(Get-Module -ListAvailable | Where-Object {$_.name -eq $name}) +{ +\t(Get-Module -ListAvailable | Where-Object{ $_.Name -eq $name}) | +\tSelect Version, Name, Author, PowerShellVersion | Format-List; +\tWrite-Output \"Azure Powershell is installed\" +} +else +{ +\t#Provides the link to install Azure Powershell, if it is not installed +\tWrite-Warning \"Please install Azure Powershell. To install Azure Powershell go to http://bit.ly/AzurePowershellDownload\" +\tExit 1 +} + + + +#-------------------------------------------------------------------- + +#Initialises the Azure Credentials based on the Storage Account Name and the Storage Account Key, +#so that we can invoke the APIs further down. +$storageContext = New-AzureStorageContext -StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey + +#-------------------------------------------------------------------- + +Get-ChildItem -Path $localFolder -File $recurse | Set-AzureStorageBlobContent -Container $containerName -Blob $blobName -Context $storageContext $force + +Write-Output \"All files in $localFolder uploaded to $containerName!\" +", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "4cb76611-ff29-4256-9475-3769fd890e0f", + "Name": "storageAccountName", + "Label": "Azure Storage Account Name", + "HelpText": "Name of the account that the files and folders will be uploaded to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cc0eb63d-4545-4d9d-aca3-7844e1e0a54e", + "Name": "storageAccountKey", + "Label": "Azure Storage Account Key", + "HelpText": "The key that is used to log into the account.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "8245c4b0-014d-467c-a95d-ab6aac230075", + "Name": "containerName", + "Label": "Azure Container Name", + "HelpText": "The name of the container the files and folder will be uploaded to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "45f6df33-d04d-44bd-8a26-1ab45c634afc", + "Name": "localFolder", + "Label": "Name of the Parent Folder", + "HelpText": "Name of the Parent Folder being uploaded", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8c559f0f-2d6e-4202-8614-65cabb29e643", + "Name": "doRecurse", + "Label": "Recursive", + "HelpText": null, + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "8a06615b-69a2-4d32-be29-981b6c5725fc", + "Name": "doForce", + "Label": "Force", + "HelpText": "Override is enabled or not", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "kemyke", + "$Meta": { + "ExportedAt": "2016-10-25T19:17:25.877+00:00", + "OctopusVersion": "3.4.5", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-web-app-disable-appoffline.json.human b/step-templates/azure-web-app-disable-appoffline.json.human new file mode 100644 index 000000000..3f7d784c1 --- /dev/null +++ b/step-templates/azure-web-app-disable-appoffline.json.human @@ -0,0 +1,155 @@ +{ + "Id": "143ba6fd-968f-4f24-980b-49e47aa98f71", + "Name": "Azure Web App - Disable app_offline", + "Description": "This step template will remove an app_offline file from an Azure WebApp to safely bring the app domain online following a deployment. + +It requires a set of [deployment credentials](https://docs.microsoft.com/en-gb/azure/app-service/deploy-configure-credentials) for the Azure Web App. + +**Required:** +- Credentials with access to the [Kudu VFS API](https://github.com/projectkudu/kudu/wiki/REST-API#vfs) + +Notes: + +- Tested on Octopus `2021.2`. +- Tested with both Windows PowerShell and PowerShell Core on Linux.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\"; +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Variables +$AzWebAppName = $OctopusParameters[\"AzWebApp.DisableAppOffline.AzWebAppName\"] +$Filename = $OctopusParameters[\"AzWebApp.DisableAppOffline.Filename\"] +$DeployUsername = $OctopusParameters[\"AzWebApp.DisableAppOffline.Deployment.Username\"] +$DeployPassword = $OctopusParameters[\"AzWebApp.DisableAppOffline.Deployment.Password\"] +$DeploymentUrl = $OctopusParameters[\"AzWebApp.DisableAppOffline.Deployment.KuduRestApiUrl\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($AzWebAppName)) { + throw \"Required parameter AzWebApp.DisableAppOffline.AzWebAppName not specified\" +} + +if ([string]::IsNullOrWhiteSpace($Filename)) { + throw \"Required parameter AzWebApp.DisableAppOffline.Filename not specified\" +} +if ([string]::IsNullOrWhiteSpace($DeployUsername)) { + throw \"Required parameter AzWebApp.DisableAppOffline.Deployment.Username not specified\" +} +if ([string]::IsNullOrWhiteSpace($DeployPassword)) { + throw \"Required parameter AzWebApp.DisableAppOffline.Deployment.Password not specified\" +} +if ([string]::IsNullOrWhiteSpace($DeploymentUrl)) { + throw \"Required parameter AzWebApp.DisableAppOffline.Deployment.KuduRestApiUrl not specified\" +} + +$DeploymentUrl = $DeploymentUrl.TrimEnd('/') + +# Local variables +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +Write-Verbose \"AzWebApp.DisableAppOffline.AzWebAppName: $AzWebAppName\" +Write-Verbose \"AzWebApp.DisableAppOffline.Filename: $FileName\" +Write-Verbose \"AzWebApp.DisableAppOffline.Deployment.Username: $DeployUsername\" +Write-Verbose \"AzWebApp.DisableAppOffline.Deployment.Password: ********\" +Write-Verbose \"AzWebApp.DisableAppOffline.Deployment.KuduRestApiUrl: $DeploymentUrl\" + +Write-Verbose \"Step Name: $StepName\" + +try { + $credPair = \"$($DeployUsername):$($DeployPassword)\" + $encodedCredentials = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($credPair)) + $headers = @{ + Authorization = \"Basic $encodedCredentials\" + # Ignore E-Tag + \"If-Match\" = \"*\" + } + + $filePathUri = \"$DeploymentUrl/site/wwwroot/$filename\" + Write-Host \"Invoking Delete request for '$filePathUri'\" + $response = Invoke-RestMethod -Method Delete -Uri $filePathUri -Headers $headers + + Write-Verbose \"Response: $response\" +} +catch { + $ExceptionMessage = $_.Exception.Message + $ErrorDetails = $_.ErrorDetails.Message + $Message = \"An error occurred invoking the Azure Web App REST API: $ExceptionMessage\" + if (![string]::IsNullOrWhiteSpace($ErrorDetails)) { + $Message += \"`nDetail: $ErrorDetails\" + } + + Write-Error $Message -Category ConnectionError +}" + }, + "Parameters": [ + { + "Id": "b847fd97-bc71-4c89-99fb-bd5c2327027a", + "Name": "AzWebApp.DisableAppOffline.AzWebAppName", + "Label": "Azure Web App name", + "HelpText": "Provide the Azure Web App name.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0b2edf69-da0d-4750-882a-4036c67f0129", + "Name": "AzWebApp.DisableAppOffline.Filename", + "Label": "AppOffline file name", + "HelpText": "*Optional:* Choose the variation of the name of the app offline file. Default: `app_offline.htm` + +Available options: + +- `app_offline.htm` +- `app_offline.html`", + "DefaultValue": "app_offline.htm", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "app_offline.htm|app_offline.htm +app_offline.html|app_offline.html" + } + }, + { + "Id": "c873f396-0ca1-4df9-b083-860541d24b61", + "Name": "AzWebApp.DisableAppOffline.Deployment.Username", + "Label": "Deployment username", + "HelpText": "Provide the user or application-scoped [deployment](https://docs.microsoft.com/en-gb/azure/app-service/deploy-configure-credentials) username for the Azure Web App. Default: `$#{AzWebApp.DisableAppOffline.WebAppName}`.", + "DefaultValue": "$#{AzWebApp.DisableAppOffline.WebAppName}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1c18f666-c91d-411c-a86b-b92d0bddc4ed", + "Name": "AzWebApp.DisableAppOffline.Deployment.Password", + "Label": "Deployment password", + "HelpText": "Provide the user or application-scoped [deployment](https://docs.microsoft.com/en-gb/azure/app-service/deploy-configure-credentials) password for the Azure Web App.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "023c4e13-2a25-4e29-916f-aaafea1d8284", + "Name": "AzWebApp.DisableAppOffline.Deployment.KuduRestApiUrl", + "Label": "Deployment REST API Url", + "HelpText": "*Optional:* Provide a custom deployment REST API URL. Default is: `https://#{AzWebApp.DisableAppOffline.AzWebAppName}.scm.azurewebsites.net/api/vfs`.", + "DefaultValue": "https://#{AzWebApp.DisableAppOffline.AzWebAppName}.scm.azurewebsites.net/api/vfs", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-09-17T17:52:46.792Z", + "OctopusVersion": "2021.2.7536", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "azure" + } diff --git a/step-templates/azure-web-app-enable-appoffline.json.human b/step-templates/azure-web-app-enable-appoffline.json.human new file mode 100644 index 000000000..8bf2ca649 --- /dev/null +++ b/step-templates/azure-web-app-enable-appoffline.json.human @@ -0,0 +1,206 @@ +{ + "Id": "852b78ae-eb32-43c0-bf55-0a5bdd7bebc8", + "Name": "Azure Web App - Enable app_offline", + "Description": "This step template will take a provided app_offline file from a package and upload it to an Azure Web App to enable a way to safely bring down the app domain for a subsequent deployment. + +It requires a set of [deployment credentials](https://docs.microsoft.com/en-gb/azure/app-service/deploy-configure-credentials) for the Azure Web App. + +**Required:** +- Credentials with access to the [Kudu VFS API](https://github.com/projectkudu/kudu/wiki/REST-API#vfs) + +Notes: + +- Tested on Octopus `2021.2`. +- Tested with both Windows PowerShell and PowerShell Core on Linux.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "ff7d24cc-7288-428f-985a-155c467d63ff", + "Name": "AzWebApp.EnableAppOffline.SourcePackage", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "AzWebApp.EnableAppOffline.SourcePackage" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\"; +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Variables +$SourcePackage = \"AzWebApp.EnableAppOffline.SourcePackage\" +$AzWebAppName = $OctopusParameters[\"AzWebApp.EnableAppOffline.AzWebAppName\"] +$FilePath = $OctopusParameters[\"AzWebApp.EnableAppOffline.FilePath\"] +$Filename = $OctopusParameters[\"AzWebApp.EnableAppOffline.Filename\"] +$DeployUsername = $OctopusParameters[\"AzWebApp.EnableAppOffline.Deployment.Username\"] +$DeployPassword = $OctopusParameters[\"AzWebApp.EnableAppOffline.Deployment.Password\"] +$DeploymentUrl = $OctopusParameters[\"AzWebApp.EnableAppOffline.Deployment.KuduRestApiUrl\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($AzWebAppName)) { + throw \"Required parameter AzWebApp.EnableAppOffline.AzWebAppName not specified\" +} +if ([string]::IsNullOrWhiteSpace($Filename)) { + throw \"Required parameter AzWebApp.EnableAppOffline.Filename not specified\" +} +if ([string]::IsNullOrWhiteSpace($DeployUsername)) { + throw \"Required parameter AzWebApp.EnableAppOffline.Deployment.Username not specified\" +} +if ([string]::IsNullOrWhiteSpace($DeployPassword)) { + throw \"Required parameter AzWebApp.EnableAppOffline.Deployment.Password not specified\" +} +if ([string]::IsNullOrWhiteSpace($DeploymentUrl)) { + throw \"Required parameter AzWebApp.EnableAppOffline.Deployment.KuduRestApiUrl not specified\" +} + +$DeploymentUrl = $DeploymentUrl.TrimEnd('/') +$ExtractPathKey = \"Octopus.Action.Package[$($SourcePackage)].ExtractedPath\" + +$ExtractPath = $OctopusParameters[$ExtractPathKey] +$FilePath = Join-Path -Path $ExtractPath -ChildPath $FilePath +if (!(Test-Path $FilePath)) { + throw \"Either the local or package extraction folder $FilePath does not exist or the Octopus Tentacle does not have permission to access it.\" +} + +$sourceFilePath = Join-Path -Path $FilePath -ChildPath $Filename + +if (!(Test-Path $sourceFilePath)) { + throw \"The file located at '$sourceFilePath' does not exist or the Octopus Tentacle does not have permission to access it.\" +} +$destinationFilePathUri = \"$DeploymentUrl/site/wwwroot/$filename\" + +# Local variables +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +Write-Verbose \"AzWebApp.EnableAppOffline.AzWebAppName: $AzWebAppName\" +Write-Verbose \"AzWebApp.EnableAppOffline.FilePath: $FilePath\" +Write-Verbose \"AzWebApp.EnableAppOffline.Filename: $FileName\" +Write-Verbose \"AzWebApp.EnableAppOffline.Deployment.Username: $DeployUsername\" +Write-Verbose \"AzWebApp.EnableAppOffline.Deployment.Password: ********\" +Write-Verbose \"AzWebApp.EnableAppOffline.Deployment.KuduRestApiUrl: $DeploymentUrl\" + +Write-Verbose \"Step Name: $StepName\" + +try { + $credPair = \"$($DeployUsername):$($DeployPassword)\" + $encodedCredentials = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($credPair)) + $headers = @{ + Authorization = \"Basic $encodedCredentials\" + # Ignore E-Tag + \"If-Match\" = \"*\" + } + + Write-Host \"Invoking Put request for '$sourceFilePath' to '$destinationFilePathUri'\" + $response = Invoke-RestMethod -Method Put -Infile $sourceFilePath -Uri $destinationFilePathUri -Headers $headers -UserAgent 'powershell/1.0' -ContentType 'application/json' + + Write-Verbose \"Response: $response\" +} +catch { + $ExceptionMessage = $_.Exception.Message + $ErrorDetails = $_.ErrorDetails.Message + $Message = \"An error occurred invoking the Azure Web App REST API: $ExceptionMessage\" + if (![string]::IsNullOrWhiteSpace($ErrorDetails)) { + $Message += \"`nDetail: $ErrorDetails\" + } + + Write-Error $Message -Category ConnectionError +}" + }, + "Parameters": [ + { + "Id": "b847fd97-bc71-4c89-99fb-bd5c2327027a", + "Name": "AzWebApp.EnableAppOffline.AzWebAppName", + "Label": "Azure Web App name", + "HelpText": "Provide the Azure Web App name.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e15156cc-1fbd-439f-bbea-5eb1f5da8f2f", + "Name": "AzWebApp.EnableAppOffline.SourcePackage", + "Label": "AppOffline package source", + "HelpText": "Provide the package to source the `app_offline.htm` file from. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "22fdf143-fd7b-48d8-ba52-eed69268b61c", + "Name": "AzWebApp.EnableAppOffline.FilePath", + "Label": "AppOffline file path", + "HelpText": "Provide the path (relative to the package) for the app offline file to be uploaded. + +*Note:* If left blank or empty, the file will be sourced from the root of the package. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0b2edf69-da0d-4750-882a-4036c67f0129", + "Name": "AzWebApp.EnableAppOffline.Filename", + "Label": "AppOffline file name", + "HelpText": "*Optional:* Choose the variation of the name of the app offline file. Default: `app_offline.htm` + +Available options: + +- `app_offline.htm` +- `app_offline.html`", + "DefaultValue": "app_offline.htm", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "app_offline.htm|app_offline.htm +app_offline.html|app_offline.html" + } + }, + { + "Id": "c873f396-0ca1-4df9-b083-860541d24b61", + "Name": "AzWebApp.EnableAppOffline.Deployment.Username", + "Label": "Deployment username", + "HelpText": "Provide the user or application-scoped [deployment](https://docs.microsoft.com/en-gb/azure/app-service/deploy-configure-credentials) username for the Azure Web App. Default: `$#{AzWebApp.EnableAppOffline.WebAppName}`.", + "DefaultValue": "$#{AzWebApp.EnableAppOffline.WebAppName}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1c18f666-c91d-411c-a86b-b92d0bddc4ed", + "Name": "AzWebApp.EnableAppOffline.Deployment.Password", + "Label": "Deployment password", + "HelpText": "Provide the user or application-scoped [deployment](https://docs.microsoft.com/en-gb/azure/app-service/deploy-configure-credentials) password for the Azure Web App.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "023c4e13-2a25-4e29-916f-aaafea1d8284", + "Name": "AzWebApp.EnableAppOffline.Deployment.KuduRestApiUrl", + "Label": "Deployment REST API Url", + "HelpText": "*Optional:* Provide a custom deployment REST API URL. Default is: `https://#{AzWebApp.EnableAppOffline.AzWebAppName}.scm.azurewebsites.net/api/vfs`.", + "DefaultValue": "https://#{AzWebApp.EnableAppOffline.AzWebAppName}.scm.azurewebsites.net/api/vfs", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-09-17T17:45:50.539Z", + "OctopusVersion": "2021.2.7536", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "azure" + } diff --git a/step-templates/azure-web-app-get-deployment-user.json.human b/step-templates/azure-web-app-get-deployment-user.json.human new file mode 100644 index 000000000..2aad3881a --- /dev/null +++ b/step-templates/azure-web-app-get-deployment-user.json.human @@ -0,0 +1,120 @@ +{ + "Id": "f87564d3-50e0-4265-8ed7-d95e3e21f869", + "Name": "Azure Web App - Get Deployment User", + "Description": "This step template will retrieve a set of [deployment credentials](https://docs.microsoft.com/en-gb/azure/app-service/deploy-configure-credentials) for an Azure Web App. + +It will create two Octopus [sensitive output variables](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) for use in other deployment or runbook steps. They will be named: + +- `userName` - the username from the provided deployment credentials. +- `userPwd` - the password from the provided deployment credentials. + +**Required:** +- An azure account with permissions to retrieve deployment credentials for the named Azure Web App. +- The Azure CLI (`az`) is required to run this script. If running on a worker or deployment target, ensure this is installed. + +Notes: + +- Tested on Octopus `2021.2`. +- Tested with both Windows PowerShell and PowerShell Core on Linux.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\"; +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$resourceGroupName = $OctopusParameters[\"AzWebApp.DeploymentCreds.AzResourceGroup\"] +$WebAppName = $OctopusParameters[\"AzWebApp.DeploymentCreds.AzWebAppName\"] +$PublishCredentialType = $OctopusParameters[\"AzWebApp.DeploymentCreds.PublishCredentialType\"] +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($resourceGroupName)) { + throw \"Required parameter AzWebApp.DeploymentCreds.AzResourceGroup not specified\" +} +if ([string]::IsNullOrWhiteSpace($WebAppName)) { + throw \"Required parameter AzWebApp.DeploymentCreds.AzWebAppName not specified\" +} +if ([string]::IsNullOrWhiteSpace($PublishCredentialType)) { + throw \"Required parameter AzWebApp.DeploymentCreds.PublishCredentialType not specified\" +} + +Write-Verbose \"Azure Resource Group Name: $resourceGroupName\" +Write-Verbose \"Azure Web App Name: $WebAppName\" +Write-Verbose \"Publish Credential Type: $PublishCredentialType\" + +Write-Host \"Getting $PublishCredentialType publish profile deployment credentials...\" + +$profiles = az webapp deployment list-publishing-profiles --resource-group $resourceGroupName --name $WebAppName | ConvertFrom-Json | where { $_.publishMethod -ieq $PublishCredentialType } + +Set-OctopusVariable -name \"userName\" -value $profiles.userName -Sensitive +Write-Highlight \"Created output variable: ##{Octopus.Action[$StepName].Output.userName}\" +Set-OctopusVariable -name \"userPWD\" -value $profiles.userPWD -Sensitive +Write-Highlight \"Created output variable: ##{Octopus.Action[$StepName].Output.userPWD}\" + +Write-Host \"Output variables generated for deployment credentials!\"", + "Octopus.Action.Azure.AccountId": "#{AzWebApp.DeploymentCreds.AzAccount}", + "OctopusUseBundledTooling": "False" + }, + "Parameters": [ + { + "Id": "7f36b854-6cc8-4a64-a7b4-c3110044cd4b", + "Name": "AzWebApp.DeploymentCreds.AzWebAppName", + "Label": "Azure Web App name", + "HelpText": "Provide the Azure Web App name.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "41c6ca57-2078-4cbb-8e92-26c2207a3ef7", + "Name": "AzWebApp.DeploymentCreds.AzResourceGroup", + "Label": "Azure Resource Group", + "HelpText": "Provide Azure resource group.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "149ef6c1-716c-4738-9985-ea47e5fc6e9b", + "Name": "AzWebApp.DeploymentCreds.AzAccount", + "Label": "Azure Account", + "HelpText": "Provide Azure Account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "9ac94483-5e3b-49b3-9014-eb4875b95798", + "Name": "AzWebApp.DeploymentCreds.PublishCredentialType", + "Label": "Deployment credential type", + "HelpText": "Choose type of Deployment credentials should be retrieved. Default is: `MSDeploy` + +Available options are: + +- `MSDeploy` +- `FTP` +- `ZipDeploy`", + "DefaultValue": "MSDeploy", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "MSDeploy|MSDeploy +FTP|FTP +ZipDeploy|ZipDeploy" + } + } + ], + "$Meta": { + "ExportedAt": "2021-09-17T17:13:19.362Z", + "OctopusVersion": "2021.2.7536", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "adamoctoclose", + "Category": "azure" + } diff --git a/step-templates/azure-web-app-restart.json.human b/step-templates/azure-web-app-restart.json.human new file mode 100644 index 000000000..387acb590 --- /dev/null +++ b/step-templates/azure-web-app-restart.json.human @@ -0,0 +1,94 @@ +{ + "Id": "1f40e418-17bf-4b3e-bbe7-c6d41cbded93", + "Name": "Azure Web App - Restart", + "Description": "Restarts an azure web app. +
+ +*

Note This template is designed to run against an azure web app octopus target

* +*

Depends on Azure CLI and powershell to be installed on the running machine

*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Azure.AccountId": "#{azWebApp.AzureAcct}", + "Octopus.Action.Script.ScriptBody": "try {az --version} +catch +{ + throw \"az CLI not installed\" +} + +$webApp = $OctopusParameters[\"Octopus.Action.Azure.WebAppName\"] +$resourceGroup = $OctopusParameters[\"Octopus.Action.Azure.ResourceGroupName\"] +$startIfStopped = $OctopusParameters[\"azWebApp.StartIfStopped\"] + +Write-Host \"Checking webapp $webApp status in resource group $resourceGroup\" + +$appState = az webapp list --resource-group $resourceGroup --query \"[?name=='$webApp'].{state: state, hostName: defaultHostName}\" | ConvertFrom-Json +if($appState.state -eq \"stopped\") +{ + if($startIfStopped -eq 'true') + { + Write-Host \"Webapp is not running. Starting...\" -NoNewline + \taz webapp start --name $webApp --resource-group $resourceGroup + Write-Host \"Done\" + } + else + { + Throw \"Webapp is not running.\" + } +} + +Write-Host \"Webapp running, restarting\" + +else +{ +\tWrite-Host \"Restarting $webApp in resource group $resourceGroup\" +\taz webapp restart --name $webApp --resource-group $resourceGroup +} + +Start-Sleep -s 5 + +$appState = az webapp list --resource-group $resourceGroup --query \"[?name=='$webApp'].{state: state, hostName: defaultHostName}\" | ConvertFrom-Json + +if($appState.state -ne \"running\") +{ +\tThrow \"Webapp failed to start. Check the app's activity/error log\" +} + +write-host \"Webapp $webApp running. Check at: $($appState.hostName)\" +" + }, + "Parameters": [ + { + "Id": "e732ddaa-a43b-4369-8813-065286069d65", + "Name": "azWebApp.AzureAcct", + "Label": "Azure Account", + "HelpText": "The azure account that has access to the web app", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "901ec60b-0ffd-4d08-be6f-fa9e8aec457a", + "Name": "azWebApp.StartIfStopped", + "Label": "Start if stopped", + "HelpText": "If the web app is stopped, start it", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2022-05-17T10:59:12.694Z", + "OctopusVersion": "2022.1.2495", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "zogamorph", + "Category": "azure" +} diff --git a/step-templates/azure-web-app-rolling-restart.json.human b/step-templates/azure-web-app-rolling-restart.json.human new file mode 100644 index 000000000..7cae51d95 --- /dev/null +++ b/step-templates/azure-web-app-rolling-restart.json.human @@ -0,0 +1,104 @@ +{ + "Id": "43cfc12d-5ae9-425d-ab01-7124ffdd9ee6", + "Name": "Azure Web App - Rolling Restart", + "Description": "Performs a delayed rolling restart of all instances within an Azure Web App +
+ +*

Note This template is designed to run against an azure web app octopus target

* +*

Depends on Azure CLI and powershell to be installed on the running machine

*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Azure.AccountId": "#{azWebApp.AzureAcct}", + "Octopus.Action.Script.ScriptBody": "try {az --version} +catch +{ + throw \"az CLI not installed\" +} + +$webAppName = $OctopusParameters[\"azWebApp.WebAppName\"] +$resourceGroup = $OctopusParameters[\"azWebApp.ResourceGroup\"] +$restartDelay = $OctopusParameters[\"azWebApp.RestartDelay\"] + + + +Write-Host \"Web App Name: $webAppName\" +Write-Host \"Resource Group: $resourceGroup\" +Write-Host \"Restart Delay: $restartDelay\" + +Write-Host \"Checking $webAppName status in resource group $resourceGroup\" + +$appState = az webapp list --resource-group $resourceGroup --query \"[?name=='$webAppName'].{state: state, hostName: defaultHostName}\" | ConvertFrom-Json + +# only execute if running +if($appState.state -eq \"running\") +{ + $appInstances = az webapp list-instances -n $webAppName --resource-group $resourceGroup --query '[].{Id: id}' | ConvertFrom-Json + Write-Host \"\" $appInstances.Count \"Instance(s) found`r`n\" -ForegroundColor Green + + if($appInstances.count -gt 0){ + foreach ($instance in $appInstances){ + Write-Host \"Restarting Instance: $instance\" + az webapp restart --ids $instance.Id + Write-Host \"Pausing $restartDelay second(s)`r`n\" -ForegroundColor Yellow + Start-Sleep -Seconds $restartDelay + } + } +} +" + }, + "Parameters": [ + { + "Id": "6670dc01-4323-41b8-87f4-a824608bad71", + "Name": "azWebApp.AzureAcct", + "Label": "Azure Account", + "HelpText": "The azure account that has access to the web app", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "56dc3fcb-237d-4618-944a-441bd3d089c5", + "Name": "azWebApp.WebAppName", + "Label": "App Service Name", + "HelpText": "The azure web app name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7fc59af9-00cf-4e3f-995d-1ccca3057324", + "Name": "azWebApp.ResourceGroup", + "Label": "Azure Resource Group", + "HelpText": "The azure resource group", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8fda8110-e3bb-4881-89dc-c148a7bd1943", + "Name": "azWebApp.RestartDelay", + "Label": "Restart Delay", + "HelpText": "Delay between instance restart", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-04-08T20:19:36.856Z", + "OctopusVersion": "2020.5.6", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "tekguy", + "Category": "azure" + } diff --git a/step-templates/azure-web-app-set-app-settings.json.human b/step-templates/azure-web-app-set-app-settings.json.human new file mode 100644 index 000000000..7454b17fe --- /dev/null +++ b/step-templates/azure-web-app-set-app-settings.json.human @@ -0,0 +1,96 @@ +{ + "Id": "850667b2-567d-46ba-a87d-d85dc31ebc83", + "Name": "Azure Web App - Set App Settings", + "Description": "Sets the Azure web app settings of a targeted Azure web app deployment target. Will use the deployment slot if defined. +
+ +*

Note This template is designed to run against an azure web app octopus target

* +*

Depends on Azure CLI and powershell to be installed on the worker

*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{azureWebAppSettings.azAccount}", + "Octopus.Action.Script.ScriptBody": "$webAppName = $OctopusParameters[\"Octopus.Action.Azure.WebAppName\"] +$rg = $OctopusParameters[\"Octopus.Action.Azure.ResourceGroupName\"] +$slot = $OctopusParameters[\"Octopus.Action.Azure.DeploymentSlot\"] +$isSlotSettings = $OctopusParameters[\"azureWebAppSettings.isSlotSettings\"] + +$settingsType = \"settings\" + +$appSettings = $OctopusParameters[\"azureWebAppSettings.settings\"] + +$settings = \"\" + +Write-Host \"Parsing Settings\" + +$appSettings -split \"`n\" | ForEach-Object { $settings += \"$_ \"} + +$settings = $settings.TrimEnd(' ') + +$cmdArgs = \"--name $webAppName --resource-group $rg\" + +if(![string]::IsNullOrEmpty($slot)) +{ +\tif($isSlotSettings -eq 'true') + { + \t$settingsType = \"slot-settings\" + } + +\t$settings += \" --slot $slot\" +} + +$settingsArgs = \" --$settingsType $settings\" + +Write-Host \"Setting app settings\" + +$cmd = \"az webapp config appsettings set $cmdArgs $settingsArgs\" + +write-verbose \"command to execute: $cmd\" + +Invoke-Expression $cmd +" + }, + "Parameters": [ + { + "Id": "598025e6-2d89-49bf-8abb-fda6da181de5", + "Name": "azureWebAppSettings.azAccount", + "Label": "Azure Account", + "HelpText": "An Azure account with permissions to the subscription and web app being modified", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "5d232cfd-8e9f-4b3e-8c4e-7bf3a75ced19", + "Name": "azureWebAppSettings.settings", + "Label": "Web App Settings", + "HelpText": "The web app settings to append or update. Each new setting should be on a new line as a key value pair using the template `key=value`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "9bbee2cf-c57c-45db-8a7f-62a752779b56", + "Name": "azureWebAppSettings.isSlotSettings", + "Label": "Is Slot Settings", + "HelpText": "This is used to distinguish what settings are slot specific and which one are not. Slot setting do not follow the deployed app when the slot is swapped.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-30T15:48:06.905Z", + "OctopusVersion": "2020.2.10", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "xtreampb", + "Category": "azure" +} diff --git a/step-templates/azure-web-app-set-traffic.json.human b/step-templates/azure-web-app-set-traffic.json.human new file mode 100644 index 000000000..d9e2e8b95 --- /dev/null +++ b/step-templates/azure-web-app-set-traffic.json.human @@ -0,0 +1,86 @@ +{ + "Id": "36791d2d-aa55-4bc7-bee4-a0d12d73f78e", + "Name": "Azure Web App - Set Traffic", + "Description": "Sets the traffic distribution between multiple web app slots. +
+ +*

Note This template is designed to run against an azure web app octopus target, but will not use the slot defined.

* +*

Depends on Azure CLI and powershell to be installed on the worker

*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{azWebAppSetTraffic.AzureAcct}", + "Octopus.Action.Script.ScriptBody": "$webAppName = $OctopusParameters[\"Octopus.Action.Azure.WebAppName\"] +$rg = $OctopusParameters[\"Octopus.Action.Azure.ResourceGroupName\"] + +$trafficDistro = $OctopusParameters[\"azWebAppSetTraffic.trafficDistro\"] + +$cmdArgs = \"--name $webAppName --resource-group $rg\" + +$cmdAction = \"clear\" + +write-host \"Checking distribution\" +if(![string]::IsNullOrEmpty($trafficDistro)) +{ +\t$distribution = \"\" + +\t$trafficDistro -split \"`n\" | ForEach-Object { $distribution += \"$_ \"} + +\t$distribution = $distribution.TrimEnd(' ') + + $cmdArgs += \" --distribution $distribution\" + + $cmdAction = \"set\" +} + + +$cmd = \"az webapp traffic-routing $cmdAction $cmdArgs\" + +write-verbose \"cmd to invoke: $cmd\" + +write-host \"setting distributions\" +invoke-expression $cmd +" + }, + "Parameters": [ + { + "Id": "53d1306d-691c-4739-a09a-8fc9d66c60d3", + "Name": "azWebAppSetTraffic.AzureAcct", + "Label": "Azure Account", + "HelpText": "An Azure account with permissions to the subscription and web app being modified", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "1f788822-9980-44cc-9e9c-f87db5998dd5", + "Name": "azWebAppSetTraffic.trafficDistro", + "Label": "Traffic Distribution", + "HelpText": "

+The distribution of traffic in percent (0-100). Each web app slot should be defined on a separate line. Any remaining percentage will automatically be applied to production. +

+*Example* +``` +myOtherSlot=10 +stage=30 +``` +

", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-29T16:52:24.007Z", + "OctopusVersion": "2020.2.10", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "xtreampb", + "Category": "azure" +} diff --git a/step-templates/azure-web-app-slot-swap.json.human b/step-templates/azure-web-app-slot-swap.json.human new file mode 100644 index 000000000..1a2e0e366 --- /dev/null +++ b/step-templates/azure-web-app-slot-swap.json.human @@ -0,0 +1,77 @@ +{ + "Id": "f256502a-4143-4d29-914e-80e541b05783", + "Name": "Azure Web App - Slot Swap", + "Description": "Swaps an azure web app slot. Defaults to the deployment slot defined in the web app target. +
+ +*

Note This template is designed to run against an azure web app octopus target

* +*

Depends on Azure CLI and powershell to be installed on the running machine

*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$rg = $OctopusParameters[\"Octopus.Action.Azure.ResourceGroupName\"] +$webAppName = $OctopusParameters[\"Octopus.Action.Azure.WebAppName\"] +$destinationSlot = $OctopusParameters[\"azWebAppSwap.targetSlot\"] +$sourceSlot = $OctopusParameters[\"azWebAppSwap.sourceSlot\"] + +if([string]::IsNullOrEmpty($sourceSlot)) +{ +\tthrow \"value for source slot must be provided\" +} + +$cmdArgs = \"-g $rg -n $webAppName -s $sourceSlot\" + +if(![string]::IsNullOrEmpty($destinationSlot)) {$cmdArgs += \" --target-slot $destinationSlot\"} + +$cmd = \"az webapp deployment slot swap $cmdArgs\" + +write-verbose \"command being invoked: $cmd\" + +Invoke-Expression $cmd", + "Octopus.Action.Azure.AccountId": "#{azureWebAppSwap.AzAccount}" + }, + "Parameters": [ + { + "Id": "a204505b-f995-40ff-95b3-300638d9b9df", + "Name": "azureWebAppSwap.AzAccount", + "Label": "Azure Account", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "0b3383f9-31ae-402c-af57-be3b82a3d5b9", + "Name": "azWebAppSwap.targetSlot", + "Label": "Target Slot", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7e4ed213-d5bb-4725-b13f-78e366535479", + "Name": "azWebAppSwap.sourceSlot", + "Label": "Source Slot", + "HelpText": null, + "DefaultValue": "#{Octopus.Action.Azure.DeploymentSlot}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-25T15:05:37.259Z", + "OctopusVersion": "2020.2.10", + "Type": "ActionTemplate" + }, + "LastModifiedAt": "2020-08-21T08:42:48.772Z", + "LastModifiedBy": "harrisonmeister", + "Category": "azure" + } diff --git a/step-templates/azure-web-app-ssl.json.human b/step-templates/azure-web-app-ssl.json.human new file mode 100644 index 000000000..b6961b0f2 --- /dev/null +++ b/step-templates/azure-web-app-ssl.json.human @@ -0,0 +1,138 @@ +{ + "Id": "72a32f48-2de9-4dac-9c47-b491413478e2", + "Name": "Azure - Set Web App SSL Certificate", + "Description": "Configures the SSL binding for an Azure Web App to use an [Octopus-managed certificate](https://octopus.com/docs/deploying-applications/certificates)", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Azure.AccountId": "#{SslAzureSubscription}", + "Octopus.Action.Script.ScriptBody": "<# +Takes an Octopus certificate variable and + 1) Writes it to a temporary file with a password (as Azure requires the PFX have a password) + 2) Invokes New-AzureRmWebAppSSLBinding + 3) Removes the temporary certificate file +#> + +$ErrorActionPreference = 'Stop' + +Write-Verbose \"Creating temporary certificate file\" +$TempCertificateFile = New-TemporaryFile +# The PFX upload to Azure must have a password. So we give it a GUID. +$Password = [guid]::NewGuid().ToString(\"N\") + +$CertificateName = $OctopusParameters[\"SslCertificate.Name\"] + +Write-Host \"Creating HTTPS binding on web app '$SslWebApp' for domain $SslDomainName using certificate '$CertificateName' \" + +$CertificateBytes = [Convert]::FromBase64String($OctopusParameters[\"SslCertificate.Pfx\"]) +[IO.File]::WriteAllBytes($TempCertificateFile.FullName, $CertificateBytes) +Get-PfxData -FilePath $TempCertificateFile.FullName | Export-PfxCertificate -FilePath $TempCertificateFile.FullName -Password (ConvertTo-SecureString -String $Password -AsPlainText -Force) + +$BindingParams = @{ + WebAppName = $SslWebApp + ResourceGroupName = $SslResourceGroup + Name = $SslDomainName + CertificateFilePath = $TempCertificateFile.FullName + CertificatePassword = $Password + SslState = $SslState +} + +if ($SslSlot) { $BindingParams['Slot'] = $SslSlot } + +New-AzureRmWebAppSSLBinding @BindingParams + +Write-Verbose \"Removing temporary certificate file\" +Remove-Item $TempCertificateFile.FullName -Force" + }, + "Parameters": [ + { + "Id": "a8d7da9d-39d6-4bcd-9a08-fe81210e364c", + "Name": "SslAzureSubscription", + "Label": "Azure Subscription", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + }, + "Links": {} + }, + { + "Id": "4b978b3d-4f87-49d7-b814-14e2f9ef878a", + "Name": "SslWebApp", + "Label": "Azure Web App", + "HelpText": "The name of the Azure Web App for which the SSL binding will be created.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "df5ee03d-1a63-44c0-b3c4-d75020b1f2b8", + "Name": "SslResourceGroup", + "Label": "Resource Group", + "HelpText": "The name of the Azure Resource Group containing the Web App", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "ff318b09-27de-483c-a9c8-b0583e15208c", + "Name": "SslSlot", + "Label": "Slot", + "HelpText": "The Azure Deployment Slot (optional)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "9f488222-f373-4f70-841b-3f340357bb85", + "Name": "SslDomainName", + "Label": "Domain Name", + "HelpText": "The fully qualified domain name for the SSL binding. e.g. store.acme.com", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b8bda333-894a-4223-a07d-afb336b8f75f", + "Name": "SslCertificate", + "Label": "Certificate", + "HelpText": "The certificate to be used for the SSL binding", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Certificate" + }, + "Links": {} + }, + { + "Id": "b9c953f5-a21d-4c64-b134-7295d6d7d48e", + "Name": "SslState", + "Label": "SSL State", + "HelpText": "Specifies the SSL state of the certificate", + "DefaultValue": "SniEnabled", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "SniEnabled +IpBasedEnabled +Disabled" + }, + "Links": {} + } + ], + "LastModifiedBy": "MJRichardson", + "$Meta": { + "ExportedAt": "2018-03-29T00:48:05.333Z", + "OctopusVersion": "3.11.0", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/azure-web-app-start.json.human b/step-templates/azure-web-app-start.json.human new file mode 100644 index 000000000..d6695d87f --- /dev/null +++ b/step-templates/azure-web-app-start.json.human @@ -0,0 +1,71 @@ +{ + "Id": "96ce9c63-91b5-4773-9511-830a2e106083", + "Name": "Azure Web App - Start", + "Description": "Starts an azure web app. +
+ +*

Note This template is designed to run against an azure web app octopus target

* +*

Depends on Azure CLI and powershell to be installed on the running machine

*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Azure.AccountId": "#{azWebApp.AzureAcct}", + "Octopus.Action.Script.ScriptBody": "try +{ +\taz --version +} + +catch +{ +\tthrow \"az cli not installed\" +} + +$webApp = $OctopusParameters[\"Octopus.Action.Azure.WebAppName\"] +$resourceGroup = $OctopusParameters[\"Octopus.Action.Azure.ResourceGroupName\"] + +$appState = az webapp list --resource-group $resourceGroup --query \"[?name=='$webApp'].{state: state, hostName: defaultHostName}\" | ConvertFrom-Json + +if($appState.state -eq 'running') +{ +\tWrite-Host \"Web App $webApp already running\" + return +} + +Write-Host \"Starting web app $webApp in resource group $resourceGroup\" +az webapp start --name $webApp --resource-group $resourceGroup + +Start-Sleep -s 5 + +$appState = az webapp list --resource-group $resourceGroup --query \"[?name=='$webApp'].{state: state, hostName: defaultHostName}\" | ConvertFrom-Json + +if($appState.state -ne \"running\") +{ +\tThrow \"Webapp failed to start. Check the app's activity/error log\" +} +" + }, + "Parameters": [ + { + "Id": "bcb5fe61-4f02-44af-85b9-8c9197567d50", + "Name": "azWebApp.AzureAcct", + "Label": "Azure Account", + "HelpText": "The azure account that has access to the web app", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-08T18:36:48.569Z", + "OctopusVersion": "2020.2.10", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "xtreampb", + "Category": "azure" +} diff --git a/step-templates/azure-web-app-stop.json.human b/step-templates/azure-web-app-stop.json.human new file mode 100644 index 000000000..8160f43e8 --- /dev/null +++ b/step-templates/azure-web-app-stop.json.human @@ -0,0 +1,62 @@ +{ + "Id": "5178ba6d-e0b9-4176-8487-148060ed3a70", + "Name": "Azure Web App - Stop", + "Description": "Stops an azure web app. +
+ +*

Note This template is designed to run against an azure web app octopus target

* +*

Depends on Azure CLI and powershell to be installed on the running machine

*", + "ActionType": "Octopus.AzurePowerShell", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Azure.AccountId": "#{azWebApp.AzureAcct}", + "Octopus.Action.Script.ScriptBody": "try +{ +\taz --version +} + +catch +{ +\tthrow \"az cli not installed\" +} + +$webApp = $OctopusParameters[\"Octopus.Action.Azure.WebAppName\"] +$resourceGroup = $OctopusParameters[\"Octopus.Action.Azure.ResourceGroupName\"] + +$appState = az webapp list --resource-group $resourceGroup --query \"[?name=='$webApp'].{state: state, hostName: defaultHostName}\" | ConvertFrom-Json + +if($appState.state -eq 'stopped') +{ +\tWrite-Host \"Web App $webApp already stopped\" + return +} + +Write-Host \"Stopping webapp $webApp in group $resourceGroup\" +az webapp stop --name $webApp --resource-group $resourceGroup +" + }, + "Parameters": [ + { + "Id": "bcb5fe61-4f02-44af-85b9-8c9197567d50", + "Name": "azWebApp.AzureAcct", + "Label": "Azure Account", + "HelpText": "The azure account that has access to the web app", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-08T18:36:07.610Z", + "OctopusVersion": "2020.2.10", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "xtreampb", + "Category": "azure" +} diff --git a/step-templates/backup-azure-database-to-azure-storage.json.human b/step-templates/backup-azure-database-to-azure-storage.json.human new file mode 100644 index 000000000..8a882454e --- /dev/null +++ b/step-templates/backup-azure-database-to-azure-storage.json.human @@ -0,0 +1,115 @@ +{ + "Id": "633e7b90-7025-45d4-b16f-f92d6cf25c9e", + "Name": "Backup Azure Database To Azure Storage", + "Description": "Create a backup of SQL Azure Database and store it in Azure Blob Storage. Requires Azure PowerShell to be installed on Tentacle machine.", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Check if Windows Azure Powershell is avaiable \r +try{ \r + Import-Module Azure -ErrorAction Stop\r +}catch{\r + throw \"Windows Azure Powershell not found! Please make sure to install them from http://www.windowsazure.com/en-us/downloads/#cmd-line-tools\" \r +}\r +\r +\r +$dateTime = get-date -Format u\r +$blobName = \"Deployment-Backup/$DatabaseName/$dateTime.bacpac\"\r +Write-Host \"Using blobName: $blobName\"\r +\r +# Create Database Connection\r +$securedPassword = ConvertTo-SecureString -String $DatabasePassword -asPlainText -Force\r +$serverCredential = new-object System.Management.Automation.PSCredential($DatabaseUsername, $securedPassword) \r +$databaseContext = New-AzureSqlDatabaseServerContext -ServerName $DatabaseServerName -Credential $serverCredential\r +\r +# Create Storage Connection\r +$storageContext = New-AzureStorageContext -StorageAccountName $StorageName -StorageAccountKey $StorageKey\r +\r +# Initiate the Export\r +$operationStatus = Start-AzureSqlDatabaseExport -StorageContext $storageContext -SqlConnectionContext $databaseContext -BlobName $blobName -DatabaseName $DatabaseName -StorageContainerName $StorageContainerName\r +\r +# Wait for the operation to finish\r +do{\r + $status = Get-AzureSqlDatabaseImportExportStatus -Request $operationStatus \r + Start-Sleep -s 3\r + $progress =$status.Status.ToString()\r + Write-Host \"Waiting for database export completion. Operation status: $progress\" \r +}until ($status.Status -eq \"Completed\")\r +Write-Host \"Database export is complete\"", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "DatabaseServerName", + "Label": "Database Server Name", + "HelpText": "Azure SQL Server name, i.e. \"d340fe8ok\". Not a full name, just the machine name", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to be backed-up", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DatabasePassword", + "Label": "Database Password", + "HelpText": "SQL Server Password to access the database", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "DatabaseUsername", + "Label": "Database Username", + "HelpText": "SQL Server Username to access the database", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "StorageName", + "Label": "Storage Name", + "HelpText": "Name of the storage account where the backup should go", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "StorageKey", + "Label": "Storage Key", + "HelpText": "Access key to the storage account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "StorageContainerName", + "Label": "Storage Container Name", + "HelpText": "Name of the container where the backup should go", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-03-25T08:41:21.754+00:00", + "LastModifiedBy": "trailmax", + "$Meta": { + "ExportedAt": "2015-03-25T08:48:19.055+00:00", + "OctopusVersion": "2.6.3.886", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/bash-check-net-core-framework-version.json.human b/step-templates/bash-check-net-core-framework-version.json.human new file mode 100644 index 000000000..2ce6dc646 --- /dev/null +++ b/step-templates/bash-check-net-core-framework-version.json.human @@ -0,0 +1,86 @@ +{ + "Id": "ac73fb02-8107-4747-bedf-7e39effa31d4", + "Name": ".NET Core - Check .NET Core Framework Version (Bash)", + "Description": "Check if given .NET Core framework version (or greater) is installed.", + "ActionType": "Octopus.Script", + "Version": 43, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "targetVersion=$(get_octopusvariable \"TargetVersion\") +exact=$(get_octopusvariable \"Exact\") + +# required arguments checking +if [[ ! $targetVersion ]] || [[ ! $exact ]] +then + echo \"[ERROR]: Missing required argument. Exit!\" + exit 1; +fi + +dotNetCorePath=/usr/share/dotnet/shared/Microsoft.NETCore.App +dotNetCoreVersions=() +if [ -d \"$dotNetCorePath\" ]; then +\tcd $dotNetCorePath + dotNetCoreVersions=(*/) +fi + +matchedVersions=() +for i in ${dotNetCoreVersions[@]}; do +\tif [ $exact = true ] || [ $exact = True ] + then + \tif [[ $i = $targetVersion/ ]] + then + \tmatchedVersions+=(${i%/}) + fi + else + \tif [[ ! $i < $targetVersion/ ]] + then + \tmatchedVersions+=(${i%/}) + fi + fi +done + +if [ ${#matchedVersions[@]} -eq 0 ]; then + echo \"Can't find .NET Core Runtime $targetVersion installed in the machine.\" + exit 1 +else + for i in ${matchedVersions[@]}; do + \techo \"Found .NET Core Runtime $i installed in the machine.\" +\tdone +fi" + }, + "Parameters": [ + { + "Id": "694207ec-0e0b-42a1-841b-81aa4ee5cb7d", + "Name": "TargetVersion", + "Label": "Target .NET Core framework version", + "HelpText": "The target .NET Core framework version you expect to be installed in the machine. For example, 2.0.5.", + "DefaultValue": "2.0.5", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b570ac4a-44a8-42ef-90c4-822530ff6d52", + "Name": "Exact", + "Label": "Exact", + "HelpText": "If you check \"Exact\", it means the installed .NET Core framework version MUST match target version. + +Otherwise, as long as the installed .NET Coreframework version is greater than or equal to target version, the check will pass.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "$Meta": { + "ExportedAt": "2018-03-30T10:07:36.152Z", + "OctopusVersion": "2018.3.4", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "cjuroz", + "Category": "bash" +} diff --git a/step-templates/bash-http-test-url.json.human b/step-templates/bash-http-test-url.json.human new file mode 100644 index 000000000..65e537885 --- /dev/null +++ b/step-templates/bash-http-test-url.json.human @@ -0,0 +1,96 @@ +{ + "Id": "17092e2b-7fae-4aae-b8df-bb1c7ec76ff9", + "Name": "HTTP - Test URL (Bash)", + "Description": "Makes a GET request to a HTTP(S) end point and verifies that a particular status code is returned within a specified period of time", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "uri=$(get_octopusvariable \"uri\")\r +expectedCode=$(get_octopusvariable \"expectedCode\")\r +timeout=$(get_octopusvariable \"timeout\")\r +success=false\r +\r +# required arguments checking\r +if [[ $expectedCode == \"Unrecognized variable\"* ]] || [[ $uri == \"Unrecognized variable\"* ]] || [[ $timeout == \"Unrecognized variable\"* ]]\r +then\r + echo \"[ERROR]: Missing required argument. Exit!\"\r + exit 1;\r +fi\r +\r +echo \"Starting verification request to $uri\"\r +echo \"Expecting response code $expectedCode\"\r +\r +end=$((SECONDS+$timeout))\r +\r +until $success || [ $SECONDS -ge $end ];\r +do\r + code=$(curl --write-out %{http_code} --silent --output /dev/null $uri)\r + echo \"Recieved response code: $code\"\r + \r + if [ $code -eq $expectedCode ]\r + then\r + echo \"Sucesss! Found status code $expectedCode\"\r + success=true\r + exit 0\r + else\r + echo \"Trying again in 5 seconds...\"\r + sleep 5\r + fi\r +done\r +\r +if ! $success\r +then\r + echo \"Verification failed - giving up.\"\r + exit 1\r +fi", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "d0191097-38d0-4b73-ae74-783a1e439cd4", + "Name": "uri", + "Label": "URI", + "HelpText": "The full Uri of the endpoint", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "cfc51640-dbc9-453f-8e59-9ed33607a6bd", + "Name": "expectedCode", + "Label": "Expected code", + "HelpText": "The expected HTTP status code", + "DefaultValue": "200", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1f60efab-65fe-415b-8d9b-2f248543e302", + "Name": "timeout", + "Label": "Timeout (seconds)", + "HelpText": "The number of seconds before the step fails and times out", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "cjuroz", + "$Meta": { + "ExportedAt": "2017-02-17T05:46:18.745Z", + "OctopusVersion": "3.8.4", + "Type": "ActionTemplate" + }, + "Category": "bash" +} diff --git a/step-templates/bash-service-check-running.json.human b/step-templates/bash-service-check-running.json.human new file mode 100644 index 000000000..138ff3c11 --- /dev/null +++ b/step-templates/bash-service-check-running.json.human @@ -0,0 +1,64 @@ +{ + "Id": "2617b82a-c5f8-4a66-a9eb-e99b37e127d8", + "Name": "Service - Check Running (Bash)", + "Description": "Check if any given service or list of services is/are running (uses `ps` command)", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "services=$(get_octopusvariable \"services\")\r +failed=false\r +\r +# required arguments checking\r +if [[ $services == \"Unrecognized variable\"* ]]\r +then\r + echo \"[ERROR]: Missing required argument. Exit!\"\r + exit 1;\r +fi\r +\r +for service in ${services//,/ }\r +do\r + if (( $(ps -ef | grep -v grep | grep $service | wc -l) > 0 ))\r + then\r + echo \"$service is running!!!\"\r + else\r + echo \"$service is not running!!!\"\r + failed=true\r + fi\r +done\r +\r +if $failed; then\r + echo \"At least one service is not running!!!\"\r + exit 1\r +fi\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "3e260398-4df1-4a59-a325-752029c52b46", + "Name": "services", + "Label": "Service names", + "HelpText": "The set of service names as should be listed by `ps` command. This can be either a single service or a comma separated list of services to check. +- Example 1: httpd +- Example 2: httpd, sshd, memcached", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "cjuroz", + "$Meta": { + "ExportedAt": "2017-02-17T05:50:08.653Z", + "OctopusVersion": "3.8.4", + "Type": "ActionTemplate" + }, + "Category": "bash" +} diff --git a/step-templates/block-progression.json.human b/step-templates/block-progression.json.human new file mode 100644 index 000000000..92f6695de --- /dev/null +++ b/step-templates/block-progression.json.human @@ -0,0 +1,78 @@ +{ + "Id": "78a182b3-5369-4e13-9292-b7f991295ad1", + "Name": "Block Release Progression", + "Description": "Step template to block the release progression of an Octopus Deploy release so it cannot be promoted beyond the current environment.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$apiKey = $OctopusParameters[\"Block.Octopus.Api.Key\"] +$previousReleaseId = $OctopusParameters[\"Block.Octopus.Previous.Release.Id\"] +$reason = $OctopusParameters[\"Block.Octopus.Reason\"] +$octopusBaseUrl = $OctopusParameters[\"Block.Octopus.Url\"] +$spaceId = $OctopusParameters[\"Octopus.Space.Id\"] + +$body = @{ +\tDescription = $reason +} +$bodyAsJson = $body | ConvertTo-JSON -Depth 10 + +$headers = @{\"X-Octopus-ApiKey\"=\"$apiKey\"} + +Write-Host \"Blocking the release $previousReleaseId from progressing\" +Invoke-RestMethod -Uri \"$($octopusBaseUrl)/api/$($spaceId)/releases/$($previousReleaseId)/defects\" -Method POST -Headers $headers -Body $bodyAsJSON -ContentType 'application/json; charset=utf-8'" + }, + "Parameters": [ + { + "Id": "fc366d69-e56b-476b-8005-5431f1ce8c05", + "Name": "Block.Octopus.Url", + "Label": "Octopus Url", + "HelpText": "The base url (`https://samples.octopus.app`) of your Octopus Server. Defaults to `Octopus.Web.BaseUrl`.", + "DefaultValue": "#{Octopus.Web.BaseUrl}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f03e6e96-f1d5-4b91-9ae6-f67fd8ba043c", + "Name": "Block.Octopus.Api.Key", + "Label": "Octopus API Key", + "HelpText": "The API key of a user who has permission to block releases.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "cdcbd826-e075-476c-b67a-ddf0e75a576c", + "Name": "Block.Octopus.Previous.Release.Id", + "Label": "Release Id to Block", + "HelpText": "The Octopus ID (`Releases-123`) of the release to block. Defaults to `Octopus.Release.CurrentForEnvironment.Id`", + "DefaultValue": "#{Octopus.Release.CurrentForEnvironment.Id}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5c9c1d6a-ec08-4d79-b442-180f9d0460c6", + "Name": "Block.Octopus.Reason", + "Label": "Reason", + "HelpText": "The reason why this release is being blocked.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-09-13T16:14:01.845Z", + "OctopusVersion": "2021.2.7428", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "bobjwalker", + "Category": "octopus" + } diff --git a/step-templates/calculate-deployment-mode.json.human b/step-templates/calculate-deployment-mode.json.human new file mode 100644 index 000000000..dd7330fbe --- /dev/null +++ b/step-templates/calculate-deployment-mode.json.human @@ -0,0 +1,270 @@ +{ + "Id": "d166457a-1421-4731-b143-dd6766fb95d5", + "Name": "Calculate Deployment Mode", + "Description": "This step uses Octopus [System Variables](https://octopus.com/docs/projects/variables/system-variables) to calculate the deployment mode. + +# Deployment Mode +The potential modes are: +- **Deploy**: A newer version is being deployed to the target environment. For example, `2021.1.4` is being deployed to **Production** to replace `2021.0.5`. +- **Rollback**: An older version is being deployed to the target environment. For example, `2021.0.5` is being deployed to **Production** to replace `2021.1.4`. +- **Redeploy**: The same version is being deployed to the target environment. For example, `2021.1.4` is being deployed to **Production** which already has `2021.1.4`. + +**Please note**: This step template uses the release numbers to calculate the deployment mode. It doesn't look at any packages. + +# Version Difference +After calculating the deployment mode, the step template will calculate the version difference. The potential options are: +- **Identical**: No differences between the previous release and the current release were found. +- **Major**: The first number (2021 in 2021.1.2.10) is different between the previous release and the current release. +- **Minor**: The second number (1 in 2021.1.2.10) is different between the previous release and the current release. +- **Build**: The third number (2 in 2021.1.2.10) is different between the previous release and the current release. +- **Revision**: The fourth number (10 in 2021.1.2.10) is different between the previous release and the current release. + +# Manual or Automatic Trigger +The step template will also determine if the deployment was caused by a trigger or is a manual deployment. Potential values are `True` (caused by a trigger) or `False` (manual deployment). + +# Output Variables + +The following output variables will be set: +- **DeploymentMode**: Will either be `Deploy`, `Rollback` or `Redeploy`. +- **Trigger**: Will either be `True` or `False`. Indicates if this deployment was caused by a trigger (scheduled or deployment target). +- **VersionChange**: Will either be `Identical`, `Major`, `Minor`, `Build`, or `Revision`. + +## Variable Run Condition Output Variables +To make it easier to use, the step template will set a number of run condition output variables. + +### Variable Run Condition Usage +Variable Run Conditions will _always_ be evaluated. Even if there is an error. If the run condition comes back as **Truthy** it will run the step. + +To limit when the step runs, wrap the output variable with an if/then or unless clause: +- **Always Run**: `#{Octopus.Action[Calculate Deployment Mode].Output.RunOnDeploy}` +- **Success**: Only run when previous steps succeeds `#{unless Octopus.Deployment.Error}#{Octopus.Action[Calculate Deployment Mode].Output.RunOnDeploy}#{/unless}` +- **Failure**: Only run when previous steps fail `#{if Octopus.Deployment.Error}#{Octopus.Action[Calculate Deployment Mode].Output.RunOnDeploy}#{/if}` + +**Hint:** Replace **RunOnDeploy** from the above examples with one of the variables from below. + +### Deployment Mode Run Conditions +- **RunOnDeploy**: Only run the step when the **DeploymentMode** is `Deploy`. +- **RunOnRollback**: Only run the step when the **DeploymentMode** is `Rollback`. +- **RunOnRedeploy**: Only run the step when the **DeploymentMode** is `Redeploy`. +- **RunOnDeployOrRollback**: Only run the step when the **DeploymentMode** is`Deploy` or `Rollback`. +- **RunOnDeployOrRedeploy**: Only run the step when the **DeploymentMode** is`Deploy` or `Redeploy`. +- **RunOnRedeployOrRollback**: Only run the step when the **DeploymentMode** is `Redeploy` or `Rollback`. + +### Version Change Run Conditions +- **RunOnMajorVersionChange**: Only run the step when the **VersionChange** is `Major`. +- **RunOnMinorVersionChange**: Only run the step when the **VersionChange** is `Minor`. +- **RunOnMajorOrMinorVersionChange**: Only run the step when the **VersionChange** is `Major` or `Minor`. +- **RunOnBuildVersionChange**: Only run the step when the **VersionChange** is `Build`. +- **RunOnRevisionVersionChange**: Only run the step when the **VersionChange** is `Revision`. + +# Usage + +**Important:** This step template is designed for deployment processes only. Runbooks have no concept of deployments, redeployments, or rollbacks. + +This step was designed to run on a worker (or the Octopus Server). It can run on targets, but the output variables will all be the same; running on targets will do nothing but waste compute cycles.", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$currentReleaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$previousReleaseNumber = $OctopusParameters[\"Octopus.Release.CurrentForEnvironment.Number\"] +$lastAttemptedReleaseNumber = $OctopusParameters[\"Octopus.Release.PreviousForEnvironment.Number\"] +$stepName = $OctopusParameters[\"Octopus.Action.StepName\"] +$triggerName = $OctopusParameters[\"Octopus.Deployment.Trigger.Name\"] + +Write-Host \"The current release number is $currentReleaseNumber\" +Write-Host \"The last succesful release to this environment was $previousReleaseNumber\" +Write-Host \"The last release that was attempted on this environment was $lastAttemptedReleaseNumber\" +Write-Host \"The deployment name is $deploymentName\" + +if ($previousReleaseNumber -like \"*-*\") +{ +\t$previousReleaseNumber = $previousReleaseNumber.SubString(0, $previousReleaseNumber.IndexOf(\"-\")) +} + +if ($currentReleaseNumber -like \"*-*\") +{ +\t$currentReleaseNumber = $currentReleaseNumber.SubString(0, $currentReleaseNumber.IndexOf(\"-\")) +} + +if ($lastAttemptedReleaseNumber -like \"*-*\") +{ +\t$lastAttemptedReleaseNumber = $lastAttemptedReleaseNumber.SubString(0, $lastAttemptedReleaseNumber.IndexOf(\"-\")) +} + +Write-Host \"The non-pre release tag previous version for the environment was $previousReleaseNumber\" +Write-Host \"The non-pre release tag current release number is $currentReleaseNumber\" +Write-Host \"The non-pre release tag of the last attempted version for the environment was $lastAttemptedReleaseNumber\" + +$currentVersion = [System.Version]$currentReleaseNumber +$previousVersion = [System.Version]$previousReleaseNumber +$lastAttemptedVersion = [System.Version]$lastAttemptedReleaseNumber + +$differentVersions = $false +$versionToCompare = $previousVersion +if ($currentVersion -gt $previousVersion) +{ +\tWrite-Host \"The current release number $currentReleaseNumber is greater than the previous successful release number $previousReleaseNumber.\" +\tif ($currentVersion -lt $lastAttemptedVersion) + { + \tWrite-Host \"The current release number $currentReleaseNumber is less than the last attempted release number $lastAttemptedReleaseNumber. Setting deployment mode to rollback.\" +\t $deploymentMode = \"Rollback\" + $versionToCompare = $lastAttemptedVersion + } + else + { + \tWrite-Host \"The current release number $curentReleaseNumber is greater than the last attempted release number $lastAttemptedReleaseNumber. Setting deployment mode to deploy.\" + $deploymentMode = \"Deploy\" + } +} +elseif ($currentVersion -lt $previousVersion) +{ +\tWrite-Host \"The current release number $currentReleaseNumber is less than the previous successful release number $previousReleaseNumber. Setting deployment mode to rollback.\" + $deploymentMode = \"Rollback\" + $differentVersions = $true +} +elseif ($currentVersion -lt $lastAttemptedVersion) +{ +\tWrite-Host \"The current release number $currentReleaseNumber is less than the last attempted release number $lastAttemptedReleaseNumber. Setting the deployment mode to rollback.\" + $deploymentMode = \"Rollback\" + $differentVersions = $true + $versionToCompare = $lastAttemptedVersion +} +else +{ +\tWrite-Host \"The current release number $currentReleaseNumber matches the previous release number $previousReleaseNumber. Setting deployment mode to redeployment.\" + $deploymentMode = \"Redeploy\" +} + +$differenceKind = \"Identical\" +if ($differentVersions) +{ +\tif ($currentVersion.Major -ne $versionToCompare.Major) + { + \tWrite-Host \"$currentReleaseNumber is a major version change from $versionToCompare\" + \t$differenceKind = \"Major\" + } + elseif ($currentVersion.Minor -ne $versionToCompare.Minor) + { + \tWrite-Host \"$currentReleaseNumber is a minor version change from $versionToCompare\" + \t$differenceKind = \"Minor\" + } + elseif ($currentVersion.Build -ne $versionToCompare.Build) + { + \tWrite-Host \"$currentReleaseNumber is a build version change from $versionToCompare\" + \t$differenceKind = \"Build\" + } + elseif ($currentVersion.Revision -ne $versionToCompare.Revision) + { + \tWrite-Host \"$currentReleaseNumber is a revision version change from $versionToCompare\" + \t$differenceKind = \"Revision\" + } +} + +$trigger = $false +if ([string]::IsNullOrWhiteSpace($triggerName) -eq $false) +{ +\tWrite-Host \"This task was created by trigger $triggerName.\" + $trigger = $true +} + +Set-OctopusVariable -Name \"DeploymentMode\" -Value $deploymentMode +Set-OctopusVariable -Name \"VersionChange\" -Value $differenceKind +Set-OctopusVariable -Name \"Trigger\" -Value $trigger + +Write-Highlight @\" +Output Variables Created: + \t- Octopus.Action[$($stepName)].Output.DeploymentMode - Set to '$deploymentMode' + - Octopus.Action[$($stepName)].Output.VersionChange - Set to '$differenceKind' + - Octopus.Action[$($stepName)].Output.Trigger - Set to '$trigger' + +Deployment Mode Run Conditions Output Variables: + \t- Octopus.Action[$($stepName)].Output.RunOnRollback + - Octopus.Action[$($stepName)].Output.RunOnDeploy + - Octopus.Action[$($stepName)].Output.RunOnRedeploy + - Octopus.Action[$($stepName)].Output.RunOnDeployOrRollback + - Octopus.Action[$($stepName)].Output.RunOnDeployOrRedeploy + - Octopus.Action[$($stepName)].Output.RunOnRollbackOrRedeploy + +Version Change Run Conditions Output Variables: + \t- Octopus.Action[$($stepName)].Output.RunOnMajorVersionChange + - Octopus.Action[$($stepName)].Output.RunOnMinorVersionChange + - Octopus.Action[$($stepName)].Output.RunOnMajorOrMinorVersionChange + - Octopus.Action[$($stepName)].Output.RunOnBuildVersionChange + - Octopus.Action[$($stepName)].Output.RunOnRevisionVersionChange + +Variable run conditions are always evaluated, even if there is an error. Use the following examples to control when your step runs. Replace RunOnDeploy from below examples with one of the variables from above. +- Always Run: `#{Octopus.Action[$stepName].Output.RunOnDeploy}` +- Success: Only run when previous steps succeeds `##{unless Octopus.Deployment.Error}#{Octopus.Action[$stepName].Output.RunOnDeploy}##{/unless}` +- Failure: Only run when previous steps fail `##{if Octopus.Deployment.Error}#{Octopus.Action[$stepName].Output.RunOnDeploy}##{/if}` + +\"@ + +$runOnRollback = \"#{if Octopus.Action[$($stepName)].Output.DeploymentMode == \"\"Rollback\"\"}True#{else}False#{/if}\" +Write-Host \"Setting the output variable 'Octopus.Action[$($stepName)].Output.RunOnRollback' so you can use it as a run condition\" +Write-Verbose $runOnRollback +Set-OctopusVariable -Name \"RunOnRollback\" -Value $runOnRollback + +$runOnDeploy = \"#{if Octopus.Action[$($stepName)].Output.DeploymentMode == \"\"Deploy\"\"}True#{else}False#{/if}\" +Write-Host \"Setting the output variable 'Octopus.Action[$($stepName)].Output.RunOnDeploy' so you can use it as a run condition\" +Write-Verbose $runOnDeploy +Set-OctopusVariable -Name \"RunOnDeploy\" -Value $runOnDeploy + +$runOnRedeploy = \"#{if Octopus.Action[$($stepName)].Output.DeploymentMode == \"\"Redeploy\"\"}True#{else}False#{/if}\" +Write-Host \"Setting the output variable 'Octopus.Action[$($stepName)].Output.RunOnRedeploy' so you can use it as a run condition\" +Write-Verbose $runOnRedeploy +Set-OctopusVariable -Name \"RunOnRedeploy\" -Value $runOnRedeploy + +$runOnDeployOrRollback = \"#{if Octopus.Action[$($stepName)].Output.DeploymentMode != \"\"Redeploy\"\"}True#{else}False#{/if}\" +Write-Host \"Setting the output variable 'Octopus.Action[$($stepName)].Output.RunOnDeployOrRollback' so you can use it as a run condition\" +Write-Verbose $runOnDeployOrRollback +Set-OctopusVariable -Name \"RunOnDeployOrRollback\" -Value $runOnDeployOrRollback + +$runOnDeployOrRedeploy = \"#{if Octopus.Action[$($stepName)].Output.DeploymentMode != \"\"Rollback\"\"}True#{else}False#{/if}\" +Write-Host \"Setting the output variable 'Octopus.Action[$($stepName)].Output.RunOnDeployOrRedeploy' so you can use it as a run condition\" +Write-Verbose $runOnDeployOrRedeploy +Set-OctopusVariable -Name \"RunOnDeployOrRedeploy\" -Value $runOnDeployOrRedeploy + +$runOnRedeployOrRollback = \"#{if Octopus.Action[$($stepName)].Output.DeploymentMode != \"\"Deploy\"\"}True#{else}False#{/if}\" +Write-Host \"Setting the output variable 'Octopus.Action[$($stepName)].Output.RunOnRedeployOrRollback' so you can use it as a run condition\" +Write-Verbose $runOnRedeployOrRollback +Set-OctopusVariable -Name \"RunOnRedeployOrRollback\" -Value $runOnRedeployOrRollback + +$runOnMajorVersionChange = \"#{if Octopus.Action[$($stepName)].Output.VersionChange == \"\"Major\"\"}True#{else}False#{/if}\" +Write-Host \"Setting the output variable 'Octopus.Action[$($stepName)].Output.RunOnMajorVersionChange' so you can use it as a run condition\" +Write-Verbose $runOnMajorVersionChange +Set-OctopusVariable -Name \"RunOnMajorVersionChange\" -Value $runOnMajorVersionChange + +$runOnMinorVersionChange = \"#{if Octopus.Action[$($stepName)].Output.VersionChange == \"\"Minor\"\"}True#{else}False#{/if}\" +Write-Host \"Setting the output variable 'Octopus.Action[$($stepName)].Output.RunOnMinorVersionChange' so you can use it as a run condition\" +Write-Verbose $runOnMinorVersionChange +Set-OctopusVariable -Name \"RunOnMinorVersionChange\" -Value $runOnMinorVersionChange + +$runOnMajorOrMinorVersionChange = \"#{if Octopus.Action[$stepName].Output.VersionChange == \"\"Major\"\"}True#{else}#{if Octopus.Action[$stepName].Output.VersionChange == \"\"Minor\"\"}True#{else}False#{/if}#{/if}\" +Write-Host \"Setting the output variable 'Octopus.Action[$($stepName)].Output.RunOnMajorOrMinorVersionChange' so you can use it as a run condition\" +Write-Verbose $runOnMajorOrMinorVersionChange +Set-OctopusVariable -Name \"RunOnMajorOrMinorVersionChange\" -Value $runOnMajorOrMinorVersionChange + +$runOnBuildVersionChange = \"#{if Octopus.Action[$($stepName)].Output.VersionChange == \"\"Build\"\"}True#{else}False#{/if}\" +Write-Host \"Setting the output variable 'Octopus.Action[$($stepName)].Output.RunOnBuildVersionChange' so you can use it as a run condition\" +Write-Verbose $runOnBuildVersionChange +Set-OctopusVariable -Name \"RunOnBuildVersionChange\" -Value $runOnBuildVersionChange + +$runOnRevisionVersionChange = \"#{if Octopus.Action[$($stepName)].Output.VersionChange == \"\"Revision\"\"}True#{else}False#{/if}\" +Write-Host \"Setting the output variable 'Octopus.Action[$($stepName)].Output.RunOnRevisionVersionChange' so you can use it as a run condition\" +Write-Verbose $runOnRevisionVersionChange +Set-OctopusVariable -Name \"RunOnRevisionVersionChange\" -Value $runOnRevisionVersionChange" + }, + "Parameters": [], + "$Meta": { + "ExportedAt": "2021-11-02T13:48:09.120Z", + "OctopusVersion": "2021.1.7738", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "BobJWalker", + "Category": "octopus" + } diff --git a/step-templates/cassandra-create-keyspace.json.human b/step-templates/cassandra-create-keyspace.json.human new file mode 100644 index 000000000..478451fd8 --- /dev/null +++ b/step-templates/cassandra-create-keyspace.json.human @@ -0,0 +1,161 @@ +{ + "Id": "8ab26143-22d7-4e2f-83a8-f0e2d74a4de2", + "Name": "Cassandra - Create database if not exists", + "Description": "This template creates a keyspace on a Cassandra server if it doesn't already exist. **Note** this template is written in Python and requires that `pip` is installed to function correctly.,", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Python", + "Octopus.Action.Script.ScriptBody": "# Import subprocess +import subprocess + +# Define function to install specified package +def install(package): + subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", package]) + + +# Supress warning when in a docker container +print('##octopus[stderr-ignore]',flush = True) + +# Check to see if cassandra-module is installed +print('Checking for Cassandra module ...',flush = True) +if 'cassandra-driver' not in sys.modules: + # Install the cassandra-driver module + print('Installing cassandra-driver module ...',flush = True) + install('cassandra-driver') +else: + print('cassandra-driver module is present ...',flush = True) + +# Import cassandra modules +from cassandra.cluster import Cluster +from cassandra.auth import PlainTextAuthProvider + +# Set username/password authentication provider +auth_provider = PlainTextAuthProvider( + username='#{Cassandra.User.Name}', password='#{Cassandra.User.Password}') + +# Connect to server +print('Connecting to server ...',flush = True) +cluster = None + +if '#{Cassandra.User.Name}' != '' and '#{Cassandra.User.Password}' != '': +\tcluster = Cluster(['#{Cassandra.Server.Name}'], auth_provider=auth_provider, port=#{Cassandra.Server.Port}) +else: +\tcluster = Cluster(['#{Cassandra.Server.Name}'], port=#{Cassandra.Server.Port}) + +# Conect to cluster +session = cluster.connect() +rows = session.execute(\"SELECT keyspace_name FROM system_schema.keyspaces;\") +keyspace = next((x for x in rows if x.keyspace_name == '#{Cassandra.Keyspace.Name}'), None) + +if keyspace == None: + # Create json document + strategyjson = None + if '#{Cassandra.Server.Mode}' == \"SimpleStrategy\": + strategyjson = { 'class' : '#{Cassandra.Server.Mode}', 'replication_factor': '#{Cassandra.Replicas.Number}' } + + if '#{Cassandra.Server.Mode}' == \"NetworkTopologyStrategy\": + strategyjson = { 'class' : '#{Cassandra.Server.Mode}', '#{Cassandra.Server.Name}' : '#{Cassandra.Replicas.Number}'} + + # Create keyspace + print('Creating keyspace #{Cassandra.Keyspace.Name} ...',flush = True) + session.execute(\"CREATE KEYSPACE IF NOT EXISTS #{Cassandra.Keyspace.Name} WITH REPLICATION = {0};\".format(strategyjson)) + + # Verify keyspace was created + rows = session.execute(\"SELECT keyspace_name FROM system_schema.keyspaces;\") + + keyspace = next((x for x in rows if x.keyspace_name == '#{Cassandra.Keyspace.Name}'), None) + + if keyspace != None: + print('#{Cassandra.Keyspace.Name} created successfully!',flush = True) + else: + print('#{Cassandra.Keyspace.Name} was not created!',flush = True) + exit(1) +else: + print('Keyspace #{Cassandra.Keyspace.Name} already exists.',flush = True)" + }, + "Parameters": [ + { + "Id": "93076332-862f-44c5-b003-f8d6c9138d2b", + "Name": "Cassandra.Server.Name", + "Label": "Server Name", + "HelpText": "Hostname or IP address of the Cassandra database server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8abf47c6-eec5-428d-be90-4b4443295867", + "Name": "Cassandra.Server.Port", + "Label": "Port", + "HelpText": "Port number that the Cassandra server is listening on.", + "DefaultValue": "9042", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5630dc27-80d2-421c-bb99-a61b2e6bd439", + "Name": "Cassandra.User.Name", + "Label": "(Optional) Username", + "HelpText": "Username of the account that can create databases. Leave blank if not using Username/Password authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1e7e73db-ca36-4bd6-9c5a-3f49506c7adf", + "Name": "Cassandra.User.Password", + "Label": "(Optional) Password", + "HelpText": "Password for the user account that can create databases. Leave blank if not using Username/Password authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "484d22fc-4a84-4459-ac4e-166731432709", + "Name": "Cassandra.Server.Mode", + "Label": "Server mode", + "HelpText": "The mode in which the Cassandra server is operating.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "NetworkTopologyStrategy|Network Topology Strategy +SimpleStrategy|Simple Strategy" + } + }, + { + "Id": "b2c433be-66bb-4ee0-9246-59e62818b7bb", + "Name": "Cassandra.Keyspace.Name", + "Label": "Keyspace", + "HelpText": "Name of the Keyspace to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "73a696ca-26e3-4069-852a-3be63d5bd090", + "Name": "Cassandra.Replicas.Number", + "Label": "Number of replicas", + "HelpText": "The number of replicas to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2023-05-03T22:44:27.358Z", + "OctopusVersion": "2023.1.10046", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "cassandra" + } diff --git a/step-templates/chef-run-cookbook.json.human b/step-templates/chef-run-cookbook.json.human new file mode 100644 index 000000000..aca3433b6 --- /dev/null +++ b/step-templates/chef-run-cookbook.json.human @@ -0,0 +1,79 @@ +{ + "Id": "877815ea-0cfb-4dfa-80a3-983140f8cf52", + "Name": "Chef - Run Cookbook", + "Description": "Runs a chef cookbook using chef-client.", + "ActionType": "Octopus.Script", + "Version": 1, + "LastModifiedOn": "2018-10-16T14:31:14.894Z", + "LastModifiedBy": "margani", + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.Script.ScriptParameters": "-CookBookFolder #{CookBookDir}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$cookBookFolder = $OctopusParameters[\"CookBookDir\"] +$overrideRunList = $OctopusParameters[\"OverrideRunList\"] + +if (-not $cookBookFolder -or -not $overrideRunList) { +\tthrow \"The parameters are mandatory.\" +} + +$ClientPath = Join-Path $cookBookFolder \"client.rb\" +$WorkingPath = Join-Path $cookBookFolder \"temp\" +$DatabagPath = Join-Path $WorkingPath \"data_bags\" +$EnvironmentPath = Join-Path $WorkingPath \"environments\" +$CookbooksPath = Join-Path $WorkingPath \"cookbooks\" +$FileCachePath = Join-Path $WorkingPath \"cache\" + +$ClientContent = @\" +data_bag_path \"$($DatabagPath -replace '\\\\','/')\" +environment_path \"$($EnvironmentPath -replace '\\\\','/')\" +cookbook_path \"$($CookbooksPath -replace '\\\\','/')\" +file_cache_path \"$($FileCachePath -replace '\\\\','/')\" +ssl_verify_mode :verify_peer +\"@ + +[System.IO.File]::WriteAllText($ClientPath, $ClientContent) + +@($DatabagPath, $EnvironmentPath, $CookbooksPath, $FileCachePath) | %{ +\tif (!(Test-Path $_) ){ +\t\tmkdir -p $_ +\t} +} + +Push-Location $cookBookFolder +Remove-Item *.lock +&berks vendor $CookbooksPath +&chef-client -z -c $ClientPath -o $overrideRunList +Pop-Location" + }, + "Parameters": [ + { + "Id": "dc6cfaed-3df0-4245-88a3-d59f14a64399", + "Name": "CookBookDir", + "Label": "Cookbook directory", + "HelpText": "The directory of the cookbook which has Berksfile", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6782c7cf-7562-48ff-93a2-fbeb99f0a088", + "Name": "OverrideRunList", + "Label": "Override run list", + "HelpText": "The list of cookbook/recipes which you want to run and override", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2018-10-16T14:31:14.894Z", + "OctopusVersion": "2018.6.2", + "Type": "ActionTemplate" + }, + "Category": "chef" +} diff --git a/step-templates/chocolatey-ensure-installed.json.human b/step-templates/chocolatey-ensure-installed.json.human new file mode 100644 index 000000000..ff2132d88 --- /dev/null +++ b/step-templates/chocolatey-ensure-installed.json.human @@ -0,0 +1,37 @@ +{ + "Id": "c364b0a5-a0b7-48f8-a1a4-35e9f54a82d3", + "Name": "Chocolatey - Ensure Installed", + "Description": "Ensures that the Chocolatey package manager is installed on the system. The installer is downloaded from https://chocolatey.org if required.", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +Write-Output \"Ensuring the Chocolatey package manager is installed...\" + +$chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"Machine\") + \"\\bin\" +$chocInstalled = Test-Path \"$chocolateyBin\\choco.exe\" + +if (-not $chocInstalled) { + Write-Output \"Chocolatey not found, installing...\" + + $installPs1 = ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1')) + Invoke-Expression $installPs1 + + Write-Output \"Chocolatey installation complete.\" +} else { + Write-Output \"Chocolatey was found at $chocolateyBin and won't be reinstalled.\" +} +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [], + "LastModifiedOn": "2020-07-28T13:42:22.284+00:00", + "LastModifiedBy": "pauby", + "$Meta": { + "ExportedAt": "2020-07-28T13:42:22.284+00:00", + "OctopusVersion": "2.4.7.85", + "Type": "ActionTemplate" + }, + "Category": "chocolatey" +} diff --git a/step-templates/chocolatey-install-package.json.human b/step-templates/chocolatey-install-package.json.human new file mode 100644 index 000000000..f0b9dfd20 --- /dev/null +++ b/step-templates/chocolatey-install-package.json.human @@ -0,0 +1,161 @@ +{ + "Id": "b2385b12-e5b5-440f-bed8-6598c29b2528", + "Name": "Chocolatey - Install Package", + "Description": "Installs or upgrades a package using the Chocolatey package manager.", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +$chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"Machine\") + \"\\bin\" +if(-not (Test-Path $chocolateyBin)) { + Write-Output \"Environment variable 'ChocolateyInstall' was not found in the system variables. Attempting to find it in the user variables...\" + $chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"User\") + \"\\bin\" +} + +$choco = \"$chocolateyBin\\choco.exe\" + +if (-not (Test-Path $choco)) { + throw \"Chocolatey was not found at $chocolateyBin.\" +} + +$chocoArgs = @('install') +if (-not $ChocolateyPackageId) { + throw \"Please specify the ID of an application package to install.\" +} +else { + $chocoArgs += $ChocolateyPackageId -split ' ' +} + +$chocoVersion = & $choco --version +Write-Output \"Running Chocolatey version $chocoVersion\" + +if (-not $ChocolateyPackageVersion) { + Write-Output \"Installing package(s) $ChocolateyPackageId from the Chocolatey package repository...\" +} else { + Write-Output \"Installing package $ChocolateyPackageId version $ChocolateyPackageVersion from the Chocolatey package repository...\" + $chocoArgs += @('--version', $ChocolateyPackageVersion) +} + +if([System.Version]::Parse($chocoVersion) -ge [System.Version]::Parse(\"0.9.8.33\")) { + Write-Output \"Adding --yes to arguments passed to Chocolatey\" + $chocoArgs += @(\"--yes\") +} + +if (![String]::IsNullOrEmpty($ChocolateyCacheLocation)) { + Write-Output \"Using --cache-location $ChocolateyCacheLocation\" + $chocoArgs += @(\"--cache-location\", \"`\"'$ChocolateyCacheLocation'`\"\") +} + +if (![String]::IsNullOrEmpty($ChocolateySource)) { + Write-Output \"Using package --source $ChocolateySource\" + $chocoArgs += @('--source', \"`\"'$ChocolateySource'`\"\") +} + +if (($ChocolateyNoProgress -eq 'True') -and ([System.Version]::Parse($chocoVersion) -ge [System.Version]::Parse(\"0.10.4\"))) { + Write-Output \"Disabling download progress with --no-progress\" + $chocoArgs += @('--no-progress') +} + +if (![String]::IsNullOrEmpty($ChocolateyOtherParameters)) { +\t$chocoArgs += $ChocolateyOtherParameters -split ' ' +} + +# execute the command line +Write-Output \"Running the command: $choco $chocoArgs\" +& $choco $chocoArgs + +if ($global:LASTEXITCODE -eq 3010) { +\t# ignore reboot required exit code + Write-Output \"A restart may be required for the package to work\" + $global:LASTEXITCODE = 0 +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "449e25a9-2984-4cb7-b190-ae5114cfbb19", + "Name": "ChocolateyPackageId", + "Label": "(Required) Package Name", + "HelpText": "The name of the Chocolatey package to install. Install multiple packages by separating them with a space. + +Examples: + +* _git_ +* _git_ _vscode_ _notepadplusplus_ +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "17ff15ac-f925-493c-9330-64180687f44c", + "Name": "ChocolateyPackageVersion", + "Label": "(Optional) Version", + "HelpText": "If a specific version of the Chocolatey package is required enter it here. Otherwise, leave this field blank to use the latest version. Example: _2.3.4_.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8bad47c8-9537-4e8d-8ff4-0508df29ed67", + "Name": "ChocolateyCacheLocation", + "Label": "(Optional) Cache Location", + "HelpText": "Use a specific folder to cache the Chocolatey package.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e4251d60-1a9c-4ee4-aa49-bd2ecfd5d64e", + "Name": "ChocolateySource", + "Label": "(Optional) Package Source", + "HelpText": "Use a package source to install Chocolatey packages from. This can be a http or https URL, a local folder or file share. When using URL's don't forget the trailing `/` at the end. + +For example: + +* _https://chocolatey.org/api/v2/_ +* _c:\\choco-packages_ +* _\\\\business.server.local\\chocolatey-packages_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7790d99f-9fb2-4cfb-bdcb-74b4d254dba7", + "Name": "ChocolateyNoProgress", + "Label": "(Optional) Disable Download Progress", + "HelpText": "Disables the download progress percentage being displayed as this can generate a lot of output for the logs.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "e994f28e-f306-4af9-90da-0ad68ec8f30c", + "Name": "ChocolateyOtherParameters", + "Label": "(Optional) Other Parameters", + "HelpText": "Add additional Chocolatey parameters here, separated by spaces, and they will be prefixed to the end of the Chocolatey command being executed. Examples: + +* _--ignorepackagecodes_ +* _--ignorepackagecodes --requirechecksum --allow-downgrade_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2020-08-10T15:59:10.300Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "Category": "chocolatey" +} diff --git a/step-templates/chocolatey-manage-config.json.human b/step-templates/chocolatey-manage-config.json.human new file mode 100644 index 000000000..ae55a0ca9 --- /dev/null +++ b/step-templates/chocolatey-manage-config.json.human @@ -0,0 +1,120 @@ +{ + "Id": "dd45cba9-a39b-43e0-922e-da9fb7818186", + "Name": "Chocolatey - Manage Config", + "Description": "Allows managing Chocolatey config.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +$chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"Machine\") + \"\\bin\" +if(-not (Test-Path $chocolateyBin)) { + Write-Host \"Environment variable 'ChocolateyInstall' was not found in the system variables. Attempting to find it in the user variables...\" + $chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"User\") + \"\\bin\" +} + +$choco = \"$chocolateyBin\\choco.exe\" + +if (-not (Test-Path $choco)) { + throw \"Chocolatey was not found at $chocolateyBin.\" +} + +# Report the actual version here +$chocoVersion = & $choco --version +Write-Host \"Running Chocolatey version $chocoVersion\" + +# default args +$chocoArgs = @('config', $ChocolateyConfigAction, '--yes') + +# we need a source name +if ([string]::IsNullOrEmpty($ChocolateyConfigName)) { + throw \"To manage a feature, you need to provide a feature name.\" +} +else { +\t$chocoArgs += \"--name=\"\"'$ChocolateyConfigName'\"\"\" +} + +if ($ChocolateyConfigAction -eq 'set') { + if ([string]::IsNullOrEmpty($ChocolateyConfigValue)) { + throw 'To set the config, you need to provide a value.' + } + + $chocoArgs += \"--value=\"\"'$ChocolateyConfigValue'\"\"\" +} + +# finally add any other parameters +if (-not [string]::IsNullOrEmpty($ChocolateyConfigOtherParameters)) { +\t$chocoArgs += $ChocolateyConfigOtherParameters -split ' ' +} + +# execute the command line +Write-Host \"Running the command: $choco $chocoArgs\" +& $choco $chocoArgs", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "3d26fd31-9a8a-432d-add8-caee77c88235", + "Name": "ChocolateyConfigName", + "Label": "(Required) Config Name", + "HelpText": "The config name you want to manage. This is a single config name. Multiple config names must be managed in separate steps. + +Example: + +* _cacheLocation_ +* _commandExeecutionTimeoutSeconds_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5eda17f2-d5e0-4e7b-a005-f540d1854db9", + "Name": "ChocolateyConfigAction", + "Label": "(Required) Config Action", + "HelpText": "The action to perform on the config name.", + "DefaultValue": "set", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "set|Set a config +unset|Unset a config" + } + }, + { + "Id": "120540e8-0915-469f-bfc7-5ab0f8dec244", + "Name": "ChocolateyConfigValue", + "Label": "(Required / Optional)", + "HelpText": "The value of the config name you want to set. This is not required if the action is 'unset' and required if the action is 'set'.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8ecc2ac2-4566-4b98-a979-67b755971460", + "Name": "ChocolateyConfigOtherParameters", + "Label": "(Optional) Other Chocolatey Parameters", + "HelpText": "Other parameters to pass to Chocolatey. You can pass multiple parameters separated by a space. + +Examples: + +* _--debug_ +* _--debug_ _--verbose_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-13T20:03:44.800Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModfiedAt": "2020-08-13T20:03:44.800Z", + "LastModifiedBy": "pauby", + "Category": "chocolatey" +} diff --git a/step-templates/chocolatey-manage-features.json.human b/step-templates/chocolatey-manage-features.json.human new file mode 100644 index 000000000..c4174594e --- /dev/null +++ b/step-templates/chocolatey-manage-features.json.human @@ -0,0 +1,104 @@ +{ + "Id": "718f6e95-e176-4f13-9512-a3a8f1bb10a0", + "Name": "Chocolatey - Manage Features", + "Description": "Allows enabling and disabling of Chocolatey features.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +$chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"Machine\") + \"\\bin\" +if(-not (Test-Path $chocolateyBin)) { + Write-Host \"Environment variable 'ChocolateyInstall' was not found in the system variables. Attempting to find it in the user variables...\" + $chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"User\") + \"\\bin\" +} + +$choco = \"$chocolateyBin\\choco.exe\" + +if (-not (Test-Path $choco)) { + throw \"Chocolatey was not found at $chocolateyBin.\" +} + +# Report the actual version here +$chocoVersion = & $choco --version +Write-Host \"Running Chocolatey version $chocoVersion\" + +# default args +$chocoArgs = @('feature', $ChocolateyFeatureAction, '--yes') + +# we need a source name +if ([string]::IsNullOrEmpty($ChocolateyFeatureName)) { + throw \"To manage a feature, you need to provide a feature name.\" +} + +# finally add any other parameters +if (-not [string]::IsNullOrEmpty($ChocolateyFeatureOtherParameters)) { +\t$chocoArgs += $ChocolateyFeatureOtherParameters -split ' ' +} + +$featureNames = $ChocolateyFeatureName -split ' ' +ForEach ($name in $featureNames) { +\t$cmdArgs = $chocoArgs + \"--name=\"\"'$name'\"\"\" + + # execute the command line + Write-Host \"Running the command: $choco $cmdArgs\" + & $choco $cmdArgs +}", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "b0258989-3337-4e22-ab96-ba54e1a533f7", + "Name": "ChocolateyFeatureName", + "Label": "(Required) Feature Name", + "HelpText": "The feature name to manage. Multiple feature can be specified separated by a space. + +Examples: + +* _checksumFiles_ +* _autoUninstaller_ _allowGlobalConfirmation_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "31522ad7-f257-4deb-9973-7d4c7eb4fa16", + "Name": "ChocolateyFeatureAction", + "Label": "(Required) Feature Action", + "HelpText": "The action to perform on the feature name.", + "DefaultValue": "enable", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "enable|Enable the feature +disable|Disable the feature" + } + }, + { + "Id": "218c539e-fe74-408a-9b6d-0b8654ae82dd", + "Name": "ChocolateyFeatureOtherParameters", + "Label": "(Optional) Other Chocolatey Parameters", + "HelpText": "Other parameters to pass to Chocolatey. You can pass multiple parameters separated by a space. + +Examples: + +* _--debug_ +* _--debug_ _--verbose_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-13T17:03:20.000Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedAt": "2020-08-13T17:03:20.000Z", + "LastModifiedBy": "pauby", + "Category": "chocolatey" +} diff --git a/step-templates/chocolatey-manage-sources.json.human b/step-templates/chocolatey-manage-sources.json.human new file mode 100644 index 000000000..0756968db --- /dev/null +++ b/step-templates/chocolatey-manage-sources.json.human @@ -0,0 +1,277 @@ +{ + "Id": "131ba9b0-c95e-464f-a2ff-aacedbcd29a1", + "Name": "Chocolatey - Manage Sources", + "Description": "Allows managing Chocolatey Package sources.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +$chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"Machine\") + \"\\bin\" +if(-not (Test-Path $chocolateyBin)) { + Write-Host \"Environment variable 'ChocolateyInstall' was not found in the system variables. Attempting to find it in the user variables...\" + $chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"User\") + \"\\bin\" +} + +$choco = \"$chocolateyBin\\choco.exe\" + +if (-not (Test-Path $choco)) { + throw \"Chocolatey was not found at $chocolateyBin.\" +} + +# Report the actual version here +$chocoVersion = & $choco --version +Write-Host \"Running Chocolatey version $chocoVersion\" + +# You cannot use [version] with SemVer 2 versions +# this allows pre-release versions to still work by stripping everything after the '-' as we could have +# 0.10.15-beta-20200101. We are only interested in the major.minor.build version +$chocoVersion = ($chocoVersion -split '-')[0].ToString() + +# default args +$chocoArgs = @('source', $ChocolateySourceAction, '--yes') + +# we need a source name +if ([string]::IsNullOrEmpty($ChocolateySourceName)) { + throw \"To manage a source you need to provide a source name.\" +} +else { +\t$chocoArgs += \"--name=\"\"'$ChocolateySourceName'\"\"\" +} + +# we are adding a source - check all of the parameters +if ($ChocolateySourceAction -eq 'add') { +\tif ([string]::IsNullOrEmpty($ChocolateySourceLocation)) { +\t\tthrow 'To add a source you need to provide a source location.' +\t} + else { + \t$chocoArgs += \"--source=\"\"'$ChocolateySourceLocation'\"\"\" + } + + # source priority + if (-not [string]::IsNullOrEmpty($ChocolateySourcePriority)) { + \tif ([version]$chocoVersion -ge [version]'0.9.9.9') { + \t\t$chocoArgs += \"--priority=\"\"'$ChocolateySourcePriority'\"\"\" + } + else { + \tWrite-Warning 'To use a source priority you must have Chocolatey version 0.9.9.9 or later. Ignoring source priority.' + } + } + + # allow self service + if ($ChocolateySourceAllowSelfService) { + \t$edition = & $choco + \tif ($edition -like '*Business*' -and [version]$chocoVersion -ge [version]'0.10.4') { + \t$chocoArgs += '--allow-self-service' + } + else { + \tWrite-Warning 'To allow self service on a source you must have Chocolatey For Business version 0.10.4 or later. Ignoring allowing self service.' + } + } + + # allow admin only + if ($ChocolateySourceEnableAdminOnly) { + # we are not going to check for the Business Edition but the chocolatey.extension version need to check we have the chocolatey.extension installed + $licensedExtension = & $choco list chocolatey.extension --exact --limit-output --local-only | ConvertFrom-Csv -Delimiter '|' -Header 'Name', 'Version' + + # lets get the major.minor.build licensed extension version by stripping any pre-release + $licensedExtensionVersion = ($licensedExtension.Version -split '-')[0].ToString() + if ((-not [string]::IsNullOrEmpty($licensedExtensionVersion)) -and ([version]$chocoVersion -ge [version]'0.10.8' -and [version]$licensedExtensionVersion -ge [version]'1.12.2')) { + \t$chocoArgs += '--enable-admin-only' + } + else { + \tWrite-Warning 'To enable admin only on a source you must have Chocolatey For Business Licensed Extension (chocolatey.extension package) version 1.12.2 or later and Chocolatey version 0.10.8 or later. Ignoring admin only enablement.' + } +\t} + + # we need both a username and a password - if one is used without the other then throw + if (($ChocolateySourceUsername -and -not $ChocolateySourcePassword) -or ($ChocolateySourcePassword -and -not $ChocolateySourceUsername)) { + \tthrow 'If you are using an authenticated source you must provide both a username AND a password.' + } + elseif ($ChocolateySourceUsername -and $ChocolateySourcePassword) { +\t\t$chocoArgs += @(\"--user=\"\"'$ChocolateySourceUsername'\"\"\", \"--password=\"\"'$ChocolateySourcePassword'\"\"\") + } + + # check if we have a certificate path + if (-not [string]::IsNullOrEmpty($ChocolateySourceCertificatePath)) { + \tif (-not (Test-Path -Path $ChocolateySourceCertificatePath)) { + \tthrow \"The certificate at '$ChocolateySourceCertificatePath' does not exist. Please make sure the certificate exists on the target at the provided path.\" + } + + $chocoArgs += \"--cert=\"\"'$ChocolateySourceCertificatePath'\"\"\" + + # if we have a password it can only be used with a certificate path + if (-not [string]::IsNullOrEmpty($ChocolateySourceCertificatePassword)) { +\t\t\t$chocoArgs += \"--certpassword=\"\"'$ChocolateySourceCertificatePassword'\"\"\" + } + } + elseif (-not [string]::IsNullOrEmpty($ChocolateySourceCertificatePassword)) { + \tWrite-Warning 'You have provided a client certificate password but no client certificate path. Ignoring client certificate password.' + } +} + +# finally add any other parameters +if (-not [string]::IsNullOrEmpty($ChocolateySourceOtherParameters)) { +\t$chocoArgs += $ChocolateySourceOtherParameters -split ' ' +} + +# execute the command line +Write-Host \"Running the command: $choco $chocoArgs\" +& $choco $chocoArgs", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "86c495d0-0098-4af0-bf10-cde87bfbc348", + "Name": "ChocolateySourceName", + "Label": "(Required) Source Name", + "HelpText": "The name of the source to manage. Examples: + +* _production-repo_ +* _my-smb-share_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f2b13125-1328-48b5-b341-3c5a78869a8c", + "Name": "ChocolateySourceAction", + "Label": "(Required) Source Action", + "HelpText": "The action to perform on the source. + +Note that you can use the 'add' action to change a source configuration. So you may not need to remove it and add it again.", + "DefaultValue": "add", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "add|Add a source +remove|Remove a source +enable|Enable a source +disable|Disable a source" + } + }, + { + "Id": "80507f89-d52e-43d7-b857-9697e9f837fa", + "Name": "ChocolateySourceLocation", + "Label": "(Required / Optional) Source Location", + "HelpText": "This can be a folder / file share or an http / https location. If it is a URL, it will be a location you can go to in a browser and it returns OData with something that says 'Packages' in the browser, similar to what you see when you go to https://chocolatey.org/api/v2/. + +**Required with 'add' action. Optional with all other actions.** + +Examples: + +* _c:\\local-packages_ +* _\\\\\\\\my-server\\packages_ +* _https://internalserver.local/packages/_ (note the trailing slash)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7580abfd-89ae-4bc0-af86-cbe58b9d8074", + "Name": "ChocolateySourcePriority", + "Label": "(Optional) Source Priority", + "HelpText": "The priority order of this source as compared to other sources, lower is better. Defaults to 0 (no priority). + +All priorities above 0 will be evaluated first, then zero-based values will be evaluated in config file order. + +_Available in Chocolatey version 0.9.9.9 and later._", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8517408c-5812-448b-986a-7fabc970039f", + "Name": "ChocolateySourceAllowSelfService", + "Label": "(Optional) Allow Self Service Source", + "HelpText": "Should this source be allowed to be used with self-service? Defaults to false. + +_Available in Chocolatey For Business version 0.10.4 and later. Requires feature 'useBackgroundServiceWithSelfServiceSourcesOnly' to be enabled._", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "6e12a4e6-eae4-4ce9-b759-76f4a9b4b0c5", + "Name": "ChocolateySourceEnableAdminOnly", + "Label": "(Optional) Enable Admin Only Source", + "HelpText": "Should this source be visible to non-administrators? + +_Requires Chocolatey For Business Extension version 1.12.2 or later and Chocolatey version 0.10.8 or later._", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "b3e074ba-ba3a-43dc-8e23-7e92647cb246", + "Name": "ChocolateySourceUsername", + "Label": "(Optional) Source Authentication Username", + "HelpText": "Username to authenticate to the source with.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3af96a08-19ba-43a3-893d-6afe929eba0a", + "Name": "ChocolateySourcePassword", + "Label": "(Optional) Source Authentication Password", + "HelpText": "Password to authenticate to the source with.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "53457094-5867-4bde-95c0-7f7100e7b561", + "Name": "ChocolateySourceCertificatePath", + "Label": "(Optional) Source Certificate Path", + "HelpText": "Path to the PFX client certificate used for an x509 authenticated source. + +_NOTE: The certificate path must exist and be local to the computer this step is running on. So you will need to add additional steps to ensure the certificate is available at the path._ + +_Available in Chocolatey from version 0.9.10._", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8e431f4f-8c24-4aa0-b7ba-2045d4efab3a", + "Name": "ChocolateySourceCertificatePassword", + "Label": "(Optional) Chocolatey Certificate Password", + "HelpText": "The client certificate's password to the source. Can only be used if a client certificate path has been provided. + +_Available in Chocolatey version 0.9.10 or later._", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "67ab1c30-a3e6-4ca8-a3a1-802702e34019", + "Name": "ChocolateySourceOtherParameters", + "Label": "(Optional) Other Parameters", + "HelpText": "Add additional Chocolatey parameters here, separated by spaces, and they will be prefixed to the end of the Chocolatey command being executed. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-13T13:35:57.400Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2020-08-13T13:35:57.400Z", + "LastModifiedBy": "pauby", + "Category": "chocolatey" +} diff --git a/step-templates/chocolatey-pin-package.json.human b/step-templates/chocolatey-pin-package.json.human new file mode 100644 index 000000000..7975903f9 --- /dev/null +++ b/step-templates/chocolatey-pin-package.json.human @@ -0,0 +1,123 @@ +{ + "Id": "81d9b602-7969-4cb2-a9c0-b5c961937db4", + "Name": "Chocolatey - Pin Package", + "Description": "Allows pinning versions of packages using the Chocolatey package manager.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +$chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"Machine\") + \"\\bin\" +if(-not (Test-Path $chocolateyBin)) { + Write-Output \"Environment variable 'ChocolateyInstall' was not found in the system variables. Attempting to find it in the user variables...\" + $chocolateyBin = [Environment]::GetEnvironmentVariable(\"ChocolateyInstall\", \"User\") + \"\\bin\" +} + +$choco = \"$chocolateyBin\\choco.exe\" + +if (-not (Test-Path $choco)) { + throw \"Chocolatey was not found at $chocolateyBin.\" +} + +# check we have required parameters +if (-not $ChocolateyPackagePinId) { + throw \"Please specify the ID of an application package to install.\" +} + +$chocoVersion = & $choco --version +Write-Output \"Running Chocolatey version $chocoVersion\" + +# base arguments to use with choco.exe +$chocoBaseArgs = @('pin', $ChocolateyPackagePinAction) + +# this parameter only works in Chocolatey licensed editions +if ($ChocolateyPackagePinReason) { + \t# determine if this is a licensed edition +\t$edition = & $choco + if ($edition -like '*Business*' -and [version]$chocoVersion -ge [version]'1.12.2') { + \tWrite-Output \"Using reason '$ChocolateyPackagePinReason' when pinning packages.\" + \t$chocoBaseArgs += \"--reason=\"\"'$ChocolateyPackagePinReason'\"\"\" + } +\telse { + \tWrite-Output \"Using a reason for a package pin only works with Chocolatey For Business licensed editions. Ignoring the pin reason '$ChocolateyPackagePinReason'.\" +\t} +} + +if ($ChocolateyPackagePinVersion) { +\t$chocoBaseArgs += \"--version=$ChocolateyPackagePinVersion\" +} + +$chocoPackages = $ChocolateyPackagePinId -split ' ' +ForEach ($package in $chocoPackages) { +\tWrite-Output \"Pinning Chocolatey package $package.\" + $chocoArgs = $chocoBaseArgs + @(\"--name=\"\"'$package'\"\"\") + + # execute the command line +\tWrite-Output \"Running the command: $choco $chocoArgs\" +\t& $choco $chocoArgs +}", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "2194aed6-a753-4e95-8067-b98a2a1a54bb", + "Name": "ChocolateyPackagePinId", + "Label": "(Required) Package Name", + "HelpText": "The name of the Chocolatey package to install. Install multiple packages by separating them with a space. + +Examples: + +* _git_ +* _git_ _vscode_ _notepadplusplus_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "65b0dd7d-7643-4d55-8a63-d8506b674688", + "Name": "ChocolateyPackagePinVersion", + "Label": "(Optional) Package Version", + "HelpText": "If a specific version of the Chocolatey package is required enter it here. If you specify a version this will apply to all packages in 'Package Name'. Otherwise, leave this field blank to use the latest version. Example: _2.3.4_.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "32722882-c1cc-42fe-b3c9-4405218795ba", + "Name": "ChocolateyPackagePinAction", + "Label": "(Required) Pin Action", + "HelpText": "The Chocolatey pin action to take place. You can add a pin or remove an existing pin.", + "DefaultValue": "add", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "add|Add a package pin +remove|Remove an existing package pin" + } + }, + { + "Id": "00ef4776-72bc-4a1a-8a73-ee2a5c86a810", + "Name": "ChocolateyPackagePinReason", + "Label": "(Optional) Pin Reason", + "HelpText": "The reason for the package being pinned. + +_NOTE: This only works with Chocolatey For Business editions 1.12.2+_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-10T17:30:40.400Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2020-08-10T17:30:40.400Z", + "LastModifiedBy": "pauby", + "Category": "chocolatey" +} diff --git a/step-templates/clickonce-from-deployed-package.json.human b/step-templates/clickonce-from-deployed-package.json.human new file mode 100644 index 000000000..ecafbb52b --- /dev/null +++ b/step-templates/clickonce-from-deployed-package.json.human @@ -0,0 +1,199 @@ +{ + "Id": "1b7909be-4870-4f81-8957-8357b9342c0f", + "Name": "ClickOnce - Create from deployed package", + "Description": "Create binaries manifest & CO application and sign them with given code sign certificate ... using mage.exe.", + "ActionType": "Octopus.Script", + "Version": 28, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "Write-Host \"Building clickonce application ...\"\r +\r +function Validate-Parameter([REF]$f, $name, $value) {\r + if (!$value) {\r + throw ('Missing required value for parameter ''{0}''.' -f $name)\r + }\r + \r + $f.Value = $value\r + Write-Host \"Parameters [$name] has been initialized with : [$value]\"\r +}\r +\r +### Parameters\r +$deployStepName = $null\r +$appName = $null\r +$exeFileName = $null\r +$destinationPath = $null\r +$certFilePath = $null\r +$certPassword = $null\r +$publisher = $null\r +$mageExe = $null\r +$version = $null\r +$binariesFolderPath = $null\r +$coAppName = $null\r +$iconFile = $null\r +\r +Validate-Parameter ([REF]$deployStepName) 'Deploy step name to read binaries from' $OctopusParameters['DeployStepName']\r +Validate-Parameter ([REF]$appName) 'Project name' $OctopusParameters['Octopus.Project.Name']\r +Validate-Parameter ([REF]$coAppName) 'Application display name' $OctopusParameters['DisplayName']\r +\r +Validate-Parameter ([REF]$exeFileName) 'Executable file name' $OctopusParameters['ExeFileName']\r +Validate-Parameter ([REF]$destinationPath) 'Path to the directory where to deploy the ClickOnce package' $OctopusParameters['DestinationPath']\r +Validate-Parameter ([REF]$certFilePath) 'Path to the certification file' $OctopusParameters['SignCertFilePath']\r +Validate-Parameter ([REF]$certPassword) 'Password of the certification file' $OctopusParameters['SignCertPassword']\r +Validate-Parameter ([REF]$publisher) 'Publisher name' $OctopusParameters['Publisher']\r +Validate-Parameter ([REF]$mageExe) 'Path to the mage.exe' $OctopusParameters['MageExePath']\r +Validate-Parameter ([REF]$iconFile) 'Icon file' $OctopusParameters['IconFile']\r +\r +### end of parameters\r +\r +Validate-Parameter ([REF]$version) 'Version number (from release)' $OctopusParameters['Octopus.Release.Number']\r +\r +$binariesFolderParameter = -join(\"Octopus.Action[\",$deployStepName,\"].Output.Package.InstallationDirectoryPath\")\r +Write-Host \"Trying to get Installation folder parameter value for : [$binariesFolderParameter]\"\r +\r +$binariesFolderPath = $OctopusParameters[$binariesFolderParameter]\r +if(!$binariesFolderPath){\r + throw ('Unable to retrieve binaries path from previous step execution for step with name ''{0}''.' -f $deployStepName)\r +}\r +\r +$appVersionAndNumber = -join($appName, \"_\", $version)\r +$packageDestinationSubDirectory = -join (\"Application_Files/\", $appVersionAndNumber)\r +$packageDestinationPath = -join ($destinationPath, \"/\", $packageDestinationSubDirectory) \r +$appManifestRelativePath = -join (\"Application_Files/\",$appVersionAndNumber, \"/\", $exeFileName, \".manifest\")\r +$appManifestFilePath = -join ($binariesFolderPath, \"/\", $exeFileName, \".manifest\")\r +\r +$coAppFilePath = -join($binariesFolderPath, \"\\\", $appName, \".application\")\r +$coAppFilePathServer = -join($destinationPath, \"\\\", $appName, \".application\")\r +\r +### Create Application manifest\r +Write-Host \"Creating application manifest at \"$appManifestFilePath\r +& $mageExe -New Application -t \"$appManifestFilePath\" -n \"$coAppName\" -v $version -p msil -fd \"$binariesFolderPath\" -tr FullTrust -a sha256RSA -if $iconFile\r +Write-Host \"Signing application manifest ...\"\r +& $mageExe -Sign \"$appManifestFilePath\" -cf $certFilePath -pwd $certPassword\r +\r +### Create Deployment application\r +Write-Host \"Creating CO application [$coAppName] at \"$coAppFilePath\r +& $mageExe -New Deployment -t \"$coAppFilePath\" -n \"$coAppName\" -v $version -p msil -appm $appManifestFilePath -ip true -i true -um true -pub $publisher -pu \"$coAppFilePathServer\" -appc $appManifestRelativePath -a sha256RSA\r +\r +Write-Host \"Updating minimum version to force auto-update\"\r +& $mageExe -Update $coAppFilePath -mv $version -pub $publisher -a sha256RSA\r +\r +Write-Host \"Changing expiration max age => before application startup (hacking xml) of \"$coAppFilePath\r +$content = Get-Content $coAppFilePath\r +$content -replace \"\", \"\" | set-content $coAppFilePath\r +\r +Write-Host \"Signing CO application [$coAppName] ...\"\r +& $mageExe -Sign \"$coAppFilePath\" -cf $certFilePath -pwd $certPassword\r +\r +\r +Write-Host \"Copying binaries from \"$binariesFolderPath\r +Write-Host \"to destination \"$packageDestinationPath\r +\r +### Remove any existing files from the package destination folder\r +Remove-Item $packageDestinationPath -Recurse -ErrorAction SilentlyContinue\r +\r +### Ensure target directory exists in order not to fail the copy\r +New-Item $packageDestinationPath -ItemType directory > $null\r +\r +### Copy binaries to destination folder\r +Copy-Item $binariesFolderPath\"/*\" $packageDestinationPath -recurse -Force > $null\r +Copy-Item $coAppFilePath $destinationPath -Force > $null\r +\r +Write-Host \"Building clickonce application script completed.\"\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "DeployStepName", + "Label": "Deploy step to read binaries from", + "HelpText": "Name of the previous step used to deploy binaries that will be packed in CO package", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Name": "DestinationPath", + "Label": "Path to the directory where to deploy the ClickOnce package", + "HelpText": "Path to the target directory for the ClickOnce package. This will contain the created *.application file and the \"Application Files\" folder", + "DefaultValue": "\\\\MyServer\\MyClickOnceRepo\\#{Octopus.Project.Name}\\#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ExeFileName", + "Label": "Name of the target executable file", + "HelpText": "Name of the executable file of the application to pack", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Publisher", + "Label": "Publisher name", + "HelpText": "Publisher name to use when creating the CO package", + "DefaultValue": "MyCompany", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SignCertFilePath", + "Label": "Path to the certification file", + "HelpText": "Path to the certification pfx file", + "DefaultValue": "\\\\MyServer\\Auth\\cert\\my-cert.pfx", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SignCertPassword", + "Label": "Password of the certification file", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "MageExePath", + "Label": "Path to mage.exe", + "HelpText": "Path to mage.exe which is used to update manifest and .application files and sign them.", + "DefaultValue": "\\\\MyServer\\Tools\\Octopus\\Tentacles\\mage.exe", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "IconFile", + "Label": "Icon filename", + "HelpText": "Name of icon file in the package, eg. ApplicationIcon.ico", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DisplayName", + "Label": "Application display name", + "HelpText": "Name which appears in the Start Menu", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2017-02-28T06:53:27.970+00:00", + "LastModifiedBy": "Grendizr", + "$Meta": { + "ExportedAt": "2016-09-15T06:53:27.970+00:00", + "OctopusVersion": "3.4.0", + "Type": "ActionTemplate" + }, + "Category": "clickOnce" +} diff --git a/step-templates/clickonce-re-sign.json.human b/step-templates/clickonce-re-sign.json.human new file mode 100644 index 000000000..90e79e3f4 --- /dev/null +++ b/step-templates/clickonce-re-sign.json.human @@ -0,0 +1,71 @@ +{ + "Id": "a9e5843e-d382-44fd-84dd-d999dab54993", + "Name": "ClickOnceRe-Sign", + "Description": "This Template will help to re-sign the .application and manifest files. Finally add .deploy extenstion to the files", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$xml = [xml](Get-Content \"$baseDeployPath\\$AppName.application\")\r +$manifestpath = $xml.assembly.dependency.dependentAssembly.codebase\r +$ApplicationWithVersion = $manifestpath.Split('\\\\')[1]\r +\r +#Manifest Resign\r +& \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v8.0A\\bin\\NETFX 4.0 Tools\\mage.exe\" -Update \"$baseDeployPath\\$manifestpath\" -FromDirectory \"$baseDeployPath\\Application Files\\$ApplicationWithVersion\"\r +& \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v8.0A\\bin\\NETFX 4.0 Tools\\mage.exe\" -Sign \"$baseDeployPath\\$manifestpath\" -CertFile $signcertpath -Password $signCertPass\r +\r +#Application Resign\r +& \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v7.0A\\Bin\\mage.exe\" -Update \"$baseDeployPath\\$AppName.application\" -AppManifest \"$baseDeployPath\\$manifestpath\"\r +& \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v7.0A\\Bin\\mage.exe\" -Sign \"$baseDeployPath\\$AppName.application\" -CertFile $signcertpath -Password $signCertPass\r +\r +#Rename files back to the .deploy extension, skipping the files that shouldn't be renamed\r +Get-ChildItem -Path \"$baseDeployPath\\Application Files\\*\" -Recurse | Where-Object {!$_.PSIsContainer -and $_.Name -notlike \"*.manifest\" -and $_.Name -notlike \"*.vsto\"} | Rename-Item -NewName {$_.Name + \".deploy\"} " + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "signcertpath", + "Label": "Signing Certificate Path", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "signCertPass", + "Label": "Signing Certificate Password", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "baseDeployPath", + "Label": "Deployment Path", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AppName", + "Label": "Application Name", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-10-20T12:44:19.041+00:00", + "LastModifiedBy": "mani0070", + "$Meta": { + "ExportedAt": "2015-10-20T20:26:27.693+00:00", + "OctopusVersion": "3.1.0", + "Type": "ActionTemplate" + }, + "Category": "clickOnce" +} diff --git a/step-templates/clickonce-sign-file.json.human b/step-templates/clickonce-sign-file.json.human new file mode 100644 index 000000000..8b68081a1 --- /dev/null +++ b/step-templates/clickonce-sign-file.json.human @@ -0,0 +1,80 @@ +{ + "Id": "c2d2cb47-b737-49a9-9063-41e84db53097", + "Name": "ClickOnce - Sign file", + "Description": "Sign file with given code sign certificate using mage.exe.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$find = Get-ChildItem \"$PackagePath\\$SignFileFilter\" +$PathToFile = $find.FullName + +$splittedParams = $AdvencedMageParameters.Split(\" \") +& \"$MagePath\\mage.exe\" -Sign \"$PathToFile\" -CertFile $SignCert -Password $SignCertPass $splittedParams" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "SignFileFilter", + "Label": "Filter to find file to sign", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SignCert", + "Label": "Path to the certification file", + "HelpText": "Path to the certification pfx file", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SignCertPass", + "Label": "Password of the certification file", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AdvencedMageParameters", + "Label": "Addition parameters for mage.exe", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "PackagePath", + "Label": "Path to the root directory of ClickOnce package", + "HelpText": "Path to the root drectory of ClickOnce package. This is where you can find the setup.exe, and *.application files and the \"Application Files\" folder", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MagePath", + "Label": "Path to mage.exe", + "HelpText": "Path to mage.exe which is used to update manifest and .application files and sign them.", + "DefaultValue": "c:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.6.1 Tools\\", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "Kemyke", + "$Meta": { + "ExportedAt": "2016-04-05T14:42:48.861+00:00", + "OctopusVersion": "3.3.0-beta0001", + "Type": "ActionTemplate" + }, + "Category": "clickOnce" +} diff --git a/step-templates/clickonce-update-application-file.json.human b/step-templates/clickonce-update-application-file.json.human new file mode 100644 index 000000000..193c82e78 --- /dev/null +++ b/step-templates/clickonce-update-application-file.json.human @@ -0,0 +1,65 @@ +{ + "Id": "0185c15c-3bde-446b-a5cf-10f475dc0008", + "Name": "ClickOnce - Update .application file", + "Description": "Update .application file after updating the manifest file.", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$xml = [xml](Get-Content \"$PackagePath\\$AppName.application\") +$manifestpath = $xml.assembly.dependency.dependentAssembly.codebase + +$splittedParams = $AdvencedMageParameters.Split(\" \") +cd \"$PackagePath\" +& \"$MagePath\\mage.exe\" -Update \".\\$AppName.application\" -AppManifest \".\\$manifestpath\" $splittedParams + +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "PackagePath", + "Label": "Path to the root directory of ClickOnce package", + "HelpText": "Path to the root drectory of ClickOnce package. This is where you can find the setup.exe, and *.application files and the \"Application Files\" folder", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AppName", + "Label": "Name of the ClickOnce application", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AdvencedMageParameters", + "Label": "Addition parameters for mage.exe", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MagePath", + "Label": "Path to mage.exe", + "HelpText": "Path to mage.exe which is used to update manifest and .application files and sign them.", + "DefaultValue": "c:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.6.1 Tools\\", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "Kemyke", + "$Meta": { + "ExportedAt": "2016-04-05T14:43:35.076+00:00", + "OctopusVersion": "3.3.0-beta0001", + "Type": "ActionTemplate" + }, + "Category": "clickOnce" +} diff --git a/step-templates/clickonce-update-manifest-file.json.human b/step-templates/clickonce-update-manifest-file.json.human new file mode 100644 index 000000000..bed78aa2e --- /dev/null +++ b/step-templates/clickonce-update-manifest-file.json.human @@ -0,0 +1,63 @@ +{ + "Id": "f52ace18-b8ee-4235-9c6f-231f2ec477a6", + "Name": "ClickOnce - Update manifest file", + "Description": "Update manifest file after changing the configuration files of the ClickOnce application.", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$xml = [xml](Get-Content \"$PackagePath\\$AppName.application\") +$manifestpath = $xml.assembly.dependency.dependentAssembly.codebase +$ApplicationWithVersion = $manifestpath.Split('\\\\')[1] + +$splittedParams = $AdvencedMageParameters.Split(\" \") +& \"$MagePath\\mage.exe\" -Update \"$PackagePath\\$manifestpath\" -FromDirectory \"$PackagePath\\Application Files\\$ApplicationWithVersion\" $splittedParams" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "PackagePath", + "Label": "Path to the root directory of ClickOnce package", + "HelpText": "Path to the root drectory of ClickOnce package. This is where you can find the setup.exe, and *.application files and the \"Application Files\" folder", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AppName", + "Label": "Name of the ClickOnce application", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AdvencedMageParameters", + "Label": "Addition parameters for mage.exe", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MagePath", + "Label": "Path to mage.exe", + "HelpText": "Path to mage.exe which is used to update manifest and .application files and sign them.", + "DefaultValue": "c:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.6.1 Tools\\", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "Kemyke", + "$Meta": { + "ExportedAt": "2016-04-05T14:46:10.321+00:00", + "OctopusVersion": "3.3.0-beta0001", + "Type": "ActionTemplate" + }, + "Category": "clickOnce" +} diff --git a/step-templates/clone-tenant.json.human b/step-templates/clone-tenant.json.human new file mode 100644 index 000000000..43a4d3080 --- /dev/null +++ b/step-templates/clone-tenant.json.human @@ -0,0 +1,201 @@ +{ + "Id": "3b0f8df0-93b8-44eb-86dd-264d1283ae70", + "Name": "Clone Tenant", + "Description": "Clone an Octopus [tenant](https://octopus.com/docs/deployment-patterns/multi-tenant-deployments). The project connections and tenant tags will be cloned and the non-sensitive variables can optionally be cloned.", + "ActionType": "Octopus.Script", + "Version": 4, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$securityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 +[Net.ServicePointManager]::SecurityProtocol = $securityProtocol + +$octopusBaseUrl = $CloneTenantStep_OctopusUrl.Trim('/') +$apiKey = $CloneTenantStep_ApiKey +$tenantToClone = $CloneTenantStep_TenantIdToClone +$tenantName = $CloneTenantStep_TenantName +$cloneVariables = $CloneTenantStep_CloneVariables +$cloneTags = $CloneTenantStep_CloneTags +$spaceId = $CloneTenantStep_SpaceId + +$ErrorActionPreference = 'Stop' + +if ([string]::IsNullOrWhiteSpace($octopusBaseUrl)) { + throw \"The step parameter 'Octopus Base Url' was not found. This step requires the Octopus Server URL to function, please provide one and try again.\" +} + +if ([string]::IsNullOrWhiteSpace($apiKey)) { + throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\" +} + +if ([string]::IsNullOrWhiteSpace($tenantToClone)) { + throw \"The step parameter 'Id of Tenant to Clone' was not found. Please provide one and try again.\" +} + +if ([string]::IsNullOrWhiteSpace($tenantName)) { + throw \"The step parameter 'New Tenant Name' was not found. Please provide one and try again.\" +} + +function Invoke-OctopusApi { + param( + [Parameter(Position = 0, Mandatory)]$Uri, + [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get', + $Body + ) + + $uriParts = @($octopusBaseUrl, $Uri.TrimStart('/')) + $uri = ($uriParts -join '/') + + Write-Verbose \"Uri: $uri\" + + $requestParameters = @{ + Uri = $uri + Method = $Method + Headers = @{ \"X-Octopus-ApiKey\" = $apiKey } + UseBasicParsing = $true + } + + if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) } + + return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json +} + +function Test-SpacesApi { +\tWrite-Verbose \"Checking API compatibility\"; +\t$rootDocument = Invoke-OctopusApi 'api/'; + if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) { + \tWrite-Verbose \"Spaces API found\" + \treturn $true; + } + Write-Verbose \"Pre-spaces API found\" + return $false; +} + +if([string]::IsNullOrWhiteSpace($spaceId)) { +\tif(Test-SpacesApi) { + \t$spaceId = $OctopusParameters['Octopus.Space.Id']; + \tif([string]::IsNullOrWhiteSpace($spaceId)) { + \tthrow \"This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which isn't expected - please reach out to our support team at https://help.octopus.com if you encounter this error or try providing the Space Id parameter.\"; + \t} +\t} +} + +$apiPrefix = \"api/\" +$tenantUrlBase = @($octopusBaseUrl, 'app#') + +if ($spaceId) { +\t$apiPrefix += $spaceId + $tenantUrlBase += $spaceId +} + +Write-Host \"Fetching source tenant\" +$tenant = Invoke-OctopusApi \"$apiPrefix/tenants/$tenantToClone\" + +$sourceTenantId = $tenant.Id +$sourceTenantName = $tenant.Name +$tenant.Id = $null +$tenant.Name = $tenantName + +if ($cloneTags -ne $true) { +\tWrite-Host \"Clearing tenant tags\" + $tenant.TenantTags = @() +} + +Write-Host \"Creating new tenant\" +$newTenant = Invoke-OctopusApi \"$apiPrefix/tenants\" -Method Post -Body $tenant + +if ($cloneVariables -eq $true) { +\tWrite-Host \"Cloning variables\" + $variables = Invoke-OctopusApi $tenant.Links.Variables + $variables.TenantId = $newTenant.Id + $variables.TenantName = $tenantName + + Invoke-OctopusApi $newTenant.Links.Variables -Method Put -Body $variables | Out-Null +} + +$tenantUrl = ($tenantUrlBase + \"tenants\" + $newTenant.Id + \"overview\") -join '/' +$sourceTenantUrl = ($tenantUrlBase + \"tenants\" + $sourceTenantId + \"overview\") -join '/' + +Write-Highlight \"New tenant [$tenantName]($tenantUrl) has been cloned from [$sourceTenantName]($sourceTenantUrl)\"" + }, + "Parameters": [ + { + "Id": "cbedd129-210e-4bab-a446-3f89192653c7", + "Name": "CloneTenantStep_OctopusUrl", + "Label": "Octopus URL", + "HelpText": "The URL of the Octopus Server to clone the tenant to.", + "DefaultValue": "#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ea2614f0-bd41-4011-b263-7d2b12af5977", + "Name": "CloneTenantStep_ApiKey", + "Label": "Octopus API Key", + "HelpText": "The Octopus API Key to use for the API requests", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "b29e27fb-362e-45e2-a244-6d590875fb68", + "Name": "CloneTenantStep_TenantIdToClone", + "Label": "Id of Tenant to Clone", + "HelpText": "The Id of the tenant to clone. This will be in the format of *Tenants-1*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b9b3a6c7-6c83-4a79-9a94-febc6c5818d3", + "Name": "CloneTenantStep_TenantName", + "Label": "New Tenant Name", + "HelpText": "The name of the tenant to create. *Note this must be unique*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "be821ad4-b195-4b11-81c2-f432ff05b86d", + "Name": "CloneTenantStep_CloneVariables", + "Label": "Clone Variables?", + "HelpText": "Flag indicating whether the source tenant's variables should be cloned to the new tenant. *Note this does not copy sensitive variables*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "831db28c-edc2-496b-b082-e6de827ca5df", + "Name": "CloneTenantStep_CloneTags", + "Label": "Clone Tags?", + "HelpText": "Flag indicating whether the source tenant's tags should be cloned.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "f9320a59-9752-43a5-b46a-a4d1486cfced", + "Name": "CloneTenantStep_SpaceId", + "Label": "Space Id", + "HelpText": "The Id of the Space used to clone the tenant. **Leave blank if you are using an Octopus version earlier than 2019.1 or if you wish to use the Octopus.Space.Id variable value.**", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-08-23T12:40:10.975Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "Category": "octopus", + "LastModifiedBy": "benjimac93" +} diff --git a/step-templates/com-component-register-unregister.json.human b/step-templates/com-component-register-unregister.json.human new file mode 100644 index 000000000..710c08b1e --- /dev/null +++ b/step-templates/com-component-register-unregister.json.human @@ -0,0 +1,149 @@ +{ + "Id": "cf8634b6-313f-4435-bae6-88520c58d81d", + "Name": "Com Component - Register and Unregister using Regsvr32.exe", + "Description": "Uses regsvr32.exe to register com components", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\r +param(\r + [string]$DllFilePaths,\r + [string]$Uninstall\r +)\r +\r +$ErrorActionPreference = \"Stop\" \r +\r +function Get-Param($Name, [switch]$Required, $Default) {\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + $variable = Get-Variable $Name -EA SilentlyContinue \r + if ($variable -ne $null) {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null) {\r + if ($Required) {\r + throw \"Missing parameter value $Name\"\r + } else {\r + $result = $Default\r + }\r + }\r +\r + return $result\r +}\r +\r +& {\r + param(\r + [string]$DllFilePaths,\r + [string]$Uninstall\r + ) \r +\r + $isUninstall = $($Uninstall.ToLower() -eq 'true')\r +\r + Write-Host \"COM Component - Register\"\r + Write-Host \"DllFilePaths: $DllFilePaths\"\r +\r + $DllFilePaths.split(\";\") | ForEach {\r + $dllFilePath = $_.Trim();\r + Write-Host $dllFilePath\r + \r + if($dllFilePath.Length -lt 1){\r + break;\r + }\r + \r + Write-Host \"Attempting to register $dllFilePath\"\r +\r + if(!(Test-Path \"$dllFilePath\"))\r + {\r + Write-Host \"FILE NOT FOUND $dllFilePath.\" -ForegroundColor Yellow;\r + return;\r + }\r +\r + Write-Host \"Attempting to register $dllFilePath\"\r + \r + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\r +\r + $cmd = \"$env:windir\\System32\\regsvr32.exe\"\r +\r + Write-Host \"Registering with: $env:windir\\System32\\regsvr32.exe\"\r +\r + $pinfo.FileName = \"$cmd\"\r +\r + $pinfo.RedirectStandardError = $true\r + $pinfo.RedirectStandardOutput = $true\r + $pinfo.UseShellExecute = $false\r + \r + if($isUninstall){\r + $args = \"/u\"\r + }\r + $args = \"$args /s `\"$dllFilePath`\"\"\r +\r + $pinfo.Arguments = $args\r + \r + $p = New-Object System.Diagnostics.Process\r +\r + $p.StartInfo = $pinfo\r +\r + Write-Host \"Command:\"\r + Write-Host \"$cmd $args\"\r +\r + if ($p.Start())\r + {\r + Write-Host $p.StandardOutput.ReadToEnd().ToString()\r +\r + if($p.ExitCode -ne 0)\r + {\r + \r + Write-Host \"FAILED $($p.ExitCode) - Register\" -ForegroundColor Red \r + Write-Host $p.StandardError.ReadToEnd() -ForegroundColor Red\r + \r + throw $p.StandardError.ReadToEnd()\r + }\r + \r + Write-Host \"SUCCESS- Register\" -ForegroundColor Green \r + }\r +\r + \r + }\r +\r + } `\r + (Get-Param 'DllFilePaths' -Required) `\r + (Get-Param 'Uninstall' -Required)", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "DllFilePaths", + "Label": "DllFilePaths", + "HelpText": "List of dlls to be registered separated by a ; and can appear on separate lines", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "Uninstall", + "Label": "Un-register", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2015-06-29T20:18:21.590+00:00", + "LastModifiedBy": "jbennett", + "$Meta": { + "ExportedAt": "2015-06-30T18:55:08.050+00:00", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/configuration-encrypt-app-config-section.json.human b/step-templates/configuration-encrypt-app-config-section.json.human new file mode 100644 index 000000000..7cf1fdc4d --- /dev/null +++ b/step-templates/configuration-encrypt-app-config-section.json.human @@ -0,0 +1,105 @@ +{ + "Id": "d80a7d9a-8c7b-4aa6-934e-0929bce606fe", + "Name": "Configuration - Encrypt App.config Section", + "Description": "Encrypts configuration sections for the specified executable.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\" +function Get-Parameter($Name, $Default, [switch]$Required) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +function HandleError($message) { +\tif (!$whatIf) { +\t\tthrow $message +\t} else { +\t\tWrite-Host $message -Foreground Yellow +\t} +} + +$appPath = Get-Parameter \"ExecutablePath\" -Required +$sectionsToEncrypt = (Get-Parameter \"SectionsToEncrypt\" -Required) -split ',' | where {$_} | %{$_.Trim()} +$provider = Get-Parameter \"Provider\" \"DataProtectionConfigurationProvider\" + +Write-Host \"Configuration - Encrypt .config\" +Write-Host \"ExecutablePath: $appPath\" +Write-Host \"SectionToEncrypt: $sectionName\" +Write-Host \"Provider: $provider\" + +if (!(Test-Path $appPath)) { + HandleError \"The directory $appPath must exist\" +} + +$configurationAssembly = \"System.Configuration, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a\" +[void] [Reflection.Assembly]::Load($configurationAssembly) + +$configuration = [System.Configuration.ConfigurationManager]::OpenExeConfiguration($appPath) + +foreach ($sectionToEncrypt in $sectionsToEncrypt){ +\t$section = $configuration.GetSection($sectionToEncrypt) + + if (-not $section.SectionInformation.IsProtected) + { + $section.SectionInformation.ProtectSection($provider); + $section.SectionInformation.ForceSave = [System.Boolean]::True; + } +} + +$configuration.Save([System.Configuration.ConfigurationSaveMode]::Modified);", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ExecutablePath", + "Label": "Executable path", + "HelpText": "The path to the executable that has a corresponding `[Executable].exe.config` file. + +You can get the InstallationDirectoryPath like so `#{Octopus.Action[StepName].Output.Package.InstallationDirectoryPath}`", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SectionsToEncrypt", + "Label": "Sections to encrypt", + "HelpText": "The name of the section in the App config to encrypt e.g. `appSettings`, `connectionStrings` etc. For multiple sections, separate with a comma (,)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Provider", + "Label": "Provider Name", + "HelpText": "The provider to use for encryption", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "kp-tseng", + "$Meta": { + "ExportedAt": "2016-12-13T07:19:21.741Z", + "OctopusVersion": "3.7.2", + "Type": "ActionTemplate" + }, + "Category": "encrypt" +} diff --git a/step-templates/configuration-encrypt-app-or-web-config-section.json.human b/step-templates/configuration-encrypt-app-or-web-config-section.json.human new file mode 100644 index 000000000..5c3424696 --- /dev/null +++ b/step-templates/configuration-encrypt-app-or-web-config-section.json.human @@ -0,0 +1,207 @@ +{ + "Id": "c79b5e6b-88ac-47d5-8678-99e8ab2a1cd9", + "Name": "Configuration - Encrypt App or Web Config Section", + "Description": "Encrypts a configuration section for the specified executable.", + "ActionType": "Octopus.Script", + "Version": 17, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\" +function Get-Parameter($Name, $Default, [switch]$Required) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + Write-Verbose \"Get-Parameter for '$($Name)' [value='$($result)' default='$($Default)']\" + + return $result +} + +function HandleError($message) { +\tif (!$whatIf) { +\t\tthrow $message +\t} else { +\t\tWrite-Host $message -Foreground Yellow +\t} +} + +function Invoke-EncryptAppConfigFile() { + + if (!(Test-Path $appPath)) { + HandleError \"The directory $appPath must exist\" + } + + $configurationAssembly = \"System.Configuration, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a\" + [void] [Reflection.Assembly]::Load($configurationAssembly) + $configuration = [System.Configuration.ConfigurationManager]::OpenExeConfiguration($appPath) + + Invoke-ProtectSections $configuration +} + +function Invoke-EncryptWebConfigFile() { + Import-module WebAdministration + +\t$IISPath = \"IIS:\\Sites\\$($webSiteName)$($appPath)\\\" + + if (Test-Path $IISPath) { + Write-Verbose \"$webSiteName web site exists.\" + + $configurationAssembly = \"System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a\" + [void] [Reflection.Assembly]::Load($configurationAssembly) + $configuration = [System.Web.Configuration.WebConfigurationManager]::OpenWebConfiguration($appPath, $webSiteName) + + Invoke-ProtectSections $configuration + } + else { + HandleError \"$webSiteName web site doesn't exists. Please check if the web site is installed.\" + } +} + +function Invoke-ProtectSections($configuration) { + + $saveConfigFile = $false + + foreach ($sectionName in $sections) { + $sectionName = $sectionName.Trim() # compatible with Powershell 2.0 + $section = $configuration.GetSection($sectionName) + + if ($section) { + if (-not $section.SectionInformation.IsProtected) + { + Write-Verbose \"Encrypting $($section.SectionInformation.SectionName) section.\" + $section.SectionInformation.ProtectSection($provider); + $section.SectionInformation.ForceSave = [System.Boolean]::True; + $saveConfigFile = $true + } + else { + Write-Host \"Section $($section.SectionInformation.SectionName) is already protected.\" + } + } + else { + Write-Warning \"Section $($sectionName) doesn't exists in the configuratoin file.\" + } + + } + + if ($saveConfigFile) { + $configuration.Save([System.Configuration.ConfigurationSaveMode]::Modified); + Write-Host \"Encryption completed successfully.\" + } + else { + Write-Host \"No section(s) in the configuration were encrypted.\" + } +} + +$appType = Get-Parameter \"ApplicationType\" -Required +if ($appType -eq \"Web\") { + $appPath = Get-Parameter \"ExecutablePath\" \"/\" + $webSiteName = Get-Parameter \"WebSiteName\" +} +else { + $appPath = Get-Parameter \"ExecutablePath\" -Required +} +$sectionName = Get-Parameter \"SectionToEncrypt\" -Required +$sections = $sectionName.Split(',') # adding .Trim() doesn't work on Powershell 2.0 or below +$provider = Get-Parameter \"Provider\" \"DataProtectionConfigurationProvider\" + +Write-Host \"Configuration - Encrypt config file\" +Write-Host \"Application Type: $appType\" +Write-Host \"Application Path: $appPath\" +if ($appType -eq \"Web\") { Write-Host \"Web Site Name: $webSiteName\" } +Write-Host \"Section to Encrypt: $sectionName\" +Write-Host \"Provider: $provider\" + +if ($appType -eq \"Web\") { + Invoke-EncryptWebConfigFile +} +else { + Invoke-EncryptAppConfigFile +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "9ab1281d-cf2e-4248-a583-b08e9609c96d", + "Name": "ExecutablePath", + "Label": "Executable path", + "HelpText": "For Web: +The virtual path to the web site. + +For Windows: +The path to the executable that has a corresponding `[Executable].exe.config` file. + +You can get the InstallationDirectoryPath like so `#{Octopus.Action[StepName].Output.Package.InstallationDirectoryPath}`", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0cb4f8ec-5415-47e3-87f1-3e086cc1caa1", + "Name": "SectionToEncrypt", + "Label": "Section to encrypt", + "HelpText": "The name of the section(s) in the config to encrypt e.g. appSettings, connectionStrings etc. + +Separate multiple sections with comma ','.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a30203d3-53d1-450d-bbcd-92a6eb31e906", + "Name": "Provider", + "Label": "Provider Name", + "HelpText": "The provider to use for encryption", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9116d4b3-b033-416f-844c-2a351d3bbc09", + "Name": "ApplicationType", + "Label": "Application Type", + "HelpText": "The application type would be web or windows. +Web will be used to encrypt web.config file. +And Windows type application will encrypt file with \"exe.config\" or \"dll.config\" extension.", + "DefaultValue": "Web", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Web|Web Application +Windows|Windows Service/Console App/Class Library (Dll)" + } + }, + { + "Id": "90a1578e-efa2-44e4-84c0-34b037882cc5", + "Name": "WebSiteName", + "Label": "Web Site Name", + "HelpText": "Enter the web site name installed in IIS.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "KevinKelchen", + "$Meta": { + "ExportedAt": "2017-09-19T22:33:58.801Z", + "OctopusVersion": "3.12.4", + "Type": "ActionTemplate" + }, + "Category": "encrypt" +} diff --git a/step-templates/configuration-encrypt-web-config-section.json.human b/step-templates/configuration-encrypt-web-config-section.json.human new file mode 100644 index 000000000..4e8625ee1 --- /dev/null +++ b/step-templates/configuration-encrypt-web-config-section.json.human @@ -0,0 +1,202 @@ +{ + "Id": "193628c4-2251-41e9-a782-225e632ef871", + "Name": "Configuration - Encrypt Section", + "Description": "Encrypts several configuration sections for the specified file in a given directory.", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\" +function Get-Parameter($Name, $Default, [switch]$Required) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +function HandleError($message) { +\tif (!$whatIf) { +\t\tthrow $message +\t} else { +\t\tWrite-Host $message -Foreground Yellow +\t} +} + +$websiteDirectory = Get-Parameter \"WebsiteDirectory\" -Required +$sectionsToEncrypt = (Get-Parameter \"SectionsToEncrypt\" -Required) -split ',' | where {$_} | %{$_.Trim()} +$provider = Get-Parameter \"Provider\" \"\" +$configFile = Get-Parameter \"ConfigFile\" \"web.config\" +$otherFiles = (Get-Parameter \"OtherFiles\" \"\") -split ',' | where {$_} | %{$_.Trim()} + +Write-Host \"Configuration - Encrypt .config\" +Write-Host \"WebsiteDirectory: $websiteDirectory\" +Write-Host \"SectionsToEncrypt: $sectionsToEncrypt\" +Write-Host \"Provider: $provider\" +Write-Host \"ConfigFile: $configFile\" + + +if (!(Test-Path $websiteDirectory)) { +\tHandleError \"The directory $websiteDirectory must exist\" +} + +$configFilePath = Join-Path $websiteDirectory $configFile +Write-Host \"configFilePath: $configFilePath\" +if (!(Test-Path $configFilePath)) { +\tHandleError \"Specified file $configFile or a Web.Config file must exist in the directory $websiteDirectory\" +} + +$frameworkPath = [System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory(); +$regiis = \"$frameworkPath\\aspnet_regiis.exe\" + +if (!(Test-Path $regiis)) { +\tHandleError \"The tool aspnet_regiis does not exist in the directory $frameworkPath\" +} + +# Create a temp directory to work out of and copy our config file to web.config +$tempPath = Join-Path $websiteDirectory $([guid]::NewGuid()).ToString() +if (!$whatIf) { +\tNew-Item $tempPath -ItemType \"directory\" +} else { +\tWrite-Host \"WhatIf: New-Item $tempPath -ItemType \"\"directory\"\"\" -Foreground Yellow +} + +$tempFile = Join-Path $tempPath \"web.config\" +if (!$whatIf) { + New-Item -ItemType File -Path $tempFile -Force +\tCopy-Item $configFilePath $tempFile -Force +} else { +\tWrite-Host \"WhatIf: Copy-Item $configFilePath $tempFile\" -Foreground Yellow +} + +Foreach($fileName in $otherFiles){ + if (!$whatIf) { + New-Item -ItemType File -Path (Join-Path $tempPath $fileName) -Force +\t Copy-Item (Join-Path $websiteDirectory $fileName) (Join-Path $tempPath $fileName) -Force + } else { +\t Write-Host \"WhatIf: Copy-Item $configFilePath $tempFile\" -Foreground Yellow + } +} + +Foreach($sectionToEncrypt in $sectionsToEncrypt){ +\t# Determine arguments +\tif ($provider) { +\t\t$args = \"-pef\", $sectionToEncrypt, $tempPath, \"-prov\", $provider +\t} else { +\t\t$args = \"-pef\", $sectionToEncrypt, $tempPath +\t} + +\t# Encrypt Web.Config file in directory +\tif (!$whatIf) { +\t\t& $regiis $args +\t\tif ($LASTEXITCODE) { +\t\t HandleError \"There was an error trying to encrypt section: $sectionToEncrypt\" +\t\t} +\t} else { +\t\tWrite-Host \"WhatIf: $regiis $args\" -Foreground Yellow +\t} +} + +# Copy the web.config back to original file and delete the temp dir +if (!$whatIf) { +\tCopy-Item $tempFile $configFilePath -Force + + Foreach($fileName in $otherFiles){ + if (!$whatIf) { + \t Copy-Item (Join-Path $tempPath $fileName) (Join-Path $websiteDirectory $fileName) -Force + } else { + \t Write-Host \"WhatIf: Copy-Item $configFilePath $tempFile\" -Foreground Yellow + } + } + + Remove-Item $tempPath -Recurse +} else { +\tWrite-Host \"WhatIf: Copy-Item $tempFile $configFilePath -Force\" -Foreground Yellow +\tWrite-Host \"WhatIf: Remove-Item $tempPath -Recurse\" -Foreground Yellow +} +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "0cae0018-f915-47c5-a8d6-cdef437346df", + "Name": "WebsiteDirectory", + "Label": "Website directory", + "HelpText": "The path to the website physical directory that contains a `Web.Config` file. + +You can get the InstallationDirectoryPath like so `#{Octopus.Action[StepName].Output.Package.InstallationDirectoryPath}`", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "3d7e5485-106e-415e-83b7-cf12c59e776b", + "Name": "SectionsToEncrypt", + "Label": "Section to encrypt", + "HelpText": "The name of the section in the web config to encrypt e.g. `appSettings`, `connectionStrings` etc. +For multiple sections, separate with a comma (,)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "12a46b72-3835-4086-b8d6-bd5ad3f99f10", + "Name": "Provider", + "Label": "Provider Name", + "HelpText": "The provider to use for encryption", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "998aa949-74b6-4da6-a838-5637e7a5a322", + "Name": "ConfigFile", + "Label": "Configuration File", + "HelpText": "The configuration file to encrypt.", + "DefaultValue": "web.config", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "67415967-4722-449c-8d81-28eff7bf9876", + "Name": "OtherFiles", + "Label": "Other Files", + "HelpText": "A list of other files in the `#{WebsiteDirectory}` folder that should be included when encrypting the specified `#{SectionToEncrypt}`. For example, `connectionStrings.config`. Values should be separated by a comma.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "cjuroz", + "$Meta": { + "ExportedAt": "2016-12-27T03:36:42.383Z", + "OctopusVersion": "3.7.6", + "Type": "ActionTemplate" + }, + "Category": "encrypt" +} diff --git a/step-templates/configure-spm-client.json.human b/step-templates/configure-spm-client.json.human new file mode 100644 index 000000000..07f4d2a4e --- /dev/null +++ b/step-templates/configure-spm-client.json.human @@ -0,0 +1,90 @@ +{ + "Id": "044392b0-5ee7-4f8d-b961-016f07ec6d50", + "Name": "Configure SPM Client", + "Description": "This Step Template will Configure the SPM Client for Solr and Zookeeper", + "ActionType": "Octopus.Script", + "Version": 11, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "APP_GUID=\"#{appguid}\" +APP_TYPE=\"#{applicationtype}\" +APP_MODE=\"#{applicationmode}\" +JMXADDR=\"#{jmxhostaddress}\" +JMXPORT=\"#{jmxhostport}\" + +echo sudo bash /opt/spm/bin/spm-client-setup-conf.sh ${APP_GUID} ${APP_TYPE} ${APP_MODE} jmxhost:${JMXADDR} jmxport:${JMXPORT} +sudo bash /opt/spm/bin/spm-client-setup-conf.sh ${APP_GUID} ${APP_TYPE} ${APP_MODE} jmxhost:${JMXADDR} jmxport:${JMXPORT} +sudo service spm-monitor restart", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "90e816e7-9a09-49da-87ac-cb498a7a4a64", + "Name": "appguid", + "Label": "SemaText Application GUID", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f1baea32-7272-40f4-a2d0-95ead1427a0a", + "Name": "applicationtype", + "Label": "Application Type", + "HelpText": "Value can be solr or zk", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1e3f73eb-153d-4aa6-9b8a-53b5d1737b84", + "Name": "applicationmode", + "Label": "Application Mode", + "HelpText": "standalone", + "DefaultValue": "standalone", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "90b9eeae-83c0-47ad-84b2-60a78ee5049a", + "Name": "jmxhostaddress", + "Label": "JMX Host Address", + "HelpText": "JMX Host Address", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2e7941e8-5d32-4619-b4d3-0e67a9b4dda0", + "Name": "jmxhostport", + "Label": "JMX Host Port", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2017-01-08T11:12:19.041+00:00", + "LastModifiedBy": "mani0070", + "$Meta": { + "ExportedAt": "2017-01-08T11:09:57.757Z", + "OctopusVersion": "3.7.6", + "Type": "ActionTemplate" + }, + "Category": "Linux" +} diff --git a/step-templates/create-azure-website-staging-slot.json.human b/step-templates/create-azure-website-staging-slot.json.human new file mode 100644 index 000000000..ce50ff6cf --- /dev/null +++ b/step-templates/create-azure-website-staging-slot.json.human @@ -0,0 +1,65 @@ +{ + "Id": "2f763d9e-81e1-4ce2-a897-3fe2c72ab9f0", + "Name": "Create Azure Website Staging Slot. Requires Azure PowerShell to be installed on Tentacle machine", + "Description": "Creates a staging slot for Azure Website. + +Provides Azure publish url as variable: + + #{Octopus.Action[Create Staging Slot Step Name].Output.AzurePublishUrl} + +And Username/Password for publishing: + + #{Octopus.Action[Create Staging Slot Step Name].Output.AzureUsername} + #{Octopus.Action[Create Staging Slot Step Name].Output.AzurePassword}", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Check if Windows Azure Powershell is avaiable \r +try{ \r + Import-Module Azure -ErrorAction Stop\r +}catch{\r + throw \"Windows Azure Powershell not found! Please make sure to install them from http://www.windowsazure.com/en-us/downloads/#cmd-line-tools\" \r +}\r +\r +\r +$stagingWebsite = Get-AzureWebsite -Name $AzureWebSite -Slot staging -ErrorAction SilentlyContinue\r +\r +\r +if($stagingWebsite -eq $null)\r +{\r + Write-Host \"Creating staging slot\"\r + $stagingWebsite = New-AzureWebsite -Name $AzureWebSite -Slot staging -Location $Location\r +}\r +\r +\r +Set-OctopusVariable -name \"AzurePassword\" -value $stagingWebsite.PublishingPassword\r +Set-OctopusVariable -name \"AzureUsername\" -value $stagingWebsite.PublishingUsername\r +\r +$urlString = ($stagingWebsite.SiteProperties.Properties | ?{ $_.Name -eq \"RepositoryURI\" }).Value.ToString()\r +$url = [System.Uri]$urlString\r +\r +\r +Set-OctopusVariable -Name \"AzurePublishUrl\" -value $url.Host", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "AzureWebSite", + "Label": "Azure Web Site Name", + "HelpText": "Name of the web-site in Azure", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-03-25T08:42:32.838+00:00", + "LastModifiedBy": "trailmax", + "$Meta": { + "ExportedAt": "2015-03-25T08:51:15.660+00:00", + "OctopusVersion": "2.6.3.886", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/create-tenant.json.human b/step-templates/create-tenant.json.human new file mode 100644 index 000000000..f050bfe70 --- /dev/null +++ b/step-templates/create-tenant.json.human @@ -0,0 +1,174 @@ +{ + "Id": "581e7211-c9e2-4d7b-8934-bcdac421d022", + "Name": "Create Tenant", + "Description": "Create an Octopus [tenant](https://octopus.com/docs/deployment-patterns/multi-tenant-deployments) with optional tenant tags and project connections.", + "ActionType": "Octopus.Script", + "Version": 4, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$securityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 +[Net.ServicePointManager]::SecurityProtocol = $securityProtocol + +$ErrorActionPreference = 'Stop' + +$octopusBaseUrl = $CloneTenantStep_OctopusUrl.Trim('/') +$apiKey = $CreateTenantStep_ApiKey +$tenantName = $CreateTenantStep_TenantName +$tenantTags = if ($CreateTenantStep_TenantTags -eq $null) { @() } else { $CreateTenantStep_TenantTags | ConvertFrom-Json } +$projectEnvironments = if ($CreateTenantStep_ProjectEnvironments -eq $null) { @{} } else { $CreateTenantStep_ProjectEnvironments | ConvertFrom-Json } +$spaceId = $CloneTenantStep_SpaceId + +if ([string]::IsNullOrWhiteSpace($octopusBaseUrl)) { + throw \"The step parameter 'Octopus Base Url' was not found. This step requires the Octopus Server URL to function, please provide one and try again.\" +} + +if ([string]::IsNullOrWhiteSpace($apiKey)) { + throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\" +} + +if ([string]::IsNullOrWhiteSpace($tenantName)) { + throw \"The step parameter 'Tenant Name' was not found. Please provide one and try again.\" +} + +function Invoke-OctopusApi { + param( + [Parameter(Position = 0, Mandatory)]$Uri, + [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get', + $Body + ) + + $uriParts = @($octopusBaseUrl, $Uri.TrimStart('/')) + $uri = ($uriParts -join '/') + + Write-Verbose \"Uri: $uri\" + + $requestParameters = @{ + Uri = $uri + Method = $Method + Headers = @{ \"X-Octopus-ApiKey\" = $apiKey } + UseBasicParsing = $true + } + + if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) } + + return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json +} + +function Test-SpacesApi { +\tWrite-Verbose \"Checking API compatibility\"; +\t$rootDocument = Invoke-OctopusApi 'api/'; + if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) { + \tWrite-Verbose \"Spaces API found\" + \treturn $true; + } + Write-Verbose \"Pre-spaces API found\" + return $false; +} + +if([string]::IsNullOrWhiteSpace($spaceId)) { +\tif(Test-SpacesApi) { + \t$spaceId = $OctopusParameters['Octopus.Space.Id']; + \tif([string]::IsNullOrWhiteSpace($spaceId)) { + \tthrow \"This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which isn't expected - please reach out to our support team at https://help.octopus.com if you encounter this error or try providing the Space Id parameter.\"; + \t} +\t} +} + +$apiPrefix = \"api/\" +$tenantUrlBase = @($octopusBaseUrl, 'app#') + +if ($spaceId) { +\tWrite-Host \"Using Space $spaceId\" +\t$apiPrefix += $spaceId + $tenantUrlBase += $spaceId +} + +$body = @{ +\tId = $null + Name = $tenantName + TenantTags = @($tenantTags) + ProjectEnvironments = $projectEnvironments +} + +Write-Host \"Creating tenant $tenantName\" +$tenant = Invoke-OctopusApi \"$apiPrefix/tenants\" -Method Post -Body $body +$tenantUrl = ($tenantUrlBase + \"tenants\" + $tenant.Id + \"overview\") -join '/' + +Write-Highlight \"New tenant [$tenantName]($tenantUrl) has been created.\"" + }, + "Parameters": [ + { + "Id": "9c6ce5ed-e370-4150-88c0-880984531a48", + "Name": "CloneTenantStep_OctopusUrl", + "Label": "Octopus URL", + "HelpText": "The URL of the Octopus Server to create the tenant on.", + "DefaultValue": "#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a57b379b-a0dd-4d0e-8229-7bd358448766", + "Name": "CreateTenantStep_ApiKey", + "Label": "Octopus API Key", + "HelpText": "The Octopus API Key to use for the API requests", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e02927d1-e2a3-4887-b5d3-c49b12a297a2", + "Name": "CreateTenantStep_TenantName", + "Label": "Tenant Name", + "HelpText": "The name of the tenant to create. *Note this must be unique*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3c3364b8-69f9-458f-9b34-efc591c8882e", + "Name": "CreateTenantStep_TenantTags", + "Label": "Tenant Tags JSON", + "HelpText": "The tenant tags in a JSON array format. Example below. + +*[\"TagSet1/Tag1\", \"TagSet2/Tag1\", \"TagSet2/Tag2\"]*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a68945f1-7adc-4015-80da-dc3a3ce0b571", + "Name": "CreateTenantStep_ProjectEnvironments", + "Label": "Project and Environments JSON", + "HelpText": "The projects and environments to connect the tenant to in JSON format. Example below. + +*{ \"Projects-1\": [\"Environments-1\", \"Environments-2\"], \"Projects-2\": [\"Environments-1\", \"Environments-2\", \"Environments-3\"] }*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5f80b46a-d09c-4092-8a0f-fcc96c49fde2", + "Name": "CloneTenantStep_SpaceId", + "Label": "Space Id", + "HelpText": "The Id of the Space used to clone the tenant. **Leave blank if you are using an Octopus version earlier than 2019.1 or if you wish to use the Octopus.Space.Id variable value.**", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-08-23T12:40:10.975Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "Category": "octopus", + "LastModifiedBy": "benjimac93" +} diff --git a/step-templates/cyberark-conjur-retrieve-a-secret.json.human b/step-templates/cyberark-conjur-retrieve-a-secret.json.human new file mode 100644 index 000000000..4e1566e46 --- /dev/null +++ b/step-templates/cyberark-conjur-retrieve-a-secret.json.human @@ -0,0 +1,155 @@ +{ + "Id": "eafe9740-1008-4375-9e82-0d193109b669", + "Name": "CyberArk Conjur - Retrieve a Secret", + "Description": "This step reads a secret from CyberArk Conjur and makes them available as an Output Variable. Rest API is used and the details can be found at [https://docs.conjur.org/Latest/en/Content/Developer/Conjur_API_Retrieve_Secret.htm](https://docs.conjur.org/Latest/en/Content/Developer/Conjur_API_Retrieve_Secret.htm)", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "# Set TLS 1.2 +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +function CreateUriWithoutIncorrectSlashEncoding { + param( + [Parameter(Mandatory)][string]$uri + ) + $newUri = New-Object System.Uri $uri + [void]$newUri.PathAndQuery # need to access PathAndQuery (presumably modifies internal state) + $flagsFieldInfo = $newUri.GetType().GetField(\"m_Flags\", [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) + $flags = $flagsFieldInfo.GetValue($newUri) + $flags = $flags -band (-bnot 0x30) # remove Flags.PathNotCanonical|Flags.QueryNotCanonical (private enum) + $flagsFieldInfo.SetValue($newUri, $flags) + $newUri +} + +$CONJUR_APPLIANCE_URL = \"#{CONJUR_APPLIANCE_URL}\" +$CONJUR_ACCOUNT = \"#{CONJUR_ACCOUNT}\" +$CONJUR_AUTHN_LOGIN = \"#{CONJUR_AUTHN_LOGIN}\" +$CONJUR_AUTHN_API_KEY = \"#{CONJUR_AUTHN_API_KEY}\" +$VARIABLE_ID = \"#{CONJUR_VARIABLE_ID}\" + +$encodedLogin = ($CONJUR_AUTHN_LOGIN).Replace(\"/\",\"%2F\") +$encodedPath = ($VARIABLE_ID).Replace(\"/\",\"%2F\") + +$headers = New-Object \"System.Collections.Generic.Dictionary[[String],[String]]\" +$headers.Add(\"Content-Type\", \"application/json\") +$headers.Add(\"Accept-Encoding\", \"base64\") + +$body = $CONJUR_AUTHN_API_KEY + +$url1 = \"$CONJUR_APPLIANCE_URL/authn/$CONJUR_ACCOUNT/$encodedLogin/authenticate\" +if (\"#{CONJUR_FIX_SLASH_ENCODING}\" -eq \"True\") { $url1 = CreateUriWithoutIncorrectSlashEncoding \"$url1\" } + +$response = Invoke-RestMethod -uri $url1 -Method 'POST' -Headers $headers -Body $body -UseBasicParsing + +$token=\"Token token=\"\"$($response)\"\"\" + +$headers = New-Object \"System.Collections.Generic.Dictionary[[String],[String]]\" +$headers.Add(\"Authorization\", \"$token\") + +$url2 = CreateUriWithoutIncorrectSlashEncoding \"$CONJUR_APPLIANCE_URL/secrets/$CONJUR_ACCOUNT/variable/$encodedPath\" +if (\"#{CONJUR_FIX_SLASH_ENCODING}\" -eq \"True\") { $url2 = CreateUriWithoutIncorrectSlashEncoding \"$url2\" } + +$secretvalue = Invoke-RestMethod $url2 -Method 'GET' -Headers $headers -UseBasicParsing + +$sensitiveOutputVariablesSupported = ((Get-Command 'Set-OctopusVariable').Parameters.GetEnumerator() | Where-Object { $_.key -eq \"Sensitive\" }) -ne $null +if ($sensitiveOutputVariablesSupported -and (\"#{CONJUR_STAY_SENSITIVE}\" -eq \"True\")) { +\tSet-OctopusVariable -name \"#{CONJUR_OUTPUT_NAME}\" -value $secretvalue -sensitive +} else { +\tSet-OctopusVariable -name \"#{CONJUR_OUTPUT_NAME}\" -value $secretvalue +}" + }, + "Parameters": [ + { + "Id": "5098cbd2-71aa-4dce-ad6d-7e44796c70f3", + "Name": "CONJUR_ACCOUNT", + "Label": "Conjur Account", + "HelpText": "Conjur account that you are connecting to. This value is set during Conjur deployment", + "DefaultValue": "default", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f3b7ae83-4117-415c-bdfc-daecce43b35d", + "Name": "CONJUR_APPLIANCE_URL", + "Label": "Conjur Appliance URL", + "HelpText": "The URL of the Conjur instance you are connecting to. When connecting to DAP configured for high availability, this should be the URL of the master load balancer (if performing read and write operations) or the URL of a follower load balancer (if performing read-only operations)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "18762dfa-1366-4054-863f-0912ed5f887d", + "Name": "CONJUR_AUTHN_LOGIN", + "Label": "Conjur Authentication Login", + "HelpText": "User/host identity", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3273ee5a-6acf-4734-bde2-08c1718ea8c0", + "Name": "CONJUR_AUTHN_API_KEY", + "Label": "Conjur Authentication API Key", + "HelpText": "User/host API key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ed1a91df-d0fa-453e-8b31-bdae379b593b", + "Name": "CONJUR_VARIABLE_ID", + "Label": "Variable ID of Conjur Secret", + "HelpText": " Variable ID of Conjur Secret", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "738037a9-80ad-43a4-b6dd-6ff51330b84a", + "Name": "CONJUR_OUTPUT_NAME", + "Label": "Output Variable Name", + "HelpText": "This specifies the output variable. For more details of output variables, please refer to https://octopus.com/docs/projects/variables/output-variables", + "DefaultValue": "Secret", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "28391ac5-8915-4c85-bc64-7d88a03b812c", + "Name": "CONJUR_STAY_SENSITIVE", + "Label": "Stay Sensitive", + "HelpText": "By default, the output variable will be saved as sensitive. Only disable this for debug purposes in non-production environments.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "91cfcddd-8f16-4aba-9f03-c8bb6dcf9218", + "Name": "CONJUR_FIX_SLASH_ENCODING", + "Label": "Fix Incorrect Slash Encoding", + "HelpText": "PowerShell may incorrectly decode slashes in URL. If an error 404 is returned, toggling this option may fix the issue", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2021-03-03T02:39:08.645Z", + "OctopusVersion": "2020.6.4634", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "QuincyCheng", + "Category": "cyberark" +} diff --git a/step-templates/cyberark-conjur-retrieve-secrets.json.human b/step-templates/cyberark-conjur-retrieve-secrets.json.human new file mode 100644 index 000000000..fb821f4ec --- /dev/null +++ b/step-templates/cyberark-conjur-retrieve-secrets.json.human @@ -0,0 +1,429 @@ +{ + "Id": "522c7010-7189-4b2e-a3c8-36cb1759422a", + "Name": "CyberArk Conjur - Retrieve Secrets", + "Description": "This step retrieves one or more secrets from [CyberArk Conjur](https://www.conjur.org/) and creates [sensitive output variables](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) for each value retrieved. These values can be used in other steps in your deployment or runbook process. + +This step differs from the [CyberArk Conjur - Retrieve a Secret](https://library.octopus.com/step-templates/eafe9740-1008-4375-9e82-0d193109b669/actiontemplate-cyberark-conjur-retrieve-a-secret) step template as it offers the ability to retrieve multiple secrets (with optional version) and performs a batched call where possible - see below for further details. + +--- + +**Retrieval Behavior:** + +- If any of the Conjur Variables have a version specified to retrieve, then the step template will retrieve **all** of the secrets individually using the [Conjur REST API - Secret Retrieve](https://docs.conjur.org/Latest/en/Content/Developer/Conjur_API_Retrieve_Secret.htm) endpoint. +- If none of the Conjur Variables have a version specified (i.e. retrieve the latest version) then the step template will retrieve the secrets using the [Conjur REST API - Batch Retrieval](https://docs.conjur.org/Latest/en/Content/Developer/Conjur_API_Batch_Retrieve.htm) endpoint. + +*Hint:* If performance is important to you, don't include specific versions of Conjur Variables. It’s faster to fetch secrets in a batch than to fetch them one at a time. + +--- + +**Required:** +- PowerShell **5.1** or higher. +- A set of credentials with permissions to retrieve secrets from CyberArk Conjur. +- Access to the Conjur instance from the Worker or target where this step executes. + +Notes: + +- Tested on Conjur **v1.13.2** / API **v5.2.0**. +- Tested on Octopus **2021.2**. +- Tested on both Windows Server 2019 and Ubuntu 20.04.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ErrorActionPreference = 'Stop' + +# Variables +$ConjurUrl = $OctopusParameters[\"CyberArk.Conjur.RetrieveSecrets.Url\"] +$ConjurAccount = $OctopusParameters[\"CyberArk.Conjur.RetrieveSecrets.Account\"] +$ConjurLogin = $OctopusParameters[\"CyberArk.Conjur.RetrieveSecrets.Login\"] +$ConjurApiKey = $OctopusParameters[\"CyberArk.Conjur.RetrieveSecrets.ApiKey\"] +$ConjurSecretVariables = $OctopusParameters[\"CyberArk.Conjur.RetrieveSecrets.SecretVariables\"] +$PrintVariableNames = $OctopusParameters[\"CyberArk.Conjur.RetrieveSecrets.PrintVariableNames\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($ConjurUrl)) { + throw \"Required parameter CyberArk.Conjur.RetrieveSecrets.Url not specified\" +} +if ([string]::IsNullOrWhiteSpace($ConjurAccount)) { + throw \"Required parameter CyberArk.Conjur.RetrieveSecrets.Account not specified\" +} +if ([string]::IsNullOrWhiteSpace($ConjurLogin)) { + throw \"Required parameter CyberArk.Conjur.RetrieveSecrets.Login not specified\" +} +if ([string]::IsNullOrWhiteSpace($ConjurApiKey)) { + throw \"Required parameter CyberArk.Conjur.RetrieveSecrets.ApiKey not specified\" +} +if ([string]::IsNullOrWhiteSpace($ConjurSecretVariables)) { + throw \"Required parameter CyberArk.Conjur.RetrieveSecrets.SecretVariables not specified\" +} + +### Helper functions + +# This function creates a URI and prevents Urls that have been Url encoded from being re-encoded. +# Typically this happens on Windows (dynamic) workers in Octopus, and not PS Core. +# Helpful background - https://stackoverflow.com/questions/25596564/percent-encoded-slash-is-decoded-before-the-request-dispatch +# Function based from https://github.com/IISResetMe/PSdotNETRuntimeHacks/blob/trunk/Set-DontUnescapePathDotsAndSlashes.ps1 +function New-DontUnescapePathDotsAndSlashes-Uri { + param( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [string]$SourceUri + ) + + $uri = New-Object System.Uri $SourceUri + + # If running PS Core, not affected + if ($PSEdition -eq \"Core\") { + return $uri + } + + # Retrieve the private Syntax field from the uri class, + # this is our indirect reference to the attached parser + $syntaxFieldInfo = $uri.GetType().GetField('m_Syntax', 'NonPublic,Instance') + if (-not $syntaxFieldInfo) { + throw [System.MissingFieldException]\"'m_Syntax' field not found\" + } + + # Retrieve the private Flags field from the parser class, + # this is the value we're looking to update at runtime + $flagsFieldInfo = [System.UriParser].GetField('m_Flags', 'NonPublic,Instance') + if (-not $flagsFieldInfo) { + throw [System.MissingFieldException]\"'m_Flags' field not found\" + } + + # Retrieve the actual instances + $uriParser = $syntaxFieldInfo.GetValue($uri) + $uriSyntaxFlags = $flagsFieldInfo.GetValue($uriParser) + + # Define the bit flags we want to remove + $UnEscapeDotsAndSlashes = 0x2000000 + $SimpleUserSyntax = 0x20000 + + # Clear the flags that we don't want + $uriSyntaxFlags = [int]$uriSyntaxFlags -band -bnot($UnEscapeDotsAndSlashes) + $uriSyntaxFlags = [int]$uriSyntaxFlags -band -bnot($SimpleUserSyntax) + + # Overwrite the existing Flags field + $flagsFieldInfo.SetValue($uriParser, $uriSyntaxFlags) + + return $uri +} + +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + $rawResponse = \"\" + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $rawResponse = $reader.ReadToEnd() + } + } + else { + $rawResponse = $RequestError.ErrorDetails.Message + } + + try { $response = $rawResponse | ConvertFrom-Json } catch { $response = $rawResponse } + return $response +} + +function Format-SecretName { + [CmdletBinding()] + Param( + [string] $Name, + [string] $Version + ) + $displayName = \"'$Name'\" + if (![string]::IsNullOrWhiteSpace($Version)) { + $displayName += \" (v:$($Version))\" + } + return $displayName +} + +### End Helper function + +$Secrets = @() +$VariablesCreated = 0 +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] +$ConjurUrl = $ConjurUrl.TrimEnd(\"/\") + +# Extract secret names +@(($ConjurSecretVariables -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + Write-Verbose \"Working on: '$_'\" + $secretDefinition = ($_ -Split \"\\|\") + $secretName = $secretDefinition[0].Trim() + $secretNameAndVersion = ($secretName -Split \" \") + $secretVersion = \"\" + if ($secretNameAndVersion.Count -gt 1) { + $secretName = $secretNameAndVersion[0].Trim() + $secretVersion = $secretNameAndVersion[1].Trim() + } + if ([string]::IsNullOrWhiteSpace($secretName)) { + throw \"Unable to establish secret name from: '$($_)'\" + } + + $UriEscapedName = [uri]::EscapeDataString($secretName) + $VariableIdPrefix = \"$($ConjurAccount):variable\" + + $secret = [PsCustomObject]@{ + Name = $secretName + UriEscapedName = $uriEscapedName + Version = $secretVersion + VariableName = if (![string]::IsNullOrWhiteSpace($secretDefinition[1])) { $secretDefinition[1].Trim() } else { \"\" } + VariableId = \"$($VariableIdPrefix):$($secretName)\" + UriEscapedVariableId = \"$($VariableIdPrefix):$($UriEscapedName)\" + } + $Secrets += $secret + } +} +$SecretsWithVersionSpecified = @($Secrets | Where-Object { ![string]::IsNullOrWhiteSpace($_.Version) }) + +Write-Verbose \"Conjur Url: $ConjurUrl\" +Write-Verbose \"Conjur Account: $ConjurAccount\" +Write-Verbose \"Conjur Login: $ConjurLogin\" +Write-Verbose \"Conjur API Key: ********\" +Write-Verbose \"Secrets to retrieve: $($Secrets.Count)\" +Write-Verbose \"Secrets with Version specified: $($SecretsWithVersionSpecified.Count)\" +Write-Verbose \"Print variables: $PrintVariableNames\" + +try { + + $headers = @{ + \"Content-Type\" = \"application/json\"; + \"Accept-Encoding\" = \"base64\" + } + + $body = $ConjurApiKey + $loginUriSegment = [uri]::EscapeDataString($ConjurLogin) + $authnUri = New-DontUnescapePathDotsAndSlashes-Uri -SourceUri \"$ConjurUrl/authn/$ConjurAccount/$loginUriSegment/authenticate\" + $authToken = Invoke-RestMethod -Uri $authnUri -Method Post -Headers $headers -Body $body +} +catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred logging in to Conjur: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.error) { + $AdditionalDetail = \"$($ErrorBody.error.code) - $($ErrorBody.error.message)\" + } + else { + $AdditionalDetail += $ErrorBody + } + } + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`nDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category AuthenticationError +} + +if ([string]::IsNullOrWhiteSpace($authToken)) { + Write-Error \"Null or Empty token!\" + return +} + +# Set token auth header +$headers = @{ + \"Authorization\" = \"Token token=`\"$($authToken)`\"\"; +} + +if ($SecretsWithVersionSpecified.Count -gt 0) { + Write-Verbose \"Retrieving secrets individually as at least one has a version specified.\" + foreach ($secret in $Secrets) { + try { + $name = $secret.Name + $uriEscapedName = $secret.UriEscapedName + $secretVersion = $secret.Version + $variableName = $secret.VariableName + $displayName = Format-SecretName -Name $name -Version $secretVersion + + if ([string]::IsNullOrWhiteSpace($variableName)) { + $variableName = \"$($name.Trim().Replace(\"/\",\".\"))\" + } + $secretUri = \"$ConjurUrl/secrets/$ConjurAccount/variable/$uriEscapedName\" + if (![string]::IsNullOrWhiteSpace($secretVersion)) { + $secretUri += \"?version=$($secretVersion)\" + } + $secretUri = New-DontUnescapePathDotsAndSlashes-Uri -SourceUri \"$secretUri\" + Write-Verbose \"Retrieving Secret $displayName\" + $secretValue = Invoke-RestMethod -Uri $secretUri -Method Get -Headers $headers + + if ([string]::IsNullOrWhiteSpace($secretValue)) { + Write-Error \"Error: Secret $displayName not found or has no versions.\" + break; + } + + Set-OctopusVariable -Name $variableName -Value $secretValue -Sensitive + + if ($PrintVariableNames -eq $True) { + Write-Output \"Created output variable: ##{Octopus.Action[$StepName].Output.$variableName}\" + } + $VariablesCreated += 1 + } + catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred retrieving secret $($displayName) from Conjur: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.error) { + $AdditionalDetail = \"$($ErrorBody.error.code) - $($ErrorBody.error.message)\" + } + else { + $AdditionalDetail += $ErrorBody + } + } + + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`nDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category ReadError + break; + } + } +} +else { + Write-Verbose \"Retrieving secrets by batch as no versions specified.\" + $uriEscapedVariableIds = @($Secrets | ForEach-Object { \"$($_.UriEscapedVariableId)\" }) -Join \",\" + + try { + $secretsUri = New-DontUnescapePathDotsAndSlashes-Uri -SourceUri \"$ConjurUrl/secrets?variable_ids=$($uriEscapedVariableIds)\" + $secretValues = Invoke-RestMethod -Uri $secretsUri -Method Get -Headers $headers + $secretKeyValues = $secretValues | Get-Member | Where-Object { $_.MemberType -eq \"NoteProperty\" } | Select-Object -ExpandProperty \"Name\" + foreach ($secret in $Secrets) { + $name = $secret.Name + $variableId = $secret.VariableId + $variableName = $secret.VariableName + + Write-Verbose \"Extracting Secret '$($name)' from Conjur batched response\" + + if ([string]::IsNullOrWhiteSpace($variableName)) { + $variableName = \"$($name.Trim().Replace(\"/\",\".\"))\" + } + if ($secretKeyValues -inotcontains $variableId) { + Write-Error \"Secret '$name' not found in Conjur response.\" + return + } + + $variableValue = $secretValues.$variableId + Set-OctopusVariable -Name $variableName -Value $variableValue -Sensitive + + if ($PrintVariableNames -eq $True) { + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$variableName}\" + } + $VariablesCreated += 1 + } + } + catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred retrieving batched secrets from Conjur: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.error) { + $AdditionalDetail = \"$($ErrorBody.error.code) - $($ErrorBody.error.message)\" + } + else { + $AdditionalDetail += $ErrorBody + } + } + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`nDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category AuthenticationError + } +} + +Write-Host \"Created $variablesCreated output variables\"" + }, + "Parameters": [ + { + "Id": "66a70ea0-8d3a-4682-9575-c1dae8ad75e6", + "Name": "CyberArk.Conjur.RetrieveSecrets.Url", + "Label": "Conjur URL", + "HelpText": "The URL of the Conjur instance you are connecting to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ceae6659-4643-490d-9d8d-f642c1c1b4a0", + "Name": "CyberArk.Conjur.RetrieveSecrets.Account", + "Label": "Conjur Account", + "HelpText": "The Conjur account. This is the Conjur appliance identifier provided during Conjur configuration.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0f501e04-207f-4a05-9d21-1e2462bf5b73", + "Name": "CyberArk.Conjur.RetrieveSecrets.Login", + "Label": "Conjur Login", + "HelpText": "The username (from the point of view of the authenticator) of the user or machine (host) requesting authentication. For a host, the id assigned when the host was created should be used, prepended with the literal `host/`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3985b74e-1ea8-4bda-affa-9e7b22056c58", + "Name": "CyberArk.Conjur.RetrieveSecrets.ApiKey", + "Label": "Conjur Api Key", + "HelpText": "The API Key corresponding to the user/host provided.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "eefe9594-7dd1-4fa4-9bb1-65ced026fdda", + "Name": "CyberArk.Conjur.RetrieveSecrets.SecretVariables", + "Label": "Conjur Secret Variable IDs", + "HelpText": "Specify the names of the secrets to be returned from Conjur, in the format: + +`VariableID Version | OutputVariableName` where: + +- `VariableID` is the Variable ID of the Conjur secret. +- `Version` is the version of the secret to retrieve. *If this value isn't specified, the latest version will be retrieved*. +- `OutputVariableName` is the _optional_ Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) name to store the secret's value in. *If this value isn't specified, an output name will be generated dynamically*. + +**Note:** Multiple Variable IDs can be retrieved by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "462f3385-dccd-4ac1-a30b-b06029fbfc18", + "Name": "CyberArk.Conjur.RetrieveSecrets.PrintVariableNames", + "Label": "Print output variable names", + "HelpText": "Write out the Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) names to the task log. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2021-10-19T10:19:57.315Z", + "OctopusVersion": "2021.2.7697", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "cyberark" + } diff --git a/step-templates/datadog-create-event.json.human b/step-templates/datadog-create-event.json.human new file mode 100644 index 000000000..dadd98806 --- /dev/null +++ b/step-templates/datadog-create-event.json.human @@ -0,0 +1,136 @@ +{ + "Id": "40af3b8d-83b0-499e-99ed-e4b1093a7633", + "Name": "Datadog - Create Event", + "Description": "Datadog is cloud monitoring service which allows you to push arbitrary events into via an api. This task allows you to create an event to correlate with other data.", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Lets handle our own errors here +$ErrorActionPreference = \"continue\" + +$apiKey = $OctopusParameters['ApiKey'] +$title = $OctopusParameters['EventTitle'] +$body = $OctopusParameters['EventBody'] +$alertType = $OctopusParameters['AlertType'] +$priority = $OctopusParameters['Priority'] +$tags = $OctopusParameters['Tags'] +$endpoint = $OctopusParameters['DatadogEndpoint'] +$eventsApiEndpoint = \"/api/v1/events\" + +# Write out some debug information +Write-Host \"Event Title: $title\" +Write-Host \"Event Body: $body\" +Write-Host \"Alert Type: $alertType\" +Write-Host \"Priority: $priority\" +Write-Host \"Tags: $tags\" +Write-Host \"Datadog Endpoint: $endpoint$eventsApiEndpoint\" + +# Create the url from basic information +$url = \"$endpoint$eventsApiEndpoint`?api_key=$apiKey\" +$tagString = [system.String]::Join(\"`\",`\"\", $tags.Split(\",\")) + +$json = @\" +{ + \"title\": \"$title\", + \"text\": \"$body\", + \"priority\": \"$priority\", + \"tags\": [\"$tagString\"], + \"alert_type\": \"$alertType\" + } +\"@ + +# Make the response and handle exceptions **Requires PS 3.0 + +try { + $response = Invoke-WebRequest -Uri $url -Method POST -Body ($json | ConvertFrom-Json | ConvertTo-Json) -ContentType \"Application/json\" -UseBasicParsing +}catch{ + Write-Error \"Error: $_\" + EXIT 0 +} + +# Some Error handling here +if($response.StatusCode -ne 202){ + Write-Error \"There was an error listing response content below to debug\" + $response.RawContent +}else{ + Write-Host \"Sent Successfully\" +} + +# We usually don't want to fail a deployment because of this +EXIT 0", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ApiKey", + "Label": "Datadog Api Key", + "HelpText": "The api key used to authenticate with Datadog.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "EventTitle", + "Label": "Title", + "HelpText": "The title for the event to publish.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "EventBody", + "Label": "Body", + "HelpText": "The text to provide more information about the event.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AlertType", + "Label": "Alert Type", + "HelpText": "The alert type out of the following options: \"error\", \"warning\", \"info\" or \"success\".", + "DefaultValue": "info", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Priority", + "Label": "Priority", + "HelpText": "The priority out of the following: \"normal\" or \"low\".", + "DefaultValue": "normal", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Tags", + "Label": "Tags", + "HelpText": "A comma separated list of tags to identify the event.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DatadogEndpoint", + "Label": "Datadog Endpoint", + "HelpText": "The endpoint for datadog, for most (if not all) instances this should just be \"https://app.datadoghq.com\"", + "DefaultValue": "https://app.datadoghq.com", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-07-08T02:19:06.141+00:00", + "OctopusVersion": "2.4.10.235", + "Type": "ActionTemplate" + }, + "Category": "datadog" +} diff --git a/step-templates/datadog-schedule-downtime.json.human b/step-templates/datadog-schedule-downtime.json.human new file mode 100644 index 000000000..139554fcc --- /dev/null +++ b/step-templates/datadog-schedule-downtime.json.human @@ -0,0 +1,115 @@ +{ + "Id": "4db094ce-9c74-499d-8129-ce973cdaa9d4", + "Name": "Datadog - Schedule Downtime", + "Description": "Datadog is cloud monitoring service which allows you to push arbitrary events into via an api. This task allows you to schedule a downtime event to prevent error alerts during a deployment.", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Lets handle our own errors here\r +$ErrorActionPreference = \"continue\"\r +\r +$apiKey = $OctopusParameters['ApiKey']\r +$appKey = $OctopusParameters['AppKey']\r +$endpoint = $OctopusParameters['DatadogEndpoint']\r +$downtimeApiEndpoint = \"/api/v1/downtime\"\r +$scope = $OctopusParameters['Environment']\r +$durstring = $OctopusParameters['Duration']\r +\r +[int]$duration = [convert]::ToInt32($durstring,10)\r +\r +# Write out some debug information\r +Write-Host \"Scheduling Downtime for: $scope\"\r +Write-Host \"Datadog Endpoint: $endpoint$downtimeApiEndpoint\"\r +\r +# Create the url from basic information\r +$url = \"$endpoint$downtimeApiEndpoint`?api_key=$apiKey&application_key=$appKey\"\r +\r +Write-Host $url\r +\r +$start=[Math]::Floor([decimal](Get-Date(Get-Date).ToUniversalTime()-uformat \"%s\"))\r +$end = $start + $duration\r +$json = @\"\r +{\r + \"scope\": \"env:$scope\",\r + \"start\": \"$start\",\r + \"end\": \"$end\"\r + }\r +\"@\r +\r +# Make the response and handle exceptions **Requires PS 3.0 + \r +try {\r + $response = Invoke-WebRequest -Uri $url -Method POST -Body ($json | ConvertFrom-Json | ConvertTo-Json) -ContentType \"Application/json\" -UseBasicParsing\r +}catch{\r + Write-Error \"Error: $_\"\r + EXIT 0\r +}\r +\r +# Some Error handling here\r +if($response.StatusCode -ne 200){\r + Write-Error \"There was an error listing response content below to debug\"\r + $response.RawContent\r +}else{\r + Write-Host \"Sent Successfully\"\r +}\r +\r +# We usually don't want to fail a deployment because of this\r +EXIT 0" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ApiKey", + "Label": "Datadog Api Key", + "HelpText": "The api key used to authenticate with Datadog", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AppKey", + "Label": "Datadog Application Key", + "HelpText": "The Application key used to authenticate with Datadog", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DatadogEndpoint", + "Label": "Datadog Endpoint", + "HelpText": "The endpoint for datadog, for most (if not all) instances this should just be \"https://app.datadoghq.com\"", + "DefaultValue": "https://app.datadoghq.com", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Environment", + "Label": "Environment", + "HelpText": "Environment tag in Datadog to schedule the downtime for", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Duration", + "Label": "Downtime Duration (seconds)", + "HelpText": "How long should the downtime be scheduled for, in seconds default is 10 minutes", + "DefaultValue": "600", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2019-09-18T00:54:17.669Z", + "LastModifiedBy": "baynewc1", + "$Meta": { + "ExportedAt": "2016-03-01T23:11:19.809Z", + "OctopusVersion": "3.2.0", + "Type": "ActionTemplate" + }, + "Category": "datadog" +} diff --git a/step-templates/datadog-task-log.json.human b/step-templates/datadog-task-log.json.human new file mode 100644 index 000000000..bb21fcc43 --- /dev/null +++ b/step-templates/datadog-task-log.json.human @@ -0,0 +1,216 @@ +{ + "Id": "e8550a62-3ed7-4200-b5c7-ba91cbda5e2a", + "Name": "Datadog - Log Task", + "Description": "Log details of a task to Datadog, including error detail. + +**Configuration**: + +* In Datadog, add a [standard attribute](https://docs.datadoghq.com/logs/processing/attributes_naming_convention/#standard-attributes-and-aliasing) of `octopus.deployment.properties` to see and access JSON details of the task. + +* When using the step, set the [run condition](https://octopus.com/docs/projects/steps/conditions) of the step to `Always run` to ensure logs are sent for errors as well as successful tasks.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": " +function Send-DatadogEvent ( + $datadog, + [string] $text, + [string] $level, + $properties = @{}, + [string] $exception = $null, + [switch] $template) { + + + if (-not $level) { + $level = 'Information' + } + + if (@('Verbose', 'Debug', 'Information', 'Warning', 'Error', 'Fatal') -notcontains $level) { + $level = 'Information' + } + + + $ddtags = \"project:$($properties.ProjectName),deploymentname:$($properties.DeploymentName),env:$($properties.EnvironmentName)\" + if ($properties[\"TaskType\"] -eq \"Runbook\") { + $ddtags += \",runbookname:$($properties.RunbookName),tasktype:runbook\" + } + else { + $ddtags += \",tasktype:deployment\" + } + + $body = New-Object \"System.Collections.Generic.Dictionary[[String],[String]]\" + $body.Add(\"ddsource\", \"Octopus Deploy\") + $body.Add(\"ddtags\", $ddtags) + $body.Add(\"service\", $DatadogServiceName) + $body.Add(\"hostname\", \"https://octopus.the-crock.com/\") + $body.Add(\"http.url\", \"$($properties[\"TaskLink\"])\") + $body.Add(\"octopus.deployment.properties\", \"$($properties | ConvertTo-Json)\") + + if ($exception) { + $body.Add(\"error.message\", \"$($properties[\"Error\"])\") + $body.Add(\"error.stack\", \"$($exception)\") + } + + $body.Add(\"level\", \"$($level)\") + + $headers = New-Object \"System.Collections.Generic.Dictionary[[String],[String]]\" + $headers.Add(\"Content-Type\", \"application/json\") + $headers.Add(\"DD-APPLICATION-KEY\", \"$($DatadogApplicationKey)\") + $headers.Add(\"DD-API-KEY\", \"$($DatadogApiKey)\") + + Invoke-RestMethod -Uri $DatadogUrl -Body $($body | ConvertTo-Json) -ContentType \"application/json\" -Method POST -Headers $headers +} + +function Set-ErrorDetails(){ + + $octopusAPIHeader = @{ \"X-Octopus-ApiKey\" = $DatadogOctopusAPIKey } + $taskDetailUri = \"$($OctopusParameters['Octopus.Web.ServerUri'])/api/tasks/$($OctopusParameters[\"Octopus.Task.Id\"])/details\" + + $taskDetails = Invoke-RestMethod -Method Get -Uri $taskDetailUri -Headers $octopusAPIHeader + $errorMessage = \"\"; + $errorFirstLine = \"\"; + $isFirstLine = $true; + + foreach ($activityLog in $taskDetails.ActivityLogs) { + foreach ($activityLogChild1 in $activityLog.Children) { + foreach ($activityLogChild2 in $activityLogChild1.Children) { + foreach ($logElement in $activityLogChild2.LogElements) { + if ($logElement.Category -eq \"Error\") { + if ($isFirstLine -eq $true) { + $errorFirstLine = $logElement.MessageText; + $isFirstLine = $false; + } + + $errorMessage += $logElement.MessageText + \" `n\" + } + } + } + } + } + + $exInfo = @{ + firstLine = $errorFirstLine + message = $errorMessage + } + + return $exInfo; +} + +function Set-TaskProperties(){ + $taskProperties = @{ + ProjectName = $OctopusParameters['Octopus.Project.Name']; + Result = \"succeeded\"; + InstanceUrl = $OctopusParameters['Octopus.Web.ServerUri']; + EnvironmentName = $OctopusParameters['Octopus.Environment.Name']; + DeploymentName = $OctopusParameters['Octopus.Deployment.Name']; + TenantName = $OctopusParameters[\"Octopus.Deployment.Tenant.Name\"] + TaskLink = $taskLink + } + + if ([string]::IsNullOrEmpty($OctopusParameters[\"Octopus.Runbook.Id\"]) -eq $false) { + $taskProperties[\"TaskType\"] \t\t\t= \"Runbook\" + $taskProperties[\"RunbookSnapshotName\"] \t= $OctopusParameters[\"Octopus.RunbookSnapshot.Name\"] + $taskProperties[\"RunbookName\"] \t= $OctopusParameters[\"Octopus.Runbook.Name\"] + } + else { + $taskProperties[\"TaskType\"] \t\t= \"Deployment\" + $taskProperties[\"ReleaseNumber\"] \t= $OctopusParameters['Octopus.Release.Number']; + $taskProperties[\"Channel\"] \t\t= $OctopusParameters['Octopus.Release.Channel.Name']; + } + + return $taskProperties; +} + +#****************************************************************** + +$taskLink = $OctopusParameters['Octopus.Web.ServerUri'] + \"/app#/\" + $OctopusParameters[\"Octopus.Space.Id\"] + \"/tasks/\" + $OctopusParameters[\"Octopus.Task.Id\"] +$level = \"Information\" +$exception = $null + +Write-Output \"Logging the deployment result to Datadog at $DatadogServerUrl...\" + +$properties = Set-TaskProperties + +if ($OctopusParameters['Octopus.Deployment.Error']) { + $exceptionInfo = Set-ErrorDetails + $properties[\"Result\"] = \"failed\" + $properties[\"Error\"] = $exceptionInfo[\"firstLine\"] + $exception = $exceptionInfo[\"message\"] + $level = \"Error\" +} + +try { + Send-DatadogEvent $datadog \"A deployment of $($properties.ProjectName) release $($properties.ReleaseNumber) $($properties.Result) in $($properties.EnvironmentName)\" -level $level -template -properties $properties -exception $exception +} +catch [Exception] { + Write-Error \"Unable to write task details to Datadog\" + $_.Exception | format-list -force +} +" + }, + "Parameters": [ + { + "Id": "6a1f7310-294a-4593-898b-afc32bd93bb3", + "Name": "DatadogUrl", + "Label": "Datadog URL", + "HelpText": "The URL for POSTing log information", + "DefaultValue": "https://http-intake.logs.datadoghq.eu/v1/input", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2eb26500-3ced-466c-962e-18b91ce6ea46", + "Name": "DatadogAPIKey", + "Label": "Datadog API Key", + "HelpText": "[API Key](https://docs.datadoghq.com/account_management/api-app-keys/#api-keys) required for accessing API.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "289728cb-9442-41b3-a3be-17f4e8287380", + "Name": "DatadogApplicationKey", + "Label": "Datadog Application Key", + "HelpText": "Information on Datadog [Application Keys](https://docs.datadoghq.com/account_management/api-app-keys/#application-keys)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "6afbdc2a-3f85-4807-abff-d7f3e0dcba77", + "Name": "DatadogServiceName", + "Label": "Datadog Service Name", + "HelpText": null, + "DefaultValue": "Octopus Deploy", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "62b47d15-e05f-4e0b-a4b9-4a80b98b62f9", + "Name": "DatadogOctopusAPIKey", + "Label": "", + "HelpText": "Only used when an exception has occurred. + +Octopus instance [API Key](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key). Requires at least `TaskView` [permission](https://octopus.com/docs/security/users-and-teams/system-and-space-permissions).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2021-03-04T11:25:36.400Z", + "OctopusVersion": "2020.5.249", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "octocrock", + "Category": "datadog" +} diff --git a/step-templates/deploy-child-project.json.human b/step-templates/deploy-child-project.json.human new file mode 100644 index 000000000..9e80e74b4 --- /dev/null +++ b/step-templates/deploy-child-project.json.human @@ -0,0 +1,2493 @@ +{ + "Id": "0dac2fe6-91d5-4c05-bdfb-1b97adf1e12e", + "Name": "Deploy Child Octopus Deploy Project", + "Description": "This step will find the latest release in a source environment matching your criteria and deploy it. + +Use cases: +- As a user, I want to create a single parent release `2020.2.1`. When I promote the parent release I want the latest child releases matching `2020.2.1.*` to be promoted to the next environment. +- As a user, I want the latest release in the dev environment to be promoted to the test environment. Not the most recently created release, the most recent release deployed that environment. +- As a user, when we are finished with our QA process, we want to automatically push the latest releases from QA to Staging without having to manually promote each release. +- As a user, I'd like to set up a nightly build to promote the latest releases from Dev to QA +- As a user, I'd like to be able to deploy a suite of applications to a tenant. If the tenant isn't assigned to the project then skip over. +- As a user, I'd like to see what releases would go to production and approve those releases without having to manually verify and approve each one. +- As a user, I'd like to be able to target specific machines in my parent project and only have child projects deploy associated with those machines. +- As a user, I'd like to be able to exclude specific machines in my parent project and only have child projects deploy to the remaining machines. +- As a user, I'd like to have a single deployment target trigger on my parent project and when I scale up my servers deploy the appropriate child projects. +- As a user, I'd like to be able to approve the deployments and then schedule them to be deployed at 7 PM +- As a user, I'd like to be able to have one space for orchestration projects and another space for developers to work in. + +This step template also allows you to skip deployments to the destination environment if it has already been deployed.", + "ActionType": "Octopus.Script", + "Version": 29, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +# Supplied Octopus Parameters +$parentReleaseId = $OctopusParameters[\"Octopus.Release.Id\"] +$parentChannelId = $OctopusParameters[\"Octopus.Release.Channel.Id\"] +$destinationSpaceId = $OctopusParameters[\"Octopus.Space.Id\"] +$specificMachines = $OctopusParameters[\"Octopus.Deployment.SpecificMachines\"] +$excludeMachines = $OctopusParameters[\"Octopus.Deployment.ExcludedMachines\"] +$deploymentMachines = $OctopusParameters[\"Octopus.Deployment.Machines\"] +$parentDeploymentTaskId = $OctopusParameters[\"Octopus.Task.Id\"] +$parentProjectName = $OctopusParameters[\"Octopus.Project.Name\"] +$parentReleaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$parentEnvironmentName = $OctopusParameters[\"Octopus.Environment.Name\"] +$parentEnvironmentId = $OctopusParameters[\"Octopus.Environment.Id\"] +$parentSpaceId = $OctopusParameters[\"Octopus.Space.Id\"] + +# User Parameters +$octopusApiKey = $OctopusParameters[\"ChildProject.Api.Key\"] +$projectName = $OctopusParameters[\"ChildProject.Project.Name\"] +$channelName = $OctopusParameters[\"ChildProject.Channel.Name\"] +$releaseNumber = $OctopusParameters[\"ChildProject.Release.Number\"] +$environmentName = $OctopusParameters[\"ChildProject.Destination.EnvironmentName\"] +$sourceEnvironmentName = $OctopusParameters[\"ChildProject.SourceEnvironment.Name\"] +$formValues = $OctopusParameters[\"ChildProject.Prompted.Variables\"] +$destinationSpaceName = $OctopusParameters[\"ChildProject.Space.Name\"] +$whatIfValue = $OctopusParameters[\"ChildProject.WhatIf.Value\"] +$waitForFinishValue = $OctopusParameters[\"ChildProject.WaitForFinish.Value\"] +$enableEnhancedLoggingValue = $OctopusParameters['ChildProject.EnableEnhancedLogging.Value'] +$deploymentCancelInSeconds = $OctopusParameters[\"ChildProject.CancelDeployment.Seconds\"] +$ignoreSpecificMachineMismatchValue = $OctopusParameters[\"ChildProject.Deployment.IgnoreSpecificMachineMismatch\"] +$autoapproveChildManualInterventionsValue = $OctopusParameters[\"ChildProject.ManualInterventions.UseApprovalsFromParent\"] +$saveReleaseNotesAsArtifactValue = $OctopusParameters[\"ChildProject.ReleaseNotes.SaveAsArtifact\"] +$futureDeploymentDate = $OctopusParameters[\"ChildProject.Deployment.FutureTime\"] +$errorHandleForNoRelease = $OctopusParameters[\"ChildProject.Release.NotFoundError\"] +$approvalEnvironmentName = $OctopusParameters[\"ChildProject.ManualIntervention.EnvironmentToUse\"] +$approvalTenantName = $OctopusParameters[\"ChildProject.ManualIntervention.Tenant.Name\"] +$refreshVariableSnapShot = $OctopusParameters[\"ChildProject.RefreshVariableSnapShots.Option\"] +$deploymentMode = $OctopusParameters[\"ChildProject.DeploymentMode.Value\"] +$targetMachines = $OctopusParameters[\"ChildProject.Target.MachineNames\"] +$deploymentTenantName = $OctopusParameters[\"ChildProject.Tenant.Name\"] +$defaultUrl = $OctopusParameters[\"ChildProject.Web.ServerUrl\"] + +$cachedResults = @{} + +function Write-OctopusVerbose +{ + param($message) + + Write-Verbose $message +} + +function Write-OctopusInformation +{ + param($message) + + Write-Host $message +} + +function Write-OctopusSuccess +{ + param($message) + + Write-Highlight $message +} + +function Write-OctopusWarning +{ + param($message) + + Write-Warning \"$message\" +} + +function Write-OctopusCritical +{ + param ($message) + + Write-Error \"$message\" +} + +function Test-RequiredValues +{ +\tparam ( + \t$variableToCheck, + $variableName + ) + + if ([string]::IsNullOrWhiteSpace($variableToCheck) -eq $true) + { + \tWrite-OctopusCritical \"$variableName is required.\" + return $false + } + + return $true +} + +function Invoke-OctopusApi +{ + param + ( + $octopusUrl, + $endPoint, + $spaceId, + $apiKey, + $method, + $item, + $ignoreCache + ) + + if ([string]::IsNullOrWhiteSpace($SpaceId)) + { + $url = \"$OctopusUrl/api/$EndPoint\" + } + else + { + $url = \"$OctopusUrl/api/$spaceId/$EndPoint\" + } + + try + { + if ($null -ne $item) + { + $body = $item | ConvertTo-Json -Depth 10 + Write-OctopusVerbose $body + + Write-OctopusInformation \"Invoking $method $url\" + return Invoke-RestMethod -Method $method -Uri $url -Headers @{\"X-Octopus-ApiKey\" = \"$ApiKey\" } -Body $body -ContentType 'application/json; charset=utf-8' + } + + if (($null -eq $ignoreCache -or $ignoreCache -eq $false) -and $method.ToUpper().Trim() -eq \"GET\") + { + Write-OctopusVerbose \"Checking to see if $url is already in the cache\" + if ($cachedResults.ContainsKey($url) -eq $true) + { + Write-OctopusVerbose \"$url is already in the cache, returning the result\" + return $cachedResults[$url] + } + } + else + { + Write-OctopusVerbose \"Ignoring cache.\" + } + + Write-OctopusVerbose \"No data to post or put, calling bog standard invoke-restmethod for $url\" + $result = Invoke-RestMethod -Method $method -Uri $url -Headers @{\"X-Octopus-ApiKey\" = \"$ApiKey\" } -ContentType 'application/json; charset=utf-8' + + if ($cachedResults.ContainsKey($url) -eq $true) + { + $cachedResults.Remove($url) + } + Write-OctopusVerbose \"Adding $url to the cache\" + $cachedResults.add($url, $result) + + return $result + + + } + catch + { + if ($null -ne $_.Exception.Response) + { + if ($_.Exception.Response.StatusCode -eq 401) + { + Write-OctopusCritical \"Unauthorized error returned from $url, please verify API key and try again\" + } + elseif ($_.Exception.Response.statusCode -eq 403) + { + Write-OctopusCritical \"Forbidden error returned from $url, please verify API key and try again\" + } + else + { + Write-OctopusVerbose -Message \"Error calling $url $($_.Exception.Message) StatusCode: $($_.Exception.Response.StatusCode )\" + } + } + else + { + Write-OctopusVerbose $_.Exception + } + } + + Throw \"There was an error calling the Octopus API please check the log for more details\" +} + +function Get-ListFromOctopusApi +{ + param ( + $octopusUrl, + $endPoint, + $spaceId, + $apiKey, + $propertyName + ) + + $rawItemList = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint $endPoint -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + + $returnList = @($rawItemList.$propertyName) + + Write-OctopusVerbose \"The endpoint $endPoint returned a list with $($returnList.Count) items\" + + return ,$returnList +} + +function Get-FilteredOctopusItem +{ + param( + $itemList, + $itemName + ) + + if ($itemList.Count -eq 0) + { + Write-OctopusCritical \"Unable to find $itemName. Exiting with an exit code of 1.\" + Exit 1 + } + + $item = $itemList | Where-Object { $_.Name.ToLower().Trim() -eq $itemName.ToLower().Trim() } + + if ($null -eq $item) + { + Write-OctopusCritical \"Unable to find $itemName. Exiting with an exit code of 1.\" + exit 1 + } + + return $item +} + +function Test-PhaseContainsEnvironmentId +{ + param ( + $phase, + $environmentId + ) + + Write-OctopusVerbose \"Checking to see if $($phase.Name) automatic deployment environments $($phase.AutomaticDeploymentTargets) contains $environmentId\" + if ($phase.AutomaticDeploymentTargets -contains $environmentId) + { + Write-OctopusVerbose \"It does, returning true\" + return $true + } + + Write-OctopusVerbose \"Checking to see if $($phase.Name) optional deployment environments $($phase.OptionalDeploymentTargets) contains $environmentId\" + if ($phase.OptionalDeploymentTargets -contains $environmentId) + { + Write-OctopusVerbose \"It does, returning true\" + return $true + } + + Write-OctopusVerbose \"The phase does not contain the environment returning false\" + return $false +} + +function Get-OctopusItemByName +{ + param( + $itemName, + $itemType, + $endpoint, + $defaultValue, + $spaceId, + $defaultUrl, + $octopusApiKey + ) + + if ([string]::IsNullOrWhiteSpace($itemName) -or $itemName -like \"#{Octopus*\") + { + Write-OctopusVerbose \"The item name passed in was $itemName, returning the default value for $itemType\" + return $defaultValue + } + + Write-OctopusInformation \"Attempting to find $itemType with the name of $itemName\" + + $itemList = Get-ListFromOctopusApi -octopusUrl $defaultUrl -endPoint \"$($endPoint)?partialName=$([uri]::EscapeDataString($itemName))&skip=0&take=100\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" -propertyName \"Items\" + $item = Get-FilteredOctopusItem -itemList $itemList -itemName $itemName + + Write-OctopusInformation \"Successfully found $itemName with id of $($item.Id)\" + + return $item +} + +function Get-OctopusItemById +{ + param( + $itemId, + $itemType, + $endpoint, + $defaultValue, + $spaceId, + $defaultUrl, + $octopusApiKey + ) + + if ([string]::IsNullOrWhiteSpace($itemId)) + { + return $defaultValue + } + + Write-OctopusInformation \"Attempting to find $itemType with the id of $itemId\" + + $item = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"$endPoint/$itemId\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + + if ($null -eq $item) + { + Write-OctopusCritical \"Unable to find $itemType with the id of $itemId\" + exit 1 + } + else + { + Write-OctopusInformation \"Successfully found $itemId with name of $($item.Name)\" + } + + return $item +} + +function Get-OctopusSpaceIdByName +{ +\tparam( + \t$spaceName, + $spaceId, + $defaultUrl, + $octopusApiKey + ) + + if ([string]::IsNullOrWhiteSpace($spaceName)) + { + \treturn $spaceId + } + + $space = Get-OctopusItemByName -itemName $spaceName -itemType \"Space\" -endpoint \"spaces\" -defaultValue $spaceId -spaceId $null -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey + + return $space.Id +} + +function Get-OctopusProjectByName +{ + param ( + $projectName, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + return Get-OctopusItemByName -itemName $projectName -itemType \"Project\" -endpoint \"projects\" -defaultValue $null -spaceId $spaceId -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey +} + +function Get-OctopusEnvironmentByName +{ + param ( + $environmentName, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + return Get-OctopusItemByName -itemName $environmentName -itemType \"Environment\" -endpoint \"environments\" -defaultValue $null -spaceId $spaceId -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey +} + +function Get-OctopusTenantByName +{ + param ( + $tenantName, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + return Get-OctopusItemByName -itemName $tenantName -itemType \"Tenant\" -endpoint \"tenants\" -defaultValue $null -spaceId $spaceId -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey +} + +function Get-OctopusApprovalTenant +{ + param ( + $tenantToDeploy, + $approvalTenantName, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + Write-OctopusInformation \"Checking to see if there is an approval tenant to consider\" + + if ($null -eq $tenantToDeploy) + { + Write-OctopusInformation \"Not doing tenant deployments, skipping this check\" + return $null + } + + if ([string]::IsNullOrWhiteSpace($approvalTenantName) -eq $true -or $approvalTenantName -eq \"#{Octopus.Deployment.Tenant.Name}\") + { + Write-OctopusInformation \"No approval tenant was provided, returning $($tenantToDeploy.Id)\" + return $tenantToDeploy + } + + if ($approvalTenantName.ToLower().Trim() -eq $tenantToDeploy.Name.ToLower().Trim()) + { + Write-OctopusInformation \"The approval tenant name matches the deployment tenant name, using the current tenant\" + return $tenantToDeploy + } + + return Get-OctopusTenantByName -tenantName $approvalTenantName -spaceId $spaceId -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey +} + +function Get-OctopusChannel +{ + param ( + $channelName, + $project, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + Write-OctopusInformation \"Attempting to find the channel information for project $projectName matching the channel name $channelName\" + $channelList = Get-ListFromOctopusApi -octopusUrl $defaultUrl -endPoint \"projects/$($project.Id)/channels?skip=0&take=1000\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" -propertyName \"Items\" + $channelToUse = $null + foreach ($channel in $channelList) + { + if ([string]::IsNullOrWhiteSpace($channelName) -eq $true -and $channel.IsDefault -eq $true) + { + Write-OctopusVerbose \"The channel name specified is null or empty and the current channel $($channel.Name) is the default, using that\" + $channelToUse = $channel + break + } + + if ([string]::IsNullOrWhiteSpace($channelName) -eq $false -and $channel.Name.Trim().ToLowerInvariant() -eq $channelName.Trim().ToLowerInvariant()) + { + Write-OctopusVerbose \"The channel name specified $channelName matches the the current channel $($channel.Name) using that\" + $channelToUse = $channel + break + } + } + + if ($null -eq $channelToUse) + { + Write-OctopusCritical \"Unable to find a channel to use. Exiting with an exit code of 1.\" + exit 1 + } + + return $channelToUse +} + +function Get-OctopusLifecyclePhases +{ + param ( + $channel, + $defaultUrl, + $spaceId, + $octopusApiKey, + $project + ) + + Write-OctopusInformation \"Attempting to find the lifecycle information $($channel.Name)\" + if ($null -eq $channel.LifecycleId) + { + return Get-ListFromOctopusApi -octopusUrl $defaultUrl -endPoint \"lifecycles/$($project.LifecycleId)/preview\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" -propertyName \"Phases\" + } + else + { + return Get-ListFromOctopusApi -octopusUrl $defaultUrl -endPoint \"lifecycles/$($channel.LifecycleId)/preview\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" -propertyName \"Phases\" + } +} + +function Get-SourceDestinationEnvironmentInformation +{ + param ( + $phaseList, + $targetEnvironment, + $sourceEnvironment, + $isPromotionMode, + $isAlwaysLatestMode + ) + + Write-OctopusVerbose \"Attempting to pull the environment ids from the source and destination phases\" + + $destTargetEnvironmentInfo = @{ + TargetEnvironment = $targetEnvironment + SourceEnvironmentList = @() + FirstLifecyclePhase = $false + HasRequiredPhase = $false + } + + if ($isPromotionMode -eq $false) + { + if ($isAlwaysLatestMode -eq $true) + { + Write-OctopusInformation \"Currently running in AlwaysLatest mode, setting the source environment to the target environment.\" + } + else + { + Write-OctopusInformation \"Currently running in redeploy mode, setting the source environment to the target environment.\" + + } + $destTargetEnvironmentInfo.SourceEnvironmentList = $targetEnvironment.Id + + return $destTargetEnvironmentInfo + } + + $indexOfTargetEnvironment = $null + for ($i = 0; $i -lt $phaseList.Length; $i++) + { + Write-OctopusInformation \"Checking to see if lifecycle phase $($phaseList[$i].Name) contains the target environment id $($targetEnvironment.Id)\" + + if (Test-PhaseContainsEnvironmentId -phase $phaseList[$i] -environmentId $targetEnvironment.Id) + { + Write-OctopusVerbose \"The phase $($phaseList[$i].Name) has the environment $($targetEnvironment.Name).\" + $indexOfTargetEnvironment = $i + break + } + } + + if ($null -eq $indexOfTargetEnvironment) + { + Write-OctopusCritical \"Unable to find the target phase in this lifecycle attached to this channel. Exiting with exit code of 1\" + Exit 1 + } + + if ($indexOfTargetEnvironment -eq 0) + { + Write-OctopusInformation \"This is the first phase in the lifecycle. The current mode is promotion. Going to get the latest release created that matches the release number rules for the channel.\" + $destTargetEnvironmentInfo.FirstLifecyclePhase = $true + $destTargetEnvironmentInfo.SourceEnvironmentList += $targetEnvironment.Id + + return $destTargetEnvironmentInfo + } + + if ($null -ne $sourceEnvironment) + { + Write-OctopusInformation \"The source environment $($sourceEnvironment.Name) was provided, using that as the source environment\" + $destTargetEnvironmentInfo.SourceEnvironmentList += $sourceEnvironment.Id + + return $destTargetEnvironmentInfo + } + + Write-OctopusVerbose \"Looping through all the previous phases until a required phase is found.\" + $startingIndex = ($indexOfTargetEnvironment - 1) + for($i = $startingIndex; $i -ge 0; $i--) + { + $previousPhase = $phaseList[$i] + Write-OctopusInformation \"Adding environments from the phase $($previousPhase.Name)\" + foreach ($environmentId in $previousPhase.AutomaticDeploymentTargets) + { + $destTargetEnvironmentInfo.SourceEnvironmentList += $environmentId + } + + foreach ($environmentId in $previousPhase.OptionalDeploymentTargets) + { + $destTargetEnvironmentInfo.SourceEnvironmentList += $environmentId + } + + if ($previousPhase.IsOptionalPhase -eq $false) + { + Write-OctopusVerbose \"The phase $($previousPhase.Name) is a required phase, exiting previous phase loop\" + $destTargetEnvironmentInfo.HasRequiredPhase = $true + break + } + elseif ($i -gt 0) + { + Write-OctopusVerbose \"The phase $($previousPhase.Name) is an optional phase, continuing going to check the next phase\" + } + else + { + Write-OctopusVerbose \"The phase $($previousPhase.Name) is an optional phase. This is the last phase so I'm stopping now.\" + } + } + + return $destTargetEnvironmentInfo +} + +function Get-ReleaseCanBeDeployedToTargetEnvironment +{ + param ( + $release, + $defaultUrl, + $spaceId, + $octopusApiKey, + $sourceDestinationEnvironmentInfo, + $tenantToDeploy, + $isPromotionMode, + $isAlwaysLatestMode + ) + + if ($isPromotionMode -eq $false -and $isAlwaysLatestMode -eq $false) + { + Write-OctopusInformation \"The current mode is redeploy. Of course the release can be deployed to the target environment, no need to recheck it.\" + return $true + } + + Write-OctopusInformation \"Pulling the deployment template information for release $($release.Version)\" + $releaseDeploymentTemplate = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"releases/$($release.Id)/deployments/template\" -spaceId $spaceId -method GET -apiKey $octopusApiKey + + $releaseCanBeDeployedToDestination = $false + Write-OctopusInformation \"Looping through deployment template list for $($release.Version) to see if it can be deployed to $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name).\" + foreach ($promoteToEnvironment in $releaseDeploymentTemplate.PromoteTo) + { + if ($promoteToEnvironment.Id -eq $sourceDestinationEnvironmentInfo.TargetEnvironment.Id) + { + Write-OctopusInformation \"The environment $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name) was found in the list of environments to promote to\" + $releaseCanBeDeployedToDestination = $true + break + } + } + + if ($null -eq $tenantToDeploy -or $releaseDeploymentTemplate.TenantPromotions.Length -le 0) + { + return $releaseCanBeDeployedToDestination + } + + $releaseCanBeDeployedToDestination = $false + Write-OctopusInformation \"The tenant id was supplied, looping through the tenant templates to see if it can be deployed to $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name).\" + foreach ($tenantPromotion in $releaseDeploymentTemplate.TenantPromotions) + { + if ($tenantPromotion.Id -ne $tenantToDeploy.Id) + { + Write-OctopusVerbose \"The tenant ids $($tenantPromotion.Id) and $($tenantToDeploy.Id) don't match, moving onto the next one\" + continue + } + + Write-OctopusVerbose \"The tenant Id matches checking to see if the environment can be promoted to.\" + foreach ($promoteToEnvironment in $tenantPromotion.PromoteTo) + { + if ($promoteToEnvironment.Id -ne $sourceDestinationEnvironmentInfo.TargetEnvironment.Id) + { + Write-OctopusVerbose \"The environmentIds $($promoteToEnvironment.Id) and $($sourceDestinationEnvironmentInfo.TargetEnvironment.Id) don't match, moving onto the next one.\" + continue + } + + Write-OctopusInformation \"The environment $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name) was found in the list of environments tenant $($tenantToDeploy.Id) can be promoted to\" + $releaseCanBeDeployedToDestination = $true + } + } + + return $releaseCanBeDeployedToDestination +} + +function Get-DeploymentPreview +{ + param ( + $releaseToDeploy, + $defaultUrl, + $spaceId, + $octopusApiKey, + $targetEnvironment, + $deploymentTenant + ) + + if ($null -eq $deploymentTenant) + { + Write-OctopusInformation \"The deployment tenant id was not sent in, generating a preview by hitting releases/$($releaseToDeploy.Id)/deployments/preview/$($targetEnvironment.Id)?includeDisabledSteps=true\" + return Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"releases/$($releaseToDeploy.Id)/deployments/preview/$($targetEnvironment.Id)?includeDisabledSteps=true\" -apiKey $octopusApiKey -method \"GET\" -spaceId $spaceId + } + + Write-OctopusInformation \"The deployment tenant id was sent in, generating a preview by hitting releases/$($releaseToDeploy.Id)/deployments/previews\" + $requestBody = @{ + \t\tDeploymentPreviews = @( + \t\t\t@{ + \tTenantId = $deploymentTenant.Id; + \t\tEnvironmentId = $targetEnvironment.Id + } + ) + } + return Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"releases/$($releaseToDeploy.Id)/deployments/previews\" -apiKey $octopusApiKey -method \"POST\" -spaceId $spaceId -item $requestBody -itemIsArray $true +} + +function Get-ValuesForPromptedVariables +{ + param ( + $deploymentPreview, + $formValues + ) + + $deploymentFormValues = @{} + if ([string]::IsNullOrWhiteSpace($formValues) -eq $true) + { + return $deploymentFormValues + } + + $promptedValueList = @(($formValues -Split \"`n\").Trim()) + Write-OctopusVerbose $promptedValueList.Length + + foreach($element in $deploymentPreview.Form.Elements) + { + $nameToSearchFor = $element.Control.Name + $uniqueName = $element.Name + $isRequired = $element.Control.Required + + $promptedVariablefound = $false + + Write-OctopusVerbose \"Looking for the prompted variable value for $nameToSearchFor\" + foreach ($promptedValue in $promptedValueList) + { + $splitValue = $promptedValue -Split \"::\" + Write-OctopusVerbose \"Comparing $nameToSearchFor with provided prompted variable $($promptedValue[0])\" + if ($splitValue.Length -gt 1) + { + if ($nameToSearchFor.ToLower().Trim() -eq $splitValue[0].ToLower().Trim()) + { + Write-OctopusVerbose \"Found the prompted variable value $nameToSearchFor\" + $deploymentFormValues[$uniqueName] = $splitValue[1] + $promptedVariableFound = $true + break + } + } + } + + if ($promptedVariableFound -eq $false -and $isRequired -eq $true) + { + Write-OctopusCritical \"Unable to find a value for the required prompted variable $nameToSearchFor, exiting\" + Exit 1 + } + } + + return $deploymentFormValues +} + +function Test-ProjectTenantSettings +{ + param ( + $tenantToDeploy, + $project, + $targetEnvironment + ) + + Write-OctopusVerbose \"About to check if $tenantToDeploy is not null and tenant deploy mode on the project $($project.TenantedDeploymentMode) <> Untenanted\" + if ($null -eq $tenantToDeploy) + { + Write-OctopusInformation \"Not doing a tenanted deployment, no need to check if the project supports tenanted deployments.\" + return $null + } + + if ($project.TenantedDeploymentMode -eq \"Untenanted\") + { + Write-OctopusInformation \"The project is not tenanted, but we are doing a tenanted deployment, removing the tenant from the equation\" + return $null + } + + Write-OctopusInformation \"Found the tenant $($tenantToDeploy.Name) checking to see if $($project.Name) is assigned to it.\" + + Write-OctopusVerbose \"Checking to see if $($tenantToDeploy.ProjectEnvironments) has $($project.Id) as a property.\" + if ($null -eq (Get-Member -InputObject $tenantToDeploy.ProjectEnvironments -Name $project.Id -MemberType Properties)) + { + Write-OctopusSuccess \"The tenant $($tenantToDeploy.Name) is not assigned to $($project.Name). Exiting.\" + Insert-EmptyOutputVariables -releaseToDeploy $null + + Exit 0 + } + + Write-OctopusInformation \"The tenant $($tenantToDeploy.Name) is assigned to $($project.Name). Now checking to see if it can be deployed to the target environment.\" + $tenantProjectId = $project.Id + + Write-OctopusVerbose \"Checking to see if $($tenantToDeploy.ProjectEnvironments.$tenantProjectId) has $($targetEnvironment.Id)\" + if ($tenantToDeploy.ProjectEnvironments.$tenantProjectId -notcontains $targetEnvironment.Id) + { + Write-OctopusSuccess \"The tenant $($tenantToDeploy.Name) is assigned to $($project.Name), but not to the environment $($targetEnvironment.Name). Exiting.\" + Insert-EmptyOutputVariables -releaseToDeploy $null + + Exit 0 + } + + return $tenantToDeploy +} + +function Test-ReleaseToDeploy +{ +\tparam ( + \t$releaseToDeploy, + $errorHandleForNoRelease, + $releaseNumber, + $sourceDestinationEnvironmentInfo, + $environmentList + ) + + if ($null -ne $releaseToDeploy) + { + \treturn + } + + $errorMessage = \"No releases were found in environment(s)\" + + $environmentMessage = @() + foreach ($environmentId in $sourceDestinationEnvironmentInfo.SourceEnvironmentList) + { + $environment = $environmentList | Where-Object {$_.Id -eq $environmentId } + + if ($null -ne $environment) + { + $environmentMessage += $environment.Name + } + } + + $errorMessage += \" $($environmentMessage -join \",\")\" + + if ([string]::IsNullOrWhitespace($releaseNumber) -eq $false) + { + \t$errorMessage = \"$errorMessage matching $releaseNumber\" + } + + $errorMessage = \"$errorMessage that can be deployed to $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name)\" + + if ($errorHandleForNoRelease -eq \"Error\") + { + \tWrite-OctopusCritical $errorMessage + exit 1 + } + + Insert-EmptyOutputVariables -releaseToDeploy $null + + if ($errorHandleForNoRelease -eq \"Skip\") + { + \tWrite-OctopusInformation $errorMessage + exit 0 + } + + Write-OctopusSuccess $errorMessage + exit 0 +} + +function Get-TenantIsAssignedToPreviousEnvironments +{ + param ( + $tenantToDeploy, + $sourceDestinationEnvironmentInfo, + $projectId, + $isPromotionMode + ) + + if ($null -eq $tenantToDeploy) + { + Write-OctopusVerbose \"The tenant is null, skipping the check to see if it is assigned to the previous environment list.\" + return $false + } + + if ($isPromotionMode -eq $false) + { + Write-OctopusVerbose \"The current mode is redeploy, the source and destination environment are the same, no need to check.\" + return $true + } + + Write-OctopusVerbose \"Checking to see if $($tenantToDeploy.Name) is assigned to the previous environments.\" + Write-OctopusVerbose \"Checking to see if $($tenantToDeploy.ProjectEnvironments.$projectId) is assigned to the source environments(s) $($sourceDestinationEnvironmentInfo.SourceEnvironmentList)\" + + foreach ($environmentId in $tenantToDeploy.ProjectEnvironments.$projectId) + { + Write-OctopusVerbose \"Checking to see if $environmentId appears in $($sourceDestinationEnvironmentInfo.SourceEnvironmentList)\" + if ($sourceDestinationEnvironmentInfo.SourceEnvironmentList -contains $environmentId) + { + Write-OctopusVerbose \"Found the environment $environmentId assigned to $($tenantToDeploy.Name), attempting to find the latest release for this tenant\" + return $true + } + } + + Write-OctopusVerbose \"The tenant is not assigned to any environment in the source environments $($sourceDestinationEnvironmentInfo.SourceEnvironmentList), pulling the latest release to the environment regardless of tenant.\" + return $false +} + +function Create-NewOctopusDeployment +{ +\tparam ( + \t$releaseToDeploy, + $targetEnvironment, + $createdDeployment, + $project, + $waitForFinish, + $enableEnhancedLogging, + $deploymentCancelInSeconds, + $defaultUrl, + $octopusApiKey, + $spaceId, + $parentDeploymentApprovers, + $parentProjectName, + $parentReleaseNumber, + $parentEnvironmentName, + $parentDeploymentTaskId, + $autoapproveChildManualInterventions, + $approvalTenant + ) + + Write-OctopusSuccess \"Deploying $($releaseToDeploy.Version) to $($targetEnvironment.Name)\" + + $createdDeploymentResponse = Invoke-OctopusApi -method \"POST\" -endPoint \"deployments\" -octopusUrl $defaultUrl -apiKey $octopusApiKey -spaceId $spaceId -item $createdDeployment + Write-OctopusInformation \"The task id for the new deployment is $($createdDeploymentResponse.TaskId)\" + + Write-OctopusSuccess \"Deployment was successfully invoked, you can access the deployment [here]($defaultUrl/app#/$spaceId/projects/$($project.Slug)/deployments/releases/$($releaseToDeploy.Version)/deployments/$($createdDeploymentResponse.Id)?activeTab=taskSummary)\" + + if ($null -ne $createdDeployment.QueueTime -and $waitForFinish -eq $true) + { + \tWrite-OctopusWarning \"The option to wait for the deployment to finish was set to yes AND a future deployment date was set to a future value. Ignoring the wait for finish option and exiting.\" + return + } + + if ($waitForFinish -eq $true) + { + Write-OctopusSuccess \"Waiting until deployment has finished\" + $startTime = Get-Date + $currentTime = Get-Date + $dateDifference = $currentTime - $startTime + $lastEnhancedLoggingWriteTime = [datetime]::MinValue + + $numberOfWaits = 0 + + While ($dateDifference.TotalSeconds -lt $deploymentCancelInSeconds) + { +\t $numberOfWaits += 1 + + Write-Host \"Waiting 5 seconds to check status\" + Start-Sleep -Seconds 5 + $taskStatusResponse = Invoke-OctopusApi -octopusUrl $defaultUrl -spaceId $spaceId -apiKey $octopusApiKey -endPoint \"tasks/$($createdDeploymentResponse.TaskId)\" -method \"GET\" -ignoreCache $true + $taskStatusResponseState = $taskStatusResponse.State + + if ($taskStatusResponseState -eq \"Success\") + { + if ($enableEnhancedLogging -eq $true) + { + $lastEnhancedLoggingWriteTime = Get-TaskDetails -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -taskId $createdDeploymentResponse.TaskId -lastEnhancedLoggingWriteTime $lastEnhancedLoggingWriteTime + } + Write-OctopusSuccess \"The task has finished with a status of Success\" + exit 0 + } + elseif($taskStatusResponseState -eq \"Failed\" -or $taskStatusResponseState -eq \"Canceled\") + { + Write-OctopusSuccess \"The task has finished with a status of $taskStatusResponseState status, stopping the deployment\" + exit 1 + } + elseif($taskStatusResponse.HasPendingInterruptions -eq $true) + { + \tif ($autoapproveChildManualInterventions -eq $true) + { + \tSubmit-ChildProjectDeploymentForAutoApproval -createdDeployment $createdDeploymentResponse -parentDeploymentApprovers $parentDeploymentApprovers -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey -spaceId $spaceId -parentProjectName $parentProjectName -parentReleaseNumber $parentReleaseNumber -parentEnvironmentName $parentEnvironmentName -parentDeploymentTaskId $parentDeploymentTaskId -approvalTenant $approvalTenant + } + else + { + \tif ($numberOfWaits -ge 10) + { + \t\tWrite-OctopusSuccess \"The child project has pending manual intervention(s). Unless you approve it, this task will time out.\" + } + else + { + \tWrite-OctopusInformation \"The child project has pending manual intervention(s). Unless you approve it, this task will time out.\" + } + } + } + + if ($numberOfWaits -ge 10) + { + if ($enableEnhancedLogging -eq $true) + { + $lastEnhancedLoggingWriteTime = Get-TaskDetails -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -taskId $createdDeploymentResponse.TaskId -lastEnhancedLoggingWriteTime $lastEnhancedLoggingWriteTime + } + else + { + Write-OctopusSuccess \"The task state is currently $taskStatusResponseState\" + $numberOfWaits = 0 + } + } + else + { + if ($enableEnhancedLogging -eq $true) + { + $lastEnhancedLoggingWriteTime = Get-TaskDetails -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -taskId $createdDeploymentResponse.TaskId -lastEnhancedLoggingWriteTime $lastEnhancedLoggingWriteTime + } + else + { + Write-OctopusInformation \"The task state is currently $taskStatusResponseState\" + } + } + + $startTime = $taskStatusResponse.StartTime + if ($null -eq $startTime -or [string]::IsNullOrWhiteSpace($startTime) -eq $true) + { + Write-Host \"The task is still queued, let's wait a bit longer\" + $startTime = Get-Date + } + $startTime = [DateTime]$startTime + + $currentTime = Get-Date + $dateDifference = $currentTime - $startTime + } + + Write-OctopusCritical \"The cancel timeout has been reached, cancelling the deployment\" + Invoke-OctopusApi -octopusUrl $defaultUrl -apiKey $octopusApiKey -spaceId $spaceId -method \"POST\" -endPoint \"tasks/$($createdDeploymentResponse.TaskId)/cancel\" + Write-OctopusInformation \"Exiting with an error code of 1 because we reached the timeout\" + exit 1 + } +} + +function Get-ChildDeploymentSpecificMachines +{ + param ( + $deploymentPreview, + $deploymentMachines, + $specificMachineDeployment + ) + + if ($specificMachineDeployment -eq $false) + { + Write-OctopusVerbose \"Not doing specific machine deployments, returning any empty list of specific machines to deploy to\" + return @() + } + + $filteredList = @() + $deploymentMachineList = $deploymentMachines -split \",\" + + Write-OctopusInformation \"Doing a specific machine deployment, comparing the machines being targeted with the machines the child project can deploy to. The number of machines being targeted is $($deploymentMachineList.Count)\" + + foreach ($deploymentMachine in $deploymentMachineList) + { + $deploymentMachineLowerTrim = $deploymentMachine.Trim().ToLower() + + foreach ($step in $deploymentPreview.StepsToExecute) + { + foreach ($machine in $step.Machines) + { + $machineLowerTrim = $machine.Id.Trim().ToLower() + + Write-OctopusVerbose \"Comparing $deploymentMachineLowerTrim with $machineLowerTrim\" + if ($deploymentMachineLowerTrim -ne $machineLowerTrim) + { + Write-OctopusVerbose \"The two machine ids do not match, moving on to the next machine\" + continue + } + + Write-OctopusVerbose \"Checking to see if $machineLowerTrim is already in the filtered list.\" + if ($filteredList -notcontains $machine.Id) + { + Write-OctopusVerbose \"The machine is not in the list, adding it to the list.\" + $filteredList += $machine.Id + } + } + } + } + + if ($filteredList.Count -gt 0) + { + Write-OctopusSuccess \"The machines applicable to this project are $filteredList.\" + } + + return $filteredList +} + +function Test-ChildProjectDeploymentCanProceed +{ +\tparam ( + \t$releaseToDeploy, + $specificMachineDeployment, + $environmentName, + $childDeploymentSpecificMachines, + $project, + $ignoreSpecificMachineMismatch, + $deploymentMachines, + $releaseHasAlreadyBeenDeployed, + $isPromotionMode + ) + +\tif ($releaseHasAlreadyBeenDeployed -eq $true -and $isPromotionMode -eq $true) + {\t \t + \tWrite-OctopusSuccess \"Release $($releaseToDeploy.Version) is the most recent version deployed to $environmentName. The deployment mode is Promote. If you wish to redeploy this release then set the deployment mode to Redeploy. Skipping this project.\" + + if ($specificMachineDeployment -eq $true -and $childDeploymentSpecificMachines.Length -gt 0) + { + Write-OctopusSuccess \"$($project.Name) can deploy to $childDeploymentSpecificMachines but redeployments are not allowed.\" + } + + Insert-EmptyOutputVariables -releaseToDeploy $releaseToDeploy + + exit 0 + } + + if ($childDeploymentSpecificMachines.Length -le 0 -and $specificMachineDeployment -eq $true -and $ignoreSpecificMachineMismatch -eq $false) + { + Write-OctopusSuccess \"$($project.Name) does not deploy to $($deploymentMachines -replace \",\", \" OR \"). The value for \"\"Ignore specific machine mismatch\"\" is set to \"\"No\"\". Skipping this project.\" + + Insert-EmptyOutputVariables -releaseToDeploy $releaseToDeploy + + Exit 0 + } + + if ($childDeploymentSpecificMachines.Length -le 0 -and $specificMachineDeployment -eq $true -and $ignoreSpecificMachineMismatch -eq $true) + { + Write-OctopusSuccess \"You are doing a deployment for specific machines but $($project.Name) does not deploy to $($deploymentMachines -replace \",\", \" OR \"). You have set the value for \"\"Ignore specific machine mismatch\"\" to \"\"Yes\"\". The child project will be deployed to, but it will do this for all machines, not any specific machines.\" + } +} + +function Get-ParentDeploymentApprovers +{ + param ( + $parentDeploymentTaskId, + $spaceId, + $defaultUrl, + $octopusApiKey + ) + + $approverList = @() + if ($null -eq $parentDeploymentTaskId) + { + \tWrite-OctopusInformation \"The deployment task id to pull the approvers from is null, return an empty approver list\" + \treturn $approverList + } + + Write-OctopusInformation \"Getting all the events from the parent project\" + $parentDeploymentEvents = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"events?regardingAny=$parentDeploymentTaskId&spaces=$spaceId&includeSystem=true\" -apiKey $octopusApiKey -method \"GET\" + + foreach ($parentDeploymentEvent in $parentDeploymentEvents.Items) + { + Write-OctopusVerbose \"Checking $($parentDeploymentEvent.Message) for manual intervention\" + if ($parentDeploymentEvent.Message -like \"Submitted interruption*\") + { + Write-OctopusVerbose \"The event $($parentDeploymentEvent.Id) is a manual intervention approval event which was approved by $($parentDeploymentEvent.Username).\" + + $approverExists = $approverList | Where-Object {$_.Id -eq $parentDeploymentEvent.UserId} + + if ($null -eq $approverExists) + { + $approverInformation = @{ + Id = $parentDeploymentEvent.UserId; + Username = $parentDeploymentEvent.Username; + Teams = @() + } + + $approverInformation.Teams = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"teammembership?userId=$($approverInformation.Id)&spaces=$spaceId&includeSystem=true\" -apiKey $octopusApiKey -method \"GET\" + + Write-OctopusVerbose \"Adding $($approverInformation.Id) to the approval list\" + $approverList += $approverInformation + } + } + } + + return $approverList +} + +function Submit-ChildProjectDeploymentForAutoApproval +{ + param ( + $createdDeployment, + $parentDeploymentApprovers, + $defaultUrl, + $octopusApiKey, + $spaceId, + $parentProjectName, + $parentReleaseNumber, + $parentEnvironmentName, + $parentDeploymentTaskId, + $approvalTenant + ) + + Write-OctopusSuccess \"The task has a pending manual intervention. Checking parent approvals.\" + $manualInterventionInformation = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"interruptions?regarding=$($createdDeployment.TaskId)\" -method \"GET\" -apiKey $octopusApiKey -spaceId $spaceId -ignoreCache $true + foreach ($manualIntervention in $manualInterventionInformation.Items) + { + if ($manualIntervention.IsPending -eq $false) + { + Write-OctopusInformation \"This manual intervention has already been approved. Proceeding onto the next one.\" + continue + } + + if ($manualIntervention.CanTakeResponsibility -eq $false) + { + Write-OctopusSuccess \"The user associated with the API key doesn't have permissions to take responsibility for the manual intervention.\" + Write-OctopusSuccess \"If you wish to leverage the auto-approval functionality give the user permissions.\" + continue + } + + $automaticApprover = $null + Write-OctopusVerbose \"Checking to see if one of the parent project approvers is assigned to one of the manual intervention teams $($manualIntervention.ResponsibleTeamIds)\" + foreach ($approver in $parentDeploymentApprovers) + { + foreach ($approverTeam in $approver.Teams) + { + Write-OctopusVerbose \"Checking to see if $($manualIntervention.ResponsibleTeamIds) contains $($approverTeam.TeamId)\" + if ($manualIntervention.ResponsibleTeamIds -contains $approverTeam.TeamId) + { + $automaticApprover = $approver + break + } + } + + if ($null -ne $automaticApprover) + { + break + } + } + + if ($null -ne $automaticApprover) + { + Write-OctopusVerbose \"Found matching approvers, attempting to auto approve.\" + if ($manualIntervention.HasResponsibility -eq $false) + { + Write-OctopusInformation \"Taking over responsibility for this manual intervention.\" + $takeResponsiblilityResponse = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"interruptions/$($manualIntervention.Id)/responsible\" -method \"PUT\" -apiKey $octopusApiKey -spaceId $spaceId -ignoreCache $true + Write-OctopusVerbose \"Response from taking responsibility $($takeResponsiblilityResponse.Id)\" + } + + if ($null -ne $approvalTenant) + { + $approvalMessage = \"Parent project $parentProjectName release $parentReleaseNumber to $parentEnvironmentName for the tenant $($approvalTenant.Name) with the task id $parentDeploymentTaskId was approved by $($automaticApprover.UserName).\" + } + else + { + $approvalMessage = \"Parent project $parentProjectName release $parentReleaseNumber to $parentEnvironmentName with the task id $parentDeploymentTaskId was approved by $($automaticApprover.UserName).\" + } + + $submitApprovalBody = @{ + Instructions = $null; + Notes = \"Auto-approving this deployment. $approvalMessage That user is a member of one of the teams this manual intervention requires. You can view that deployment $defaultUrl/app#/$spaceId/tasks/$parentDeploymentTaskId\"; + Result = \"Proceed\" + } + $submitResult = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"interruptions/$($manualIntervention.Id)/submit\" -method \"POST\" -apiKey $octopusApiKey -item $submitApprovalBody -spaceId $spaceId -ignoreCache $true + Write-OctopusSuccess \"Successfully auto approved the manual intervention $($submitResult.Id)\" + } + else + { + Write-OctopusSuccess \"Couldn't find an approver to auto-approve the child project. Waiting until timeout or child project is approved.\" + } + } +} + +function Get-ReleaseNotes +{ +\tparam ( + \t$releaseToDeploy, + $deploymentPreview, + $channel, + $spaceId, + $defaultUrl, + $octopusApiKey + ) + + $releaseNotes = @(\"\") + $releaseNotes += \"**Release Information**\" + $releaseNotes += \"\" + + $packageVersionAdded = @() + $workItemsAdded = @() + $commitsAdded = @() + + if ($null -ne $releaseToDeploy.BuildInformation -and $releaseToDeploy.BuildInformation.Count -gt 0) + { + $releaseNotes += \"- Package Versions\" + foreach ($change in $deploymentPreview.Changes) + { + foreach ($package in $change.BuildInformation) + { + $packageInformation = \"$($package.PackageId).$($package.Version)\" + if ($packageVersionAdded -notcontains $packageInformation) + { + $releaseNotes += \" - $packageInformation\" + $packageVersionAdded += $packageInformation + } + } + } + +\t\t$releaseNotes += \"\" + $releaseNotes += \"- Work Items\" + foreach ($change in $deploymentPreview.Changes) + { + foreach ($workItem in $change.WorkItems) + { + if ($workItemsAdded -notcontains $workItem.Id) + { + $workItemInformation = \"[$($workItem.Id)]($($workItem.LinkUrl)) - $($workItem.Description)\" + $releaseNotes += \" - $workItemInformation\" + $workItemsAdded += $workItem.Id + } + } + } + +\t\t$releaseNotes += \"\" + $releaseNotes += \"- Commits\" + foreach ($change in $deploymentPreview.Changes) + { + foreach ($commit in $change.Commits) + { + if ($commitsAdded -notcontains $commit.Id) + { + $commitInformation = \"[$($commit.Id)]($($commit.LinkUrl)) - $($commit.Comment)\" + $releaseNotes += \" - $commitInformation\" + $commitsAdded += $commit.Id + } + } + } + } + else + { + $releaseNotes += $releaseToDeploy.ReleaseNotes + $releaseNotes += \"\" + $releaseNotes += \"Package Versions\" + + $releaseDeploymentTemplate = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"deploymentprocesses/$($releaseToDeploy.ProjectDeploymentProcessSnapshotId)/template?channel=$($channel.Id)&releaseId=$($releaseToDeploy.Id)\" -method \"GET\" -apiKey $octopusApiKey -spaceId $spaceId + + foreach ($package in $releaseToDeploy.SelectedPackages) + { + \tWrite-OctopusVerbose \"Attempting to find $($package.StepName) and $($package.ActionName)\" + + $deploymentProcessPackageInformation = $releaseDeploymentTemplate.Packages | Where-Object {$_.StepName -eq $package.StepName -and $_.actionName -eq $package.ActionName} + if ($null -ne $deploymentProcessPackageInformation) + { + $packageInformation = \"$($deploymentProcessPackageInformation.PackageId).$($package.Version)\" + if ($packageVersionAdded -notcontains $packageInformation) + { + $releaseNotes += \" - $packageInformation\" + $packageVersionAdded += $packageInformation + } + } + } + } + + return $releaseNotes -join \"`n\" +} + +function Get-QueueDate +{ +\tparam ( + \t$futureDeploymentDate + ) + + if ([string]::IsNullOrWhiteSpace($futureDeploymentDate) -or $futureDeploymentDate -eq \"N/A\") + { + \treturn $null + } + + [datetime]$outputDate = New-Object DateTime + $currentDate = Get-Date + + if ([datetime]::TryParse($futureDeploymentDate, [ref]$outputDate) -eq $false) + { + Write-OctopusCritical \"The suppplied date $futureDeploymentDate cannot be parsed by DateTime.TryParse. Please verify format and try again. Please [refer to Microsoft's Documentation](https://docs.microsoft.com/en-us/dotnet/api/system.datetime.tryparse) on supported formats.\" + exit 1 + } + + if ($currentDate -gt $outputDate) + { + \tWrite-OctopusCritical \"The supplied date $futureDeploymentDate is set for the past. All queued deployments must be in the future.\" + exit 1 + } + + return $outputDate +} + +function Get-QueueExpiryDate +{ +\tparam ( + \t$queueDate + ) + + if ($null -eq $queueDate) + { + \treturn $null + } + + return $queueDate.AddHours(1) +} + +function Insert-EmptyOutputVariables +{ +\tparam ( + \t$releaseToDeploy + ) + +\tif ($null -ne $releaseToDeploy) + { +\t\tSet-OctopusVariable -Name \"ReleaseToPromote\" -Value $($releaseToDeploy.Version) + Set-OctopusVariable -Name \"ReleaseNotes\" -value \"Release already deployed to destination environment.\" + } + else + { + \tSet-OctopusVariable -Name \"ReleaseToPromote\" -Value \"N/A\" + Set-OctopusVariable -Name \"ReleaseNotes\" -value \"No release found\" + } + + Write-OctopusInformation \"Setting the output variable ChildReleaseToDeploy to $false\" + Set-OctopusVariable -Name \"ChildReleaseToDeploy\" -Value $false +} + +function Get-ApprovalDeploymentTaskId +{ +\tparam ( + \t$autoapproveChildManualInterventions, + $parentDeploymentTaskId, + $parentReleaseId, + $parentEnvironmentName, + $approvalEnvironmentName, + $defaultUrl, + $spaceId, + $octopusApiKey, + $parentChannelId, + $parentEnvironmentId, + $approvalTenant, + $parentProject + ) + + if ($autoapproveChildManualInterventions -eq $false) + { + \tWrite-OctopusInformation \"Auto approvals are disabled, skipping pulling the approval deployment task id\" + return $null + } + + if ([string]::IsNullOrWhiteSpace($approvalEnvironmentName) -eq $true) + { + \tWrite-OctopusInformation \"Approval environment not supplied, using the current environment id for approvals.\" + return $parentDeploymentTaskId + } + + if ($approvalEnvironmentName.ToLower().Trim() -eq $parentEnvironmentName.ToLower().Trim()) + { + Write-OctopusInformation \"The approval environment is the same as the current environment, using the current task id $parentDeploymentTaskId\" + return $parentDeploymentTaskId + } + + $approvalEnvironment = Get-OctopusEnvironmentByName -environmentName $approvalEnvironmentName -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey + $releaseDeploymentList = Get-ListFromOctopusApi -octopusUrl $defaultUrl -endPoint \"releases/$parentReleaseId/deployments?skip=0&take=1000\" -method \"GET\" -apiKey $octopusApiKey -spaceId $spaceId -propertyName \"Items\" + + $lastDeploymentTime = $(Get-Date).AddYears(-50) + $approvalTaskId = $null + foreach ($deployment in $releaseDeploymentList) + { + if ($deployment.EnvironmentId -ne $approvalEnvironment.Id) + { + Write-OctopusInformation \"The deployment $($deployment.Id) deployed to $($deployment.EnvironmentId) which doesn't match $($approvalEnvironment.Id). Moving onto the next deployment.\" + continue + } + + if ($null -ne $approvalTenant -and $null -ne $deployment.TenantId -and $deployment.TenantId -ne $approvalTenant.Id) + { + Write-OctopusInformation \"The deployment $($deployment.Id) was deployed to the correct environment, $($approvalEnvironment.Id), but the deployment tenant $($deployment.TenantId) doesn't match the approval tenant $($approvalTenant.Id). Moving onto the next deployment.\" + continue + } + + Write-OctopusInformation \"The deployment $($deployment.Id) was deployed to the approval environment $($approvalEnvironment.Id).\" + + $deploymentTask = Invoke-OctopusApi -octopusUrl $defaultUrl -spaceId $null -endPoint \"tasks/$($deployment.TaskId)\" -apiKey $octopusApiKey -Method \"Get\" + if ($deploymentTask.IsCompleted -eq $false) + { + Write-OctopusInformation \"The deployment $($deployment.Id) is being deployed to the approval environment, but it hasn't completed, moving onto the next deployment.\" + continue + } + + if ($deploymentTask.IsCompleted -eq $true -and $deploymentTask.FinishedSuccessfully -eq $false) + { + Write-Information \"The deployment $($deployment.Id) was deployed to the approval environment, but it encountered a failure, moving onto the next deployment.\" + continue + } + + if ($deploymentTask.StartTime -gt $lastDeploymentTime) + { + $approvalTaskId = $deploymentTask.Id + $lastDeploymentTime = $deploymentTask.StartTime + } + } + + if ($null -eq $approvalTaskId) + { + \tWrite-OctopusVerbose \"Unable to find a deployment to the environment, determining if it should've happened already.\" + $channelInformation = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"channels/$parentChannelId\" -method \"GET\" -apiKey $octopusApiKey -spaceId $spaceId + $lifecyclePhases = Get-OctopusLifeCyclePhases -channel $channelInformation -defaultUrl $defaultUrl -spaceId $spaceId -OctopusApiKey $octopusApiKey -project $parentProject + + $foundDestinationFirst = $false + $foundApprovalFirst = $false + + foreach ($phase in $lifecyclePhases) + { + \tif (Test-PhaseContainsEnvironmentId -phase $phase -environmentId $parentEnvironmentId) + { + \tif ($foundApprovalFirst -eq $false) + { + \t$foundDestinationFirst = $true + } + } + + if (Test-PhaseContainsEnvironmentId -phase $phase -environmentId $approvalEnvironment.Id) + { + \tif ($foundDestinationFirst -eq $false) + { + \t$foundApprovalFirst = $true + } + } + } + + $messageToLog = \"Unable to find a deployment for the environment $approvalEnvironmentName. Auto approvals are disabled.\" + if ($foundApprovalFirst -eq $true) + { + \tWrite-OctopusWarning $messageToLog + } + else + { + \tWrite-OctopusInformation $messageToLog + } + + return $null + } + + return $approvalTaskId +} + +function Invoke-RefreshVariableSnapshot +{ +\tparam ( + \t$refreshVariableSnapShot, + $whatIf, + $releaseToDeploy, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + Write-OctopusVerbose \"Checking to see if variable snapshot will be updated.\" + + if ($refreshVariableSnapShot -eq \"No\") + { + \tWrite-OctopusVerbose \"Refreshing variables is set to no, skipping\" + \treturn + } + + $releaseDeploymentTemplate = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"releases/$($releaseToDeploy.Id)/deployments/template\" -spaceId $spaceId -method GET -apiKey $octopusApiKey + + if ($releaseDeploymentTemplate.IsVariableSetModified -eq $false -and $releaseDeploymentTemplate.IsLibraryVariableSetModified -eq $false) + { + \tWrite-OctopusVerbose \"Variables have not been updated since release creation, skipping\" + return + } + + if ($whatIf -eq $true) + { + \tWrite-OctopusSuccess \"Variables have been updated since release creation, whatif set to true, no update will occur.\" + return + } + + $snapshotVariables = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"releases/$($releaseToDeploy.Id)/snapshot-variables\" -spaceId $spaceId -method \"POST\" -apiKey $octopusApiKey + Write-OctopusSuccess \"Variables have been modified since release creation. Variable snapshot was updated on $($snapshotVariables.LastModifiedOn)\" +} + +function Get-MatchingOctopusDeploymentTasks +{ + param ( + $spaceId, + $project, + $tenantToDeploy, + $tenantIsAssignedToPreviousEnvironments, + $sourceDestinationEnvironmentInfo, + $defaultUrl, + $octopusApiKey + ) + + $taskEndPoint = \"tasks?skip=0&take=100&spaces=$spaceId&includeSystem=false&project=$($project.Id)&name=Deploy&states=Success\" + + if ($null -ne $tenantToDeploy -and $tenantIsAssignedToPreviousEnvironments -eq $true) + { + $taskEndPoint += \"&tenant=$($tenantToDeploy.Id)\" + } + + $taskList = @() + + foreach ($sourceEnvironmentId in $sourceDestinationEnvironmentInfo.SourceEnvironmentList) + { + $octopusTaskList = Get-ListFromOctopusApi -octopusUrl $DefaultUrl -endPoint \"$($taskEndPoint)&environment=$sourceEnvironmentId\" -spaceId $null -apiKey $octopusApiKey -method \"GET\" -propertyName \"Items\" + $taskList += $octopusTaskList + } + + $orderedTaskList = @($taskList | Sort-Object -Property StartTime -Descending) + Write-OctopusVerbose \"We have $($orderedTaskList.Count) number of tasks to loop through\" + + return $orderedTaskList +} + +function Get-TaskDetails +{ + param ( + $defaultUrl, + $spaceId, + $octopusApiKey, + $taskId, + $lastEnhancedLoggingWriteTime + ) + + $taskDetails = Invoke-OctopusApi -octopusUrl $defaultUrl -spaceId $spaceId -apiKey $octopusApiKey -endPoint \"tasks/$taskId/details\" -method 'GET' -ignoreCache $true + $activityLogs = $taskDetails.ActivityLogs + $writeStepName = $writeTargetName = $true + $returnTime = [datetime]::MinValue + + foreach ($step in $activityLogs.Children) + { + foreach ($target in $step.Children) + { + foreach ($logElement in $target.LogElements) + { + $occurredAt = [datetime]($logElement.OccurredAt) + if ($occurredAt -gt $lastEnhancedLoggingWriteTime) + { + if ($writeStepName -eq $true) + { + $trailingCount = 66 - $step.Name.Length + if ($trailingCount -lt 0) { $trailingCount = 0 } + Write-OctopusInformation \"╔═ $($step.Name) $(\"═\" * $trailingCount)\" + $writeStepName = $false + } + if ($writeTargetName -eq $true) + { + $trailingCount = 64 - $target.Name.Length + if ($trailingCount -lt 0) { $trailingCount = 0 } + Write-OctopusInformation \"║ ┌─ $($target.Name) $('─' * $trailingCount)\" + $writeTargetName = $false + } + Write-OctopusInformation \"║ │ $($logElement.MessageText)\" + if ($occurredAt -gt $returnTime) { $returnTime = $occurredAt} + } + } + if ($writeTargetName -eq $false) + { + Write-OctopusInformation \"║ └$('─' * 67)\" + $writeTargetName = $true + } + } + if ($writeStepName -eq $false) + { + Write-OctopusInformation \"╚$('═' * 69)\" + $writeStepName = $true + } + } + + return $returnTime +} + +function Get-ReleaseToDeployFromTaskList +{ + param ( + $taskList, + $channel, + $releaseNumber, + $tenantToDeploy, + $sourceDestinationEnvironmentInfo, + $defaultUrl, + $spaceId, + $octopusApiKey, + $isPromotionMode + ) + + foreach ($task in $taskList) + { + Write-OctopusVerbose \"Pulling the deployment information for $($task.Id)\" + $deploymentInformation = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"deployments/$($task.Arguments.DeploymentId)\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + + if ($deploymentInformation.ChannelId -ne $channel.Id) + { + Write-OctopusInformation \"The deployment was not for the channel we want to deploy to, moving onto next task.\" + continue + } + + Write-OctopusVerbose \"Pulling the release information for $($deploymentInformation.Id)\" + $releaseInformation = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"releases/$($deploymentInformation.ReleaseId)\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + + if ($isPromotionMode -eq $false) + { + Write-OctopusInformation \"Current mode is set to redeploy, the release is for the correct channel and was successful, using it.\" + return $releaseInformation + } + + if ([string]::IsNullOrWhiteSpace($releaseNumber) -eq $false -and $releaseInformation.Version -notlike $releaseNumber) + { + Write-OctopusInformation \"The release version $($releaseInformation.Version) does not match $releaseNumber. Moving onto the next task.\" + continue + } + + $releaseCanBeDeployed = Get-ReleaseCanBeDeployedToTargetEnvironment -defaultUrl $defaultUrl -release $releaseInformation -spaceId $spaceId -octopusApiKey $octopusApiKey -sourceDestinationEnvironmentInfo $sourceDestinationEnvironmentInfo -tenantToDeploy $tenantToDeploy -isPromotionMode $isPromotionMode -isAlwaysLatestMode $isAlwaysLatestMode + + if ($releaseCanBeDeployed -eq $true) + { + Write-OctopusInformation \"The release $($releaseInformation.Version) can be deployed to $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name).\" + return $releaseInformation + } + + Write-OctopusInformation \"The release $($releaseInformation.Version) cannot be deployed to $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name). Moving onto next task\" + } + + return $null +} + +function Get-ReleaseToDeployFromChannel +{ + param ( + $channel, + $releaseNumber, + $tenantToDeploy, + $sourceDestinationEnvironmentInfo, + $defaultUrl, + $spaceId, + $octopusApiKey, + $isPromotionMode, + $isAlwaysLatestMode + ) + + if ([string]::IsNullOrWhiteSpace($releaseNumber) -eq $false) + { + $urlReleaseNumber = $releaseNumber.Replace(\"*\", \"\") + Write-OctopusInformation \"The release number was sent in, sending $urlReleaseNumber to the channel endpoint to have the server filter on that number first.\" + $releaseChannelList = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"channels/$($channel.Id)/releases?skip=0&take=100&searchByVersion=$urlReleaseNumber\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + } + else + { + Write-OctopusInformation \"The release number was not sent in, attempting to find the latest release from the channel to deploy.\" + $releaseChannelList = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"channels/$($channel.Id)/releases?skip=0&take=100\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + } + + Write-OctopusInformation \"There are $($releaseChannelList.Items.Count) potential releases to go through.\" + + foreach ($releaseInformation in $releaseChannelList.Items) + { + if ([string]::IsNullOrWhiteSpace($releaseNumber) -eq $false -and $releaseInformation.Version -notlike $releaseNumber) + { + Write-OctopusInformation \"The release version $($releaseInformation.Version) does not match $releaseNumber. Moving onto the next release in the channel.\" + continue + } + + $releaseCanBeDeployed = Get-ReleaseCanBeDeployedToTargetEnvironment -defaultUrl $defaultUrl -release $releaseInformation -spaceId $spaceId -octopusApiKey $octopusApiKey -sourceDestinationEnvironmentInfo $sourceDestinationEnvironmentInfo -tenantToDeploy $tenantToDeploy -isPromotionMode $isPromotionMode -isAlwaysLatestMode $isAlwaysLatestMode + + if ($releaseCanBeDeployed -eq $true) + { + Write-OctopusInformation \"The release $($releaseInformation.Version) can be deployed to $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name).\" + return $releaseInformation + } + + Write-OctopusInformation \"The release $($releaseInformation.Version) cannot be deployed to $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name). Moving onto next release in the channel.\" + } + + return $null +} + +function Get-ReleaseHasAlreadyBeenPromotedToTargetEnvironment +{ + param ( + $releaseToDeploy, + $tenantToDeploy, + $sourceDestinationEnvironmentInfo, + $isPromotionMode, + $isAlwaysLatestMode, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + if ($isPromotionMode -eq $false -and $isAlwaysLatestMode -eq $false) + { + Write-OctopusInformation \"Currently in redeploy mode, of course the release has already been deployed to the target environment. Exiting the Release Has Already Been Promoted To Target Environment check.\" + return $true + } + + Write-OctopusVerbose \"Pulling the last release for the target environment to see if the release to deploy is the latest one in that environment.\" + $taskEndPoint = \"tasks?skip=0&take=1&spaces=$spaceId&includeSystem=false&project=$($releaseToDeploy.ProjectId)&name=Deploy&states=Success&environment=$($sourceDestinationEnvironmentInfo.TargetEnvironment.Id)\" + + if ($null -ne $tenantToDeploy) + { + $taskEndPoint += \"&tenant=$($tenantToDeploy.Id)\" + } + + $octopusTaskList = Get-ListFromOctopusApi -octopusUrl $DefaultUrl -endPoint \"$taskEndPoint\" -spaceId $null -apiKey $octopusApiKey -method \"GET\" -propertyName \"Items\" + + if ($octopusTaskList.Count -eq 0) + { + Write-OctopusInformation \"There have been no releases to $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name) for this project.\" + return $false + } + + $task = $octopusTaskList[0] + $deploymentInformation = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"deployments/$($task.Arguments.DeploymentId)\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + + if ($releaseToDeploy.Id -eq $deploymentInformation.ReleaseId) + { + Write-OctopusInformation \"The release to deploy $($release.ReleaseNumber) is the last successful release to $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name)\" + return $true + } + + Write-OctopusInformation \"The release to deploy $($release.ReleaseNumber) is different than the last successful release to $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name)\" + return $false +} + +function Get-MachineIdsFromMachineNames +{ + param ( + $targetMachines, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + $targetMachineList = $targetMachines -split \",\" + $translatedList = @() + + foreach ($machineName in $targetMachineList) + { + \t$trimmedMachineName = $machineName.Trim() + Write-OctopusVerbose \"Translating $trimmedMachineName into an Octopus Id\" + \tif ($trimmedMachineName -like \"Machines-*\") + { + \tWrite-OctopusVerbose \"$trimmedMachineName is already an Octopus Id, adding it to the list\" + \t$translatedList += $machineName + continue + } + + $machineObject = Get-OctopusItemByName -itemName $trimmedMachineName -itemType \"Deployment Target\" -endpoint \"machines\" -defaultValue $null -spaceId $spaceId -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey + + $translatedList += $machineObject.Id + } + + return $translatedList -join \",\" +} + +function Write-ReleaseInformation +{ + param ( + $releaseToDeploy, + $environmentList + ) + + $releaseDeployments = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"releases/$($releaseToDeploy.Id)/deployments\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + $releaseEnvironmentList = @() + + foreach ($deployment in $releaseDeployments.Items) + { + $releaseEnvironment = $environmentList | Where-Object {$_.Id -eq $deployment.EnvironmentId } + + if ($null -ne $releaseEnvironment -and $releaseEnvironmentList -notcontains $releaseEnvironment.Name) + { + Write-OctopusVerbose \"Adding $($releaseEnvironment.Name) to the list of environments this release has been deployed to\" + $releaseEnvironmentList += $releaseEnvironment.Name + } + } + + if ($releaseEnvironmentList.Count -gt 0) + { + Write-OctopusSuccess \"The release to deploy is $($releaseToDeploy.Version) which has been deployed to $($releaseEnvironmentList -join \",\")\" + } + else + { + Write-OctopusSuccess \"The release to deploy is $($releaseToDeploy.Version) which currently has no deployments.\" + } +} + +function Get-GuidedFailureMode +{ +\tparam ( + \t$projectToDeploy, + $environmentToDeployTo + ) + + Write-OctopusInformation \"Checking $($projectToDeploy.DefaultGuidedFailureMode) and $($environmentToDeployTo.UseGuidedFailure) to determine guided failure mode.\" + + if ($projectToDeploy.DefaultGuidedFailureMode -eq \"EnvironmentDefault\" -and $environmentToDeployTo.UseGuidedFailure -eq $true) + { + \tWrite-OctopusInformation \"Guided failure for the project is set to environment default, and destination environment says to use guided failure. Setting guided failure to true.\" + return $true + } + + if ($projectToDeploy.DefaultGuidedFailureMode -eq \"On\") + { + \tWrite-OctopusInformation \"Guided failure for the project is set to always use guided falure. Setting guided failure to true.\" + return $true + } + + Write-OctopusInformation \"Guided failure is not turned on for the project nor the environment. Setting to false.\" + return $false +} + +Write-OctopusInformation \"Octopus SpaceId: $destinationSpaceId\" +Write-OctopusInformation \"Octopus Deployment Task Id: $parentDeploymentTaskId\" +Write-OctopusInformation \"Octopus Project Name: $parentProjectName\" +Write-OctopusInformation \"Octopus Release Number: $parentReleaseNumber\" +Write-OctopusInformation \"Octopus Release Id: $parentReleaseId\" +Write-OctopusInformation \"Octopus Environment Name: $parentEnvironmentName\" +Write-OctopusInformation \"Octopus Release Channel Id: $parentChannelId\" +Write-OctopusInformation \"Octopus Specific deployment machines: $specificMachines\" +Write-OctopusInformation \"Octopus Exclude deployment machines: $excludeMachines\" +Write-OctopusInformation \"Octopus deployment machines: $deploymentMachines\" + +Write-OctopusInformation \"Child Project Name: $projectName\" +Write-OctopusInformation \"Child Project Space Name: $destinationSpaceName\" +Write-OctopusInformation \"Child Project Channel Name: $channelName\" +Write-OctopusInformation \"Child Project Release Number: $releaseNumber\" +Write-OctopusInformation \"Child Project Error Handle No Release Found: $errorHandleForNoRelease\" +Write-OctopusInformation \"Destination Environment Name: $environmentName\" +Write-OctopusInformation \"Source Environment Name: $sourceEnvironmentName\" +Write-OctopusInformation \"Ignore specific machine mismatch: $ignoreSpecificMachineMismatchValue\" +Write-OctopusInformation \"Save release notes as artifact: $saveReleaseNotesAsArtifactValue\" +Write-OctopusInformation \"What If: $whatIfValue\" +Write-OctopusInformation \"Wait for finish: $waitForFinishValue\" +Write-OctopusInformation \"Cancel deployment in seconds: $deploymentCancelInSeconds\" +Write-OctopusInformation \"Scheduling: $futureDeploymentDate\" +Write-OctopusInformation \"Auto-Approve Child Project Manual Interventions: $autoapproveChildManualInterventionsValue\" +Write-OctopusInformation \"Approval Environment: $approvalEnvironmentName\" +Write-OctopusInformation \"Approval Tenant: $approvalTenantName\" +Write-OctopusInformation \"Refresh Variable Snapshot: $refreshVariableSnapShot\" +Write-OctopusInformation \"Deployment Mode: $deploymentMode\" +Write-OctopusInformation \"Target Machine Names: $targetMachines\" +Write-OctopusInformation \"Deployment Tenant Name: $deploymentTenantName\" + +$whatIf = $whatIfValue -eq \"Yes\" +$waitForFinish = $waitForFinishValue -eq \"Yes\" +$enableEnhancedLogging = $enableEnhancedLoggingValue -eq \"Yes\" +$ignoreSpecificMachineMismatch = $ignoreSpecificMachineMismatchValue -eq \"Yes\" +$autoapproveChildManualInterventions = $autoapproveChildManualInterventionsValue -eq \"Yes\" +$saveReleaseNotesAsArtifact = $saveReleaseNotesAsArtifactValue -eq \"Yes\" + +$verificationPassed = @() +$verificationPassed += Test-RequiredValues -variableToCheck $octopusApiKey -variableName \"Octopus API Key\" +$verificationPassed += Test-RequiredValues -variableToCheck $destinationSpaceName -variableName \"Child Project Space\" +$verificationPassed += Test-RequiredValues -variableToCheck $projectName -variableName \"Child Project Name\" +$verificationPassed += Test-RequiredValues -variableToCheck $environmentName -variableName \"Destination Environment Name\" + +if ($verificationPassed -contains $false) +{ +\tWrite-OctopusInformation \"Required values missing\" +\tExit 1 +} + +$isPromotionMode = $deploymentMode -eq \"Promote\" +$isAlwaysLatestMode = $deploymentMode -eq 'AlwaysLatest' +$spaceId = Get-OctopusSpaceIdByName -spaceName $destinationSpaceName -spaceId $destinationSpaceId -defaultUrl $defaultUrl -OctopusApiKey $octopusApiKey + +Write-OctopusSuccess \"The current mode of the step template is $deploymentMode\" + +if ($isAlwaysLatestMode -eq $true) +{ + Write-OctopusSuccess \"Currently in AlwaysLatest mode, release number filter will be ignored, source environment will be set to the target environment, all redeployment checks will be ignored.\" +} + +if ($isPromotionMode -eq $false -and $isAlwaysLatestMode -eq $false) +{ + Write-OctopusSuccess \"Currently in redeploy mode, release number filter will be ignored, source environment will be set to the target environment, all redeployment checks will be ignored.\" +} + +if ($isPromotionMode -eq $true -and [string]::IsNullOrWhiteSpace($sourceEnvironmentName) -eq $false -and $sourceEnvironmentName.ToLower().Trim() -eq $environmentName.ToLower().Trim()) +{ + Write-OctopusSuccess \"The current mode is promotion. Both the source environment and destination environment are the same. You cannot promote from the same environment as the source environment. Exiting. Change the deployment mode value to redeploy if you want to redeploy.\" + Exit 0 +} + +$specificMachineDeployment = $false +if ([string]::IsNullOrWhiteSpace($specificMachines) -eq $false) +{ +\tWrite-OctopusSuccess \"This deployment is targeting the specific machines $specificMachines.\" +\t$specificMachineDeployment = $true +} + +if ([string]::IsNullOrWhiteSpace($excludeMachines) -eq $false) +{ +\tWrite-OctopusSuccess \"This deployment is excluding the specific machines $excludeMachines. The machines being deployed to are: $deploymentMachines.\" + $specificMachineDeployment = $true +} + +if ([string]::IsNullOrWhiteSpace($targetMachines) -eq $false -and $targetMachines -ne \"N/A\") +{ + Write-OctopusSuccess \"You have specified specific machines to target in this deployment. Ignoring the machines that triggered this deployment.\" + $specificMachineDeployment = $true + $deploymentMachines = Get-MachineIdsFromMachineNames -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey -spaceId $spaceId -targetMachines $targetMachines +} + +$project = Get-OctopusProjectByName -projectName $projectName -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey +$parentProject = Get-OctopusProjectByName -projectName $parentProjectName -defaultUrl $defaultUrl -spaceId $parentSpaceId -octopusApiKey $octopusApiKey +$tenantToDeploy = Get-OctopusTenantByName -tenantName $deploymentTenantName -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey +$targetEnvironment = Get-OctopusEnvironmentByName -environmentName $environmentName -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey +$tenantToDeploy = Test-ProjectTenantSettings -tenantToDeploy $tenantToDeploy -project $project -targetEnvironment $targetEnvironment + +$sourceEnvironment = Get-OctopusEnvironmentByName -environmentName $sourceEnvironmentName -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey +$channel = Get-OctopusChannel -channelName $channelName -defaultUrl $defaultUrl -project $project -spaceId $spaceId -octopusApiKey $octopusApiKey +$phaseList = Get-OctopusLifecyclePhases -channel $channel -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -project $project +$sourceDestinationEnvironmentInfo = Get-SourceDestinationEnvironmentInformation -phaseList $phaseList -targetEnvironment $targetEnvironment -sourceEnvironment $sourceEnvironment -isPromotionMode $isPromotionMode -isAlwaysLatestMode $isAlwaysLatestMode + +if ($deploymentMode -eq 'AlwaysLatest') +{ + Write-OctopusInformation \"Finding the latest release that can be deployed.\" + $releaseToDeploy = Get-ReleaseToDeployFromChannel -channel $channel -releaseNumber $releaseNumber -tenantToDeploy $tenantToDeploy -sourceDestinationEnvironmentInfo $sourceDestinationEnvironmentInfo -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -isPromotionMode $isPromotionMode -isAlwaysLatestMode $isAlwaysLatestMode +} +elseif ($sourceDestinationEnvironmentInfo.FirstLifecyclePhase -eq $false) +{ + $tenantIsAssignedToPreviousEnvironments = Get-TenantIsAssignedToPreviousEnvironments -tenantToDeploy $tenantToDeploy -sourceDestinationEnvironmentInfo $sourceDestinationEnvironmentInfo -projectId $project.Id -isPromotionMode $isPromotionMode + $taskList = Get-MatchingOctopusDeploymentTasks -spaceId $spaceId -project $project -tenantToDeploy $tenantToDeploy -tenantIsAssignedToPreviousEnvironments $tenantIsAssignedToPreviousEnvironments -sourceDestinationEnvironmentInfo $sourceDestinationEnvironmentInfo -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey + $releaseToDeploy = Get-ReleaseToDeployFromTaskList -taskList $taskList -channel $channel -releaseNumber $releaseNumber -tenantToDeploy $tenantToDeploy -sourceDestinationEnvironmentInfo $sourceDestinationEnvironmentInfo -defaultUrl $DefaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -isPromotionMode $isPromotionMode + + if ($null -eq $releaseToDeploy -and $sourceDestinationEnvironmentInfo.HasRequiredPhase -eq $false) + { + Write-OctopusInformation \"No release was found that has been deployed. However, all the phases prior to the destination phase is optional. Checking to see if any releases exist at the channel level that haven't been deployed.\" + $releaseToDeploy = Get-ReleaseToDeployFromChannel -channel $channel -releaseNumber $releaseNumber -tenantToDeploy $tenantToDeploy -sourceDestinationEnvironmentInfo $sourceDestinationEnvironmentInfo -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -isPromotionMode $isPromotionMode -isAlwaysLatestMode $isAlwaysLatestMode + } +} +else +{ + $releaseToDeploy = Get-ReleaseToDeployFromChannel -channel $channel -releaseNumber $releaseNumber -tenantToDeploy $tenantToDeploy -sourceDestinationEnvironmentInfo $sourceDestinationEnvironmentInfo -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -isPromotionMode $isPromotionMode -isAlwaysLatestMode $isAlwaysLatestMode +} + +$environmentList = Get-ListFromOctopusApi -octopusUrl $defaultUrl -endPoint \"environments?skip=0&take=1000\" -spaceId $spaceId -propertyName \"Items\" -apiKey $octopusApiKey + +Test-ReleaseToDeploy -releaseToDeploy $releaseToDeploy -errorHandleForNoRelease $errorHandleForNoRelease -releaseNumber $releaseNumber -sourceDestinationEnvironmentInfo $sourceDestinationEnvironmentInfo -environmentList $environmentList + +if ($null -ne $releaseToDeploy) +{ + Write-ReleaseInformation -releaseToDeploy $releaseToDeploy -environmentList $environmentList +} + +$releaseHasAlreadyBeenDeployed = Get-ReleaseHasAlreadyBeenPromotedToTargetEnvironment -releaseToDeploy $releaseToDeploy -tenantToDeploy $tenantToDeploy -sourceDestinationEnvironmentInfo $sourceDestinationEnvironmentInfo -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -isPromotionMode $isPromotionMode -isAlwaysLatestMode $isAlwaysLatestMode + +$deploymentPreview = Get-DeploymentPreview -releaseToDeploy $releaseToDeploy -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -targetEnvironment $targetEnvironment -deploymentTenant $tenantToDeploy +$childDeploymentSpecificMachines = Get-ChildDeploymentSpecificMachines -deploymentPreview $deploymentPreview -deploymentMachines $deploymentMachines -specificMachineDeployment $specificMachineDeployment +$deploymentFormValues = Get-ValuesForPromptedVariables -formValues $formValues -deploymentPreview $deploymentPreview + +$queueDate = Get-QueueDate -futureDeploymentDate $futureDeploymentDate +$queueExpiryDate = Get-QueueExpiryDate -queueDate $queueDate +$useGuidedFailure = Get-GuidedFailureMode -projectToDeploy $project -environmentToDeployTo $targetEnvironment + +$createdDeployment = @{ + EnvironmentId = $targetEnvironment.Id; + ExcludeMachineIds = @(); + ForcePackageDownload = $false; + ForcePackageRedeployment = $false; + FormValues = $deploymentFormValues; + QueueTime = $queueDate; + QueueTimeExpiry = $queueExpiryDate; + ReleaseId = $releaseToDeploy.Id; + SkipActions = @(); + SpecificMachineIds = @($childDeploymentSpecificMachines); + TenantId = $null; + UseGuidedFailure = $useGuidedFailure +} + +if ($null -ne $tenantToDeploy -and $project.TenantedDeploymentMode -ne \"Untenanted\") +{ + $createdDeployment.TenantId = $tenantToDeploy.Id +} + +if ($whatIf -eq $true) +{ \t + Write-OctopusVerbose \"Would have done a POST to /api/$spaceId/deployments with the body:\" + Write-OctopusVerbose $($createdDeployment | ConvertTo-JSON) + + Write-OctopusSuccess \"What If set to true.\" + Write-OctopusSuccess \"Setting the output variable ReleaseToPromote to $($releaseToDeploy.Version).\" +\tSet-OctopusVariable -Name \"ReleaseToPromote\" -Value ($releaseToDeploy.Version) +} + +Write-OctopusVerbose \"Getting the release notes\" +$releaseNotes = Get-ReleaseNotes -releaseToDeploy $releaseToDeploy -deploymentPreview $deploymentPreview -channel $channel -spaceId $spaceId -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey +Write-OctopusSuccess \"Setting the output variable ReleaseNotes which contains the release notes from the child project\" +Set-OctopusVariable -Name \"ReleaseNotes\" -value $releaseNotes + +Test-ChildProjectDeploymentCanProceed -releaseToDeploy $releaseToDeploy -specificMachineDeployment $specificMachineDeployment -environmentName $environmentName -childDeploymentSpecificMachines $childDeploymentSpecificMachines -project $project -ignoreSpecificMachineMismatch $ignoreSpecificMachineMismatch -deploymentMachines $deploymentMachines -releaseHasAlreadyBeenDeployed $releaseHasAlreadyBeenDeployed -isPromotionMode $isPromotionMode + +if ($saveReleaseNotesAsArtifact -eq $true) +{ +\t$releaseNotes | Out-File \"ReleaseNotes.txt\" + $currentDate = Get-Date +\t$currentDateFormatted = $currentDate.ToString(\"yyyy_MM_dd_HH_mm\") + $artifactName = \"$($project.Name) $($releaseToDeploy.Version) $($sourceDestinationEnvironmentInfo.TargetEnvironment.Name).ReleaseNotes_$($currentDateFormatted).txt\" + Write-OctopusInformation \"Creating the artifact $artifactName\" + +\tNew-OctopusArtifact -Path \"ReleaseNotes.txt\" -Name $artifactName +} + +Invoke-RefreshVariableSnapshot -refreshVariableSnapShot $refreshVariableSnapShot -whatIf $whatIf -releaseToDeploy $releaseToDeploy -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey + +if ($whatif -eq $true) +{ + Write-OctopusSuccess \"Exiting because What If set to true.\" + Write-OctopusInformation \"Setting the output variable ChildReleaseToDeploy to $true\" + Set-OctopusVariable -Name \"ChildReleaseToDeploy\" -Value $true + Exit 0 +} + +$approvalTenant = Get-OctopusApprovalTenant -tenantToDeploy $tenantToDeploy -approvalTenantName $approvalTenantName -spaceId $spaceId -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey +$approvalDeploymentTaskId = Get-ApprovalDeploymentTaskId -autoapproveChildManualInterventions $autoapproveChildManualInterventions -parentDeploymentTaskId $parentDeploymentTaskId -parentReleaseId $parentReleaseId -parentEnvironmentName $parentEnvironmentName -approvalEnvironmentName $approvalEnvironmentName -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -parentChannelId $parentChannelId -parentEnvironmentId $parentEnvironmentId -approvalTenant $approvalTenant -parentProject $parentProject +$parentDeploymentApprovers = Get-ParentDeploymentApprovers -parentDeploymentTaskId $approvalDeploymentTaskId -spaceId $spaceId -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey + +Create-NewOctopusDeployment -releaseToDeploy $releaseToDeploy -targetEnvironment $targetEnvironment -createdDeployment $createdDeployment -project $project -waitForFinish $waitForFinish -enableEnhancedLogging $enableEnhancedLogging -deploymentCancelInSeconds $deploymentCancelInSeconds -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey -spaceId $spaceId -parentDeploymentApprovers $parentDeploymentApprovers -parentProjectName $parentProjectName -parentReleaseNumber $parentReleaseNumber -parentEnvironmentName $approvalEnvironmentName -parentDeploymentTaskId $approvalDeploymentTaskId -autoapproveChildManualInterventions $autoapproveChildManualInterventions -approvalTenant $approvalTenant" + }, + "Parameters": [ + { + "Id": "311597af-b1d5-42c1-bb8b-88c20d322989", + "Name": "ChildProject.Web.ServerUrl", + "Label": "Octopus Base Url", + "HelpText": "**Required** + +The base URL of your instance. For example `https://samples.octopus.app`. Defaults to the system variable [Octopus.Web.ServerUri](https://octopus.com/docs/projects/variables/system-variables#Systemvariables-Server).", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f7357d18-33c3-4f1e-883d-613e13e098cd", + "Name": "ChildProject.Api.Key", + "Label": "Octopus API Key", + "HelpText": "*Required* + + +The API key of the user authorized to query and promote releases.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e3337365-c83f-4f73-a7d0-c3f36469a70d", + "Name": "ChildProject.Space.Name", + "Label": "Child Project Space", + "HelpText": "*Required* + +The name of the space the child project is located in. Defaults to the current space name", + "DefaultValue": "#{Octopus.Space.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d9afe2db-720a-40d0-928e-6d1763286fc9", + "Name": "ChildProject.Project.Name", + "Label": "Child Project Name", + "HelpText": "*Required* + + +The name of the child project you wish to deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "dfe2c259-11c4-491f-900a-db7c37d82836", + "Name": "ChildProject.Release.Number", + "Label": "Child Project Release Number", + "HelpText": "*Optional* + + +The release number to filter on. This field accepts: +- *No value (default)* - the most recent release for the channel in the calculated previous environment or specified previous environment +- *Wild card* - Example: `2020.2.*` will find the most recent release with a major version of 2020 and a minor version of 2. Please note the period is important, if you enter `2020.1*` you could end up with 2020.10 releases. +- *Specific version* - Example: `2020.2.1.2` will deploy that specific version.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c64e9163-ce84-4f56-8509-af849262a9be", + "Name": "ChildProject.Release.NotFoundError", + "Label": "Child Project Release Not Found Error Handle", + "HelpText": "What this step should do when a matching release cannot be found. + +- `Error` - Stops the deployment when no matching release is found. For example, you specified `2021.1.2.*`, but there is no release in the child project matching that pattern. Or, matching releases have been found but they cannot be promoted to the destination environment. +- `Warning` - Stops this step with a warning. The rest of the deployment continues. +- `Skip` - Stops this with no error or warning. Logs an information message. The rest of the deployment continues. + +The default is `Warning`.", + "DefaultValue": "Warning", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Error|Error +Warning|Warning +Skip|Skip" + } + }, + { + "Id": "04b2e549-888f-4a10-811c-f325690b3a80", + "Name": "ChildProject.Destination.EnvironmentName", + "Label": "Destination Environment Name", + "HelpText": "*Required* + +The name of the destination environment. + +Examples: `Development`, `Test`, or `Production` + + +The parent project and child project do *NOT* have to have the same lifecycle. The only requirement is all the previous phases' requirements in the child project must be met. For example, if the child project's life cycle is Dev->Test->Staging->Prod and the parent project lifecycle is Staging->Prod and you wish to deploy to staging, then the child project's release must go through Dev and Test first.", + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e439fdb3-0697-4ea1-8b2c-8e343bfaa5af", + "Name": "ChildProject.SourceEnvironment.Name", + "Label": "Source Environment Name", + "HelpText": "*Optional* + + +The name of the source environment. When blank the source environment will be calculated using the channel's lifecycle. + + +Examples: `Development`, `Test`, or `Production` + + +**Please Note:** The most recently created release will be selected when the destination environment is the first phase of the child project's lifecycle.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "789a658e-ee7f-4bf2-aaa3-cf1a998adf57", + "Name": "ChildProject.Channel.Name", + "Label": "Child Project Channel", + "HelpText": "*Optional* + + +The name of the channel in the child project tied to the release you wish to deploy. If left blank it will look at the project's default channel.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d462feb8-1911-401c-8329-b7c9775ac4fd", + "Name": "ChildProject.Tenant.Name", + "Label": "Tenant Name", + "HelpText": "*Optional* + +The name of the tenant you wish to deploy.", + "DefaultValue": "#{Octopus.Deployment.Tenant.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4f9b232a-43ce-484c-9b30-0d3b195ddb15", + "Name": "ChildProject.Prompted.Variables", + "Label": "Child Project Prompted Variables", + "HelpText": "*Optional* + + +Values for any prompted variables for the release. Each new line represents a new variable. This will only work with string variable types, text, and sensitive values. + + +Use the format Name::Value + + +For example: + + +``` +PromptedVariableName::My Super Awesome Value +OtherPromptedVariable::Other Super Awesome Value +```", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "3149fde0-05dc-4582-ac85-8539220fc09b", + "Name": "ChildProject.DeploymentMode.Value", + "Label": "Deployment Mode", + "HelpText": "**Required** + +Indicates if the step template will promote a release from one environment to another, or it will redeploy an existing release. + +Options: +- `Promote`: the step template will promote the release from one environment to another +- `Redeploy`: the step template will take whatever the last release deployed to the target environment and redeploy it. Useful when you are rebuilding a server. +- `AlwaysLatest`: the step template will find the latest release available to the target environment and deploy it, regardless if it has already been deployed or not. + +Default is `Promote` +", + "DefaultValue": "Promote", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Promote|Promote +Redeploy|Redeploy +AlwaysLatest|AlwaysLatest" + } + }, + { + "Id": "f231c791-d431-493f-8802-64adcb6a6de1", + "Name": "ChildProject.RefreshVariableSnapShots.Option", + "Label": "Refresh Variables Snapshots", + "HelpText": "Allows you to choose if/when variable snapshots will be refreshed. + +- `Yes`: Variable snapshot is refreshed when a change is detected +- `No`: Never refresh the variable snapshot + +Default is `No`. Recommend configuring a prompted variable to control this option at deployment time. When What If is set to `Yes` this will report a change has been made, but the snapshot refresh will not run. +", + "DefaultValue": "No", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "No|No +Yes|Yes" + } + }, + { + "Id": "6b587acb-7278-45ea-bda5-3bd8f4ecc211", + "Name": "ChildProject.Target.MachineNames", + "Label": "Specific Deployment Target", + "HelpText": "**Optional** + +A comma-separated list of deployment targets names or ids you want to deploy to. If using an id, it must start with `Machines-`. + +Please note: this step template will filter out any targets your child project cannot deploy to. If you provide a list of 5 machines and the child project can only deploy to 3 of them, then the child project will get 3 machines sent to it. + +This will overwrite the machines sent in via a deployment target trigger. + +Defaults `N/A`. If you do want to use this parameter the recommendation is to use prompted variables where you pass in specific machine names.", + "DefaultValue": "N/A", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "02805228-fde7-4c1d-853f-acad7e819ba3", + "Name": "ChildProject.Deployment.IgnoreSpecificMachineMismatch", + "Label": "Ignore specific machine mismatch", + "HelpText": "When the parent project is deploying to specific machines and the child project isn't associated with those machines the step will ignore it. + +Examples: +- A deployment target trigger fires for a newly created machine. Only 1 out of 5 child projects deploy to that newly created machine's roles. +- A redeployment needs to occur, but only for a specific machine. Only 2 out of the 4 child projects deploy to that specific machine's roles. + +In both those examples, the default behavior of this step template is to skip those child projects not tied to the machine's roles. When the child project is invoked, the specific matching machines will be sent to the child project. + +Set to `Yes` to ignore the difference. Default is `No`. Warning, setting to `Yes` could result in a failed deployment.", + "DefaultValue": "No", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "No|No +Yes|Yes" + } + }, + { + "Id": "aefc5d01-907c-47f6-a1d6-efaf97eefb6c", + "Name": "ChildProject.ReleaseNotes.SaveAsArtifact", + "Label": "Save release notes as artifact", + "HelpText": "This step will pull the release notes (or build information) from the child project and will save it to the output variable `ReleaseNotes`. + +This option allows you to save those release notes as an artifact. The default is `No`.", + "DefaultValue": "No", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "No|No +Yes|Yes" + } + }, + { + "Id": "563136dd-b848-49f8-b9ef-57acc879dbc2", + "Name": "ChildProject.WhatIf.Value", + "Label": "What If", + "HelpText": "By default, this step will trigger a deployment. + + +Setting this value to Yes will perform all the work up to triggering the deployment. This is useful for approval steps, you can run this step (or set of steps) to get the list of child releases to deploy, and then verify them via a manual intervention. + + +When this is set to `Yes` it will set an output variable `ReleaseToPromote`.", + "DefaultValue": "No", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Yes|Yes +No|No" + } + }, + { + "Id": "d1d42293-e6fc-425e-a81c-973cc81eaa1d", + "Name": "ChildProject.WaitForFinish.Value", + "Label": "Wait for finish", + "HelpText": "Set to `Yes` to avoid waiting for the deployment to finish. Will only be used when *What If* is set to `No`.", + "DefaultValue": "Yes", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Yes|Yes +No|No" + } + }, + { + "Id": "52a65ba4-4ded-4f55-82e0-dc5a8c71a2f8", + "Name": "ChildProject.EnableEnhancedLogging.Value", + "Label": "Enable Enhanced Logging", + "HelpText": "Set to `Yes` to retrieve output of the child release while waiting for the deployment to finish. Will only be used when *What If* is set to `No` and *Wait for finish* is set to `Yes`.", + "DefaultValue": "No", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Yes|Yes +No|No" + } + }, + { + "Id": "fd04d6fc-cc78-4a4f-9373-e48c349c9b07", + "Name": "ChildProject.CancelDeployment.Seconds", + "Label": "Wait for Deployment", + "HelpText": "Amount of time, in seconds, to wait for the deployment to finish. Default is 1800 seconds, or 30 minutes.", + "DefaultValue": "1800", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7c3e75d3-a91c-47a1-acaa-1c6ea9f0699d", + "Name": "ChildProject.Deployment.FutureTime", + "Label": "Scheduling", + "HelpText": "**Optional** + +Schedule the deployment for the future. Please note, if this is set, the `Wait for Deployment` option is ignored. + +Uses `DateTime.TryParse` to determine the value sent in. Supported formats: + +- `7:00 PM` will deploy at 7:00 PM today +- `21:00` will deploy at 21:00 hours or 9 PM today +- `YYYY-MM-DD HH:mm:ss` or `2021-01-14 21:00:00` will deploy at 9 PM on the 14th of January, 2021 +- `YYYY/MM/DD HH:mm:ss` or `2021/03/20 22:00:00` will deploy at 10 PM on the 20th of March, 2021 +- `MM/DD/YYYY HH:mm:ss` or `06/25/2021 19:00:00` will deploy at 7 PM on the 25th of June, 2021 +- `DD MMM YYYY HH:mm:ss` or `01 Jan 2021 18:00:00` will deploy at 6 PM on the 1st of January, 2021 + +Uses the Octopus Server's Timezone. The queue expiry time will be set to 1 hour from the supplied date. + +Default is `N/A` or not applicable. ", + "DefaultValue": "N/A", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5789f25c-6efb-4bbc-9236-2939faf7c553", + "Name": "ChildProject.ManualInterventions.UseApprovalsFromParent", + "Label": "Auto-Approve Child Project Manual Interventions", + "HelpText": "If the child project has manual interventions the step will look for manual interventions in the parent project. + +When a manual intervention in the parent project is found it will check that user's assigned teams. If that user's assigned teams can approve the child project it will do so. + +Please note, the user associated with the API key must be able to approve the child project manual interventions as well. + +The default is `Yes`, allow this to happen. Set it to `No` to skip this functionality.", + "DefaultValue": "Yes", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "No|No +Yes|Yes" + } + }, + { + "Id": "2a93a2e9-562f-4985-8933-acbf1b0e6df6", + "Name": "ChildProject.ManualIntervention.EnvironmentToUse", + "Label": "Approval Environment", + "HelpText": "*Optional* + +The name of the environment you wish to pull the approvals from for the parent project. It will look at all the deployments for the current release of the parent project and select the latest deployment to the specified environment. + +Used when you are deploying to `Production` but want to pull the approvals from `Staging` or a `Prod Approval` environment. + +Defaults to the current environment name.", + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "11101484-c68a-4a8b-aa4d-3f0a31e36ae2", + "Name": "ChildProject.ManualIntervention.Tenant.Name", + "Label": "Approval Tenant", + "HelpText": "*Optional* + +The name of the tenant you wish to pull the approvals from for the parent project. It will look at all the deployments for the current release of the parent project and select the latest deployment to the specified environment. + +Used when you are deploying to `Production` but want to pull the approvals from `Staging` or a `Prod Approval` environment. + +Defaults to the current tenant name. Will be skipped when doing a non-tenanted deployment.", + "DefaultValue": "#{Octopus.Deployment.Tenant.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-11-17T03:06:39.613Z", + "OctopusVersion": "2023.2.13113", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "REOScotte", + "Category": "octopus" +} diff --git a/step-templates/determine-rolling-deploy-window-size.json.human b/step-templates/determine-rolling-deploy-window-size.json.human new file mode 100644 index 000000000..ebce9311b --- /dev/null +++ b/step-templates/determine-rolling-deploy-window-size.json.human @@ -0,0 +1,129 @@ +{ + "Id": "cb1b825e-d945-43e4-a572-d945654ca9cc", + "Name": "Determine Rolling Deploy Window Size", + "Description": "Determine Window Size for Rolling Deploy.", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "#region Verify variables + +#No need to verify PerformRollingDeploy as this is a checkbox and will always have a boolean value. Report value back for logging. +Try +{ + $performRollingDeploy = [System.Convert]::ToBoolean($OctopusParameters['DRDWSPerformRollingDeploy']) + Write-Host ('Perform Rolling Deploy: ' + $performRollingDeploy) +} +Catch +{ + Throw \"Cannot convert Perform Rolling Deploy: '\" + $OctopusParameters['DRDWSPerformRollingDeploy'] + \"' to boolean value. Try having the expression or variable evaluate to 'True' or 'False'.\" +} + +#Verify ServerPercentageToDeploy can be converted to integer. +If ([string]::IsNullOrEmpty($OctopusParameters['DRDWSServerPercentageToDeploy'])) +{ + Throw 'Server percentage to deploy cannot be null.' +} + +[int]$serverPercentageToDeploy = 0 +[bool]$result = [int]::TryParse($OctopusParameters['DRDWSServerPercentageToDeploy'], [ref]$serverPercentageToDeploy) + +If ($result) +{ + Write-Host ('Server percentage to deploy: ' + $serverPercentageToDeploy + '%') + $serverPercentToDisconnect = $serverPercentageToDeploy / 100 +} +Else +{ + Throw \"Cannot convert Server percentage to deploy: '\" + $OctopusParameters['DRDWSServerPercentageToDeploy'] + \"' to integer.\" +} + +#Verify ServerRole is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['DRDWSServerRole'])) +{ + Throw 'Server Role for Rolling Deploy cannot be null.' +} +$role = $OctopusParameters['DRDWSServerRole'] +Write-Host ('Server Role for Rolling Deploy: ' + $role) + +#endregion + + +#region Process + +$serverCountToDeployTo = 9999 + +If ($performRollingDeploy) +{ + $servers = $OctopusParameters['Octopus.Environment.MachinesInRole[' + $role + ']'] + $totalMachines = If ([string]::IsNullOrEmpty($servers)) { 0 } else { ($servers.Split(',')).Count } + $serverCountToDeployTo = [math]::Round(($totalMachines * $serverPercentToDisconnect)) + + Write-Host ('Total machines: ' + $totalMachines) + + If ($serverCountToDeployTo -eq 0) + { + $serverCountToDeployTo++ + } +} + +Write-Host ('Window Size: ' + $serverCountToDeployTo) + +#To use this value, set Window size value to: #{Octopus.Action[Determine Rolling Deploy Window Size].Output.WindowSize} +Set-OctopusVariable -name \"WindowSize\" -value $serverCountToDeployTo + +#endregion +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "561333cc-14ea-44be-aca2-ccb06e0c582f", + "Name": "DRDWSPerformRollingDeploy", + "Label": "Perform Rolling Deploy?", + "HelpText": "If checkbox is unchecked, all servers will be deployed to. +NOTE: This can be set to use a variable or expression.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "ecf32591-130c-41cb-b8f5-405e3b1c5d28", + "Name": "DRDWSServerPercentageToDeploy", + "Label": "Server percentage to deploy", + "HelpText": "Percentage of servers to perform rolling deploy on at a time. Enter as whole number. +Example for 25%: 25", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "bed9618f-6ede-4c6b-a4b1-a6f0d0a685d4", + "Name": "DRDWSServerRole", + "Label": "Server Role for Rolling Deploy", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "StepPackageId": "Octopus.Script", + "LastModifiedBy": "mcasperson", + "$Meta": { + "ExportedAt": "2023-08-15T07:55:04.446Z", + "OctopusVersion": "2023.3.11489", + "Type": "ActionTemplate" + }, + "Category": "Octopus" +} diff --git a/step-templates/docker-create-and-push-image.json.human b/step-templates/docker-create-and-push-image.json.human new file mode 100644 index 000000000..8bae20c67 --- /dev/null +++ b/step-templates/docker-create-and-push-image.json.human @@ -0,0 +1,116 @@ +{ + "Id": "3ff1e0ae-0336-41e3-905a-a1f10f4bb1cf", + "Name": "Docker - Create and Push Image", + "Description": "Creates and pushes an Docker Image. + +- Requires the Docker infrastructure.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Get the parameters.\r +$appLocation = $OctopusParameters['ApplicationLocation']\r +$dockerFile = $OctopusParameters['DockerFile']\r +$imageName = $OctopusParameters['ImageName']\r +$tag = $OctopusParameters['ImageTag']\r +$dockerUsername = $OctopusParameters['DockerUsername']\r +$dockerPassword = $OctopusParameters['DockerPassword']\r +\r +# Check the parameters.\r +if (-NOT $dockerUsername) { throw \"You must enter a value for 'Username'.\" }\r +if (-NOT $dockerPassword) { throw \"You must enter a value for 'Password'.\" }\r +if (-NOT $imageName) { throw \"You must enter a value for 'Image Name'.\" }\r +if (-NOT $appLocation) { throw \"You must enter a value for 'Application Location'.\" }\r +\r +# If the Dockerfile parameter is not empty, save it to the file.\r +if ($dockerFile) \r +{\r + Write-Output 'Saving the Dockerfile'\r + $path = Join-Path $appLocation 'Dockerfile'\r + Set-Content -Path $path -Value $dockerFile -Force\r +}\r +\r +# If the tag parameter is empty, set it as latest.\r +if (-NOT $tag) \r +{\r + $tag = 'latest'\r +}\r +\r +# Prepare the final image name with the tag.\r +$imageName += ':' + $tag\r +\r +# Create the docker image\r +Write-Output 'Building the Docker Image'\r +docker build -t $imageName $appLocation\r +\r +# Upload to DockerHub\r +Write-Output 'Pushing the Docker Image to DockerHub'\r +docker login -u $dockerUsername -p $dockerPassword\r +docker push $imageName", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "DockerUsername", + "Label": "Docker Username", + "HelpText": "The username used to login to DockerHub", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DockerPassword", + "Label": "Docker Password", + "HelpText": "The password used to login to DockerHub", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "ApplicationLocation", + "Label": "Application Location", + "HelpText": "The application location, used to build the Docker image", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DockerFile", + "Label": "Dockerfile", + "HelpText": "The Dockerfile definition. If the Dockerfile is part of the package leave it blank", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "ImageName", + "Label": "Image Name", + "HelpText": "The image name in DockerHub", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ImageTag", + "Label": "Image Tag", + "HelpText": "The image tag. Leave it blank for 'latest'", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2017-02-03T10:49:13.997Z", + "LastModifiedBy": "joaoasrosa", + "$Meta": { + "ExportedAt": "2017-02-03T10:49:13.997Z", + "OctopusVersion": "3.3.27", + "Type": "ActionTemplate" + }, + "Category": "docker" +} diff --git a/step-templates/docker-install-linux.json.human b/step-templates/docker-install-linux.json.human new file mode 100644 index 000000000..dbcb10e3d --- /dev/null +++ b/step-templates/docker-install-linux.json.human @@ -0,0 +1,28 @@ +{ + "Id": "79496e52-6170-43e7-94bb-9e1dde6790fb", + "Name": "Install Docker on Linux", + "Description": "Installs the latest Docker Engine - Community from the stable channel, as per https://docs.docker.com/engine/install/ubuntu/#install-using-the-convenience-script + +Note: Docker will start automatically on `DEB`-based distributions. On `RPM`-based distributions, you need to start it manually using the appropriate `systemctl` or `service` command. Non-root users can’t run Docker commands by default.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "echo 'Downloading install script from https://get.docker.com' +curl -fsSL https://get.docker.com -o get-docker.sh + +echo 'Running get-docker.sh' +sudo sh get-docker.sh" + }, + "Parameters": [], + "$Meta": { + "ExportedAt": "2020-05-19T07:17:52.446Z", + "OctopusVersion": "2020.2.4", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "MJRichardson", + "Category": "docker" +} diff --git a/step-templates/docker-install-windows.json.human b/step-templates/docker-install-windows.json.human new file mode 100644 index 000000000..d96cf766e --- /dev/null +++ b/step-templates/docker-install-windows.json.human @@ -0,0 +1,46 @@ +{ + "Id": "3f78a32f-d074-43cc-a009-793f72b204f3", + "Name": "Install Docker on Windows Server", + "Description": "Automates the steps from [here](https://docs.microsoft.com/en-us/virtualization/windowscontainers/quick-start/set-up-environment?tabs=Windows-Server#install-docker). + +This step reboots the machine.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Find-PackageProvider -Name 'Nuget' -ForceBootstrap -IncludeDependencies + +$DockerModule = Get-Module -ListAvailable -Name DockerMsftProvider +if (-Not $DockerModule) { + Write-Host \"Installing DockerMsftProvider module\" + Install-Module -Name DockerMsftProvider -Repository PSGallery -Force +} else { + Write-Host \"DockerMsftProvider module already installed\" +} + +try { +\t$DockerPackage = Get-Package -Name docker +} catch [Exception] {} + +if (-Not $DockerPackage) { + Write-Host \"Installing docker package\" + Install-Package -Name docker -ProviderName DockerMsftProvider -Force + + Write-Host \"Restarting machine...\" + Restart-Computer -Force +} else { + Write-Host \"docker package already installed\" +}" + }, + "Parameters": [], + "$Meta": { + "ExportedAt": "2020-05-19T03:10:21.233Z", + "OctopusVersion": "2020.2.4", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "MJRichardson", + "Category": "docker" +} diff --git a/step-templates/dsc-windows-service.json.human b/step-templates/dsc-windows-service.json.human new file mode 100644 index 000000000..220c85454 --- /dev/null +++ b/step-templates/dsc-windows-service.json.human @@ -0,0 +1,144 @@ +{ + "Id": "1cdf63ce-50c8-45a8-cce2-f6ca2d6d617b", + "Name": "DSC Windows Service", + "Description": "Starts/stops one or more services asynchronously, and then waits for them to align to the specified state", + "ActionType": "Octopus.Script", + "Version": 15, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": " + $ServicesToManage = $OctopusParameters['Services'] + $ServicesToIgnore = $OctopusParameters['ServicesToIgnore'] + $TimeoutSeconds = $OctopusParameters['TimeoutSeconds'] + $DesiredState = $OctopusParameters['DesiredState'] + + # Gather information about the list of services + $services_status = @{} + + # For each \"service to manage\" or wildcard specified ... + $ServicesToManage -split \",\" |% ` + { + $service = $_ + + # ... retrieve all the services that match that name or wildcard ... + $service_states = Get-Service |? { $_.Name -match $service } + + # ... and add them into an array; we use a key/value array so that services only get added to the array once, even if they are + # matched by multiple wildcard specifications + $service_states |% { $services_status[$_.Name] = $_.Status } + } + + # For each \"service to ignore\" or wildcard specified ... + $ServicesToIgnore -split \",\" |% ` + { + $service = $_ + + # Copy the keys within services_status, since we will need to change services_status as we enumerate them + $keys = @() + $services_status.Keys |% { $keys += $_ } + + $keys |% ` + { + $key = $_ + + if ($key -match $service -and $service -match \"[a-z]+\") + { + $services_status.Remove($_) + } + } + } + + Write-Host \"Matched the following set of services, along with their current status:\" + $services_status + + # Now act as required to bring the services to the desired configuration state + [DateTime]$startTime = [DateTime]::Now + + # State to pass to sc + $state_type = if ($DesiredState -match \"Stopped\") { \"stop\" } else { \"start\" } + +\t$unaligned_services = ($services_status.Keys |? { $services_status[$_] -notmatch $DesiredState }) +\t# Attempt to align the remaining services +\t$unaligned_services |% ` +\t{ +\t\tWrite-Host \"Attempting to $state_type service: $_\" +\t\tStart-Process -FilePath \"cmd\" -ArgumentList \"/c sc.exe $state_type `\"$_`\"\" +\t}\t + + while ($startTime.AddSeconds($TimeoutSeconds) -gt [DateTime]::Now) + { +\t\t# Attempt to align the remaining services +\t\t$unaligned_services |% ` +\t\t{ +\t\t\tWrite-Host \"Attempting to $state_type service: $_\" +\t\t\t$services_status[$_] = Get-Service $_ | Select-Object -Property \"Status\" +\t\t}\t +\t\t$unaligned_services = ($services_status.Keys |? { $services_status[$_] -notmatch $DesiredState }) +\t\tWrite-Host \"$([DateTime]::Now): $($unaligned_services.Count) services of $($services_status.Count) not yet at status: $DesiredState\" +\t\t + if ($unaligned_services.Count -eq 0) + { + Write-Host \"All services now at desired state; exiting\" + exit 0 + } + + + # Pause for a second + [System.Threading.Thread]::Sleep(1000) + } + + throw \"Error: not all services reached the desired state within the specified timeframe: $unaligned_services\" + + +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Services", + "Label": "Services to align", + "HelpText": "A comma delimited list of services or wildcards (eg. \"Sky\") to align", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ServicesToIgnore", + "Label": "Services to ignore", + "HelpText": "A comma delimited list of services or wildcards (eg. \"Sky\") to ignore", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DesiredState", + "Label": "Desired state", + "HelpText": "The desired state of the service/s. Specify either \"Started\" or \"Stopped\"", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Running|Started +Stopped|Stopped" + } + }, + { + "Name": "TimeoutSeconds", + "Label": "Timeout in seconds", + "HelpText": "The number of seconds to wait for the service/s to align before timing out and throwing an exception", + "DefaultValue": "300", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2017-06-20T14:00:00.000+00:00", + "LastModifiedBy": "dunedinsoftware", + "$Meta": { + "ExportedAt": "2017-06-20T14:35:09.389Z", + "OctopusVersion": "3.1.5", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/dyn-create-an-A-record.json.human b/step-templates/dyn-create-an-A-record.json.human new file mode 100644 index 000000000..7453f01d8 --- /dev/null +++ b/step-templates/dyn-create-an-A-record.json.human @@ -0,0 +1,427 @@ +{ + "Id": "5e359c05-89a0-4a13-98a4-d54b0415bb45", + "Name": "Dyn - Create an A Record", + "Description": "Creates an A record in the specified zone with the specified details. + +NOTE: The API User MUST have the follow permissions: +\t- UserLogin +\t- UserChangepw +\t- RecordAdd +\t- RecordUpdate + - RecordGet +\t- ZoneGet +\t- ZoneAddNode +\t- ZonePublish +\t- ZoneChangeset +", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#--------------------------------------------------------------------\r +#Log In Variables\r +\r +$dynLogInURI = \"https://api.dynect.net/REST/Session/\"\r +\r +$dynCustomerName = $OctopusParameters[\"dynCustomerName\"] \r +\r +$dynUserName = $OctopusParameters[\"dynUserName\"] \r +\r +$dynPassword = $OctopusParameters[\"dynPassword\"] \r +\r +#--------------------------------------------------------------------\r +#Get A Record Variables\r +\r +$dynARecordURI = \"https://api.dynect.net/REST/ARecord\"\r +\r +$dynZone = $OctopusParameters[\"dynZone\"]\r +\r +$dynFQDN = $OctopusParameters[\"dynFQDN\"] \r +\r +#--------------------------------------------------------------------\r +#A Record information to check\r +\r +$createNewARecord = $FALSE\r +\r +$UpdateARecord = $FALSE\r +\r +$dynCorrectTTL = $OctopusParameters[\"dynCorrectTTL\"]\r +\r +$dynCorrectIPAddress = $OctopusParameters[\"dynCorrectIPAddress\"] \r +\r +\r +#--------------------------------------------------------------------\r +#Publish Zone Variables\r +\r +$dynPublishURI = \"https://api.dynect.net/REST/Zone\"\r +\r +$publishZone = $FALSE\r +\r +\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#Log In and Retrieve Token for this session\r +\r +Write-Output \"Logging into Dyn and retrieving session Authentication Token.\"\r +\r +$dynCredentials = @{}\r +\r +$dynCredentials.Add(\"customer_name\", $dynCustomerName)\r +$dynCredentials.Add(\"user_name\", $dynUserName)\r +$dynCredentials.Add(\"password\", $dynPassword)\r +\r +$dynCredentialsJSON = ConvertTo-Json -InputObject $dynCredentials\r +\r +$dynLoginResults = Invoke-RestMethod -Uri $dynLogInURI -Body $dynCredentialsJSON -ContentType 'application/json' -Method Post\r +\r +if($dynLoginResults.status -ne \"success\")\r +{\r + Write-Error \"Invalid Log In Details. Please try again.\" -ErrorId E4\r +}\r +else\r +{\r + Write-Output \"`nLog in was successful.\"\r +}\r +\r +\r +\r +$dynSessionToken = @{}\r +\r +$dynSessionToken.Add(\"Auth-Token\", $dynLoginResults.data.token)\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#Get A Record \r +\r +Write-Output \"Retrieving specified A record information.`n\"\r +\r +#get and search all records to get the correct record ID (not a unique id) for existing A Records\r +#this is done to check if a A Record does not exist already. This is the only way to do it without getting an error.\r +$dynAllRecordsURI = \"https://api.dynect.net/REST/AllRecord/$dynZone\"\r +\r + \r +$dynAllRecordResults = Invoke-RestMethod -Uri $dynAllRecordsURI -Headers $dynSessionToken -ContentType 'application/json' -Method Get \r +\r +for($i = 0; $i -lt $dynAllRecordResults.data.Length; $i++)\r +{\r + \r + $a = $dynAllRecordResults.data.Get($i)\r +\r + $result = $a.contains($dynFQDN)\r +\r + if($result -eq $TRUE)\r + {\r + $dynARecordString = $dynAllRecordResults.data.Get($i)\r +\r + $dynARecordExists = $TRUE\r +\r + $i = $dynAllRecordResults.data.Length\r + }\r + else\r + {\r + $dynARecordExists = $FALSE\r + }\r +}\r +\r +\r +\r +#checks to see if there is more than one A record with the same name.\r +if($dynARecordExists -eq $TRUE)\r +{\r + $dynARecordURI = \"$dynARecordURI/$dynZone/$dynFQDN\" \r + \r + $dynARecordResults = Invoke-RestMethod -Uri $dynARecordURI -Headers $dynSessionToken -ContentType 'application/json' -Method Get \r +\r + if($dynARecordResults.data.Length -gt 1)\r + {\r + Write-Error \"`nThere is more than one A record with the Fully Qualified Domain Name (FQDN) of $dynFQDN. `nThis script does not handle more than one A record witht the same FQDN\" -ErrorId E1\r + }\r +\r + if($dynARecordResults.status -ne \"success\")\r + {\r + Write-Error \"Error occurred while trying to retrieve the A Record. Please check the host name and the Fully Qualified Domain Name are correct.\" -ErrorId E1\r + }\r +}\r +\r +\r +\r +#Checks if the an A record was returned or needs to be created\r +if(($dynARecordResults.data.Length -eq 0) -or ($dynARecordExists -eq $FALSE))\r +{\r + $createNewARecord = $TRUE\r +\r + Write-Warning \"$dynFQDN does not exists. Creating $dynFQDN now.\"\r +}\r +else\r +{\r + #get information for the specified record\r + $dynARecordString = $dynARecordResults.data\r +\r + $dynARecordURI = \"https://api.dynect.net$dynARecordString/\"\r +\r + $dynARecord = Invoke-RestMethod -Uri $dynARecordURI -Headers $dynSessionToken -ContentType 'application/json' -Method Get \r +\r + $dynARecord = $dynARecord.data\r +\r + Write-Output \"`n$dynFQDN has successfully been retrieved.\"\r + \r + Write-Output \"`n-------------------------`n\"\r +\r +}\r +\r +#--------------------------------------------------------------------\r +#create new A record\r +\r +if($createNewARecord -eq $TRUE)\r +{\r + $dynCreateURI = \"https://api.dynect.net/REST/ARecord/$dynZone/$dynFQDN\" \r +\r + $rData = @{}\r +\r + $rData.Add(\"address\", $dynCorrectIPAddress)\r +\r + $dynCreateARecord = @{}\r +\r + $dynCreateARecord.Add(\"ttl\", $dynCorrectTTL)\r + $dynCreateARecord.Add(\"rdata\", $rData)\r +\r + $dynCreateARecordJSON = ConvertTo-Json -InputObject $dynCreateARecord\r +\r + $dynCreateResult = Invoke-RestMethod -Uri $dynCreateURI -ContentType 'application/json' -Headers $dynSessionToken -Body $dynCreateARecordJSON -Method Post\r +\r + if($dynCreateResult.status -ne \"success\")\r + {\r + Write-Error \"An error occurred while creating the new A Record. Please check the details that have been entered are correct and try again.\" -ErrorId E4\r +\r + }\r + else\r + {\r + Write-Output \"$dynFQDN has successfully been added to the $dynZone zone in Dyn.\"\r +\r + $publishZone = $TRUE\r + }\r +\r + Write-Output \"`n-------------------------`n\"\r +\r +\r +}\r +\r +\r +\r +#--------------------------------------------------------------------\r +#checking specified A Record to see if it is correct if it exists\r +if($createNewARecord -eq $FALSE)\r +{\r + Write-Output \"Checking to see if $dynFQDN is current and contains the correct information.\"\r +\r + if($dynARecord.rdata.address -ne $dynCorrectIPAddress)\r + {\r + $UpdateARecord = $TRUE\r +\r + Write-Warning \"`n$dynFQDN is out of date. Updating now\"\r +\r + }\r +\r + if($UpdateARecord -eq $FALSE)\r + {\r + Write-Output \"`n$dynFQDN is up-to-date\"\r + }\r +\r + Write-Output \"`n-------------------------`n\"\r +}\r +#--------------------------------------------------------------------\r +#Update A record\r +\r +if($UpdateARecord -eq $TRUE)\r +{\r + Write-Output \"Updating $dynFQDN so that is matches the current information saved in the system.\"\r +\r + $dynUpdateURI = $dynARecordURI\r +\r + $rData = @{}\r +\r + $rData.Add(\"address\", $dynCorrectIPAddress)\r +\r + $dynUpdatedARecord = @{}\r +\r + \r + $dynUpdatedARecord.Add(\"ttl\", $dynCorrectTTL)\r + $dynUpdatedARecord.Add(\"rdata\", $rData)\r +\r + $dynUpdatedARecord = ConvertTo-Json -InputObject $dynUpdatedARecord\r +\r + $dynUpdateResult = Invoke-RestMethod -Uri $dynUpdateURI -ContentType 'application/json' -Headers $dynSessionToken -Body $dynUpdatedARecord -Method Put\r + \r + if($dynUpdateResult.status -ne \"success\")\r + {\r + Write-Error \"An error occured while trying to update the $dynFQDN record\"\r + }\r + else\r + {\r + Write-Output \"`nUpdate was successful. Just needs to be published to make it offical.\"\r + \r + $publishZone = $TRUE\r +\r + }\r +\r +\r + Write-Output \"`n-------------------------`n\"\r +\r +}\r +\r +#--------------------------------------------------------------------\r +#publish update or creation of A Record\r +\r +if($publishZone -eq $TRUE)\r +{\r +\r + Write-Output \"Publishing changes made to $dynZone\"\r +\r + $publish = @{}\r + $publish.Add(\"publish\", 'true')\r +\r + $publish = ConvertTo-Json -InputObject $publish\r +\r + $dynPublishURI = \"$dynPublishURI/$dynZone/\"\r +\r + $dynPublishResults = Invoke-RestMethod -Uri $dynPublishURI -ContentType 'application/json' -Headers $dynSessionToken -Body $publish -Method Put\r +\r + if($dynPublishResults.status -ne \"success\")\r + {\r + Write-Error \"An error occurred during the publication of the $dynZone zone.\" -ErrorId E4\r + }\r + else\r + {\r + Write-Output \"`n$dynZone has successfully been published.\"\r + }\r +\r + Write-Output \"`n-------------------------`n\"\r +\r +}\r +\r +\r +\r +\r +#--------------------------------------------------------------------\r +#Log Out of session\r +\r +Write-Output \"Logging out and deleting this session's authentication token\"\r +\r +$dynLogOutResults = Invoke-RestMethod -Uri $dynLogInURI -ContentType 'application/json' -Headers $dynSessionToken -Method Delete\r +\r +While(($dynLogOutResults.status -ne \"success\") -and ($tries -lt 10))\r +{\r + Write-Output \"`nWaiting to log out of Dyn\"\r + $tries++\r + Start-Sleep -Seconds 1\r +}\r +\r +if($dynLogOutResults.status -eq \"success\")\r +{\r + $dynSessionToken.Clear()\r + Write-Output $dynSessionToken\r + Write-Output \"`nThis session has been ended successfully and the authentication token has been deleted.\"\r + \r +}\r +else\r +{\r + Write-Error \"`nAn error occurred while logging out.\" -ErrorId E4\r +}\r +\r +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "dynCustomerName", + "Label": "Dyn Customer Name", + "HelpText": "The Dyn customer name, usually the company name", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "dynUserName", + "Label": "Dyn User Name", + "HelpText": "User Name of the user that will perform this step + +NOTE: The API User MUST have the follow permissions: +\t- UserLogin +\t- UserChangepw +\t- RecordAdd +\t- RecordUpdate + - RecordGet +\t- ZoneGet +\t- ZoneAddNode +\t- ZonePublish +\t- ZoneChangeset", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "dynPassword", + "Label": "Password", + "HelpText": "Password of the user to access Dyn", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "dynZone", + "Label": "Dyn Zone", + "HelpText": "The name of the Zone, where you want the A record to be created + +For example: myzone.com", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "dynFQDN", + "Label": "Fully Qualified Domain Name", + "HelpText": "The name of the A record that is being created in Fully Qualified Domain Name format + +For example: newrecord.myzone.com", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "dynCorrectTTL", + "Label": "Time to Live", + "HelpText": "Limits the lifespan or lifetime of data in a computer network", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "dynCorrectIPAddress", + "Label": "IPv4 Address", + "HelpText": "The IPv4 address of the new A record", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-11-26T22:39:11.556+00:00", + "LastModifiedBy": "timhunt303", + "$Meta": { + "ExportedAt": "2015-11-27T06:25:11.275+00:00", + "OctopusVersion": "3.1.7", + "Type": "ActionTemplate" + }, + "Category": "dyn" +} diff --git a/step-templates/edgecast-cdn-purge.json.human b/step-templates/edgecast-cdn-purge.json.human new file mode 100644 index 000000000..464f4ad7b --- /dev/null +++ b/step-templates/edgecast-cdn-purge.json.human @@ -0,0 +1,112 @@ +{ + "Id": "d97ff2d4-250a-4a1a-bd5e-36644893888c", + "Name": "Clear EdgeCast CDN Cache", + "Description": "This step will clear (purge) the EdgeCast CDN Cache", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function Clear-EdgeCastCache +{ + [CmdletBinding()] + Param + ( + # CDN Account number, can be found in MCC + [Parameter(Mandatory=$true)] + $AccountNumber, + + # API Token + [Parameter(Mandatory=$true)] + [string]$ApiToken, + + # A string that indicates the CDN or edge CNAME URL for the asset or the location that will be purged from our edge servers. Make sure to include the proper protocol (i.e., http:// or rtmp://). + [Parameter(Mandatory=$true)] + [string] + $MediaPath, + + #An integer that indicates the service for which an asset will be purged. It should be replaced with the ID associated with the desired service., default is 3. HTTP Large + [ValidateSet(2,3,8,14)] + [int] + $MediaType=3 + ) + + Begin + { + $uri = \"https://api.edgecast.com/v2/mcc/customers/$AccountNumber/edge/purge\" + + $headers = @{ + 'Authorization' = \"tok:\" + $ApiToken + 'Accept' = 'Application/JSON' + 'Content-Type' = 'Application/JSON' + } + $RequestParameters = @{ + 'MediaPath'=$MediaPath + 'MediaType'=$MediaType + } + + $body = ConvertTo-Json $RequestParameters + + } + Process + { + Write-Verbose \"Request body $body\" + \t$request = Invoke-RestMethod -Method PUT -Uri $uri -Headers $headers -Body $body -DisableKeepAlive + $request + + } + End + { + } +} + +Clear-EdgeCastCache -AccountNumber $AccountNumber -ApiToken $ApiToken -MediaPath $MediaPath -MediaType $MediaType -Verbose +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "AccountNumber", + "Label": null, + "HelpText": "CDN Account number, can be found in MCC", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApiToken", + "Label": null, + "HelpText": "API token for accessing the EdgeCast API for the account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "MediaPath", + "Label": null, + "HelpText": "A string that indicates the CDN or edge CNAME URL for the asset or the location that will be purged from our edge servers. Make sure to include the proper protocol (i.e., http:// or rtmp://).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MediaType", + "Label": "MediaType", + "HelpText": "An integer that indicates the service for which an asset will be purged. It should be replaced with the ID associated with the desired service., default is 3. HTTP Large", + "DefaultValue": "3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-02-20T13:45:59.125+00:00", + "LastModifiedBy": "yooakim", + "$Meta": { + "ExportedAt": "2015-02-20T16:10:46.008+00:00", + "OctopusVersion": "2.6.2.845", + "Type": "ActionTemplate" + }, + "Category": "edgecast" +} diff --git a/step-templates/elmahio-notify-deployment.json.human b/step-templates/elmahio-notify-deployment.json.human new file mode 100644 index 000000000..518389622 --- /dev/null +++ b/step-templates/elmahio-notify-deployment.json.human @@ -0,0 +1,70 @@ +{ + "Id": "8CA59AC6-11A5-4624-A019-E93C1BA5F03C", + "Name": "elmah.io - Register Deployment", + "Description": "Step template for notifying elmah.io about deployments on Octopus.", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$version = $OctopusParameters['Version'] +$url = 'https://api.elmah.io/v3/deployments?api_key=' + $OctopusParameters['ApiKey'] +$body = @{ + version = $version + description = $OctopusReleaseNotes + userName = $OctopusParameters['Octopus.Deployment.CreatedBy.Username'] + userEmail = $OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress'] + logId = $OctopusParameters['LogId'] +} +Try { +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls + Invoke-RestMethod -Method Post -Uri $url -Body $body +} +Catch { + Write-Error $_.Exception.Message -ErrorAction Continue +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "0a3fe2a0-5c89-4e56-b1c3-b31bf4978ca4", + "Name": "ApiKey", + "Label": "API Key", + "HelpText": "Required: Input your elmah.io API key located on the organization settings page.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "63e08bca-37d8-45ca-bd41-912efa1dfb86", + "Name": "LogId", + "Label": "Log ID", + "HelpText": "Optional: As default, new deployments are shown on all logs on the organization. If you want the deployment to show up on a single log only, input the ID of the log here.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a75169a0-f829-4ad5-ab4b-c108cf2231e5", + "Name": "Version", + "Label": "Version", + "HelpText": "Required: Let you input a version string to use when creating the deployment on elmah.io.", + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "elmahio", + "$Meta": { + "ExportedAt": "2021-09-03T08:34:01.363Z", + "OctopusVersion": "2021.2.7428", + "Type": "ActionTemplate" + }, + "Category": "elmah" +} diff --git a/step-templates/elmahio-upload-source-map.json.human b/step-templates/elmahio-upload-source-map.json.human new file mode 100644 index 000000000..9f7e11045 --- /dev/null +++ b/step-templates/elmahio-upload-source-map.json.human @@ -0,0 +1,105 @@ +{ + "Id": "0EAF2914-E291-4CCF-833C-25EA769BF82B", + "Name": "elmah.io - Upload Source Map", + "Description": "Step template for uploading a source map and a minified JavaScript file to elmah.io.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$apiKey = $OctopusParameters['ElmahIoSourceMap_ApiKey'] +$logId = $OctopusParameters['ElmahIoSourceMap_LogId'] +$path = $OctopusParameters['ElmahIoSourceMap_Path'] +$sourceMap = $OctopusParameters['ElmahIoSourceMap_SourceMap'] +$minifiedJavaScript = $OctopusParameters['ElmahIoSourceMap_MinifiedJavaScript'] +$boundary = [System.Guid]::NewGuid().ToString() + +$mapFile = [System.IO.File]::ReadAllBytes($sourceMap) +$mapContent = [System.Text.Encoding]::UTF8.GetString($mapFile) +$mapFileName = Split-Path $sourceMap -leaf +$jsFile = [System.IO.File]::ReadAllBytes($minifiedJavaScript) +$jsContent = [System.Text.Encoding]::UTF8.GetString($jsFile) +$jsFileName = Split-Path $minifiedJavaScript -leaf + +$LF = \"`r`n\" +$bodyLines = ( + \"--$boundary\", + \"Content-Disposition: form-data; name=`\"Path`\"$LF\", + $path, + \"--$boundary\", + \"Content-Disposition: form-data; name=`\"SourceMap`\"; filename=`\"$mapFileName`\"\", + \"Content-Type: application/json$LF\", + $mapContent, + \"--$boundary\", + \"Content-Disposition: form-data; name=`\"MinifiedJavaScript`\"; filename=`\"$jsFileName`\"\", + \"Content-Type: text/javascript$LF\", + $jsContent, + \"--$boundary--$LF\" +) -join $LF + +Invoke-RestMethod \"https://api.elmah.io/v3/sourcemaps/${logId}?api_key=${apiKey}\" -Method POST -ContentType \"multipart/form-data; boundary=`\"$boundary`\"\" -Body $bodyLines", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "01c70303-6af5-4b44-992d-e2b104fdd433", + "Name": "ElmahIoSourceMap_ApiKey", + "Label": "API Key", + "HelpText": "Required: Input your elmah.io API key located on the organization settings page.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "b7963b69-c261-4008-a1ac-65133eead721", + "Name": "ElmahIoSourceMap_LogId", + "Label": "Log ID", + "HelpText": "Required: The ID of the log which should contain the minified JavaScript and source map.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4a72b7bd-5a74-4038-b4e4-8dfd0f7231e7", + "Name": "ElmahIoSourceMap_Path", + "Label": "Path", + "HelpText": "Required: An URL to the online minified JavaScript file. The URL can be absolute or relative but will always be converted to a relative path (no protocol, domain, and query parameters). elmah.io uses this path to lookup any lines in a JS stack trace that will need de-minification.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d774ab07-f626-4d2d-baa7-62a06fed7ff6", + "Name": "ElmahIoSourceMap_SourceMap", + "Label": "Source Map", + "HelpText": "Required: A path to the source map file. Only files with an extension of .map and content type of application/json will be accepted.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c2ca2875-21dc-4633-ab38-fa5e84f34f74", + "Name": "ElmahIoSourceMap_MinifiedJavaScript", + "Label": "Minified JavaScript", + "HelpText": "Required: A path to the minified JavaScript file. Only files with an extension of .js and content type of text/javascript will be accepted.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "elmahio", + "$Meta": { + "ExportedAt": "2021-09-06T08:51:54.463Z", + "OctopusVersion": "2021.2.7428", + "Type": "ActionTemplate" + }, + "Category": "elmah" +} diff --git a/step-templates/epplus-register-dll-gac.json.human b/step-templates/epplus-register-dll-gac.json.human new file mode 100644 index 000000000..2fbe66ce4 --- /dev/null +++ b/step-templates/epplus-register-dll-gac.json.human @@ -0,0 +1,122 @@ +{ + "Id": "137d2be3-a329-4c65-bde9-0c062f056889", + "Name": "Register EPPlus DLL in the GAC", + "Description": "This script registers EPPlus dll to the GAC", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[System.Reflection.Assembly]::Load(\"System.EnterpriseServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a\") + +function Expand-ZIPFile($file, $destination) +{ + $shell = new-object -com shell.application + $zip = $shell.NameSpace($file) + foreach($item in $zip.items()) + { + $shell.Namespace($destination).copyhere($item) + } +} + +$TempFolder = \"\\TempNupkg\" +$ExpandedFolder = \"\\expanded\" + +$FileDestinationPath = $RegisteringDllFolderPath + \"\\\" + $DllName +$FileSourcePath = $RegisteringDllFolderPath + $TempFolder + $ExpandedFolder + $DllPathInExpanded + \"\\\" + $DllName + +$TempPath = $RegisteringDllFolderPath + $TempFolder +$NupkgPath = $RegisteringDllFolderPath + $TempFolder + \"\\temp.zip\" +$ExpandedTempPath = $RegisteringDllFolderPath + $TempFolder + $ExpandedFolder + +$DllUrl = \"https://www.nuget.org/api/v2/package/\"+ $PackageName +\"/\" + $PackageVersion + +if (!(Test-Path $ExpandedTempPath -PathType Container)) { + New-Item -ItemType Directory -Force -Path $ExpandedTempPath +} + +Write-Host \"Allow SecurityProtocol TLS, TLS 1.1 and TLS 1.2 ...\" +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 +Write-Host \"Dowloading package ...\" +Invoke-WebRequest -Uri $DllUrl -OutFile $NupkgPath + +Write-Host \"Expanding Archive ...\" +Expand-ZIPFile –File $NupkgPath –Destination $ExpandedTempPath + +Write-Host \"Copying to destination folder ...\" +Copy-Item $FileSourcePath -Destination $FileDestinationPath + +Remove-Item -Recurse -Force $TempPath +Write-Host \"Deleteing temp folders ...\" + +Write-Host \"Library Found\" + +#Note that you should be running PowerShell as an Administrator +Write-Host \"Installing to GAC ...\" +$publish = New-Object System.EnterpriseServices.Internal.Publish +$publish.GacInstall($fileDestinationPath) +Write-Host \"Installed to GAC\"" + }, + "Parameters": [ + { + "Id": "5ee60108-20e4-4010-95cc-c13eb40c618f", + "Name": "RegisteringDllFolderPath", + "Label": "Path of the registering dll", + "HelpText": null, + "DefaultValue": "C:\\dll", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4817d840-5b87-4d8b-bdac-53711f093ae4", + "Name": "DllPathInExpanded", + "Label": "Regintering dll path inside NuGet archive", + "HelpText": null, + "DefaultValue": "\\lib\ +et40", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c5e7cff7-e67d-48a4-adb6-6019d06ba249", + "Name": "DllName", + "Label": "Dll file name", + "HelpText": null, + "DefaultValue": "EPPlus.dll", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c326490c-ea29-4b2b-99e7-37027233bd06", + "Name": "PackageName", + "Label": "NuGet gallery package name", + "HelpText": null, + "DefaultValue": "EPPlus", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ea04aa17-5fea-4e06-82e8-d344c797547c", + "Name": "PackageVersion", + "Label": "Registering dll version", + "HelpText": null, + "DefaultValue": "4.1.0.0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "fabiozanella", + "$Meta": { + "ExportedAt": "2020-11-03T09:07:40.202Z", + "OctopusVersion": "2020.4.5", + "Type": "ActionTemplate" + }, + "Category": "dll" + } diff --git a/step-templates/event-tracing-register-manifest.json.human b/step-templates/event-tracing-register-manifest.json.human new file mode 100644 index 000000000..4562b1b44 --- /dev/null +++ b/step-templates/event-tracing-register-manifest.json.human @@ -0,0 +1,68 @@ +{ + "Id": "2b7e3987-0da0-4a5d-bb60-c190b433d888", + "Name": "Event Tracing - Register an ETW manifest", + "Description": "Registers an ETW manifest", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "if(-not (Test-Path $env:SystemRoot\\System32\\wevtutil.exe)) +{ + throw \"wevtutil.exe could not be found\" +} + +if(-not (Test-Path $ManifestFile)) +{ + throw \"Manifest $manifest could not be found\" +} + +if(-not (Test-Path $ResourceFile)) +{ + throw \"Resource file $ResourceFile could not be found\" +} + +if(-not (Test-Path $MessageFile)) +{ + throw \"Message file $MessageFile could not be found\" +} + +& \"$env:SystemRoot\\System32\\wevtutil.exe\" im $ManifestFile /rf:$ResourceFile /mf:$MessageFile" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ManifestFile", + "Label": "Manifest file", + "HelpText": "Full path to the manifest file that must be registered.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ResourceFile", + "Label": "Resource file", + "HelpText": "The full path to the assembly that must be used as resource file.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MessageFile", + "Label": "Message file", + "HelpText": "The full path to the assembly that must be used as message file.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "HumanPrinter", + "$Meta": { + "ExportedAt": "2017-03-16T13:02:50.0927973Z", + "OctopusVersion": "3.11.6", + "Type": "ActionTemplate" + }, + "Category": "event-tracing" +} diff --git a/step-templates/event-tracing-unregister-manifest.json.human b/step-templates/event-tracing-unregister-manifest.json.human new file mode 100644 index 000000000..9d2259259 --- /dev/null +++ b/step-templates/event-tracing-unregister-manifest.json.human @@ -0,0 +1,41 @@ +{ + "Id": "59be43c9-e5eb-499f-9237-f388bcd7940d", + "Name": "Event Tracing - Unregister an ETW manifest", + "Description": "Unregisters an ETW manifest", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "if(-not (Test-Path $env:SystemRoot\\System32\\wevtutil.exe)) +{ + throw \"wevtutil.exe could not be found\" +} + +if(-not (Test-Path $ManifestFile)) +{ + Write-Host \"Skipping manifest $ManifestFile because it does not exist\" + Exit 0 +} + +& \"$env:SystemRoot\\System32\\wevtutil.exe\" um $ManifestFile" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ManifestFile", + "Label": "Manifest file", + "HelpText": "Full path to the manifest file that must be unregistered.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "HumanPrinter", + "$Meta": { + "ExportedAt": "2017-03-16T11:12:52.2250765Z", + "OctopusVersion": "3.11.6", + "Type": "ActionTemplate" + }, + "Category": "event-tracing" +} diff --git a/step-templates/f5-enable-disable-member.json.human b/step-templates/f5-enable-disable-member.json.human new file mode 100644 index 000000000..fb1ab09de --- /dev/null +++ b/step-templates/f5-enable-disable-member.json.human @@ -0,0 +1,220 @@ +{ + "Id": "6775501d-cafb-493e-ba67-ec95c9500562", + "Name": "F5 - Enable, Disable, or Force Offline Member with optional wait for connections to drop", + "Description": "F5 - Enable, Disable, or Force Offline Member with optional wait for connections to drop", + "ActionType": "Octopus.Script", + "Version": 36, + "Properties": { + "Octopus.Action.Script.ScriptBody": "#Load the F5 powershell iControl snapin +#https://help.octopus.com/t/the-windows-powershell-snap-in-webadministration-is-not-installed-on-this-computer/4290 +Add-PSSnapin iControlSnapin; + +function WaitFor-ConnectionCount() +{ + param( + $pool_name, + $member, + [int]$MaxWaitTime = 300, #defaults to 5 minutes + $ConnectionCount = 0 + ) + $vals = $member.Split( (, ':')); + $member_addr = $vals[0]; + $member_port = $vals[1]; + + Write-Host \"Waiting for current connections to drop to \"$OctopusParameters['ConnectionCount'] + + $MemberDef = New-Object -TypeName iControl.CommonIPPortDefinition; + $MemberDef.address = $member_addr; + $MemberDef.port = $member_port; + $MemberDefAofA = New-Object -TypeName \"iControl.CommonIPPortDefinition[][]\" 1,1 + $MemberDefAofA[0][0] = $MemberDef; + $cur_connections = 100; + $elapsed = [System.Diagnostics.Stopwatch]::StartNew(); + + while ( $cur_connections -gt $ConnectionCount -and $elapsed.ElapsedMilliseconds -lt ($MaxWaitTime * 1000)) + { + $MemberStatisticsA = (Get-F5.iControl).LocalLBPoolMember.get_statistics( (, $pool_name), $MemberDefAofA); + $MemberStatisticEntry = $MemberStatisticsA[0].statistics[0]; + $Statistics = $MemberStatisticEntry.statistics; + foreach ($Statistic in $Statistics) + { + $type = $Statistic.type; + $value = $Statistic.value; + if ( $type -eq \"STATISTIC_SERVER_SIDE_CURRENT_CONNECTIONS\" ) + { + # just use the low value. Odds are there aren't over 2^32 current connections. + # If your site is this big, you'll have to convert this to a 64 bit number. + $cur_connections = $value.low; + Write-Host \"Current Connections: $cur_connections\" + } + } + Start-Sleep -s 5 + } +} + +$Pool = $OctopusParameters['PoolName'].trim(); + +If ([string]::IsNullOrWhiteSpace($OctopusParameters['MemberIP'])) { + Write-Host \"No IP Adress was provided on the 'LTM Member IP`, using [System.Net.Dns]::GetHostAddresses to resolve it\" + $ip = $([System.Net.Dns]::GetHostAddresses(\"$($OctopusParameters['Octopus.Machine.Hostname'])\") | Where {$_.AddressFamily -ne 'InterNetworkV6'}).IpAddressToString + if ($ip -is [array]) { + Write-Host \"Found multiple ipv4 addresses, using first address $($ip[0])\" + $ip = $ip[0] + } +} Else { + $ip = $OctopusParameters['MemberIP'] +} + +$Member = $ip+\":\"+$OctopusParameters['MemberPort'] +Write-Host \"Member is $Member\" + +# Gets the hostname of the current machine being deployed. +$curhost = hostname + +Write-host \"Currently deploying to $curhost\" + +If (($OctopusParameters['EnableOrDisable'] -ne \"Enabled\") -and ($OctopusParameters['WaitForConnections'] -eq \"True\")) +{ + Initialize-F5.iControl -HostName $OctopusParameters['HostName'] -Username $OctopusParameters['Username'] -Password $OctopusParameters['Password'] + Write-Host \"Setting $curhost to $($OctopusParameters['EnableOrDisable']) in $Pool pool\"; + Set-F5.LTMPoolMemberState -Pool $Pool -Member $Member -state $OctopusParameters['EnableOrDisable']; + Write-Host \"Waiting for connections to drain before deploying. This could take a while....\" + WaitFor-ConnectionCount -pool_name $Pool -member $Member -MaxWaitTime $OctopusParameters['MaxWaitTime'] -ConnectionCount $OctopusParameters['ConnectionCount'] + if ($OctopusParameters['EnableOrDisable'] -eq \"Disabled\") + { + Write-Host \"Setting $curhost to Offline in $Pool pool\"; + # We've now waited the desired amount, go ahead and force offline and move on with deployment + Set-F5.LTMPoolMemberState -Pool $Pool -Member $Member -state Offline; + } +} +Else +{ + Initialize-F5.iControl -HostName $OctopusParameters['HostName'] -Username $OctopusParameters['Username'] -Password $OctopusParameters['Password'] + Write-host \"Setting $curhost to $($OctopusParameters['EnableOrDisable']) in $Pool pool.\" + Set-F5.LTMPoolMemberState -Pool $Pool -Member $Member -state $OctopusParameters['EnableOrDisable']; +} +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "c27ab1f7-5303-48fe-a8e5-80aa638a054f", + "Name": "WaitForConnections", + "Label": "Wait on Connections?", + "HelpText": "If checked, the deployment won't continue until current connections on the node = 0", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "becfb740-d954-4250-9818-4c561f210ec5", + "Name": "MaxWaitTime", + "Label": "Max. Wait Time (seconds)", + "HelpText": "Defaults to 5 minutes", + "DefaultValue": "300", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9a0e97c0-d972-4707-b477-c1903090f508", + "Name": "EnableOrDisable", + "Label": "LTM Status", + "HelpText": "State member will be put into (Enabled, Disabled, Offline) + +- **Disabled** continues to process persistent and active connections. It can accept new connections only if the connections belong to an existing persistence session. + +- **Offline** allows existing connections to time out, but no new connections are allowed.", + "DefaultValue": "Disabled", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Enabled|Enable +Disabled|Disable +Offline|Forced Offline" + } + }, + { + "Id": "7e1b8bc0-b950-4394-9e92-3a00ce3fce3a", + "Name": "PoolName", + "Label": "LTM Pool name", + "HelpText": "Name of F5 pool", + "DefaultValue": "pl_DummyDeployWeb", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "adf16144-16be-437a-ac81-199ca1de1e0e", + "Name": "MemberIP", + "Label": "LTM Member IP", + "HelpText": "IP Address of F5 pool member. Default will auto discover the ip address of Tentacle.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a37006fa-b34e-4f17-b16a-e7e82981b002", + "Name": "MemberPort", + "Label": "LTM Member port", + "HelpText": "", + "DefaultValue": "80", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "df633633-bf98-45d3-86cf-335b97902dca", + "Name": "HostName", + "Label": "LTM Host name", + "HelpText": null, + "DefaultValue": "192.168.45.204", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3d43e5a4-4527-4b7c-bf88-319818f0d498", + "Name": "Username", + "Label": "LTM username", + "HelpText": "Credential used to access F5 Soap API", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a3349f60-5cdf-4ab4-9213-a74843149643", + "Name": "Password", + "Label": "LTM password", + "HelpText": "Credential used to access F5 Soap API", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "0ea7a048-0aa3-4387-a4cc-11e6cd8df6cb", + "Name": "ConnectionCount", + "Label": "Kill connections when <=", + "HelpText": "The default is to wait until there are no connections left on the node. If you don't want to wait for zero connections before deploying, you can put a value here and when that number of connections is reached the deployment will happen killing the rest of the connected sessions.", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "spuder", + "$Meta": { + "ExportedAt": "2016-09-21T17:50:00.631+00:00", + "OctopusVersion": "3.4.9", + "Type": "ActionTemplate" + }, + "Category": "f5" +} diff --git a/step-templates/f5-enable-disable-multiple-pools.json.human b/step-templates/f5-enable-disable-multiple-pools.json.human new file mode 100644 index 000000000..7a33b7a8b --- /dev/null +++ b/step-templates/f5-enable-disable-multiple-pools.json.human @@ -0,0 +1,354 @@ +{ + "Id": "b3d76f46-e074-47ff-b800-3020c3f47866", + "Name": "F5 - Enable or Disable Multiple Pools", + "Description": "F5 - Enable, Disable, or Force Offline multiple pools with optional wait for connections to drop", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "#region Verify variables + +#Verify RunCondition can be converted to boolean. +$runCondition = $false +If ([string]::IsNullOrEmpty($OctopusParameters['RunCondition'])){ + Throw \"Run Condition cannot be null.\" +} +Else{ + Try{ + $runCondition = [System.Convert]::ToBoolean($OctopusParameters['RunCondition']) + Write-Host (\"Run Condition: '\" + $OctopusParameters['RunCondition'] + \"' converts to boolean: \" + $runCondition + \".\") + + #If run condition evaluates to false, just return/stop processing. + If (!$runCondition){ + Write-Host \"Skipping step.\" + return + } + } + Catch{ + Throw \"Cannot convert Run Condition: '\" + $OctopusParameters['RunCondition'] + \"' to boolean value.\" + } +} + +#No need to verify WaitForConnections as this is a checkbox and will always have a boolean value. Report value back for logging. +Write-Host (\"Wait for connections to drop to 0: \" + $OctopusParameters['WaitForConnections']) + +#Verify MaxWaitTime can be converted to integer. +If ([string]::IsNullOrEmpty($OctopusParameters['MaxWaitTime'])){ + Throw \"Maximum wait time in seconds cannot be null.\" +} + +[int]$maxWaitTime = 0 +[bool]$result = [int]::TryParse($OctopusParameters['MaxWaitTime'], [ref]$maxWaitTime ) + +If ($result){ + Write-Host (\"Maximum wait time in seconds: \" + $maxWaitTime) +} +Else{ + Throw \"Cannot convert Maximum wait time in seconds: '\" + $OctopusParameters['MaxWaitTime'] + \"' to integer.\" +} + +#No need to verify LtmStatus as this is a drop down box and will always have a value. Report back for logging. +Write-Host (\"LTM Status: \" + $OctopusParameters['LtmStatus']) + +<# +Verify List of LTM info. +LTM Info should contain a list of all Pools, IPs, and Ports. Each set should be delmited by carriage returns, each valude delimited by pipe (|). +Here is an example: +Pool_192.168.103.226_443|192.168.103.174|443 +Pool_192.168.103.226_80|192.168.103.174|80 +#> +If ([string]::IsNullOrEmpty($OctopusParameters['LtmInfo'])){ + Throw \"List of LTM info cannot be null.\" +} +#Write out LTM info. If the project is using variables (and it most likely is), it may be difficult to debug without seeing what it evaluated to. +Write-Host (\"List of LTM info: \" + [Environment]::NewLine + $OctopusParameters['LtmInfo']) +$f5Pools = ($OctopusParameters['LtmInfo']).Split([Environment]::NewLine) +Foreach ($f5Pool in $f5Pools){ + #Validate 3 values are passed in per line. + $poolInfo = $f5Pool.Split(\"|\") + If ($poolInfo.Count -ne 3){ + Throw (\"Invalid pool info. Expecting 'PoolName|IpAddress|Port': '\" + $f5Pool + \"'.\") + } + + #Validate that each value is not null. + Foreach ($f5Parm in $poolInfo){ + If ([string]::IsNullOrEmpty($f5Parm)){ + Throw (\"Invalid pool info. Expecting 'PoolName|IpAddress|Port': '\" + $f5Pool + \"'. None can be empty.\") + } + } + + #Validate IP Address (second value). + If ( !($poolInfo[1] -as [ipaddress]) ){ + Throw (\"Invalid IP Address: '\" + $poolInfo[1] + \"'.\") + } + + #Validate Port (third value). + [int]$port = 0 + [bool]$result = [int]::TryParse($poolInfo[2], [ref]$port ) + + If ( !($result) ){ + Throw (\"Invalid port (expecting integer): '\" + $poolInfo[2] + \"'.\") + } +} + +#Verify HostName is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['HostName'])){ + Throw \"LTM Host name cannot be null.\" +} +Write-Host (\"LTM Host: \" + $OctopusParameters['HostName']) + +#Verify Username is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['Username'])){ + Throw \"LTM username cannot be null.\" +} +Write-Host (\"Username: \" + $OctopusParameters['Username']) + +#Verify Password is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['Password'])){ + Throw \"LTM password cannot be null.\" +} + +#Verify ConnectionCount can be converted to integer. +If ([string]::IsNullOrEmpty($OctopusParameters['ConnectionCount'])){ + Throw \"Kill connections when less than or equal to cannot be null.\" +} + +[int]$killConnectionWhenLE = 0 +[bool]$result = [int]::TryParse($OctopusParameters['ConnectionCount'], [ref]$killConnectionWhenLE ) + +If ($result){ + Write-Host (\"Kill connections when less than or equal to: \" + $killConnectionWhenLE) +} +Else{ + Throw \"Cannot convert Kill connections when less than or equal to: '\" + $OctopusParameters['ConnectionCount'] + \"' to integer.\" +} + +#endregion + +#region Functions + +Function Set-F5PoolState{ + param( + $f5Pools, + [switch]$forceOffline + ) + + Foreach ($f5Pool in $f5Pools){ + $poolInfo = $f5Pool.Split(\"|\") + + $poolName = $poolInfo[0] + $ipAddress = $poolInfo[1] + $port = $poolInfo[2] + + $member = ($ipAddress + \":\" + $port) + + $state = $OctopusParameters['LtmStatus'] + If ($forceOffline){ + $state = \"Offline\" + } + + Write-Host \"Setting '$ipAddress' to '$state' in '$poolName' pool.\" + Set-F5.LTMPoolMemberState -Pool $poolName -Member $member -state $state + } +} + +Function Wait-ConnectionCount(){ + param( + $f5Pools, + [int]$maxWaitTime, + [int]$connectionCount + ) + + #Start stop watch now. + $elapsed = [System.Diagnostics.Stopwatch]::StartNew() + + Foreach ($f5Pool in $f5Pools){ + $poolInfo = $f5Pool.Split(\"|\") + + $poolName = $poolInfo[0] + $ipAddress = $poolInfo[1] + $port = $poolInfo[2] + + $MemberDef = New-Object -TypeName iControl.CommonIPPortDefinition + $MemberDef.address = $ipAddress + $MemberDef.port = $port + $MemberDefAofA = New-Object -TypeName \"iControl.CommonIPPortDefinition[][]\" 1,1 + $MemberDefAofA[0][0] = $MemberDef + $cur_connections = 100 + + Write-Host (\"Pool name: \" + $poolName) + Write-Host (\"IP Address: \" + $ipAddress) + Write-Host (\"Port: \" + $port) + + While ($cur_connections -gt $connectionCount -and $elapsed.ElapsedMilliseconds -lt ($maxWaitTime * 1000)){ + $MemberStatisticsA = (Get-F5.iControl).LocalLBPoolMember.get_statistics( (, $poolName), $MemberDefAofA) + $MemberStatisticEntry = $MemberStatisticsA[0].statistics[0] + $Statistics = $MemberStatisticEntry.statistics + + Foreach ($Statistic in $Statistics){ + $type = $Statistic.type; + $value = $Statistic.value; + If ( $type -eq \"STATISTIC_SERVER_SIDE_CURRENT_CONNECTIONS\" ){ + #Just use the low value. Odds are there aren't over 2^32 current connections. If your site is this big, you'll have to convert this to a 64 bit number. + $cur_connections = $value.low; + Write-Host \"Current Connections: $cur_connections\" + } + } + + If ($cur_connections -gt $connectionCount -and $elapsed.ElapsedMilliseconds -lt ($maxWaitTime * 1000)){ + Start-Sleep -Seconds 5 + } + } + } +} + +#endregion + +#region Process + +#Load the F5 powershell iControl snapin +Add-PSSnapin iControlSnapin +Initialize-F5.iControl -HostName $OctopusParameters['HostName'] -Username $OctopusParameters['Username'] -Password $OctopusParameters['Password'] + +Set-F5PoolState -f5Pools $f5Pools + +If (($OctopusParameters['LtmStatus'] -ne \"Enabled\") -and ($OctopusParameters['WaitForConnections'] -eq \"True\")) +{ + Write-Host \"Waiting for connections to drain before deploying. This could take a while...\" + Wait-ConnectionCount -f5Pools $f5Pools -maxWaitTime $maxWaitTime -connectionCount $killConnectionWhenLE + + #We have now waited the desired amount, go ahead and force offline and move on with deployment. + Set-F5PoolState -f5Pools $f5Pools -forceOffline +} + + +#endregion + +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "83c45726-806c-4d06-8ae3-22b13b89d5a7", + "Name": "RunCondition", + "Label": "Run Condition", + "HelpText": "Boolean value or expression that determines if this step should run.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "fb610f88-0d64-40cf-a74d-0bb8405755c8", + "Name": "WaitForConnections", + "Label": "Wait for connections to drop to 0?", + "HelpText": "If checked, the deployment won't continue until current connections on the node = 0", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "af92387f-ba2d-4b44-a8f1-7ebccf01032a", + "Name": "MaxWaitTime", + "Label": "Maximum wait time in seconds", + "HelpText": "Maximum wait time (in seconds) before killing connections and moving on.", + "DefaultValue": "300", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "d8c955b3-6c75-49cf-9396-25dc0fffb611", + "Name": "LtmStatus", + "Label": "LTM Status", + "HelpText": "State member will be put into (Enabled, Disabled, Offline) + +- **Disabled** continues to process persistent and active connections. It can accept new connections only if the connections belong to an existing persistence session. + +- **Offline** allows existing connections to time out, but no new connections are allowed.", + "DefaultValue": "Disabled", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Enabled|Enable +Disabled|Disable +Offline|Forced Offline" + }, + "Links": {} + }, + { + "Id": "57b114a3-fa6a-40d6-9c42-38ba80127fca", + "Name": "LtmInfo", + "Label": "List of LTM info", + "HelpText": "Enter list of all Pools, IPs, and Ports. Each set delimited by carriage returns, each value delimited by pipe (|). Here is an example: + +Pool\\_192.168.103.226\\_443|192.168.103.174|443 +Pool\\_192.168.103.226\\_80|192.168.103.174|80", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "3fdbf9bc-9ee8-4610-9c34-24f1975cf8c7", + "Name": "HostName", + "Label": "LTM Host name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "61480ba0-f715-46ef-b5ad-1c6b6f580273", + "Name": "ConnectionCount", + "Label": "Kill connections when less than or equal to", + "HelpText": "The default is to wait until there are no connections left on the node. If you don't want to wait for zero connections before deploying, you can put a value here and when that number of connections is reached the deployment will happen killing the rest of the connected sessions.", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "d0f276b6-ce27-4006-b3c2-00c669bfc42d", + "Name": "Username", + "Label": "LTM username", + "HelpText": "Credential used to access F5 Soap API", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "921b6a08-0ffe-4945-91c3-8520a3052ce9", + "Name": "Password", + "Label": "LTM password", + "HelpText": "Credential used to access F5 Soap API", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + } + ], + "LastModifiedBy": "tbrasch", + "$Meta": { + "ExportedAt": "2017-06-02T14:54:52.840Z", + "OctopusVersion": "3.13.6", + "Type": "ActionTemplate" + }, + "Category": "F5" +} + + diff --git a/step-templates/f5-gtm-enable-or-disable.json.human b/step-templates/f5-gtm-enable-or-disable.json.human new file mode 100644 index 000000000..b98d2cac8 --- /dev/null +++ b/step-templates/f5-gtm-enable-or-disable.json.human @@ -0,0 +1,103 @@ +{ + "Id": "f4566d84-85bb-42df-9f3c-0b146a57e90f", + "Name": "F5 GTM - Enable or Disable", + "Description": "Enables or disables pools in the F5 GTM +", + "ActionType": "Octopus.Script", + "Version": 12, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Add-PSSnapIn iControlSnapIn. F5 iControlSnapIn can be downloaded from here https://devcentral.f5.com/articles/icontrol-cmdlets \r +\r +Initialize-F5.iControl -HostName $OctopusParameters['HostName'] -Username $OctopusParameters['Username'] -Password $OctopusParameters['Password']\r +\r +$Pool = $OctopusParameters['PoolName'];\r +\r +$PoolA = (, $Pool);\r +$MemberEnabledState = New-Object -TypeName iControl.GlobalLBPoolMemberMemberEnabledState;\r +$MemberEnabledState.member = New-Object iControl.CommonIPPortDefinition;\r +$MemberEnabledState.member.address = $OctopusParameters['MemberIP'];\r +$MemberEnabledState.member.port = $OctopusParameters['MemberPort'];\r +$MemberEnabledState.state = $OctopusParameters['EnableOrDisable'];\r +[iControl.GlobalLBPoolMemberMemberEnabledState[]]$MemberEnabledStateA = [iControl.GlobalLBPoolMemberMemberEnabledState[]](, $MemberEnabledState);\r +[iControl.GlobalLBPoolMemberMemberEnabledState[][]]$MemberEnabledStateAofA = [iControl.GlobalLBPoolMemberMemberEnabledState[][]](, $MemberEnabledStateA);\r +\r +(Get-F5.iControl).GlobalLBPoolMember.set_enabled_state($PoolA, $MemberEnabledStateAofA);", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "EnableOrDisable", + "Label": "GTM status", + "HelpText": null, + "DefaultValue": "STATE_ENABLED", + "DisplaySettings": { + "Octopus.SelectOptions": "STATE_ENABLED|Enable +STATE_DISABLED|Disable", + "Octopus.ControlType": "Select" + } + }, + { + "Name": "PoolName", + "Label": "GTM Pool name", + "HelpText": null, + "DefaultValue": "pl_DummyDeployWeb", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MemberIP", + "Label": "GTM member IP", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MemberPort", + "Label": "GTM member port", + "HelpText": null, + "DefaultValue": "80", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HostName", + "Label": "GTM host name", + "HelpText": null, + "DefaultValue": "192.168.45.204", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Username", + "Label": "GTM username", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Password", + "Label": "GTM password", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedOn": "2014-07-24T10:41:45.863+00:00", + "LastModifiedBy": "leeenglestone", + "$Meta": { + "ExportedAt": "2014-07-29T14:51:04.864+00:00", + "OctopusVersion": "2.5.4.280", + "Type": "ActionTemplate" + }, + "Category": "f5" +} diff --git a/step-templates/f5-ltm-rest-enable-disable-member.json.human b/step-templates/f5-ltm-rest-enable-disable-member.json.human new file mode 100644 index 000000000..2665aa728 --- /dev/null +++ b/step-templates/f5-ltm-rest-enable-disable-member.json.human @@ -0,0 +1,212 @@ +{ + "Id": "4703dd4c-9f2d-471d-b15a-c1e345378695", + "Name": "F5-LTM-REST - Enable, Disable, or Force Offline Member", + "Description": "F5-LTM-REST - Enable, Disable, or Force Offline Member with optional wait for connections to drop", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Based on the F5 template by spuder. +# This template has been translated to use REST instead of the iControl SOAP commandlets. +# https://www.powershellgallery.com/packages/F5-LTM/1.4.297 +# https://devcentral.f5.com/s/articles/powershell-module-for-the-f5-ltm-rest-api +Import-Module -Name F5-LTM + +function WaitFor-ConnectionCount() +{ + param( + $pool_name, + $member, + [int]$MaxWaitTime = 300, #defaults to 5 minutes + $ConnectionCount = 0 + ) + $member_addr = $member + + Write-Host \"Waiting for current connections to drop to \"$ConnectionCount + + $cur_connections = 100; + $elapsed = [System.Diagnostics.Stopwatch]::StartNew(); + + while ( $cur_connections -gt $ConnectionCount -and $elapsed.ElapsedMilliseconds -lt ($MaxWaitTime * 1000)) + { + $MemberStatisticsA = Get-PoolMemberStats -PoolName $pool_name -Address $member_addr + $MemberStatisticEntry = $MemberStatisticsA.'serverside.curConns' + $cur_connections = $MemberStatisticEntry.Value; + + Write-Host \"Current Connections: $cur_connections\" + + Start-Sleep -s 5 + } +} + +$Pool = $OctopusParameters['F5LTM.PoolName'].trim(); + +If ([string]::IsNullOrWhiteSpace($OctopusParameters['F5LTM.MemberIP'])) { + Write-Host \"No IP Adress was provided on the 'LTM Member IP`, using [System.Net.Dns]::GetHostAddresses to resolve it\" + $ip = $([System.Net.Dns]::GetHostAddresses(\"$($OctopusParameters['Octopus.Machine.Hostname'])\") | Where {$_.AddressFamily -ne 'InterNetworkV6'}).IpAddressToString + if ($ip -is [array]) { + Write-Host \"Found multiple ipv4 addresses, using first address $($ip[0])\" + $ip = $ip[0] + } +} Else { + $ip = $OctopusParameters['F5LTM.MemberIP'] +} + +$Member = $ip + +Write-Host \"Member is $Member\" + +# Gets the hostname of the current machine being deployed. +$curhost = hostname +$hostname = $OctopusParameters['F5LTM.HostName'] +$username = $OctopusParameters['F5LTM.Username'] +$password = ConvertTo-SecureString $OctopusParameters['F5LTM.Password'] -AsPlainText -Force +$credential = New-Object System.Management.Automation.PSCredential ($username, $password) + +Write-host \"Currently deploying to $curhost\" + +If (($OctopusParameters['F5LTM.EnableOrDisable'] -ne \"Enabled\") -and ($OctopusParameters['F5LTM.WaitForConnections'] -eq \"True\")) +{ + New-F5Session -LTMName $hostname -LTMCredentials $credential + + Write-Host \"Setting $Member to $($OctopusParameters['F5LTM.EnableOrDisable']) in $Pool pool\"; + + Disable-PoolMember -PoolName $Pool -Address $Member + + Write-Host \"Waiting for connections to drain before deploying. This could take a while....\" + + WaitFor-ConnectionCount -pool_name $Pool -member $Member -MaxWaitTime $OctopusParameters['F5LTM.MaxWaitTime'] -ConnectionCount $OctopusParameters['F5LTM.ConnectionCount'] + + if ($OctopusParameters['F5LTM.EnableOrDisable'] -eq \"Disabled\") + { + Write-Host \"Setting $Member to Offline in $Pool pool\"; + + Disable-PoolMember -PoolName $Pool -Address $Member -Force + } +} +Else +{ + New-F5Session -LTMName $hostname -LTMCredentials $credential + + Write-host \"Setting $Member to $($OctopusParameters['F5LTM.EnableOrDisable']) in $Pool pool.\" + + if ($OctopusParameters['F5LTM.EnableOrDisable'] -eq \"Disabled\") + { + Disable-PoolMember -PoolName $Pool -Address $Member -Force + } + Else + { + Enable-PoolMember -PoolName $Pool -Address $Member + } +}" + }, + "Parameters": [ + { + "Id": "5ed914ec-ff22-436d-ac37-1ccc5216372e", + "Name": "F5LTM.WaitForConnections", + "Label": "Wait on Connections?", + "HelpText": "If checked, the deployment won't continue until current connections on the node = 0", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "d3b82f88-87d5-4aec-be2a-4a0b3408b8db", + "Name": "F5LTM.ConnectionCount", + "Label": "Kill connections when <=", + "HelpText": "The default is to wait until there are no connections left on the node. If you don't want to wait for zero connections before deploying, you can put a value here and when that number of connections is reached the deployment will happen killing the rest of the connected sessions.", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2207ca51-ce9c-4498-8394-e0553fbcf54c", + "Name": "F5LTM.MaxWaitTime", + "Label": "Max. Wait Time (seconds)", + "HelpText": "Maximum wait time (in seconds) before killing connections and moving on. Defaults to 5 minutes", + "DefaultValue": "300", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7f503c0b-0050-4685-a9d8-7dbc42199ac0", + "Name": "F5LTM.EnableOrDisable", + "Label": "LTM Status", + "HelpText": "State member will be put into (Enabled, Disabled, Offline) + +- **Disabled** continues to process persistent and active connections. It can accept new connections only if the connections belong to an existing persistence session. + +- **Offline** allows existing connections to time out, but no new connections are allowed.", + "DefaultValue": "Disabled", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Enabled|Enable +Disabled|Disable +Offline|Forced Offline" + } + }, + { + "Id": "e49bb08a-c72b-4bae-95ea-bb2c4c808657", + "Name": "F5LTM.PoolName", + "Label": "LTM Pool name", + "HelpText": "Name of F5 pool", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "50225d2a-503c-443e-85ca-8a42cba25ea3", + "Name": "F5LTM.MemberIP", + "Label": "LTM Member IP", + "HelpText": "IP Address of F5 pool member. Default will auto discover the ip address of Tentacle.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "559c46da-55be-4bb7-88fd-0d17b09455c5", + "Name": "F5LTM.HostName", + "Label": "LTM Host name", + "HelpText": "The DNS name or IP address of the F5 LTM device.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "25f4bb72-26df-4c97-8b4b-fbac72aa2655", + "Name": "F5LTM.Username", + "Label": "LTM username", + "HelpText": "Credential used to access F5 REST API", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4973c257-489f-4c1c-8c30-721e3ff11ccc", + "Name": "F5LTM.Password", + "Label": "LTM password", + "HelpText": "Credential used to access F5 REST API", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedBy": "gvanderberg", + "$Meta": { + "ExportedAt": "2020-06-10T10:23:31.760Z", + "OctopusVersion": "2019.12.0", + "Type": "ActionTemplate" + }, + "Category": "f5" +} diff --git a/step-templates/file-download.json.human b/step-templates/file-download.json.human new file mode 100644 index 000000000..6ae8d9515 --- /dev/null +++ b/step-templates/file-download.json.human @@ -0,0 +1,54 @@ +{ + "Id": "f444ca63-a73c-4878-955e-96b508f9e883", + "Name": "Download file", + "Description": "Downloads a file from the internet to the local machine.", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "Write-Verbose \"Downloading file $FileUrl, to the destination $FilePath\" -Verbose\r +$client = new-object System.Net.WebClient\r +$client.DownloadFile($FileUrl, $FilePath)\r +Write-Verbose \"File downloadded\" -Verbose\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "3e6a8f44-5791-4571-8e90-025f6776c6d0", + "Name": "FileUrl", + "Type": "String", + "Label": "File Url", + "HelpText": "Url of the file you want to download", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1b67fd5f-f626-40f4-80bb-f10fe681abf3", + "Name": "FilePath", + "Type": "String", + "Label": "File Destination Path", + "HelpText": "Destination path of the file on disk", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "ahmedig", + "$Meta": { + "ExportedAt": "2017-03-07T05:12:43.174Z", + "OctopusVersion": "3.11.2", + "Type": "ActionTemplate" + }, + "Category": "http" +} diff --git a/step-templates/file-system-add-hosts-entry.json.human b/step-templates/file-system-add-hosts-entry.json.human new file mode 100644 index 000000000..69ca5c2d9 --- /dev/null +++ b/step-templates/file-system-add-hosts-entry.json.human @@ -0,0 +1,134 @@ +{ + "Id": "0ad0ad00-adad-adad-adad-000000000001", + "Name": "File System - Add entry to hosts file", + "Description": "Adds an entry to etc/hosts file with the specified host name and optional ip address", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +Param( + [string] $AD_AddHostsEntry_HostName, + [string] $AD_AddHostsEntry_IpAddress = \"127.0.0.1\", + [Int16] $AD_AddHostsEntry_Attempts = 5, + [switch] $WhatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($null -ne $OctopusParameters) { + $result = $OctopusParameters[$Name] + } + + if ($null -eq $result) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($null -ne $variable) { + $result = $variable.Value + } + } + + if ($null -eq $result) { + if ($Required) { + throw \"Missing parameter value $Name\" + } + else { + $result = $Default + } + } + + return $result +} + +function Execute( + [Parameter(Mandatory = $true)][string] $HostName, + [Parameter(Mandatory = $false)][string] $IpAddress = \"127.0.0.1\", + [Parameter(Mandatory = $false)][Int16] $Attempts = 5 +) { + $attemptCount = 0 + $operationIncomplete = $true + $maxFailures = $Attempts + $sleepBetweenFailures = 1 + + $hostsFile = \"$($env:windir)\\system32\\Drivers\\etc\\hosts\" + $entry = \"$IpAddress $HostName\" + $regexMatch = \"^\\s*$IpAddress\\s+$HostName\" + while ($operationIncomplete -and $attemptCount -lt $maxFailures) { + $attemptCount = ($attemptCount + 1) + if ($attemptCount -ge 2) { + Write-Output \"Waiting for $sleepBetweenFailures seconds before retrying...\" + Start-Sleep -s $sleepBetweenFailures + Write-Output \"Retrying...\" + $sleepBetweenFailures = ($sleepBetweenFailures * 2) + } + try { + $matchingEntries = @(Get-Content $hostsFile) -match ($regexMatch) + if (-Not $matchingEntries) { + Write-Output \"Entry '$entry' doesn't exist - ADDING...\" + if (-Not ($WhatIf)) { + $formattedEntry = [environment]::newline + $entry + [System.IO.File]::AppendAllText($hostsFile, $formattedEntry) + } + Write-Output \"Entry '$entry' - ADDED\" + } + else { + Write-Output \"Entry '$entry' already exists - SKIPPING\" + } + $operationIncomplete = $false + } + catch [System.Exception] { + if ($attemptCount -lt ($maxFailures)) { + Write-Host (\"Attempt $attemptCount of $maxFailures failed: \" + $_.Exception.Message) + } + else { + throw + } + } + } +} +& Execute ` +(Get-Param 'AD_AddHostsEntry_HostName' -Required)` +(Get-Param 'AD_AddHostsEntry_IpAddress')` +(Get-Param 'AD_AddHostsEntry_Attempts') +" + }, + "Parameters": [ + { + "Name": "AD_AddHostsEntry_HostName", + "Label": "Host name", + "HelpText": "The host name", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AD_AddHostsEntry_IpAddress", + "Label": "IP address", + "HelpText": "The optional IP address", + "DefaultValue": "127.0.0.1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AD_AddHostsEntry_Attempts", + "Label": "Nr of attempts", + "HelpText": "Optional number of attempts before failing", + "DefaultValue": "5", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "anatolie-darii", + "$Meta": { + "ExportedAt": "2018-07-16T12:37:14.428Z", + "OctopusVersion": "2018.2.8", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-backup-directory.json.human b/step-templates/file-system-backup-directory.json.human new file mode 100644 index 000000000..bc101cccb --- /dev/null +++ b/step-templates/file-system-backup-directory.json.human @@ -0,0 +1,92 @@ +{ + "Id": "0acca3d2-7afa-4c51-963e-6f204b009f85", + "Name": "File System - Backup Directory", + "Description": "Uses Robocopy to backup directories and files from a source to a destination.", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function Get-Stamped-Destination($BackupDestination) {\r +\t$stampedFolderName = get-date -format \"yyyy-MM-dd\"\r +\t$count = 1\r +\t$stampedDestination = Join-Path $BackupDestination $stampedFolderName\r +\twhile(Test-Path $stampedDestination) {\r +\t\t$count++\r +\t\t$stamped = $stampedFolderName + \"(\" + $count + \")\"\r +\t\t$stampedDestination = Join-Path $BackupDestination $stamped\r +\t}\r +\treturn $stampedDestination\r +}\r +\r +$BackupSource = $OctopusParameters['BackupSource']\r +$BackupDestination = $OctopusParameters['BackupDestination']\r +$CreateStampedBackupFolder = $OctopusParameters['CreateStampedBackupFolder']\r +if($CreateStampedBackupFolder -like \"True\" ) {\r +\t$BackupDestination = get-stamped-destination $BackupDestination\r +}\r +\r +$options = $OctopusParameters['Options'] -split \"\\s+\"\r +\r +if(Test-Path -Path $BackupSource) {\r + robocopy $BackupSource $BackupDestination $options\r +}\r +\r +if($LastExitCode -gt 8) {\r + exit 1\r +}\r +else {\r + exit 0\r +}\r +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "BackupSource", + "Label": "Source", + "HelpText": "The source directory where files and folders will be copied from", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BackupDestination", + "Label": "Destination folder", + "HelpText": "The Destination where the files will be copied to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Options", + "Label": "Robocopy options", + "HelpText": "Robocopy accepts a few command line options (e.g. /S /E /Z). List of these can be [found here](http://ss64.com/nt/robocopy.html)", + "DefaultValue": "/E /V", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "CreateStampedBackupFolder", + "Label": "Create stamped backup folder", + "HelpText": "If set to _True_ then it will create a dated backup folder under the destination folder (e.g. c:\\backup\\2014-05-11)", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2016-08-25T15:50:24.584+00:00", + "OctopusVersion": "3.3.27", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-check-disk-space.json.human b/step-templates/file-system-check-disk-space.json.human new file mode 100644 index 000000000..93ead8e60 --- /dev/null +++ b/step-templates/file-system-check-disk-space.json.human @@ -0,0 +1,144 @@ +{ + "Id": "e74ff6a3-65ce-4a3a-8cbd-2224653af3a2", + "Name": "File System - Check Disk Space", + "Description": "Checks all or specified fixed drives for free space, either as an absolute number (GB) or relative (%). +If the available disk space does not meet the minimum requirements, as set in the parameters, as error is thrown. + +Author: Jim (Dimitrios) Andrakakis, [dandraka.com](https://dandraka.com)", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": " +# Jim (Dimitrios) Andrakakis +# dandraka.com +# December 2020 + +param([int]$pSpaceGB = $fschkSpaceGB, +\t[int]$pSpacePercent = $fschkSpacePercent, + [string]$pDrives = $fschkDrives) + +# ================= PARAMETERS, CONSTANTS ETC ================= + +$ErrorActionPreference = \"Stop\" +Clear-Host + +$win32_logicaldisk_LocalDiskDriveType = 3 + +Write-Host \"Parameters: SpaceGB '$pSpaceGB'\" +Write-Host \"Parameters: SpacePercent '$pSpacePercent'\" +Write-Host \"Parameters: Drives '$pDrives'\" + +[bool]$checkSpaceAbsolute = $false +[bool]$checkSpacePercent = $false +[bool]$checkAllDrives = $true +$driveList = New-Object System.Collections.ArrayList + +# ================= SANITY CHECKS ================= + +$allDrives = get-wmiobject -class win32_logicaldisk | Where-Object { $_.DriveType -eq $win32_logicaldisk_LocalDiskDriveType } +Write-Host \"Drives found in the system: $($allDrives | ForEach-Object { $_.DeviceID })\" + +if ($pSpaceGB -gt 0) { + $checkSpaceAbsolute = $true + Write-Host \"Will check that space > $pSpaceGB\" +} + +if ($pSpacePercent -gt 0) { + $checkSpacePercent = $true + Write-Host \"Will check that space > $pSpacePercent %\" +} + +if ((-not $checkSpaceAbsolute) -and (-not $checkSpacePercent)) { + Write-Error \"Neither Space(GB) nor Space(%) check was specified. Please specify at least one.\" +} + +if ([string]::IsNullOrWhiteSpace($pDrives)) { + foreach($d in $allDrives) { $driveList.Add($d) | Out-Null } + Write-Host \"Will check all fixed drives $($driveList | ForEach-Object { $_.DeviceID + \" \" })\" +} +else { + $checkAllDrives = $false + foreach($d in $allDrives) { if ($pDrives.Contains($d.DeviceID)) { $driveList.Add($d) | Out-Null | Out-Null } } + Write-Host \"Will check fixed drives $($driveList | ForEach-Object { $_.DeviceID + \" \" })\" +} + +if ($driveList.Count -eq 0) { + Write-Error \"No drives were found or, most likely, the drive list parameter does not contain any of the existing drives.\" +} + +# ================= RUN CHECKS ================= +foreach($d in $driveList) { + $driveDescr = \"$($d.DeviceID) [$($d.VolumeName)]\" + $pDrivespaceGBFree = [Math]::Round(($d.FreeSpace / [Math]::Pow(1024,3)), 1) + $pDrivespaceGBTotal = [Math]::Round(($d.Size / [Math]::Pow(1024,3)), 1) + $pDrivespacePercentFree = [Math]::Round($pDrivespaceGBFree / $pDrivespaceGBTotal,1) * 100 + Write-Host \"Drive $driveDescr : Free $pDrivespaceGBFree GB ($pDrivespacePercentFree%), Total $pDrivespaceGBTotal GB\" + + if ($checkSpaceAbsolute) { + if ($pDrivespaceGBFree -lt $pSpaceGB) { + Write-Error \"Drive $driveDescr has less than the required space ($pSpaceGB GB)\" + } + } + if ($checkSpacePercent) { + if ($pDrivespacePercentFree -lt $pSpacePercent) { + Write-Error \"Drive $driveDescr has less than the required space ($pSpacePercent %)\" + } + } +}" + }, + "Parameters": [ + { + "Id": "640be7e1-aa4f-4cc8-b01c-4fe6ef1a3757", + "Name": "fschkSpaceGB", + "Label": "Required Disk Space (GB)", + "HelpText": "Number, integer. +The minimum required space in GB. +Zero means do not check GB. +Example: 2 +which means 2GB.", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0d28eda8-4fd0-47c5-8faf-6d1bd27a7247", + "Name": "fschkSpacePercent", + "Label": "Required Disk Space (%)", + "HelpText": "Number, integer. +The minimum required space in % of the total space. +Zero means do not check %. +Example: 10 +which means 10%.", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "00037224-9932-46e3-9cfb-5723e0b8d702", + "Name": "fschkDrives", + "Label": "Drives", + "HelpText": "Comma separated list of drive names to check. +Please use the drive label, i.e. the drive letter and a colon (C: , D: etc). +Example: C:,D:,H: +which means, the check will be done on C:, D: and H:, if they exist; if some of them do not exist in the system, they will be ignored; if none of them exist, an error is thrown.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2020-12-14T11:00:00.000+00:00", + "LastModifiedBy": "dandraka", + "Category": "filesystem", + "$Meta": { + "ExportedAt": "2020-12-14T09:37:17.680Z", + "OctopusVersion": "2020.1.16", + "Type": "ActionTemplate" + } +} diff --git a/step-templates/file-system-clean-asp-net-temp-files.json.human b/step-templates/file-system-clean-asp-net-temp-files.json.human new file mode 100644 index 000000000..94e85570d --- /dev/null +++ b/step-templates/file-system-clean-asp-net-temp-files.json.human @@ -0,0 +1,194 @@ +{ + "Id": "47fa89d0-fffd-4686-978d-4d54d944df55", + "Name": "File System - Clean ASP.NET Temp Files", + "Description": "Most ASP.NET websites today make use of dynamic compilation. +The dynamically compiled assemblies are stored in the Temporary ASP.NET files directory. +However, files in this directory are not automatically removed and may build up over time. +This script will clean out all files in this directory. +You should note that this may cause running websites to be restarted.", + "ActionType": "Octopus.Script", + "Version": 9, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\r +param(\r +\t[string]$frameworkVersion,\r +\t[int]$daysToKeep,\r +\t[switch]$whatIf\r +) \r +\r +$ErrorActionPreference = \"Stop\"\r +\r +function Get-Param($Name, [switch]$Required, $Default) {\r +\t$result = $null\r +\t\r +\tif ($OctopusParameters -ne $null) {\r +\t\t$result = $OctopusParameters[$Name]\r +\t}\r +\r +\tif ($result -eq $null) {\r +\t\t$variable = Get-Variable $Name -EA SilentlyContinue\t\r +\t\tif ($variable -ne $null) {\r +\t\t\t$result = $variable.Value\r +\t\t}\r +\t}\r +\t\r +\tif ($result -eq $null) {\r +\t\tif ($Required) {\r +\t\t\tthrow \"Missing parameter value $Name\"\r +\t\t} else {\r +\t\t\t$result = $Default\r +\t\t}\r +\t}\r +\r +\treturn $result\r +}\r +\r +function RemoveSafely-Item($folder, $Old, [switch]$whatIf) {\r +\t\r +\tif ($folder.LastWriteTime -lt $old) {\r +\t\t\r +\t\ttry {\r +\t\t\tWrite-Host \"Removing: $($folder.FullName)\"\r +\t\t\t$folder | Remove-Item -Recurse -Force -WhatIf:$whatIf -EA Stop\r +\t\t} catch {\r +\t\t\t$message = $_.Exception.Message\r +\t\t\tWrite-Host \"Info: Could not remove $itemName. $message\"\r +\t\t}\r +\t}\r +}\r +\r +& {\r +\tparam(\r +\t\t[string]$frameworkVersion,\r +\t\t[int]$daysToKeep\r +\t) \r +\r +\tWrite-Host \"Cleaning Temporary ASP.NET files directory\"\r +\t\r +\tif ([string]::IsNullOrEmpty($frameworkVersion)) {\r +\t\tthrow \"You need to specify the frameworkVersion parameter\"\r +\t}\r +\t\r +\tWrite-Host \"FrameworkVersion: $frameworkVersion\"\r +\t\r +\t$dotnetPath = [System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory() | Split-Path | Split-Path\r +\t$bitnessValues = @(\"Framework\", \"Framework64\")\r +\t$versionStart = \"v\"\r +\t$versionFilter = \"$versionStart\"\r +\t$tempDir = \"Temporary ASP.NET Files\"\r +\t\r +\t$directoriesToClean = @()\r +\t\r +\tif ($frameworkVersion -ne \"All\") {\r +\t\r +\t\t# Starts with v\r +\t\tif ($frameworkVersion.StartsWith($versionStart, \"CurrentCultureIgnoreCase\")) {\r +\t\t\t$versionFilter = $frameworkVersion\r +\t\t\tif ($frameworkVersion -contains \"\\\") {\r +\t\t\t\tthrow \"Framework version cannot contain '\\'\"\r +\t\t\t}\r +\t\t} else {\r +\t\t\r +\t\t\t# Includes one \\\r +\t\t\t$firstSlash = $frameworkVersion.IndexOf(\"\\\")\r +\t\t\t\r +\t\t\t$NotAVersion = -1\r +\t\t\tif ($firstSlash -eq $NotAVersion) {\r +\t\t\t\t$bitnessValues = @($frameworkVersion)\r +\t\t\t} else {\r +\t\t\t\r +\t\t\t\t$secondSlash = $frameworkVersion.IndexOf(\"\\\", $firstSlash)\r +\t\t\t\t\r +\t\t\t\t$NoExtraSlash = -1\r +\t\t\t\tif ($secondSlash -ne $NoExtraSlash) {\r +\t\t\t\t\t\r +\t\t\t\t\t$bitnessValues = @($frameworkVersion | Split-Path)\r +\t\t\t\t\t$versionFilter = @($frameworkVersion | Split-Path -Leaf)\r +\r +\t\t\t\t} else {\r +\t\t\t\t\tthrow \"Includes more than one '\\'\"\r +\t\t\t\t}\r +\t\t\t}\r +\t\t}\r +\t}\r +\t\r +\tif (!$versionFilter.StartsWith($versionStart, \"CurrentCultureIgnoreCase\")) {\r +\t\tthrow \"Version filter must start with '$versionStart'\"\r +\t}\r +\t\r +\tforeach ($bitness in $bitnessValues) {\r +\t\t$fvPath = (Join-Path $dotnetPath $bitness)\r +\t\tif (Test-Path $fvPath) {\r +\t\t\t$directoriesToClean += (ls $fvPath -Filter \"$versionFilter*\")\r +\t\t}\r +\t}\r +\t\r +\tforeach ($dir in $directoriesToClean) {\r +\t\t$fullTempPath = Join-Path $dir.FullName $tempDir\r +\t\t\r +\t\tif (Test-Path $fullTempPath) {\r +\t\t\t$virtualDirectories = ls $fullTempPath\r +\t\t\tforeach ($virtualPathDir in $virtualDirectories) {\r +\t\t\t\t$old = (Get-Date).AddDays(-$daysToKeep)\r +\t\t\t\t\r +\t\t\t\tforeach ($siteDir in (Get-ChildItem $virtualPathDir.FullName)) {\r +\t\t\t\t\tRemoveSafely-Item $siteDir $old -WhatIf:$whatIf\r +\t\t\t\t}\r +\t\t\t}\r +\t\t}\r +\t}\r +\t\r + } `\r + (Get-Param 'frameworkVersion' -Required) `\r + (Get-Param 'daysToKeep' -Default 30) \r + ", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "FrameworkVersion", + "Label": "Framework version", + "HelpText": "This is the framework version to target. +Specifying `All` will clean out the temp files for each installed version of the framework. +If you need to target a specific version, you can specify either the bit-ness, version or both. + +Example values: +`Framework` +`Framework64` +`v4` +`v2.0.50727` +`v2.0.50727` +`Framework\\v4.0.30319` +`Framework64\\v4.0.30319` +`Framework64\\v2` + +Specifying a bit-ness value will match all versions. +Specifying only a version will match that version regarless of bit-ness. +A fully specified framework version will match only that value.", + "DefaultValue": "All", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DaysToKeep", + "Label": "Days to keep", + "HelpText": "Number of days since last write time to keep temporary files. + +Note that this is the write time of the top level folder.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2014-08-25T05:12:29.899+00:00", + "LastModifiedBy": "Lavinski", + "$Meta": { + "ExportedAt": "2014-08-25T05:12:40.750+00:00", + "OctopusVersion": "2.4.0.0", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-clean-configuration-transforms.json.human b/step-templates/file-system-clean-configuration-transforms.json.human new file mode 100644 index 000000000..a2ef465d1 --- /dev/null +++ b/step-templates/file-system-clean-configuration-transforms.json.human @@ -0,0 +1,118 @@ +{ + "Id": "9a2b84db-2940-4d9a-b61f-c82df35cee6c", + "Name": "File System - Clean Configuration Transforms", + "Description": "Clean out configuration transform files from the installation directory after a deployment (e.g. Web.Release.config, YourApp.Production.config, etc.).", + "ActionType": "Octopus.Script", + "Version": 10, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside Octopus Deploy +param( + [string]$pathToClean, + [string]$environmentName, + [switch]$whatIf +) + +function GetParam($Name, [switch]$Required) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if ($Required -and [string]::IsNullOrEmpty($result)) { + throw \"Missing parameter value $Name\" + } + + return $result +} + +& { + param( + [string]$pathToClean, + [string]$environmentName + ) + + Write-Host \"Cleaning Configuration Transform files from $pathToClean and environment: $environmentName\" + + if (Test-Path $pathToClean) { + Write-Host \"Scanning directory $pathToClean\" + $regexFilter = \"*.$environmentName.config\" + Write-Host \"Filter $regexFilter\" + + if ($pathToClean -eq \"\\\" -or $pathToClean -eq \"/\") { + throw \"Cannot clean root directory\" + } + + $filesToDelete = Get-ChildItem $pathToClean -Filter $regexFilter -Recurse | ` + Where-Object {!$_.PsIsContainer -and ($_.Name -NotMatch \"((?i)(^.*\\.exe\\.config$|.*\\.dll\\.config$)$)\")} + + if (!$filesToDelete -or $filesToDelete.Count -eq 0) { + Write-Warning \"There were no files matching the criteria\" + } else { + + Write-Host \"Deleting files\" + if ($whatIf) { + Write-Host \"What if: Performing the operation `\"Remove File`\" on targets\" + } + + foreach ($file in $filesToDelete) + { + Write-Host \"Deleting file $($file.FullName)\" + + if (!$whatIf) { + Remove-Item $file.FullName -Force + } + } + } + + } else { + Write-Warning \"Could not locate path `\"$pathToClean`\"\" + } + +} ` +(GetParam 'PathToClean' -Required) ` +(GetParam 'EnvironmentName' -Required) +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "PathToClean", + "Label": "Path to clean", + "HelpText": "The path to clean. + +Usually you would set this to `#{Octopus.Action[StepName].Output.Package.InstallationDirectoryPath}`.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "EnvironmentName", + "Label": "Environment Name", + "HelpText": "If you want to nuke all config files i.e. `*.*.config` then leave with the default value `*`. + +Otherwise to just nuke `*.EnvironmentName.config` then set as the environment name: `#{Octopus.Environment.Name}`", + "DefaultValue": "*", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2016-01-12T15:51:40.889+00:00", + "LastModifiedBy": "boro2g", + "$Meta": { + "ExportedAt": "2016-01-21T15:39:24.527+00:00", + "OctopusVersion": "3.1.4", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-clean-directory.json.human b/step-templates/file-system-clean-directory.json.human new file mode 100644 index 000000000..dba7e1be2 --- /dev/null +++ b/step-templates/file-system-clean-directory.json.human @@ -0,0 +1,211 @@ +{ + "Id": "e56aafe2-0d59-453b-9449-d7384914468d", + "Name": "File System - Clean Directory", + "Description": "Clean out unwanted files from the installation directory after a deployment.", + "ActionType": "Octopus.Script", + "Version": 27, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\r +param(\r +\t[string]$cleanInclude,\r +\t[string]$cleanIgnore,\r +\t[string]$pathsToClean,\r +\t[switch]$whatIf\r +) \r +\r +function ExpandPathExpressions($workingDirectory, $fileExpressionList) {\r +\treturn @($fileExpressionList.Split(@(\";\"), \"RemoveEmptyEntries\")) | \r +\t% { $_.Trim() } |\r +\t% { ExpandPathExpression $workingDirectory $_ }\r +}\r +\r +function ExpandPathExpression($workingDirectory, $FileExpression) {\r +\r +\t# \\**\\ denotes a recursive search\r +\t$recurse = \"**\"\r +\r +\t# Scope the clean!\r +\t$fileExpression = Join-Path $workingDirectory $fileExpression\r +\r +\t$headSegments = Split-Path $fileExpression\r +\t$lastSegment = Split-Path $fileExpression -Leaf\r +\t$secondLastSegment = $(if($headSegments -ne \"\") {Split-Path $headSegments -Leaf} else {$null}) \r +\r +\t$path = \"\\\"\r +\t$recursive = $false\r +\t$filter = \"*\"\r +\t\r +\tif ($lastSegment -eq $recurse) {\t\r +\t\r +\t\t$path = $headSegments\r +\t\t$recursive = $true\r +\t\t\r +\t} elseif ($secondLastSegment -eq $recurse) {\r +\t\t\r +\t\t$path = Split-Path $headSegments\r +\t\t$recursive = $true\r +\t\t$filter = $lastSegment\t\r +\t\r +\t} else {\r +\t\t\r +\t\t$path = $headSegments\r +\t\t$filter = $lastSegment \r +\t}\r +\r +\treturn Get-ChildItem -Path $path -Filter $filter -Recurse:$recursive | ? { !$_.PSIsContainer }\r +}\r +\r +function GetParam($Name, [switch]$Required) {\r +\t$result = $null\r +\t\r +\tif ($OctopusParameters -ne $null) {\r +\t\t$result = $OctopusParameters[$Name]\r +\t}\r +\r +\tif ($result -eq $null) {\r +\t\t$variable = Get-Variable $Name -EA SilentlyContinue\t\r +\t\tif ($variable -ne $null) {\r +\t\t\t$result = $variable.Value\r +\t\t}\r +\t}\r +\t\r +\tif ($Required -and $result -eq $null) {\r +\t\tthrow \"Missing parameter value $Name\"\r +\t}\r +\t\r +\treturn $result\r +}\r +\r +& {\r +\tparam(\r +\t\t[string]$cleanInclude,\r +\t\t[string]$cleanIgnore,\r +\t\t[string]$pathsToClean\r +\t) \r +\r +\tWrite-Host \"Cleaning files from installation directory\"\r +\tWrite-Host \"Include: $cleanInclude\"\r +\tWrite-Host \"Ignore: $cleanIgnore\"\r +\tWrite-Host \"Paths To Clean: $pathsToClean\"\r +\t\r +\tif (!$cleanInclude) {\r +\t\tthrow \"You must specify files to include\"\r +\t}\r +\t\r +\tif (!$pathsToClean) {\r +\t\tthrow \"You must specify the paths to clean\"\r +\t}\r +\t\r +\t$paths = @($pathsToClean.Split(@(\";\"), \"RemoveEmptyEntries\")) | \r +\t% { $_.Trim() }\r +\t\r +\tforeach ($pathToClean in $paths) {\r +\t\t\r +\t\tif (Test-Path $pathToClean) {\r +\t\t\tWrite-Host \"Scanning directory $pathToClean\"\r +\t\t\t\r +\t\t\tif ($pathToClean -eq \"\\\" -or $pathToClean -eq \"/\") {\r +\t\t\t\tthrow \"Cannot clean root directory\"\r +\t\t\t}\r +\t\t\t\r +\t\t\tcd $pathToClean\r +\t\t\t\r +\t\t\t$include = ExpandPathExpressions $pathToClean $cleanInclude\r +\t\t\t$exclude = ExpandPathExpressions $pathToClean $cleanIgnore\r +\t\t\t\r +\t\t\tif ($include -eq $null -or $exclude -eq $null) {\r +\t\t\t\t$deleteSet = $include\r +\t\t\t} else {\r +\t\t\t\t$exclude = $exclude | % {$_}\r +\t\t\t\t$deleteSet = Compare-Object $include $exclude | ? { $_.SideIndicator -eq \"<=\" } | % { $_.InputObject }\r +\t\t\t}\r +\t\t\t\r +\t\t\tif (!$deleteSet -or $deleteSet.Count -eq 0) {\r +\t\t\t\tWrite-Warning \"There were no files matching the criteria\"\r +\t\t\t} else {\r +\t\t\t\t\r +\t\t\t\tWrite-Host \"Deleting files\"\r +\t\t\t\tif ($whatIf) {\r +\t\t\t\t\tWrite-Host \"What if: Performing the operation `\"Remove File`\" on targets\"\r +\t\t\t\t}\r +\t\t\t\t\r +\t\t\t\t$deleteSet | Write-Host\r +\t\t\t\t\r +\t\t\t\tif (!$whatIf) {\r +\t\t\t\t\t$deleteSet | % { $_.FullName } | Remove-Item -Force -Recurse -WhatIf:$whatIf\r +\t\t\t\t}\r +\t\t\t}\r +\t\t\r +\t\t} else {\r +\t\t\t\r +\t\t\tWrite-Warning \"Could not locate path `\"$pathToClean`\"\"\r +\t\t}\r +\t}\r + } `\r + (GetParam 'CleanInclude' -Required) `\r + (GetParam 'CleanIgnore') `\r + (GetParam 'PathsToClean')\r + ", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "PathsToClean", + "Label": "Paths to clean", + "HelpText": "A semicolon-separated list of paths to clean. + +Usually you would set this to `Octopus.Action[StepName].Output.Package.InstallationDirectoryPath`.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "CleanInclude", + "Label": "Files to remove", + "HelpText": "A semicolon-separated list of path expressions that match files to be removed. + +Examples: + + - *.jpg + - web.*.config + - **\\*.dll + - \\Logs\\**\\*.txt + - web.*.config;*.txt + +`\\**\\` denotes a recursive search", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "CleanIgnore", + "Label": "Files to ignore", + "HelpText": "A semicolon-separated list of path expressions that will be not be removed. + +Examples: + + - web.log4net.config + - img\ +eeded.jpg + - **\\*.dll + - web.config;Release Notes.txt + +`\\**\\` denotes a recursive search", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-08-20T06:59:52.579+00:00", + "OctopusVersion": "2.4.0.0", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-combine-files.json.human b/step-templates/file-system-combine-files.json.human new file mode 100644 index 000000000..70645afa5 --- /dev/null +++ b/step-templates/file-system-combine-files.json.human @@ -0,0 +1,150 @@ +{ + "Id": "a1983aec-a8ca-4fda-b763-081fd0acecf8", + "Name": "File System - Combine all files in directory into single file", + "Description": "This step template will take all the files in a single directory, sort them alphabetically, and combine them into a single file. + +Optional, it will create an artifact for that file so it can be reviewed. ", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$sourceDirectoryPath = $OctopusParameters[\"CombineFiles.Directory.Source\"] +$sourceDirectoryPackagePath = $OctopusParameters[\"CombineFiles.Directory.PackageSource\"] +$sourceDirectoryFilter = $OctopusParameters[\"CombineFiles.Directory.Filter\"] +$destinationFile = $OctopusParameters[\"CombineFiles.Destination.FileName\"] +$createArtifact = $OctopusParameters[\"CombineFiles.Destination.CreateArtifact\"] +$commentCharacters = $OctopusParameters[\"CombineFiles.Destination.CommentCharacters\"] + +if ([string]::IsNullOrWhiteSpace($sourceDirectoryPackagePath) -eq $false){ +\tWrite-Host \"A previous package path was specified, grabing that\" +\t$sourceDirectory = $OctopusParameters[\"Octopus.Action[$sourceDirectoryPackagePath].Output.Package.InstallationDirectoryPath\"] + $sourceDirectory = \"$sourceDirectory\\$sourceDirectoryPath\" +} +else { +\t$sourceDirectory = \"$sourceDirectoryPath\" +} + +Write-Host \"Source Directory: $sourceDirectory\" +Write-Host \"Source File Filter: $sourceDirectoryFilter\" +Write-Host \"Combined File Name: $destinationFile\" +Write-Host \"Create Artifact: $createArtifact\" +Write-Host \"Comment Characters: $commentCharacters\" + +if ([string]::IsNullOrWhiteSpace($sourceDirectory)){ +\tthrow-exception \"The source directory variable is required.\" +} + +if ((Test-Path $sourceDirectory) -eq $false){ +\tWrite-Host \"The directory $sourceDirectory was not found, skipping\" +\texit 0 +} + +if ((Test-Path $destinationFile) -eq $false){ +\tWrite-Host \"Creating the file: $destinationFile\" +\tNew-Item -Path $destinationFile -ItemType \"file\" +} +else { +\tWrite-Host \"The file $destinationFile already exists\" +} + +if ([string]::IsNullOrWhiteSpace($sourceDirectoryFilter)){ +\tWrite-Host \"Source directory filter not specified, grabbing all files\" +\t$filePath = \"$sourceDirectory\\*\" +} +else { +\tWrite-Host \"Source directory filter specified, grabbing filtered files\" +\t$filePath = \"$sourceDirectory\\*\" +} + +Write-Host \"Getting child items using $filePath\" +$filesToCombine = Get-ChildItem -Path $filePath -File | Sort-Object -Property Name + +foreach ($file in $filesToCombine) +{ +\tWrite-Host \"Adding content to $changeScript from $file\" + +\tif ([string]::IsNullOrWhiteSpace($commentCharacters) -eq $false){ +\t\tAdd-Content -Path $destinationFile -Value \"$commentCharacters Contents from $file\" + } + +\tAdd-Content -Path $destinationFile -Value (Get-Content $file) +} + +if ($createArtifact -eq \"True\"){\t + New-OctopusArtifact -Path \"$destinationFile\" +}" + }, + "Parameters": [ + { + "Id": "4754945b-530f-4da5-bb50-94bd8b7a2239", + "Name": "CombineFiles.Directory.PackageSource", + "Label": "Source Package Step", + "HelpText": "**Optional** - The name of the package step containing the files to combine. Leave this blank if the full path is in the Source Directory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Id": "ee8d882e-f278-4211-93d7-7dadc6d9978d", + "Name": "CombineFiles.Directory.Source", + "Label": "Source Directory", + "HelpText": "**Optional** - The folder containing all the files to combine into a single file. If using this with the source package step variable then this is the relative path to that. Otherwise this is the full path to it.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "373ddecf-5038-4791-9ed5-dadc464b61de", + "Name": "CombineFiles.Directory.Filter", + "Label": "Source File Filter", + "HelpText": "**Optional** - The filter to apply when looking for tiles. Example *.sql or *.txt. If omitted it will grab all files in the directory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9690b60a-7e25-412f-b9b2-0889bfc225b7", + "Name": "CombineFiles.Destination.FileName", + "Label": "Combined File Name", + "HelpText": "**Required** - the file name of all the combined files. Must include the full path. IE C:\\Testing\\NewFile.sql", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "50b49c3d-d9a4-49a2-b4f7-896d49cbc87b", + "Name": "CombineFiles.Destination.CommentCharacters", + "Label": "Comment Characters", + "HelpText": "**Optional** - Use this if you want to include the file name in your combined file as comments. You will need to specify the comment characters for the language of the file. For example \"--\" for SQL files, \"//\" for C# files.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a06deb13-1561-4df0-8aee-d73696841863", + "Name": "CombineFiles.Destination.CreateArtifact", + "Label": "Create Artifact", + "HelpText": "Indicates if the combined file should be uploaded to Octopus Deploy as an artifact. Useful when combining multiple SQL scripts into a single file so a DBA can review it.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2018-11-07T14:34Z", + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2018-10-12T18:54:49.750Z", + "OctopusVersion": "2018.8.8", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-copy-file.json.human b/step-templates/file-system-copy-file.json.human new file mode 100644 index 000000000..b3fb0284f --- /dev/null +++ b/step-templates/file-system-copy-file.json.human @@ -0,0 +1,114 @@ +{ + "Id": "072c2939-2c77-4ed1-abd2-e41cb8a57661", + "Name": "File System - Copy File", + "Description": "Copies a file in the file system.", + "ActionType": "Octopus.Script", + "Version": 6, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "function Test-FileLocked([string]$filePath) +{ + $fileExists = Test-Path -Path $filePath + if (!$fileExists) + { + return (1 -eq 0) #false + } + Rename-Item $filePath $filePath -ErrorVariable errs -ErrorAction SilentlyContinue + return ($errs.Count -ne 0) +} + +function Wait-ForFileUnlock([string]$filePath) +{ + Write-Host \"Destinationfile at $filePath is locked\" + + for ($attemptNo = 1; $attemptNo -lt 6; $attemptNo++) { + Write-Host \"Waiting for the file to become unlocked $attemptNo/5\" + + Start-Sleep -Seconds 10 + + $fileIsLocked = Test-FileLocked($filePath) + + if (!$fileIsLocked) + { + return + } + + if ($attemptNo -eq 5) { + Write-Error \"destinationfile at location $filePath is locked and cannot be overwritten.\" + + return + } + } +} + +# +#script starts here +# + +$filePath = $OctopusParameters['sourcePath'] +$newFilePath = $OctopusParameters['destinationPath'] + +$fileExists = Test-Path -Path $filePath + +if (!$fileExists) +{ + Write-Error \"Sourcefile not found at $filePath\" + + return +} + +$fileIsLocked = Test-FileLocked($newFilePath) + + +if ($fileIsLocked) +{ + Wait-ForFileUnlock($newFilePath) +} + + +$fileIsLocked = Test-FileLocked($newFilePath) +if ($fileIsLocked) +{ + return +} + +Copy-Item -Path $filePath -Destination $newFilePath -Force + +Write-Host \"Successfully copied file at location: $filePath to $newFilePath\"" + }, + "Parameters": [ + { + "Id": "80271c96-5190-4cc6-93c0-6e28c4aba6f1", + "Name": "sourcePath", + "Label": "File source path", + "HelpText": "The full path of the file you wish to copy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "702e4cfd-6633-4206-94df-8fa4fa47e564", + "Name": "destinationPath", + "Label": "File destionation path", + "HelpText": "The full path for the destination of the file. You can rename the file by giving it a new name in the path. If the file already exists at the destination it is overwritten.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2018-03-16T12:58:03.401Z", + "LastModifiedBy": "Jens-H-Eriksen", + "$Meta": { + "ExportedAt": "2018-03-16T12:58:03.401Z", + "OctopusVersion": "2018.2.8", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-create-file.json.human b/step-templates/file-system-create-file.json.human new file mode 100644 index 000000000..48ba92d3c --- /dev/null +++ b/step-templates/file-system-create-file.json.human @@ -0,0 +1,68 @@ +{ + "Id": "175a91a9-562e-49b9-bfa6-609a4e16bc56", + "Name": "File System - Create File", + "Description": "Creates a file, using the full path that is provided.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$filePath = $OctopusParameters['FilePath'] +$fileContent = $OctopusParameters['FileContent'] +$encoding = $OctopusParameters['Encoding'] + +New-Item -ItemType file -Path $filePath -Value '' -force + +if(![string]::IsNullOrEmpty($fileContent)) +{ + Set-Content -path $filePath -value $fileContent -encoding $encoding +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "FilePath", + "Label": "The full path where the file will be created.", + "HelpText": "Provide the entire physical path to which the physical file will be created. For example, 'C:\\Temp\\MyFile.txt' will create am empty text file named 'MyFile.txt' in the 'Temp' folder on the C: drive.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "FileContent", + "Label": "File Content", + "HelpText": "The text you would like inside the file, if nothing then will create an empty file.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "43aff0e0-e7ad-40cb-ae54-a49ca03adfc5", + "Name": "Encoding", + "Type": "String", + "Label": "", + "HelpText": null, + "DefaultValue": "UTF8", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "UTF8|UTF-8 +ASCII|ASCII (7-bit) +BigEndianUnicode|UTF-16 (big-endian) +Byte|Encodes as byte sequence +Unicode|UTF-16 (little-endian) +UTF7|UTF-7 +Unknown|Binary" + }, + "Links": {} + } + ], + "LastModifiedOn": "2017-04-28T10:48:34.361Z", + "LastModifiedBy": "carlpett", + "$Meta": { + "ExportedAt": "2017-04-28T10:48:34.361Z", + "OctopusVersion": "3.11.15", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-create-folder.json.human b/step-templates/file-system-create-folder.json.human new file mode 100644 index 000000000..30960ebfd --- /dev/null +++ b/step-templates/file-system-create-folder.json.human @@ -0,0 +1,140 @@ +{ + "Id": "b7211497-59ea-41e9-b466-c9a46b4c76b3", + "Name": "File System - Create Folder", + "Description": "Creates a folder structure that is passed in.", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$item = $OctopusParameters['FolderPath'] +$readPermissionsTo = $OctopusParameters['ReadPermissionsTo'] +$writePermissionsTo = $OctopusParameters['WritePermissionsTo'] +$modifyPermissionsTo = $OctopusParameters['ModifyPermissionsTo'] + + +Write-Host \"Creating folder $item with permissions.\" + +if((Test-Path $item)) +{ + Write-Host \"Folder $item already exists\" +} +else +{ + New-Item -ItemType directory -Path $item -force +} + +# Check item exists +if(!(Test-Path $item)) +{ + throw \"$item does not exist\" +} + +# Assign read permissions + +if($readPermissionsTo) +{ + $users = $readPermissionsTo.Split(\",\") + foreach($user in $users) + { + Write-Host \"Adding read permissions for $user\" + $acl = Get-Acl $item + $acl.SetAccessRuleProtection($False, $False) + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( + $user, \"Read\", \"ContainerInherit, ObjectInherit\", \"None\", \"Allow\") + $acl.AddAccessRule($rule) + Set-Acl $item $acl + } +} + +# Assign write permissions + +if($writePermissionsTo) +{ + $users = $writePermissionsTo.Split(\",\") + foreach($user in $users) + { + Write-Host \"Adding write permissions for $user\" + $acl = Get-Acl $item + $acl.SetAccessRuleProtection($False, $False) + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( + $user, \"Write\", \"ContainerInherit, ObjectInherit\", \"None\", \"Allow\") + $acl.AddAccessRule($rule) + Set-Acl $item $acl + } +} + +# Assign modify permissions + +if($modifyPermissionsTo) +{ + $users = $modifyPermissionsTo.Split(\",\") + foreach($user in $users) + { + Write-Host \"Adding modify permissions for $user\" + $acl = Get-Acl $item + $acl.SetAccessRuleProtection($False, $False) + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( + $user, \"Modify\", \"ContainerInherit, ObjectInherit\", \"None\", \"Allow\") + $acl.AddAccessRule($rule) + Set-Acl $item $acl + } +} + +Write-Host \"Complete\"", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "FolderPath", + "Label": "The folder path to create on the filesystem.", + "HelpText": "The entire path to the folder, this step will also created nested folders. For example \"D:\\one\\two\" will create two folders ('one', and then 'two' under folder 'one'). This script will not remove items from the folders.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "becbcbd1-7026-40f0-a655-eb5861d53557", + "Name": "ReadPermissionsTo", + "Label": "Read Users", + "HelpText": "A comma separated list of users to grant read permissions to", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "029ea466-4907-460a-a3f4-c00b23ad1a96", + "Name": "WritePermissionsTo", + "Label": "Write Users", + "HelpText": "A comma separated list of users to grant write permissions to", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "860e0480-db8d-4669-9b25-00f1ce33d0ac", + "Name": "ModifyPermissionsTo", + "Label": "Modify Users", + "HelpText": "A comma separated list of users to grant modify permissions to", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2017-04-05T12:30:02.127Z", + "OctopusVersion": "3.4.11", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-create-folders.json.human b/step-templates/file-system-create-folders.json.human new file mode 100644 index 000000000..00ea3ab41 --- /dev/null +++ b/step-templates/file-system-create-folders.json.human @@ -0,0 +1,109 @@ +{ + "Id": "7eaad6c2-fd5c-40a4-b880-350c983dc51d", + "Name": "File System - Create Folders", + "Description": "Ensure/Create multiple folders separated by ;", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\r +param(\r + [string]$FolderPaths,\r + [string]$ContinueOnError\r +) \r +\r +$ErrorActionPreference = \"Stop\" \r +\r +function Get-Param($Name, [switch]$Required, $Default) {\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + $variable = Get-Variable $Name -EA SilentlyContinue \r + if ($variable -ne $null) {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null) {\r + if ($Required) {\r + throw \"Missing parameter value $Name\"\r + } else {\r + $result = $Default\r + }\r + }\r +\r + return $result\r +}\r +\r +& {\r + param(\r + [string]$FolderPaths,\r + [string]$ContinueOnError\r + ) \r +\r + Write-Host \"File System - Create Folders\"\r + Write-Host \"FolderPaths: $FolderPaths\"\r + \r + $isContinueOnError = $ContinueOnError.ToLower() -match \"(y|yes|true)\"\r +\r + $FolderPaths.Split(\";\") | ForEach {\r + $path = $_.Trim()\r +\r + if($path.Length -lt 1){\r + break;\r + }\r +\r + Write-Host \"Trying to ensure directory structure for $path.\"\r + try {\r + $newFolder = New-Item -ItemType directory -Path $path -force\r + Write-Host \"SUCCESS\" -ForegroundColor Green\r + } catch {\r + $errorMessage = \"FAILED - $_.Exception.Message\"\r + \r + if($isContinueOnError){\r + Write-Host $errorMessage -ForegroundColor Red\r + } else {\r + throw $errorMessage\r + }\r + }\r + \r + }\r +\r + } `\r + (Get-Param 'FolderPaths' -Required) `\r + (Get-Param 'ContinueOnError')", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "FolderPaths", + "Label": "Folder Paths", + "HelpText": "A list of folders to create separated by a ;", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "ContinueOnError", + "Label": "Continue On Error", + "HelpText": "When this is selected the script will log failures but continue and succeed in octopus.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2015-07-02T13:40:37.523+00:00", + "LastModifiedBy": "jbennett", + "$Meta": { + "ExportedAt": "2015-07-02T13:47:00.305+00:00", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-create-symlink.json.human b/step-templates/file-system-create-symlink.json.human new file mode 100644 index 000000000..7b1a3d163 --- /dev/null +++ b/step-templates/file-system-create-symlink.json.human @@ -0,0 +1,123 @@ +{ + "Id": "f0cae939-caef-4979-93ac-6c9b3a3c4986", + "Name": "File System - Create Symlink", + "Description": "Creates a Symbolic Link", + "ActionType": "Octopus.Script", + "Version": 23, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function New-Symlink { + <# + .SYNOPSIS + Creates a symbolic link. + #> + param ( + [Parameter(Position=0, Mandatory=$true)] + [string] $Link, + [Parameter(Position=1, Mandatory=$true)] + [string] $Target + ) + + Write-Host \"New-Symlink $Link to $Target\" + + Invoke-MKLINK -Link $Link -Target $Target +} + +function Invoke-MKLINK { + <# + .SYNOPSIS + Creates a symbolic link. + #> + param ( + [Parameter(Position=0, Mandatory=$true)] + [string] $Link, + [Parameter(Position=1, Mandatory=$true)] + [string] $Target + ) + + # Resolve the paths incase a relative path was passed in. + $Link = (Force-Resolve-Path $Link) + $Target = (Force-Resolve-Path $Target) + + # Ensure target exists. + if (-not(Test-Path $Target)) { + throw \"Target does not exist.`nTarget: $Target\" + } + + # Ensure link does not exist. + if (Test-Path $Link) { + Write-Host \"A file or directory already exists at the link path.`nLink: $Link\" + return + } + + $isDirectory = (Get-Item $Target).PSIsContainer + + # Capture the MKLINK output so we can return it properly. + # Includes a redirect of STDERR to STDOUT so we can capture it as well. + $output = cmd /c mklink /D `\"$Link`\" `\"$Target`\" 2>&1 + + Write-Host \"output : $output\" + if ($lastExitCode -ne 0) { + Write-Host \"MKLINK failed. Exit code: $lastExitCode`n$output\" + throw \"MKLINK failed. Exit code: $lastExitCode`n$output\" + } + else { + Write-Output $output + } +} + +function Force-Resolve-Path { + <# + .SYNOPSIS + Calls Resolve-Path but works for files that don't exist. + .REMARKS + From http://devhawk.net/2010/01/21/fixing-powershells-busted-resolve-path-cmdlet/ + #> + param ( + [string] $FileName + ) + + $FileName = Resolve-Path $FileName -ErrorAction SilentlyContinue ` + -ErrorVariable _frperror + if (-not($FileName)) { + $FileName = $_frperror[0].TargetObject + } + + return $FileName +} + +New-Symlink $link_path $target_path + + +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "link_path", + "Label": "LinkPath", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "target_path", + "Label": "TargetPath", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2014-08-26T15:58:31.623+00:00", + "LastModifiedBy": "diegoavanzini", + "$Meta": { + "ExportedAt": "2014-08-26T15:58:34.242+00:00", + "OctopusVersion": "2.5.7.384", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-find-and-replace.json.human b/step-templates/file-system-find-and-replace.json.human new file mode 100644 index 000000000..d7eae6df3 --- /dev/null +++ b/step-templates/file-system-find-and-replace.json.human @@ -0,0 +1,124 @@ +{ + "Id": "87cbaa94-4477-4474-a9c3-7943b5668d30", + "Name": "File System - Find and Replace", + "Description": "Find and replace text in one or more files.", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function Execute-FindReplace($target, $find, $replace, $ignoreCase) { + $options = [System.Text.RegularExpressions.RegexOptions]::None + if ($ignoreCase) { + $options = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase + } + + Write-Output \"Searching $target...\" + $orig = [System.IO.File]::ReadAllText($target) + + $escFind = [System.Text.RegularExpressions.Regex]::Escape($find) + $regex = new-object System.Text.RegularExpressions.Regex($escFind, $options) + $removed = $regex.Replace($orig, '') + + $occurrences = ($orig.Length - $removed.Length) / $find.Length + if ($occurrences -gt 0) { + Write-Output \"Found $occurrences occurrence(s), replacing...\" + + $escReplace = $replace.Replace('$', '$$') + $replaced = $regex.Replace($orig, $escReplace) + [System.IO.File]::WriteAllText($target, $replaced) + } +} + +if ([string]::IsNullOrEmpty($FRFindText)) { + throw \"A non-empty 'Find' text block is required\" +} + +Write-Output \"Replacing occurrences of '$FRFindText' with '$FRReplaceText'\" +if ([Boolean] $FRIgnoreCase) { + Write-Output \"Case will be ignored\" +} + +$FRCandidatePathGlobs.Split(\";\") | foreach { + $glob = $_.Trim() + Write-Output \"Searching for files that match $glob...\" + + $matches = $null + $splits = $glob.Split(@('/**/'), [System.StringSplitOptions]::RemoveEmptyEntries) + + if ($splits.Length -eq 1) { + $splits = $glob.Split(@('\\**\\'), [System.StringSplitOptions]::RemoveEmptyEntries) + } + + if ($splits.Length -eq 1) { + $matches = ls $glob + } else { + if ($splits.Length -eq 2) { + pushd $splits[0] + $matches = ls $splits[1] -Recurse + popd + } else { + $splits + throw \"The segment '**' can only appear once, as a directory name, in the glob expression\" + + } + } + + $matches | foreach { + + $target = $_.FullName + + Execute-FindReplace -target $target -find $FRFindText -replace $FRReplaceText -ignoreCase ([Boolean] $FRIgnoreCase) + } +} + + +Write-Output \"Done.\"", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "FRCandidatePathGlobs", + "Label": "Files", + "HelpText": "The files to search. Wildcards `*` and `**` are supported. Paths must be fully-qualified, e.g. `C:\\MyApp\\**\\*.xml`. Separate multiple paths with `;` semicolons.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "FRFindText", + "Label": "Find", + "HelpText": "The text to find in the target files.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "FRReplaceText", + "Label": "Replace with", + "HelpText": "The replacement text to insert in place of each occurrence of _Find_.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "FRIgnoreCase", + "Label": "Ignore case", + "HelpText": "If **True**, variations on the character case of _Find_ will be considered a match, for example `Bar` will match `BAR` and `bar`. If **False** only exact matches will be considered.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-14T05:38:42.041+00:00", + "OctopusVersion": "2.4.4.0", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-grant-permissions.json.human b/step-templates/file-system-grant-permissions.json.human new file mode 100644 index 000000000..c71a482e7 --- /dev/null +++ b/step-templates/file-system-grant-permissions.json.human @@ -0,0 +1,160 @@ +{ + "Id": "b77d55ae-7f54-45ab-ba57-7afff97c93a2", + "Name": "File System - Grant Permissions", + "Description": "Grant read, write and modify permissions to folders or files", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$itemsParameter = $OctopusParameters['Items'] +$readPermissionsTo = $OctopusParameters['ReadPermissionsTo'] +$writePermissionsTo = $OctopusParameters['WritePermissionsTo'] +$modifyPermissionsTo = $OctopusParameters['ModifyPermissionsTo'] + +if($readPermissionsTo) +{ + $readUsers = $readPermissionsTo.Split(\",\") +} + +if($writePermissionsTo) +{ + $writeUsers = $writePermissionsTo.Split(\",\") +} + +if($modifyPermissionsTo) +{ + $modifyUsers = $modifyPermissionsTo.Split(\",\") +} + +$items = $itemsParameter.Split(\",\") +foreach($item in $items) +{ + # Check path exists + if(!(Test-Path $item)) + { + throw \"$item does not exist\" + } + + Write-Host \"Path: $item\" + # Assign read permissions + foreach($user in $readUsers) + { + Write-Host \" Adding read permissions for $user\" + $acl = (Get-Item $item).GetAccessControl('Access') + $acl.SetAccessRuleProtection($False, $False) + $rule = + if ($acl -is [System.Security.AccessControl.DirectorySecurity]) + { + New-Object System.Security.AccessControl.FileSystemAccessRule($user, \"Read\", \"ContainerInherit, ObjectInherit\", \"None\", \"Allow\") + } + else + { + New-Object System.Security.AccessControl.FileSystemAccessRule($user, \"Read\", \"Allow\") + } + $acl.AddAccessRule($rule) + Set-Acl $item $acl + } + + # Assign write permissions + foreach($user in $writeUsers) + { + Write-Host \" Adding write permissions for $user\" + $acl = (Get-Item $item).GetAccessControl('Access') + $acl.SetAccessRuleProtection($False, $False) + $rule = + if ($acl -is [System.Security.AccessControl.DirectorySecurity]) + { + New-Object System.Security.AccessControl.FileSystemAccessRule($user, \"Write\", \"ContainerInherit, ObjectInherit\", \"None\", \"Allow\") + } + else + { + New-Object System.Security.AccessControl.FileSystemAccessRule($user, \"Write\", \"Allow\") + } + $acl.AddAccessRule($rule) + Set-Acl $item $acl + } + + # Assign modify permissions + foreach($user in $modifyUsers) + { + Write-Host \" Adding modify permissions for $user\" + $acl = (Get-Item $item).GetAccessControl('Access') + $acl.SetAccessRuleProtection($False, $False) + $rule = + if ($acl -is [System.Security.AccessControl.DirectorySecurity]) + { + New-Object System.Security.AccessControl.FileSystemAccessRule($user, \"Modify\", \"ContainerInherit, ObjectInherit\", \"None\", \"Allow\") + } + else + { + New-Object System.Security.AccessControl.FileSystemAccessRule($user, \"Modify\", \"Allow\") + } + $acl.AddAccessRule($rule) + Set-Acl $item $acl + } +} + +Write-Host \"Complete\" +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "e67e2b43-b1e4-482d-bb5d-e4b6acb2b90f", + "Name": "Items", + "Label": "Items", + "HelpText": "A comma seperated list of full paths to the files or folders", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b830e92e-ceaa-472e-b1ac-e70a19d9386b", + "Name": "ReadPermissionsTo", + "Label": "Read Users", + "HelpText": "A comma separated list of users to grant read permissions to", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "141f465e-6998-4715-be65-6d5df2604239", + "Name": "WritePermissionsTo", + "Label": "Write Users", + "HelpText": "A comma separated list of users to grant write permissions to", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c150331e-ea88-4170-8bca-52a8cfd30220", + "Name": "ModifyPermissionsTo", + "Label": "Modify Users", + "HelpText": "A comma separated list of users to grant modify permissions to", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2017-10-26T07:11:38.851Z", + "LastModifiedBy": "mcfozzy", + "$Meta": { + "ExportedAt": "2017-10-26T07:11:38.851Z", + "OctopusVersion": "3.15.8", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-rclone.json.human b/step-templates/file-system-rclone.json.human new file mode 100644 index 000000000..3ac1b77a5 --- /dev/null +++ b/step-templates/file-system-rclone.json.human @@ -0,0 +1,87 @@ +{ + "Id": "fe8bf996-4cdf-4857-9c26-5aefb4fb4025", + "Name": "File System - rclone (bash)", + "Description": "Runs [rclone](https://rclone.org/) on a target or worker.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "rclonePath=$(get_octopusvariable \"Rclone.Path\") +rcloneCommand=$(get_octopusvariable \"Rclone.Command\") +rcloneParameters=$(get_octopusvariable \"Rclone.Parameters\") +printCommand=$(get_octopusvariable \"Rclone.PrintCommand\") + +if [ ! -z \"$rclonePath\" ] ; then + \tPATH=$rclonePath:$PATH +fi + +if [ -z \"$rcloneCommand\" ] ; then + \tfail_step \"Command is a required paremeter.\" +fi + +if [ \"$printCommand\" = \"True\" ] ; then + set -x +fi + +rclone $rcloneCommand ${rcloneParameters:+ $rcloneParameters} 2>&1 + +# Check for error +if [[ $? -ne 0 ]] +then + fail_step \"The rclone command resulted in errors. Please review the logs above.\" +fi +" + }, + "Parameters": [ + { + "Id": "b5c7fe5c-062e-4c69-9746-e6275f8d5145", + "Name": "Rclone.Path", + "Label": "Executable Path", + "HelpText": "Optional path to `rclone` if it is not in the environment's path variable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "aba94574-2ab4-4657-94d6-3ae47cf6cd38", + "Name": "Rclone.Command", + "Label": "Command", + "HelpText": "The `rclone` [command](https://rclone.org/commands/) to run.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ce6fa762-d494-4e3b-8367-edfced04bc45", + "Name": "Rclone.Parameters", + "Label": "Parameters", + "HelpText": "The parameters to provide to the `rclone` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7647af01-707d-4a02-8f90-37fc943bdece", + "Name": "Rclone.PrintCommand", + "Label": "Print Command?", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-15T21:16:20.733Z", + "OctopusVersion": "2020.2.11", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "ryanrousseau", + "Category": "filesystem" +} diff --git a/step-templates/file-system-regular-expression-find-and-replace.json.human b/step-templates/file-system-regular-expression-find-and-replace.json.human new file mode 100644 index 000000000..c5866e460 --- /dev/null +++ b/step-templates/file-system-regular-expression-find-and-replace.json.human @@ -0,0 +1,123 @@ +{ + "Id": "0bef8c07-5739-4030-8c04-287ceeb51153", + "Name": "File System - Regular Expression Find and Replace (Updated)", + "Description": "Find and replace text matching a regular expression in one or more files. Now with working $ replacement.", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function Execute-RegexFindReplace($target, $find, $replace, $options) { + Write-Output \"Searching $target...\" + $orig = [System.IO.File]::ReadAllText($target) + + $regex = new-object System.Text.RegularExpressions.Regex($find, $options) + if ([string]::IsNullOrEmpty($replace)) { +$replace = '' + } + + $occurrences = $regex.Matches($orig).Count + if ($occurrences -gt 0) { + Write-Output \"Found $occurrences occurrence(s), replacing...\" + + $replaced = $regex.Replace($orig, $replace) + [System.IO.File]::WriteAllText($target, $replaced) + } +} + +if ([string]::IsNullOrEmpty($RFRFindRegex)) { + throw \"A non-empty 'Pattern' is required\" +} + +$options = [System.Text.RegularExpressions.RegexOptions]::None +$RFROptions.Split(' ') | foreach { + $opt = $_.Trim() + $flag = [System.Enum]::Parse([System.Text.RegularExpressions.RegexOptions], $opt) + $options = $options -bor $flag +} + +Write-Output \"Replacing occurrences of '$RFRFindRegex' with '$RFRSubstitution' applying options $RFROptions\" + +$RFRCandidatePathGlobs.Split(\";\") | foreach { + $glob = $_.Trim() + Write-Output \"Searching for files that match $glob...\" + + $matches = $null + $splits = $glob.Split(@('/**/'), [System.StringSplitOptions]::RemoveEmptyEntries) + + if ($splits.Length -eq 1) { + $splits = $glob.Split(@('\\**\\'), [System.StringSplitOptions]::RemoveEmptyEntries) + } + + if ($splits.Length -eq 1) { + $matches = ls $glob + } else { + if ($splits.Length -eq 2) { + pushd $splits[0] + $matches = ls $splits[1] -Recurse + popd + } else { + $splits + throw \"The segment '**' can only appear once, as a directory name, in the glob expression\" + + } + } + + $matches | foreach { + + $target = $_.FullName + + Execute-RegexFindReplace -target $target -find $RFRFindRegex -replace $RFRSubstitution -options $options + } +} + + +Write-Output \"Done.\"", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "RFRCandidatePathGlobs", + "Label": "Files", + "HelpText": "The files to search. Wildcards `*` and `**` are supported. Paths must be fully-qualified, e.g. `C:\\MyApp\\**\\*.xml`. Separate multiple paths with `;` semicolons.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "RFRFindRegex", + "Label": "Pattern", + "HelpText": "The regular expression to find in the target files.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "RFRSubstitution", + "Label": "Substitution", + "HelpText": "The text to insert in place of each occurrence of _Pattern_. Regular expression [substitutions](http://msdn.microsoft.com/en-us/library/ewy2t5e0.aspx) are supported, so any literal `$` in the substitution pattern must be escaped by doubling (`$$`).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "RFROptions", + "Label": "Options", + "HelpText": "A space-separated list of options from the [RegexOptions](http://msdn.microsoft.com/en-us/library/system.text.regularexpressions.regexoptions.aspx) enumeration.", + "DefaultValue": "ExplicitCapture", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-10-26T21:24:18.756+00:00", + "OctopusVersion": "3.0.10.2278", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-rename-file.json.human b/step-templates/file-system-rename-file.json.human new file mode 100644 index 000000000..ba188a37a --- /dev/null +++ b/step-templates/file-system-rename-file.json.human @@ -0,0 +1,99 @@ +{ + "Id": "5ca37bfb-ebbc-4b4c-ab1e-06d462e7f910", + "Name": "File System - Rename File", + "Description": "Renames a file on the file system.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$filePath = $OctopusParameters['FilePath']\r +$newName = $OctopusParameters['NewName']\r +\r +function Test-FileLocked([string]$filePath)\r +{\r + Rename-Item $filePath $filePath -ErrorVariable errs -ErrorAction SilentlyContinue\r + return ($errs.Count -ne 0)\r +}\r +\r +$fileExists = Test-Path -Path $filePath\r +\r +if (!$fileExists)\r +{\r + Write-Warning \"File not found at $filePath\"\r +\r + return\r +}\r +\r +$fileIsLocked = Test-FileLocked($filePath)\r +\r +function Wait-ForFileUnlock\r +{\r + for ($attemptNo = 1; $attemptNo -lt 6; $attemptNo++) {\r + Write-Host \"Waiting for the file to become unlocked $attemptNo/5\"\r +\r + Start-Sleep -Seconds 10\r +\r + $fileIsLocked = Test-FileLocked($filePath)\r +\r + if (!$fileIsLocked)\r + {\r + return\r + }\r +\r + if ($attemptNo -eq 5) {\r + Write-Error \"File at location $filePath is locked and cannot be renamed\"\r +\r + return\r + }\r + }\r +}\r +\r +if ($fileIsLocked)\r +{\r + Wait-ForFileUnlock\r +}\r +\r +Rename-Item -Path $filePath -NewName $newName\r +\r +Write-Host \"Successfully renamed file at location: $filePath to $newName\"", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "37fd875e-f5f7-493c-bb8c-30354bf39678", + "Name": "FilePath", + "Type": "String", + "Label": "File Path", + "HelpText": "The location of the file you wish to rename.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "facc1261-1b0e-476e-a520-13e682c4f8b7", + "Name": "NewName", + "Type": "String", + "Label": "New Name", + "HelpText": "The new name for the file you're renaming.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "daviesaus", + "$Meta": { + "ExportedAt": "2017-06-01T11:11:00.134Z", + "OctopusVersion": "3.11.11", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-restore-directory.json.human b/step-templates/file-system-restore-directory.json.human new file mode 100644 index 000000000..fd8247d4d --- /dev/null +++ b/step-templates/file-system-restore-directory.json.human @@ -0,0 +1,51 @@ +{ + "Id": "04a74a00-967d-496a-a966-1acd17fededf", + "Name": "File System - Restore Directory", + "Description": "Restores a folder and it's contents (files and sub-folders).", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$source = $OctopusParameters['Source']\r +$destination = $OctopusParameters['Destination']\r +\r +if(Test-Path $destination)\r +{\r + ## Clean the destination folder\r + Write-Host \"Cleaning $destination\"\r + Remove-Item $destination -Recurse\r +}\r +\r +## Copy recursively\r +Write-Host \"Copying from $source to $destination\"\r +Copy-Item $source $destination -Recurse", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Source", + "Label": "Source", + "HelpText": "The source directory where files and folders will be copied from", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Destination", + "Label": "Destination folder", + "HelpText": "The Destination where the files will be copied to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-12-06T00:00:00.000+00:00", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/file-system-rsync.json.human b/step-templates/file-system-rsync.json.human new file mode 100644 index 000000000..4bbccb5eb --- /dev/null +++ b/step-templates/file-system-rsync.json.human @@ -0,0 +1,87 @@ +{ + "Id": "d8dfa484-59ed-491f-a22a-3098d4c2a6be", + "Name": "File System - rsync (bash)", + "Description": "Run [rsync](http://manpages.org/rsync) on a target or worker.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "rsyncPath=$(get_octopusvariable \"Rsync.Path\") +rsyncOptions=$(get_octopusvariable \"Rsync.Options\") +rsyncSource=$(get_octopusvariable \"Rsync.Source\") +rsyncDestination=$(get_octopusvariable \"Rsync.Destination\") +printCommand=$(get_octopusvariable \"Rsync.PrintCommand\") + +if [ ! -z \"$rsyncPath\" ] ; then + \tPATH=$rsyncPath:$PATH +fi + +if [ \"$printCommand\" = \"True\" ] ; then + set -x +fi + +rsync ${rsyncOptions:+ $rsyncOptions} ${rsyncSource:+ $rsyncSource} ${rsyncDestination:+ $rsyncDestination}" + }, + "Parameters": [ + { + "Id": "da47b530-69dd-482c-9b97-f9c0b1dcca5c", + "Name": "Rsync.Path", + "Label": "Executable Path", + "HelpText": "Optional path to `rsync` if it is not in the environment's path variable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7d315721-f2b2-4f0b-8eb1-3b946f711a59", + "Name": "Rsync.Options", + "Label": "Options", + "HelpText": "[Options](http://manpages.org/rsync#options) to provide to `rsync`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a908f5a3-8284-42b9-992d-528993c5b55f", + "Name": "Rsync.Source", + "Label": "Source", + "HelpText": "The location of the source files to copy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "366ad01c-1ef9-408e-bbed-58d0a7a3e4db", + "Name": "Rsync.Destination", + "Label": "Destination", + "HelpText": "The destination to copy the source files to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d969ceca-d823-4967-afbe-80672cd55051", + "Name": "Rsync.PrintCommand", + "Label": "Print Command?", + "HelpText": "Prints the command in the logs using `set -x`. This will cause a warning when the step runs.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-15T21:11:29.969Z", + "OctopusVersion": "2020.2.11", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "ryanrousseau", + "Category": "filesystem" +} diff --git a/step-templates/file-system-zip-directory-contents.json.human b/step-templates/file-system-zip-directory-contents.json.human new file mode 100644 index 000000000..dea93e49b --- /dev/null +++ b/step-templates/file-system-zip-directory-contents.json.human @@ -0,0 +1,100 @@ +{ + "Id": "fa148b10-99e2-47be-b19f-a16dee1c8f27", + "Name": "File System - Zip Directory Contents", + "Description": "Creates a zip archive that contains the files and directories from the specified directory, uses the specified compression level, and optionally includes the base directory. + +Requires .NET 4.5 as it relies on the `System.IO.Compression.ZipFile` class.", + "ActionType": "Octopus.Script", + "Version": 12, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$SourceDirectoryName = $OctopusParameters['SourceDirectoryName']\r +$DestinationArchiveFileName = $OctopusParameters['DestinationArchiveFileName']\r +$CompressionLevel = $OctopusParameters['CompressionLevel']\r +$IncludeBaseDirectory = $OctopusParameters['IncludeBaseDirectory']\r +$OverwriteDestination = $OctopusParameters['OverwriteDestination']\r +\r +if (!$SourceDirectoryName)\r +{\r + Write-Error \"No Source Directory name was specified. Please specify the name of the directory to that will be zipped.\"\r + exit -2\r +}\r +\r +if (!$DestinationArchiveFileName)\r +{\r + Write-Error \"No Destination Archive File name was specified. Please specify the name of the zip file to be created.\"\r + exit -2\r +}\r +\r +if (($OverwriteDestination) -and (Test-Path $DestinationArchiveFileName))\r +{\r + Write-Host \"$DestinationArchiveFileName already exists. Will delete it before we create a new zip file with the same name.\"\r + Remove-Item $DestinationArchiveFileName\r +}\r +\r +Write-Host \"Creating Zip file $DestinationArchiveFileName with the contents of directory $SourceDirectoryName using compression level $CompressionLevel\"\r +\r +$assembly = [Reflection.Assembly]::LoadWithPartialName(\"System.IO.Compression.FileSystem\")\r +[System.IO.Compression.ZipFile]::CreateFromDirectory($SourceDirectoryName, $DestinationArchiveFileName, $CompressionLevel, $IncludeBaseDirectory)\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "SourceDirectoryName", + "Label": "Source Directory", + "HelpText": "The path to the directory to be archived, specified as a relative or absolute path.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DestinationArchiveFileName", + "Label": "Destination Archive File", + "HelpText": "The path of the archive to be created, specified as a relative or absolute path.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "CompressionLevel", + "Label": "Compression Level", + "HelpText": "Indicates whether to emphasize speed or compression effectiveness when creating the entry.", + "DefaultValue": "Optimal", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Optimal|Optimal +Fastest|Fastest +NoCompression|No Compression" + } + }, + { + "Name": "IncludeBaseDirectory", + "Label": "Include Base Directory", + "HelpText": "Include the directory name from Source Directory at the root of the archive.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "OverwriteDestination", + "Label": "Overwrite Destination If Exists", + "HelpText": "Overwrite the destination archive file if it already exists.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2018-02-08T22:24:00.817+00:00", + "LastModifiedBy": "curiousdev", + "$Meta": { + "ExportedAt": "2018-02-08T22:24:00.817+00:00", + "OctopusVersion": "2.5.8.447", + "Type": "ActionTemplate" + }, + "Category": "filesystem" +} diff --git a/step-templates/firebase-deploy.json.human b/step-templates/firebase-deploy.json.human new file mode 100644 index 000000000..e863cc659 --- /dev/null +++ b/step-templates/firebase-deploy.json.human @@ -0,0 +1,152 @@ +{ + "Id": "ac0dee2d-dcbe-42aa-96c6-bb6c644183b4", + "Name": "Firebase - Deploy", + "Description": "Deploys the contents of a package to a Firebase project using the [Firebase CLI deploy command](https://firebase.google.com/docs/cli/#deployment).", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "343306b7-6997-429f-9ed5-4214ca4d32ac", + "Name": "FirebaseDeploy.Package", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "FirebaseDeploy.Package" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "packagePath=$(get_octopusvariable \"Octopus.Action.Package[FirebaseDeploy.Package].ExtractedPath\") +token=$(get_octopusvariable \"FirebaseDeploy.CIToken\") +public=$(get_octopusvariable \"FirebaseDeploy.Public\") +message=$(get_octopusvariable \"FirebaseDeploy.Message\") +force=$(get_octopusvariable \"FirebaseDeploy.Force\") +only=$(get_octopusvariable \"FirebaseDeploy.Only\") +except=$(get_octopusvariable \"FirebaseDeploy.Except\") +printCommand=$(get_octopusvariable \"FirebaseDeploy.PrintCommand\") +firebasePath=$(get_octopusvariable \"FirebaseDeploy.FirebasePath\") + +if [ ! -z \"$firebasePath\" ] ; then + \tPATH=$firebasePath:$PATH +fi + +if [ \"$force\" = \"True\" ] ; then + force=true +else + force= +fi + +if [ \"$printCommand\" = \"True\" ] ; then + set -x +fi + +cd $packagePath + +firebase deploy ${public:+ -p \"$public\"} ${message:+ -m \"$message\"} ${force:+ -f} ${only:+ --only \"$only\"} ${except:+ --except \"$except\"} --token $token" + }, + "Parameters": [ + { + "Id": "55ddf9fd-bf2f-4148-912b-bc599c5f6ec6", + "Name": "FirebaseDeploy.Package", + "Label": "Package", + "HelpText": "The package containing the Firebase project being deployed.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "46874eaf-7632-40d1-bd46-4627bd0f2d0c", + "Name": "FirebaseDeploy.FirebasePath", + "Label": "Firebase Path", + "HelpText": "The path to the directory containing the Firebase CLI, if not in $PATH.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c982c1f3-a91e-4dd4-89a6-db5d99b08347", + "Name": "FirebaseDeploy.CIToken", + "Label": "CI Token", + "HelpText": "A CI token generated by the [Firebase CLI](https://firebase.google.com/docs/cli/#cli-ci-systems)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "56628161-6b99-4ca3-9c4a-1234117a0018", + "Name": "FirebaseDeploy.Public", + "Label": "Public Path", + "HelpText": "Override the Hosting public directory specified in firebase.json.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e7c41fcb-dd74-4ba2-9671-fa7313d632b8", + "Name": "FirebaseDeploy.Message", + "Label": "Message", + "HelpText": "An optional message describing this deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6a88a428-a538-4292-b6ee-b843c28887f3", + "Name": "FirebaseDeploy.Force", + "Label": "Force?", + "HelpText": "Delete Cloud Functions missing from the current working directory without confirmation.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "c0debcc3-6708-4d3c-977b-880811b48594", + "Name": "FirebaseDeploy.Only", + "Label": "Only Targets", + "HelpText": "Only deploy to specified, comma-separated targets (e.g. \"hosting,storage\"). For functions, can specify filters with colons to scope function deploys to only those functions (e.g. \"--only functions:func1,functions:func2\"). When filtering based on export groups (the exported module object keys), use dots to specify group names (e.g. \"--only functions:group1.subgroup1,functions:group2)\".", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e62a6b0f-6331-4a63-a908-c759798ccd1c", + "Name": "FirebaseDeploy.Except", + "Label": "Except Targets", + "HelpText": "Deploy to all targets except specified (e.g. \"database\").", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e2e0ac14-e5e9-4b3c-bdc1-b1da3d7be184", + "Name": "FirebaseDeploy.PrintCommand", + "Label": "Print Command?", + "HelpText": "Prints the command in the logs using `set -x`. This will cause a warning when the step runs.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-08T19:44:37.662Z", + "OctopusVersion": "2020.2.11", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "ryanrousseau", + "Category": "firebase" +} diff --git a/step-templates/flyway-database-migrations.json.human b/step-templates/flyway-database-migrations.json.human new file mode 100644 index 000000000..d1b804f43 --- /dev/null +++ b/step-templates/flyway-database-migrations.json.human @@ -0,0 +1,1087 @@ +{ + "Id": "ccebac39-79a8-4ab4-b55f-19ea570d9ebc", + "Name": "Flyway Database Migrations", + "Description": "Step template to leverage Flyway to deploy migration scripts. This is the latest and greatest Flyway step template that leverages all the newest features of both Flyway and Octopus Deploy. + +- You can include the flyway executables in your package, if you include the `flyway` (Linux) or `flyway.cmd` (Windows) in the root of the package this step template will automatically find them. +- You can use this with an execution container, negating the need to include Flyway in the package. If Flyway isn't found in the package it will attempt to find `/flyway/flyway` (when using Linux containers) or `flyway` in the environment path and use that. +- Support for all Flyway commands, including the `undo` command. +- Support for flyway community, teams, enterprise, and pro editions. + +Please note this requires Octopus Deploy **2019.10.0** or newer along with PowerShell Core installed on the machines running this step. +AWS EC2 IAM Authentication requires the AWS CLI to be installed.", + "ActionType": "Octopus.Script", + "Version": 14, + "Packages": [ + { + "Name": "Flyway.Package.Value", + "Id": "0c0d333c-d794-4a16-a3a2-4bbba4550763", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "Flyway.Package.Value" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$VerboseActionPreference=\"Continue\" + +function Get-FlywayExecutablePath +{ +\tparam ( + \t$providedPath + ) + + if ([string]::IsNullOrWhiteSpace($providedPath) -eq $false) + { + \tWrite-Host \"The executable path was provided, testing to see if it is absolute or relative\" +\t\tif ([IO.Path]::IsPathRooted($providedPath)) + { + \tWrite-Host \"The provided path is absolute, using that\" + + \treturn $providedPath + } + + Write-Host \"The provided path was relative, combining $(Get-Location) with $providedPath\" + return Join-Path $(Get-Location) $providedPath + } + + Write-Host \"Checking to see if we are currently running on Linux\" + if ($IsLinux) + { + \tWrite-Host \"Currently running on Linux\" + \tWrite-Host \"Checking to see if flyway was included with the package\" + \tif (Test-Path \"./flyway\") + { + \tWrite-Host \"It was, using that version of flyway\" + \treturn \"flyway\" + } + + Write-Host \"Testing to see if we are on an execution container with /flyway/flyway as the path\" + \tif (Test-Path \"/flyway/flyway\") + { + \tWrite-Host \"We are, using /flyway/flyway\" + \treturn \"/flyway/flyway\" + } + } + + Write-Host \"Currently running on Windows\" + + Write-Host \"Testing to see if flyway.cmd was included with the package\" + if (Test-Path \".\\flyway.cmd\") + { + \tWrite-Host \"It was, using that version.\" + \treturn \".\\flyway.cmd\" + } + + Write-Host \"Testing to see if flyway can be found in the env path\" + $flywayExecutable = (Get-Command \"flyway\" -ErrorAction SilentlyContinue) + if ($null -ne $flywayExecutable) + { + \tWrite-Host \"The flyway folder is part of the environment path\" + return $flywayExecutable.Source + } + + Fail-Step \"Unable to find flyway executable. Please include it as part of the package, or provide the path to it.\" +} + +function Test-AddParameterToCommandline +{ +\tparam ( + \t$acceptedCommands, + $selectedCommand, + $parameterValue, + $defaultValue, + $parameterName + ) + + if ([string]::IsNullOrWhiteSpace($parameterValue) -eq $true) + { \t + \tWrite-Verbose \"$parameterName is empty, returning false\" + \treturn $false + } + + if ([string]::IsNullOrWhiteSpace($defaultValue) -eq $false -and $parameterValue.ToLower().Trim() -eq $defaultValue.ToLower().Trim()) + { + \tWrite-Verbose \"$parameterName is matches the default value, returning false\" + \treturn $false + } + + if ([string]::IsNullOrWhiteSpace($acceptedCommands) -eq $true -or $acceptedCommands -eq \"any\") + { + \tWrite-Verbose \"$parameterName has a value and this is for any command, returning true\" + \treturn $true + } + + $acceptedCommandArray = $acceptedCommands -split \",\" + foreach ($command in $acceptedCommandArray) + { + \tif ($command.ToLower().Trim() -eq $selectedCommand.ToLower().Trim()) + { + \tWrite-Verbose \"$parameterName has a value and the current command $selectedCommand matches the accepted command $command, returning true\" + \treturn $true + } + } + + Write-Verbose \"$parameterName has a value but is not accepted in the current command, returning false\" + return $false +} + +function Get-ParsedUrl +{ +\t# Define parameters + param ( + \t$ConnectionUrl + ) + + # Remove the 'jdbc:' portion from the $ConnectionUrl parameter + $ConnectionUrl = $ConnectionUrl.ToLower().Replace(\"jdbc:\", \"\") + + # Parse and return the url + return [System.Uri]$ConnectionUrl +} + +# Declaring the path to the NuGet package +$flywayPackagePath = $OctopusParameters[\"Octopus.Action.Package[Flyway.Package.Value].ExtractedPath\"] +$flywayUrl = $OctopusParameters[\"Flyway.Target.Url\"] +$flywayUser = $OctopusParameters[\"Flyway.Database.User\"] +$flywayUserPassword = $OctopusParameters[\"Flyway.Database.User.Password\"] +$flywayCommand = $OctopusParameters[\"Flyway.Command.Value\"] +$flywayLicenseKey = $OctopusParameters[\"Flyway.License.Key\"] +$flywayExecutablePath = $OctopusParameters[\"Flyway.Executable.Path\"] +$flywaySchemas = $OctopusParameters[\"Flyway.Command.Schemas\"] +$flywayTarget = $OctopusParameters[\"Flyway.Command.Target\"] +$flywayInfoSinceDate = $OctopusParameters[\"Flyway.Command.InfoSinceDate\"] +$flywayInfoSinceVersion = $OctopusParameters[\"Flyway.Command.InfoSinceVersion\"] +$flywayLicensedEdition = $OctopusParameters[\"Flyway.License.Version\"] +$flywayCherryPick = $OctopusParameters[\"Flyway.Command.CherryPick\"] +$flywayOutOfOrder = $OctopusParameters[\"Flyway.Command.OutOfOrder\"] +$flywaySkipExecutingMigrations = $OctopusParameters[\"Flyway.Command.SkipExecutingMigrations\"] +$flywayPlaceHolders = $OctopusParameters[\"Flyway.Command.PlaceHolders\"] +$flywayBaseLineVersion = $OctopusParameters[\"Flyway.Command.BaselineVersion\"] +$flywayBaselineDescription = $OctopusParameters[\"Flyway.Command.BaselineDescription\"] +$flywayAuthenticationMethod = $OctopusParameters[\"Flyway.Authentication.Method\"] +$flywayLocations = $OctopusParameters[\"Flyway.Command.Locations\"] +$flywayAdditionalArguments = $OctopusParameters[\"Flyway.Additional.Arguments\"] +$flywayStepName = $OctopusParameters[\"Octopus.Action.StepName\"] +$flywayEnvironment = $OctopusParameters[\"Octopus.Environment.Name\"] +$flywayCheckBuildUrl = $OctopusParameters[\"Flyway.Command.CheckBuildUrl\"] +$flywayCheckBuildUsername = $OctopusParameters[\"Flyway.Database.Check.User\"] +$flywayCheckBuildPassword = $OctopusParameters[\"Flyway.Database.Check.User.Password\"] +$flywayBaselineOnMigrate = $OctopusParameters[\"Flyway.Command.BaseLineOnMigrate\"] +$flywaySnapshotFileName = $OctopusParameters[\"Flyway.Command.Snapshot.FileName\"] +$flywayCheckFailOnDrift = $OctopusParameters[\"Flyway.Command.FailOnDrift\"] + +if ([string]::IsNullOrWhitespace($flywayLocations)) +{ +\t$flywayLocations = \"filesystem:$flywayPackagePath\" +} + + +# Logging for troubleshooting +Write-Host \"*******************************************\" +Write-Host \"Logging variables:\" +Write-Host \" - - - - - - - - - - - - - - - - - - - - -\" +Write-Host \"PackagePath: $flywayPackagePath\" +Write-Host \"Flyway Executable Path: $flywayExecutablePath\" +Write-Host \"Flyway Command: $flywayCommand\" +Write-Host \"-url: $flywayUrl\" +Write-Host \"-user: $flywayUser\" +Write-Host \"-schemas: $flywaySchemas\" +Write-Host \"-target: $flywayTarget\" +Write-Host \"-cherryPick: $flywayCherryPick\" +Write-Host \"-outOfOrder: $flywayOutOfOrder\" +Write-Host \"-skipExecutingMigrations: $flywaySkipExecutingMigrations\" +Write-Host \"-infoSinceDate: $flywayInfoSinceDate\" +Write-Host \"-infoSinceVersion: $flywayInfoSinceVersion\" +Write-Host \"-baselineOnMigrate: $flywayBaselineOnMigrate\" +Write-Host \"-baselineVersion: $flywayBaselineVersion\" +Write-Host \"-baselineDescription: $flywayBaselineDescription\" +Write-Host \"-locations: $flywayLocations\" +Write-Host \"-check.BuildUrl: $flywayCheckBuildUrl\" +Write-Host \"-check.failOnDrift: $flywayCheckFailOnDrift\" +Write-Host \"-snapshot.FileName OR check.DeployedSnapshot: $flywaySnapshotFileName\" +Write-Host \"Additional Arguments: $flywayAdditionalArguments\" +Write-Host \"placeHolders: $flywayPlaceHolders\" +Write-Host \"*******************************************\" + +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +Write-Host \"Setting execution location to: $flywayPackagePath\" +Set-Location $flywayPackagePath + +$flywayCmd = Get-FlywayExecutablePath -providedPath $flywayExecutablePath + +$commandToUse = $flywayCommand +if ($flywayCommand -eq \"migrate dry run\") +{ +\t$commandToUse = \"migrate\" +} + +if ($flywayCommand -eq \"check dry run\" -or $flywayCommand -eq \"check changes\" -or $flywayCommand -eq \"check drift\") +{ +\t$commandToUse = \"check\" +} + +$arguments = @( +\t$commandToUse +) + +if ($flywayCommand -eq \"check dry run\") +{ +\t$arguments += \"-dryrun\" +} + +if ($flywayCommand -eq \"check changes\") +{ +\t$arguments += \"-changes\" + $arguments += \"-dryrun\" +} + +if ($flywayCommand -eq \"check drift\") +{ +\t$arguments += \"-drift\" +} + +# Deteremine authentication method +switch ($flywayAuthenticationMethod) +{ +\t\"awsiam\" + { +\t\t# Check to see if OS is Windows and running in a container + if ($IsWindows -and $env:DOTNET_RUNNING_IN_CONTAINER) + { + \tthrow \"IAM Role authentication is not supported in a Windows container.\" + } + +\t\t# Get parsed connection string url + $parsedUrl = Get-ParsedUrl -ConnectionUrl $flywayUrl + + # Region is part of the RDS endpoint, extract + $region = ($parsedUrl.Host.Split(\".\"))[2] + +\t\tWrite-Host \"Generating AWS IAM token ...\" +\t\t$flywayUserPassword = (aws rds generate-db-auth-token --hostname $parsedUrl.Host --region $region --port $parsedUrl.Port --username $flywayUser) + +\t\t$arguments += \"-user=`\"$flywayUser`\"\" + \t$arguments += \"-password=`\"$flywayUserPassword`\"\" + +\t\tbreak + } +\t\"azuremanagedidentity\" + { +\t\t# Check to see if OS is Windows and running in a container + if ($IsWindows -and $env:DOTNET_RUNNING_IN_CONTAINER) + { + \tthrow \"Azure Managed Identity is not supported in a Windows container.\" + } + + # SQL Server driver doesn't assign password + if (!$flywayUrl.ToLower().Contains(\"jdbc:sqlserver:\")) + { + # Get login token + Write-Host \"Generating Azure Managed Identity token ...\" + $token = Invoke-RestMethod -Method GET -Uri \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://ossrdbms-aad.database.windows.net\" -Headers @{\"MetaData\" = \"true\"} -UseBasicParsing + + $flywayUserPassword = $token.access_token + $arguments += \"-password=`\"$flywayUserPassword`\"\" + $arguments += \"-user=`\"$flywayUser`\"\" + } + else + { + +\t\t\t# Check to see if the querstring parameter for Azure Managed Identity is present + if (!$flywayUrl.ToLower().Contains(\"authentication=activedirectorymsi\")) + { + # Add the authentication piece to the jdbc url + if (!$flywayUrl.EndsWith(\";\")) + { + \t# Add the separator + $flywayUrl += \";\" + } + + # Add authentication piece + $flywayUrl += \"Authentication=ActiveDirectoryMSI\" + } + } + + break + } + \"gcpserviceaccount\" + { +\t\t# Check to see if OS is Windows and running in a container + if ($IsWindows -and $env:DOTNET_RUNNING_IN_CONTAINER) + { + \tthrow \"GCP Service Account authentication is not supported in a Windows container.\" + } + + # Define header + $header = @{ \"Metadata-Flavor\" = \"Google\"} + + # Retrieve service accounts + $serviceAccounts = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\" -Headers $header -UseBasicParsing + + # Results returned in plain text format, get into array and remove empty entries + $serviceAccounts = $serviceAccounts.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + + # Retreive the specific service account assigned to the VM + $serviceAccount = $serviceAccounts | Where-Object {$_.ToLower().Contains(\"iam.gserviceaccount.com\") } + +\t\tWrite-Host \"Generating GCP IAM token ...\" + # Retrieve token for account + $token = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/$serviceAccount/token\" -Headers $header -UseBasicParsing + + $flywayUserPassword = $token.access_token + + $arguments += \"-user=`\"$flywayUser`\"\" + $arguments += \"-password=`\"$flywayUserPassword`\"\" + #$env:FLYWAY_PASSWORD = $flywayUserPassword + + break + } + \"usernamepassword\" + { + \t# Add password + Write-Host \"Testing for parameters that can be applied to any command\" + if (Test-AddParameterToCommandline -parameterValue $flywayUser -acceptedCommands \"any\" -selectedCommand $flywayCommand -parameterName \"-user\") + { + Write-Host \"User provided, adding user and password command line argument\" + $arguments += \"-user=`\"$flywayUser`\"\" + $arguments += \"-password=`\"$flywayUserPassword`\"\" + } + + break + } + \"windowsauthentication\" + { + \t# Display to the user they've selected windows authentication. Though this is dictated by the jdbc url, this is added to make sure the user knows that's what is + # being used + Write-Host \"Using Windows Authentication\" + + # Check for integratedauthentication=true in url + if (!$flywayUrl.ToLower().Contains(\"integratedsecurity=true\")) + { + \t# Check to see if the connection url ends with a ; + if (!$flywayUrl.EndsWith(\";\")) + { + \t# Add the ; + $flywayUrl += \";\" + } + + $flywayUrl += \"integratedSecurity=true;\" + } + break + } +} + +$arguments += \"-url=`\"$flywayUrl`\"\" +$arguments += \"-locations=`\"$flywayLocations`\"\" + +if (Test-AddParameterToCommandline -parameterValue $flywaySchemas -acceptedCommands \"any\" -selectedCommand $flywayCommand -parameterName \"-schemas\") +{ +\tWrite-Host \"Schemas provided, adding schemas command line argument\" +\t$arguments += \"-schemas=`\"$flywaySchemas`\"\" +} + +if (Test-AddParameterToCommandline -parameterValue $flywayLicenseKey -acceptedCommands \"any\" -selectedCommand $flywayCommand -parameterName \"-licenseKey\") +{ +\tWrite-Host \"License key provided, adding -licenseKey command line argument\" +\t$arguments += \"-licenseKey=`\"$flywayLicenseKey`\"\" +} +Write-Host \"Finished testing for parameters that can be applied to any command, moving onto command specific parameters\" + +if (Test-AddParameterToCommandline -parameterValue $flywayCherryPick -acceptedCommands \"migrate,info,validate,check\" -selectedCommand $flywayCommand -parameterName \"-cherryPick\") +{ +\tWrite-Host \"Cherry pick provided, adding cherry pick command line argument\" +\t$arguments += \"-cherryPick=`\"$flywayCherryPick`\"\" +} + +if (Test-AddParameterToCommandline -parameterValue $flywayOutOfOrder -defaultValue \"false\" -acceptedCommands \"migrate,info,validate,check\" -selectedCommand $commandToUse -parameterName \"-outOfOrder\") +{ +\tWrite-Host \"Out of order is not false, adding out of order command line argument\" +\t$arguments += \"-outOfOrder=`\"$flywayOutOfOrder`\"\" +} + +if (Test-AddParameterToCommandline -parameterValue $flywayPlaceHolders -acceptedCommands \"migrate,info,validate,undo,repair,check\" -selectedCommand $commandToUse -parameterName \"-placeHolders\") +{ +\tWrite-Host \"Placeholder parameter provided, adding them to the command line arguments\" + + $placeHolderValueList = @(($flywayPlaceHolders -Split \"`n\").Trim()) + foreach ($placeHolder in $placeHolderValueList) + { + \t$placeHolderSplit = $placeHolder -Split \"::\" + $placeHolderKey = $placeHolderSplit[0] + $placeHolderValue = $placeHolderSplit[1] + Write-Host \"Adding -placeHolders.$placeHolderKey = $placeHolderValue to the argument list\" + + $arguments += \"-placeholders.$placeHolderKey=`\"$placeHolderValue`\"\" + } \t +} + +if (Test-AddParameterToCommandline -parameterValue $flywayTarget -acceptedCommands \"migrate,info,validate,undo,check\" -selectedCommand $commandToUse -parameterName \"-target\") +{ +\tWrite-Host \"Target provided, adding target command line argument\" + +\tif ($flywayTarget.ToLower().Trim() -eq \"latest\" -and $flywayCommand -eq \"undo\") +\t{ +\t\tWrite-Host \"The current target is latest, but the command is undo, changing the target to be current\" +\t\t$flywayTarget = \"current\" +\t} + +\t$arguments += \"-target=`\"$flywayTarget`\"\" +} + +if (Test-AddParameterToCommandline -parameterValue $flywaySkipExecutingMigrations -defaultValue \"false\" -acceptedCommands \"migrate\" -selectedCommand $flywayCommand -parameterName \"-skipExecutingMigrations\") +{ +\tWrite-Host \"Skip executing migrations is not false, adding skip executing migrations command line argument\" +\t$arguments += \"-skipExecutingMigrations=`\"$flywaySkipExecutingMigrations`\"\" +} + +if (Test-AddParameterToCommandline -parameterValue $flywayBaselineOnMigrate -defaultValue \"false\" -acceptedCommands \"migrate\" -selectedCommand $flywayCommand -parameterName \"-baselineOnMigrate\") +{ +\tWrite-Host \"Baseline on migrate is not false, adding the baseline on migrate argument\" +\t$arguments += \"-baselineOnMigrate=`\"$flywayBaselineOnMigrate`\"\" + + if (Test-AddParameterToCommandline -parameterValue $flywayBaselineVersion -acceptedCommands \"migrate\" -selectedCommand $flywayCommand -parameterName \"-baselineVersion\") + { + \tWrite-Host \"Baseline version has been specified, adding baseline version argument\" +\t\t$arguments += \"-baselineVersion=`\"$flywayBaselineVersion`\"\" + } +} + +if (Test-AddParameterToCommandline -parameterValue $flywayBaselineVersion -acceptedCommands \"baseline\" -selectedCommand $flywayCommand -parameterName \"-baselineVersion\") +{ +\tWrite-Host \"Doing a baseline, adding baseline version and description\" +\t$arguments += \"-baselineVersion=`\"$flywayBaselineVersion`\"\" + $arguments += \"-baselineDescription=`\"$flywayBaselineDescription`\"\" +} + +if (Test-AddParameterToCommandline -parameterValue $flywayInfoSinceDate -acceptedCommands \"info\" -selectedCommand $flywayCommand -parameterName \"-infoSinceDate\") +{ +\tWrite-Host \"Info since date has been provided, adding that to the command line arguments\" +\t$arguments += \"-infoSinceDate=`\"$flywayInfoSinceDate`\"\" +} + +if (Test-AddParameterToCommandline -parameterValue $flywayInfoSinceVersion -acceptedCommands \"info\" -selectedCommand $flywayCommand -parameterName \"-infoSinceVersion\") +{ +\tWrite-Host \"Info since version has been provided, adding that to the command line arguments\" +\t$arguments += \"-infoSinceVersion=`\"$flywayInfoSinceVersion`\"\" +} + +if (Test-AddParameterToCommandline -parameterValue $flywaySnapshotFileName -acceptedCommands \"snapshot\" -selectedCommand $commandToUse -parameterName \"-snapshot.filename\") +{ +\tWrite-Host \"Snapshot filename has been provided, adding that to the command line arguments\" + $folderName = Split-Path -Parent $flywaySnapshotFileName + if ((test-path $folderName) -eq $false) + { + \tNew-Item $folderName -ItemType Directory + } + $arguments += \"-snapshot.filename=`\"$flywaySnapshotFileName`\"\" +} + +$snapshotFileNameforCheckProvided = $false +if (Test-AddParameterToCommandline -parameterValue $flywaySnapshotFileName -acceptedCommands \"check\" -selectedCommand $commandToUse -parameterName \"-check.deployedSnapshot\") +{ +\tWrite-Host \"Snapshot filename has been provided for the check command, adding that to the command line arguments\" + $folderName = Split-Path -Parent $flywaySnapshotFileName + if ((test-path $folderName) -eq $false) + { + \tNew-Item $folderName -ItemType Directory + } + $arguments += \"-check.deployedSnapshot=`\"$flywaySnapshotFileName`\"\" + $snapshotFileNameforCheckProvided = $true +} + +if ((Test-AddParameterToCommandline -parameterValue $flywayCheckBuildUrl -acceptedCommands \"check\" -selectedCommand $commandToUse -parameterName \"-check.buildUrl\") -eq $true -and $snapshotFileNameforCheckProvided -eq $false) +{ +\tWrite-Host \"Check build URL has been provided, adding that to the command line arguments\" +\t$arguments += \"-check.buildUrl=`\"$flywayCheckBuildUrl`\"\" +} + +Write-Host \"Checking to see if the check username and password were supplied\" +if ((Test-AddParameterToCommandline -parameterValue $flywayCheckBuildUsername -acceptedCommands \"check\" -selectedCommand $commandToUse -parameterName \"-user\") -eq $true -and $snapshotFileNameforCheckProvided -eq $false) +{ +\tWrite-Host \"Check User provided, adding check user and check password command line argument\" +\t$arguments += \"-check.buildUser=`\"$flywayCheckBuildUsername`\"\" +\t$arguments += \"-check.buildPassword=`\"$flywayCheckBuildPassword`\"\" +} + +if (Test-AddParameterToCommandline -parameterValue $flywayCheckFailOnDrift -acceptedCommands \"check drift\" -selectedCommand $flywayCommand -parameterName \"-check.failOnDrift\") +{ +\tWrite-Host \"Doing a check drift command, adding the fail on drift\" +\t$arguments += \"-check.failOnDrift=`\"$flywayCheckFailOnDrift`\"\" +} + + +Write-Host \"Finished checking for command specific parameters, moving onto execution\" +$dryRunOutputFile = \"\" + +if ($flywayCommand -eq \"migrate dry run\") +{ +\t$dryRunOutputFile = Join-Path $(Get-Location) \"dryRunOutput\" + Write-Host \"Adding the argument dryRunOutput so Flyway will perform a dry run and not an actual migration.\" + $arguments += \"-dryRunOutput=`\"$dryRunOutputFile`\"\" +} + +# Check to see if there's any additional arguments to add +if (![string]::IsNullOrWhitespace($flywayAdditionalArguments)) +{ +\t# Split on space + $flywayAdditionalArgumentsArray = ($flywayAdditionalArguments.Split(\" \", [System.StringSplitOptions]::RemoveEmptyEntries)) + + # Loop through array + foreach ($newArgument in $flywayAdditionalArgumentsArray) + { + \t# Add the arguments + \t$arguments += $newArgument + } +} + +# Display what's going to be run +if (![string]::IsNullOrWhitespace($flywayUserPassword)) +{ + $flywayDisplayArguments = $arguments.PSObject.Copy() + $arrayIndex = 0 + for ($i = 0; $i -lt $flywayDisplayArguments.Count; $i++) + { + if ($null -ne $flywayDisplayArguments[$i]) + { + if ($flywayDisplayArguments[$i].Contains($flywayUserPassword)) + { + $flywayDisplayArguments[$i] = $flywayDisplayArguments[$i].Replace($flywayUserPassword, \"****\") + } + } + } + + Write-Host \"Executing the following command: $flywayCmd $flywayDisplayArguments\" +} +else +{ + Write-Host \"Executing the following command: $flywayCmd $arguments\" +} + +# Attempt to find driver path for java +$driverPath = (Get-ChildItem -Path (Get-ChildItem -Path $flywayCmd).Directory -Recurse | Where-Object {$_.PSIsContainer -eq $true -and $_.Name -eq \"drivers\"}) + +# If found, add driver path to the PATH environment varaible +if ($null -ne $driverPath) +{ +\t$env:PATH += \"$([IO.Path]::PathSeparator)$($driverPath.FullName)\" +} + +# Adjust call to flyway command based on OS +if ($IsLinux) +{ + & bash $flywayCmd $arguments +} +else +{ + & $flywayCmd $arguments +} + +# Check exit code +if ($lastExitCode -ne 0) +{ +\t# Fail the step + Write-Error \"Execution of Flyway failed!\" +} + +$currentDate = Get-Date +$currentDateFormatted = $currentDate.ToString(\"yyyyMMdd_HHmmss\") + +# Check to see if the dry run variable has a value +if (![string]::IsNullOrWhitespace($dryRunOutputFile)) +{ + $sqlDryRunFile = \"$($dryRunOutputFile).sql\" + $htmlDryRunFile = \"$($dryRunOutputFile).html\" + + if (Test-Path $sqlDryRunFile) + { + \tNew-OctopusArtifact -Path $sqlDryRunFile -Name \"$($flywayStepName)_$($flywayEnvironment)_$($currentDateFormatted)_dryRunOutput.sql\" + } + + if (Test-Path $htmlDryRunFile) + { + \tNew-OctopusArtifact -Path $htmlDryRunFile -Name \"$($flywayStepName)_$($flywayEnvironment)_$($currentDateFormatted)_dryRunOutput.html\" + } +} + +$reportFile = Join-Path $(Get-Location) \"report.html\" + +if (Test-Path $reportFile) +{ + \tNew-OctopusArtifact -Path $reportFile -Name \"$($flywayStepName)_$($flywayEnvironment)_$($currentDateFormatted)_report.html\" +}", + "Octopus.Action.PowerShell.Edition": "Core", + "Octopus.Action.EnabledFeatures": "Octopus.Features.SelectPowerShellEditionForWindows" + }, + "Parameters": [ + { + "Id": "a4ba9557-61d3-4d93-99d9-9937abaded9c", + "Name": "Flyway.Package.Value", + "Label": "Flyway Package", + "HelpText": "**Required** + +The package containing the migration scripts you want Flyway to run. Please refer to [documentation](https://flywaydb.org/documentation/concepts/migrations) for core concepts and naming conventions.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "02d50d4c-02f9-44c2-8ee0-3fc69a21a165", + "Name": "Flyway.Executable.Path", + "Label": "Flyway Executable Path", + "HelpText": "**Optional** + +The path of the flyway executable. It can either be a relative path or an absolute path. + +When not provided, this step template will test for the following. The step template places precedence on the version of the flyway included in the package. If Flyway is NOT found in the package, it will attempt to see if it is installed on the server by checking common paths. + +Running on `Linux`: +- `.flyway`: the package being deployed includes flyway and is running on Linux +- `/flyway/flyway`: The default path for the Linux execution container. + +Running on Windows: +- `\\.flyway.cmd`: the package being deployed includes flyway and is running on Windows +- `flyway`: the package is in the path on the Windows VM or Windows Execution container.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6e6e98ae-7e57-4bf7-a280-7c4a6bc57760", + "Name": "Flyway.Command.Value", + "Label": "Flyway Command", + "HelpText": "**Required** + +The [flyway command](https://flywaydb.org/documentation/usage/commandline/) you wish to run. + +- `Migrate`: Migrates the schema to the latest version +- `Migrate Dry Run`: Does a migration dry run and saves the results to a file that is uploaded as an artifact. This works for the `Teams` version of Flyway only. +- `Check Changes`: Produces a report indicating differences between applied migration scripts on your target database and pending migrations scripts (ie. the set of instructions you want to use to change your target database). This will report on the differences and perform a dry-run migration. Check Redgate's documentation to ensure your server technology is supported. Available to `Enterprise` licenses only. +- `Check Drift`: Produces a report indicating differences between the structure of your target database and the structure created by the migrations applied by Flyway. Available to `Enterprise` licenses only. +- `Check Dry Run`: Produces a report listing out the migration scripts to run. Use this to perform a dry run check or if you have a `Teams` license. +- `Info`: Prints the details and status information about all migrations. +- `Validate`: Validates applied migrations against resolved ones (on the filesystem or classpath) to detect accidental changes that may prevent the schema(s) from being recreated precisely. +- `Undo`: Undoes the most recently applied versioned migration. You must provide a Flyway `Teams` or `Enterprise` license key for this to work. +- `Repair`: Repairs the Flyway schema history table by removing any failed migrations and realigning the checksums, descriptions and types of the applied migrations with the ones of the available migrations +- `Clean`: It will effectively give you a fresh start, by wiping your configured schemas completely clean. All objects (tables, views, procedures, …) will be dropped. **DO NOT USE AGAINST A PRODUCTION DB!!!** +- `Baseline`: Introducing Flyway to existing databases by baselining them at a specific version. This will cause Migrate to ignore all migrations up to and including the baseline version. Newer migrations will then be applied as usual. +- `Snapshot`: Creates a file containing the schema of the specified database for subsequent use with the `Check Drift` option. Available to `Enterprise` licenses only. +", + "DefaultValue": "migrate", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "migrate|Migrate +migrate dry run|Migrate Dry Run +check changes|Check Changes +check drift|Check Drift +check dry run|Check Dry Run +info|Info +validate|Validate +undo|Undo +repair|Repair +clean|Clean +baseline|Baseline +snapshot|Snapshot" + } + }, + { + "Id": "e648821f-221c-4b8b-85fb-654f0d7379c5", + "Name": "Flyway.License.Key", + "Label": "License Key", + "HelpText": "**Optional** + +The [Flyway Teams](https://flywaydb.org/download) or `Enterprise` license key will enable undo functionality and the ability to dry run a migration.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "bb9d99ac-96b4-4d46-ae8f-87e5a7214278", + "Name": "Flyway.Target.Url", + "Label": "-Url", + "HelpText": "**Required** + +The [URL](https://flywaydb.org/documentation/configuration/parameters/url) parameter used in Flyway. This is the URL of the database to run the migration scripts on in the format specified in the default flyway.conf file. + +Examples: +- SQL Server: `jdbc:sqlserver://host:port;databaseName=database` +- Oracle: `jdbc:oracle:thin:@//host:port/service` or `jdbc:oracle:thin:@tns_entry` +- MySQL: `jdbc:mysql://host:port/database` +- PostgreSQL: `jdbc:postgresql://host:port/database` +- SQLite: `jdbc:sqlite:database` + +Please refer to [documentation](https://flywaydb.org/documentation/database/sqlserver) for further examples.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e9dccda1-4045-4e8e-b93e-4fac8d66630a", + "Name": "Flyway.Authentication.Method", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the database server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +azuremanagedidentity|Azure Managed Identity +gcpserviceaccount|GCP Service Account +usernamepassword|Username\\Password +windowsauthentication|Windows Authentication" + } + }, + { + "Id": "e1cb49a8-b965-4827-9f8e-01724d263541", + "Name": "Flyway.Database.User", + "Label": "-User", + "HelpText": "**Optional** + +The [user](https://flywaydb.org/documentation/configuration/parameters/user) used to connect to the database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "51fab335-82b2-4e58-8d4a-e2640ba66296", + "Name": "Flyway.Database.User.Password", + "Label": "-Password", + "HelpText": "**Optional** + +The [password](https://flywaydb.org/documentation/configuration/parameters/password) used to connect to the database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "f8787222-c404-47ca-9f0f-32fad6c656e7", + "Name": "Flyway.Command.Schemas", + "Label": "-Schemas", + "HelpText": "**Optional** + +Comma-separated case-sensitive list of [schemas](https://flywaydb.org/documentation/configuration/parameters/schemas) managed by Flyway. + +Example: `schema1,schema2` + +Flyway will attempt to create these schemas if they do not already exist and will clean them in the order of this list. If Flyway created them, then the schemas themselves will be dropped when cleaning. + +The first schema in the list will act as the default schema.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "27a4fafe-8e8b-4d6d-b25a-0953535ef2c9", + "Name": "Flyway.Command.Target", + "Label": "-Target", + "HelpText": "**Optional** for `migrate`, `info`, `validate`, `check`, and `undo`. **Ignored** for all other commands + +The [target version](https://flywaydb.org/documentation/configuration/parameters/target) up to which Flyway should consider migrations. If set to a value other than current or latest, this must be a valid migration version (e.g. `2.1`). + +When migrating forwards, Flyway will apply all migrations up to and including the target version. Migrations with a higher version number will be ignored. If the target is `current`, then no versioned migrations will be applied but repeatable migrations will be, together with any callbacks. + +When undoing migrations, Flyway will apply all undo scripts up to and including the target version. Undo scripts with a lower version number will be ignored. Specifying a target version should be done with care, as undo scripts typically destroy database objects. + +Special Values: +- `current`: designates the current version of the schema +- `latest`: the latest version of the schema, as defined by the migration with the highest version + +Default is: `latest`. When running the `undo` command, this will switch to `current` if `latest` is supplied.", + "DefaultValue": "latest", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1b0b700e-6686-4474-a630-8a89aea9b1d5", + "Name": "Flyway.Command.CherryPick", + "Label": "-CherryPick", + "HelpText": "**Optional** for `migrate`, `info`, `check`, and `validate`. **Ignored** for all other commands. + +A Comma separated list of migrations that Flyway should consider when migrating, undoing, or repairing. Migrations are considered in the order that they are supplied, overriding the default ordering. Leave blank to consider all discovered migrations. + +Each item in the list must either be a valid migration version (e.g `2.1`) or a valid migration description (e.g. `create_table`). + +See [documentation](https://flywaydb.org/documentation/configuration/parameters/cherryPick) for more details. + +The default is an empty string, meaning this command-line argument will be skipped.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "79a5ed66-eae8-4b0b-89fa-5da10850bb8d", + "Name": "Flyway.Command.OutOfOrder", + "Label": "-OutOfOrder", + "HelpText": "**Optional** for `migrate`, `info`, `check`, and `validate`. **Ignored** for all other commands. + +Allows migrations to be run “out of order”. + +If you already have versions `1.0` and `3.0` applied, and now a version `2.0` is found, it will be applied too instead of being ignored. + +See [documentation](https://flywaydb.org/documentation/configuration/parameters/outOfOrder) for more details. + +The default is `False`.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "true|True +false|False" + } + }, + { + "Id": "1d234d38-35d7-4b5e-a3fb-7ed81fa41d46", + "Name": "Flyway.Command.SkipExecutingMigrations", + "Label": "-SkipExecutingMigrations", + "HelpText": "**Optional** for `migrate`. **Ignored** for all other commands. + +Whether Flyway should skip migration execution. The remainder of the operation will run as normal - including updating the schema history table, callbacks, and so on. + +`skipExecutingMigrations` essentially allows you to mimic a migration being executed, because the schema history table is still updated as normal. + +`skipExecutingMigrations` can be used to bring an out-of-process change into Flyway’s change control process. For instance, a script run against the database outside of Flyway (like a hotfix) can be turned into a migration. The hotfix migration can be deployed with Flyway with `skipExecutingMigrations=true`. The schema history table will be updated with the new migration, but the script itself won’t be executed again. + +`skipExecutingMigrations` can be used with cherryPick to skip specific migrations. + +See [documentation](https://flywaydb.org/documentation/configuration/parameters/skipExecutingMigrations) for more details. + +The default value is `False`.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "true|True +false|False" + } + }, + { + "Id": "7e00397a-e18e-44b8-bb4d-e89b7c70d760", + "Name": "Flyway.Command.PlaceHolders", + "Label": "-PlaceHolders", + "HelpText": "**Optional** for `migrate`, `info`, `validate`, `undo`, `check`, and `repair`. **Ignored** for all other commands. + +[Placeholders](https://flywaydb.org/documentation/configuration/placeholder) to replace in SQL migrations. + +Each new line represents a new placeholder. This will only work with string variable types, text and sensitive values. + +Imagine this SQL Script + +``` +INSERT INTO ${Key1} (name) VALUES ('Mr. T') +GRANT SELECT ON SCHEMA ${flyway:defaultSchema} TO ${Key2} +``` + +Use the format **Name::Value** for example: + +``` +Key1::My Super Awesome Value + +Key2::Other Super Awesome Value +``` + +This will replace `${Key1}` with `My Super Awesome Value` and `${Key2}` with `Other Super Awesome Value`. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "3703d1da-1750-4c4f-89ba-04f5297ba0b9", + "Name": "Flyway.Command.InfoSinceDate", + "Label": "-InfoSinceDate", + "HelpText": "**Optional** with the `info` command and [Flyway Teams](https://flywaydb.org/documentation/usage/commandline/info#filtering-output) and Flyway Enterprise editions. **Ignored** for all other commands. + +Limits info to show only migrations applied after this date, and any unapplied migrations. Must be in the format `dd/MM/yyyy HH:mm` (e.g. `01/12/2020 13:00`)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0bfecdcd-ad27-4908-949c-a54745aa85f0", + "Name": "Flyway.Command.InfoSinceVersion", + "Label": "-InfoSinceVersion", + "HelpText": "**Optional** with the `info` command and [Flyway Teams](https://flywaydb.org/documentation/usage/commandline/info#filtering-output) and Flyway Enterprise editions. **Ignored** for all other commands. + +Limits info to show only migrations greater than or equal to this version, and any repeatable migrations. (e.g `1.1`)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4d4c2fc6-f4a4-4c68-bb79-9ef4d28e2f53", + "Name": "Flyway.Command.BaseLineOnMigrate", + "Label": "-baselineOnMigrate", + "HelpText": "**Optional** when using `migrate`. **Ignored** for all other commands. + +Whether to automatically call baseline when migrate is executed against a non-empty schema with no metadata table. This schema will then be baselined with the baselineVersion before executing the migrations. Only migrations above baselineVersion will then be applied. + +This is useful for initial Flyway production deployments on projects with an existing DB. + +Be careful when enabling this as it removes the safety net that ensures Flyway does not migrate the wrong database in case of a configuration mistake!", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "true|True +false|False" + } + }, + { + "Id": "f7e23a32-73a8-4118-8996-354d48f176cb", + "Name": "Flyway.Command.BaselineVersion", + "Label": "-baselineVersion", + "HelpText": "**Required** when using `Baseline` or when `BaselineOnMigrate` is set to `True`. **Ignored** for all other commands. + +The version to tag an existing schema with when executing [baseline](https://flywaydb.org/documentation/command/baseline).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c8a118ef-bdc4-4e2f-966b-596662321d1c", + "Name": "Flyway.Command.BaselineDescription", + "Label": "-baseLineDescription", + "HelpText": "**Required** when using `Baseline`. **Ignored** for all other commands. + +The Description to tag an existing schema with when executing [baseline](https://flywaydb.org/documentation/command/baseline).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d3bbe139-2bd4-4242-be70-a6f05510042b", + "Name": "Flyway.Command.Locations", + "Label": "-Locations", + "HelpText": "**Optional** with the `info`, `migrate`, `repair`, `undo`, `check`, and `validate` commands. + +Specifies the location of the script files to execute. If left blank, the `Extracted Path` of the package will be used. + +Example: filesystem:`#{Octopus.Action.Package[Flyway.Package.Value].ExtractedPath}/MySubFolder`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "57611c50-3f3f-4a9d-a6c4-02517d2f238e", + "Name": "Flyway.Command.CheckBuildUrl", + "Label": "-check.BuildUrl", + "HelpText": "**Required** when using `Check Changes` or `Check Drift` commands and `-snapshot.FileName` is empty. + +**Please Note:** When `-snapshot.FileName` is not empty, this parameter is ignored. + +Flyway uses the URL to an existing database as a temporary database to perform the check logic against. Flyway will clean this database, so if you specify a full database, you must ensure it is okay for Flyway to erase its schema. + +Examples: +- SQL Server: `jdbc:sqlserver://host:port;databaseName=database` +- Oracle: `jdbc:oracle:thin:@//host:port/service` or `jdbc:oracle:thin:@tns_entry` +- MySQL: `jdbc:mysql://host:port/database` +- PostgreSQL: `jdbc:postgresql://host:port/database` +- SQLite: `jdbc:sqlite:database` + +Please refer to [documentation](https://flywaydb.org/documentation/database/sqlserver) for further examples.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b52238f0-a7e6-4cfa-851f-f1f9fdb70c65", + "Name": "Flyway.Database.Check.User", + "Label": "-check.BuildUser", + "HelpText": "**Optional** + +The username of the user for the build database. Use this if the build database needs a separate username and password from the authentication information supplied above. + +**Please Note:** When `-snapshot.FileName` is not empty, this parameter is ignored.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "14f7c856-6ca2-4319-84b3-e96a40d65c10", + "Name": "Flyway.Database.Check.User.Password", + "Label": "-check.BuildPassword", + "HelpText": "**Optional** + +The password of the user for the build database. Use this if the build database needs a separate username and password from the authentication information supplied above. + +**Please Note:** When `-snapshot.FileName` is not empty, this parameter is ignored.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "4bcdced9-2397-4423-bdc2-776ff80f78f3", + "Name": "Flyway.Command.FailOnDrift", + "Label": "-check.failOnDrift", + "HelpText": "**Required** when using `Check Drift`. Ignored for all other commands. + +Indicates if Flyway will terminate with a non-zero return code if drift is detected. When using `Check Drift` with a snapshot and you want to see the changes, set this to `false`", + "DefaultValue": "true", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "true|True +false|False" + } + }, + { + "Id": "22bb9c1b-1ce4-4f63-b618-d642c804d3ac", + "Name": "Flyway.Command.Snapshot.FileName", + "Label": "-snapshot.FileName", + "HelpText": "**Required** when using `Snapshot`. **Optional** when using the `Check Drift` command. **Ignored** for all other commands. + +The name, including the path, of the snapshot file to create or use. + +If this parameter is populated the `-check.BuildUser`, `check.BuildPassword`, and `-check.BuildUrl` parameters are all ignored. + +**Hint:** If executing this step in an execution container, consider `../../../` as the path prefix as the script will execute in `/etc/octopus/default/Work/[Random Folder Name]/Flyway.Package.Value`. `/etc/octopus/default` is mounted as a volume. Or leverage a NAS or other file share. + +**Important:** You will need this file for future steps. It must be stored in a non-temporary location. By default, Octopus runs this script in a temporary location on a worker. That location is deleted automatically once this step is complete. If you do not supply a path, the file will be deleted.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ff9a4e49-d3f0-45ff-891a-3e5f6b72806d", + "Name": "Flyway.Additional.Arguments", + "Label": "Additional arguments", + "HelpText": "Any additional arguments that need to be passed (ie `-table=\"MyTable\")", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2024-03-05T19:05:40.264Z", + "OctopusVersion": "2024.1.11865", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "flyway" +} diff --git a/step-templates/ftp-uploadfiles-package.json.human b/step-templates/ftp-uploadfiles-package.json.human new file mode 100644 index 000000000..b9568fb19 --- /dev/null +++ b/step-templates/ftp-uploadfiles-package.json.human @@ -0,0 +1,485 @@ +{ + "Id": "8b316926-0821-459b-9869-3ca98fd9087e", + "Name": "Upload files by FTP from package", + "Description": "Upload files to a remote server via File Transfer Protocol (SFTP or FTP) using WinSCP. + +This step template uses the [WinSCP .NET Assembly](http://winscp.net/eng/docs/library#downloading_and_installing_the_assembly). In the absence of WinSCP installed on the machine, it will attempt to make use of the WinSCP PowerShell module, downloading a temporary copy if not already present. + +# Notes on usage + +This version uses a referenced package parameter and is able to be run on a Worker. + +## Cleaning up deployments + +If you aren't deploying to an application host and don't want the deployment files to persist on the tentacle irrespective of the life cycle retention policy ensure that you set the \"Delete Previous Deployment\" to `true` and they will be removed.", + "ActionType": "Octopus.Script", + "Version": 4, + "Author": "twerthi", + "Packages": [ + { + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "FtpPackage" + }, + "Id": "76cedf6d-e8b0-4fc2-af04-a745ae6659ea", + "Name": "FtpPackage" + } + ], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "## -------------------------------------------------------------------------------------- +## Input +## -------------------------------------------------------------------------------------- +$PathToWinScp = $OctopusParameters['PathToWinScp'] +$FtpHost = $OctopusParameters['FtpHost'] +$FtpUsername = $OctopusParameters['FtpUsername'] +$FtpPassword = $OctopusParameters['FtpPassword'] +$FtpHostKeyFingerprint = $OctopusParameters['FtpHostKeyFingerprint'] +$FtpPasskey = $OctopusParameters['FtpPasskey'] +$FtpPasskeyPhrase = $OctopusParameters['FtpPasskeyPhrase'] +$FtpRemoteDirectory = $OctopusParameters['FtpRemoteDirectory'] +$FtpDeleteUnrecognizedFiles = $OctopusParameters['FtpDeleteUnrecognizedFiles'] +$DeleteDeploymentStep = $OctopusParameters['DeleteDeploymentStep'] + +## -------------------------------------------------------------------------------------- +## Helpers +## -------------------------------------------------------------------------------------- + +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +# Helper for validating input parameters +function Validate-Parameter([string]$foo, [string[]]$validInput, $parameterName) { + if (! $parameterName -contains \"Password\") + { + Write-Host \"${parameterName}: $foo\" + } + if (! $foo) { + throw \"No value was set for $parameterName, and it cannot be empty\" + } +} + +# A collection of functions that can be used by script steps to determine where packages installed +# by previous steps are located on the filesystem. +function Find-InstallLocations { + $result = @() + $OctopusParameters.Keys | foreach { + if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) { + $result += $OctopusParameters[$_] + } + } + return $result +} + +function Find-InstallLocation($stepName) { + $result = $OctopusParameters.Keys | where { + $_.Equals(\"Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath\", [System.StringComparison]::OrdinalIgnoreCase) + } | select -first 1 + + if ($result) { + return $OctopusParameters[$result] + } + + throw \"No install location found for step: $stepName\" +} + +function Find-SingleInstallLocation { + $all = @(Find-InstallLocations) + if ($all.Length -eq 1) { + return $all[0] + } + if ($all.Length -eq 0) { + throw \"No package steps found\" + } + throw \"Multiple package steps have run; please specify a single step\" +} + +# Session.FileTransferred event handler +function FileTransferred +{ + param($e) + + if ($e.Error -eq $Null) + { + Write-Host (\"Upload of {0} succeeded\" -f $e.FileName) + } + else + { + Write-Error (\"Upload of {0} failed: {1}\" -f $e.FileName, $e.Error) + } + + if ($e.Chmod -ne $Null) + { + if ($e.Chmod.Error -eq $Null) + { + Write-Host \"##octopus[stdout-verbose]\" + Write-Host (\"Permisions of {0} set to {1}\" -f $e.Chmod.FileName, $e.Chmod.FilePermissions) + Write-Host \"##octopus[stdout-default]\" + } + else + { + Write-Error (\"Setting permissions of {0} failed: {1}\" -f $e.Chmod.FileName, $e.Chmod.Error) + } + + } + else + { + Write-Host \"##octopus[stdout-verbose]\" + Write-Host (\"Permissions of {0} kept with their defaults\" -f $e.Destination) + Write-Host \"##octopus[stdout-default]\" + } + + if ($e.Touch -ne $Null) + { + if ($e.Touch.Error -eq $Null) + { + Write-Host \"##octopus[stdout-verbose]\" + Write-Host (\"Timestamp of {0} set to {1}\" -f $e.Touch.FileName, $e.Touch.LastWriteTime) + Write-Host \"##octopus[stdout-default]\" + } + else + { + Write-Error (\"Setting timestamp of {0} failed: {1}\" -f $e.Touch.FileName, $e.Touch.Error) + } + + } + else + { + # This should never happen during \"local to remote\" synchronization + Write-Host \"##octopus[stdout-verbose]\" + Write-Host (\"Timestamp of {0} kept with its default (current time)\" -f $e.Destination) + Write-Host \"##octopus[stdout-default]\" + } +} + +## -------------------------------------------------------------------------------------- +## Configuration +## -------------------------------------------------------------------------------------- +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\" +$PowerShellModuleName = \"WinSCP\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if $PathToWinScp is empty +if ([string]::IsNullOrEmpty($PathToWinScp)) +{ + # Check to see if WinSCP module is installed + if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) + { + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules + + # Import module + Write-Output \"Importing $PowerShellModuleName ...\" + Import-Module $PowerShellModuleName + } +\t#Get-ChildItem -Path ([System.IO.Path]::GetDirectoryName((Get-Module $PowerShellModuleName).Path)) +\t# Get the path to where the dll resides + #$PathToWinScp = [System.IO.Path]::GetDirectoryName((Get-Module $PowerShellModuleName).Path) +} +else +{ +\tValidate-Parameter $PathToWinScp -parameterName \"Path to WinSCP .NET Assembly\" + # Load WinSCP .NET assembly + $fullPathToWinScp = \"$PathToWinScp\\WinSCPnet.dll\" + if(-not (Test-Path $fullPathToWinScp)) + { + throw \"$PathToWinScp does not contain the WinSCP .NET Assembly\" + } + Add-Type -Path $fullPathToWinScp +} + +#Validate-Parameter $PathToWinScp -parameterName \"Path to WinSCP .NET Assembly\" +Validate-Parameter $FtpHost -parameterName \"Host\" +Validate-Parameter $FtpUsername -parameterName \"Username\" +Validate-Parameter $FtpPassword -parameterName \"Password\" +Validate-Parameter $FtpRemoteDirectory -parameterName \"Remote directory\" +Validate-Parameter $FtpDeleteUnrecognizedFiles -parameterName \"Delete unrecognized files\" + +## -------------------------------------------------------------------------------------- +## Main script +## -------------------------------------------------------------------------------------- + +# Load WinSCP .NET assembly +<# +$fullPathToWinScp = \"$PathToWinScp\\WinSCPnet.dll\" +if(-not (Test-Path $fullPathToWinScp)) +{ + throw \"$PathToWinScp does not contain the WinSCP .NET Assembly\" +} +Add-Type -Path $fullPathToWinScp +#> +$stepPath = \"\" + +$stepPath = $OctopusParameters[\"Octopus.Action.Package[FtpPackage].ExtractedPath\"] + +Write-Host \"Package was installed to: $stepPath\" + +try +{ + $sessionOptions = New-Object WinSCP.SessionOptions + + # WinSCP defaults to SFTP, but it's good to ensure that's the case + if (![string]::IsNullOrEmpty($FtpHostKeyFingerprint)) { + $sessionOptions.Protocol = [WinScp.Protocol]::Sftp + $sessionOptions.SshHostKeyFingerprint = $FtpHostKeyFingerprint + } + else { + $sessionOptions.Protocol = [WinSCP.Protocol]::Ftp + } + $sessionOptions.HostName = $FtpHost + $sessionOptions.UserName = $FtpUsername + + + + # If there is a path to the private key, use that instead of a password + if (![string]::IsNullOrEmpty($FtpPasskey)) { + Write-Host \"Attempting to use passkey instead of password\" + + # Check key exists + if (!(Test-Path $FtpPasskey)) { + throw \"Unable to locate passkey at: $FtpPasskey\" + } + + $sessionOptions.SshPrivateKeyPath = $FtpPasskey + + # If the key requires a passphrase to access + if ($FtpPasskeyPhrase -ne \"\") { + $sessionOptions.PrivateKeyPassphrase = $FtpPasskeyPhrase + } + } + else { + $sessionOptions.Password = $FtpPassword + } + + $session = New-Object WinSCP.Session + + if ([string]::IsNullOrEmpty($PathToWinScp)) + { + \t# Using PowerShell module, need to set the executable location + $session.ExecutablePath = \"$([System.IO.Path]::GetDirectoryName((Get-Module $PowerShellModuleName).Path))\\bin\\winscp.exe\" + } + + try + { + + # Will continuously report progress of synchronization + $session.add_FileTransferred( { FileTransferred($_) } ) + + # Connect + $session.Open($sessionOptions) + + Write-Host \"Beginning synchronization between $stepPath and $FtpRemoteDirectory on $FtpHost\" + + if (-not $session.FileExists($FtpRemoteDirectory)) + { + Write-Host \"Remote directory not found, creating $FtpRemoteDirectory\" + $session.CreateDirectory($FtpRemoteDirectory); + } + + # Synchronize files + $synchronizationResult = $session.SynchronizeDirectories( + [WinSCP.SynchronizationMode]::Remote, $stepPath, $FtpRemoteDirectory, $FtpDeleteUnrecognizedFiles) + + # Throw on any error + $synchronizationResult.Check() + } + finally + { + # Disconnect, clean up + $session.Dispose() + + if ($DeleteDeploymentStep) { + Remove-Item -Path $stepPath -Recurse + } + } + + exit 0 +} +catch [Exception] +{ + throw $_.Exception.Message +} + +", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "a884e52a-0e90-4b98-ba36-916dbbb7302c", + "Name": "PathToWinScp", + "Label": "Path to WinScp", + "HelpText": "The directory where you extracted the WinSCP .NET Assembly.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "09f9bb2d-2c7e-402c-8824-e91c6c7dd3e4", + "Name": "FtpHost", + "Label": "Host", + "HelpText": "The address of your FTP server. Example: `ftp.yourhost.com`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "81768a2a-9fbb-47d0-b584-91cb1c93fc28", + "Name": "FtpUsername", + "Label": "Username", + "HelpText": "If no username is specified, the well-known username `anonymous` will be used.", + "DefaultValue": "anonymous", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f36ea459-db54-4ff8-9f9f-f6917f56f330", + "Name": "FtpPassword", + "Label": "Password", + "HelpText": "If no password is specified, the well-known password `guest` will be used. + +If the password field is bound, the binding expression will be visible to other authorized users.", + "DefaultValue": "guest", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c970e25e-e1cf-4143-81e7-ab158f80b7bf", + "Name": "FtpHostKeyFingerprint", + "Label": "Host Key Fingerprint(s)", + "HelpText": "By supplying a host key fingerprint (or a semicolon separated list), you will force the deployment into SFTP mode, and automatically accept the host certificates.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4c4ace47-c5a0-46e3-9898-39fb722a524d", + "Name": "FtpPasskey", + "Label": "Passkey", + "HelpText": "Path to the PPK passkey file, leave blank if using a Password", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0ecfc143-7470-45a2-a099-f0c6f37f58e5", + "Name": "FtpPasskeyPhrase", + "Label": "Passkey Phrase", + "HelpText": "If your passkey is encrypted, please supply the pass phrase. + +If the password field is bound, the binding expression will be visible to other authorized users.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "b2fa7e63-c46b-4539-8d3b-c1d7c41c71ea", + "Name": "FtpRemoteDirectory", + "Label": "Remote directory", + "HelpText": "The directory on your FTP server in which you want files to be placed. Example: `/site/wwwroot`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f02cbc65-dffb-40cc-a0b5-d87e19346b90", + "Name": "FtpDeleteUnrecognizedFiles", + "Label": "Delete unrecognized files", + "HelpText": "Files can exist on the FTP server that do not exist in the NuGet package. Examples may be binaries from a previous release, or uploaded images in a CMS. Use this option to choose how to treat these files.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "e7ca0ba2-02cc-4988-acef-0f0bb12094ad", + "Name": "DeleteDeploymentStep", + "Label": "Delete Deployment Step?", + "HelpText": "Should this script delete the deployment - for example, if you are running this on the Octopus Server, you might want to clean up the output of the previous package deploy step when this has completed.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "b7a1af1a-0670-4817-a577-132fe1db01e5", + "Name": "FtpPackage", + "Label": "Package", + "HelpText": "Select the package to FTP", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-08T00:22:01.921Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "winscp" +} diff --git a/step-templates/ftp-uploadfiles.json.human b/step-templates/ftp-uploadfiles.json.human new file mode 100644 index 000000000..88d3854ce --- /dev/null +++ b/step-templates/ftp-uploadfiles.json.human @@ -0,0 +1,366 @@ +{ + "Id": "3b534e57-e8b0-4a06-aa2c-9e7eba1f4337", + "Name": "Upload files by FTP", + "Description": "Upload files to a remote server via File Transfer Protocol (SFTP or FTP) using WinSCP. + +This step template requires the [WinSCP .NET Assembly](http://winscp.net/eng/docs/library#downloading_and_installing_the_assembly) to be installed on the server running the deployment. + +# Notes on usage + +You will need to have a \"Deploy a Package\" step before this step to supply the files to deploy. You can have this all happen on your Octopus Server rather than one of your web servers by following the steps at [Deploying Packages to your Octopus Server](https://octopus.com/docs/deployment-process/steps/how-to-run-steps-on-the-octopus-server#deploying-packages-to-your-octopus-server). + +## Cleaning up deployments + +If you aren't deploying to an application host and don't want the deployment files to persist on the tentacle irrespective of the life cycle retention policy ensure that you set the \"Delete Previous Deployment\" to `true` and they will be removed.", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "## -------------------------------------------------------------------------------------- +## Input +## -------------------------------------------------------------------------------------- +$PathToWinScp = $OctopusParameters['PathToWinScp'] +$FtpHost = $OctopusParameters['FtpHost'] +$FtpUsername = $OctopusParameters['FtpUsername'] +$FtpPassword = $OctopusParameters['FtpPassword'] +$FtpHostKeyFingerprint = $OctopusParameters['FtpHostKeyFingerprint'] +$FtpPasskey = $OctopusParameters['FtpPasskey'] +$FtpPasskeyPhrase = $OctopusParameters['FtpPasskeyPhrase'] +$FtpRemoteDirectory = $OctopusParameters['FtpRemoteDirectory'] +$FtpPackageStepName = $OctopusParameters['FtpPackageStepName'] +$FtpDeleteUnrecognizedFiles = $OctopusParameters['FtpDeleteUnrecognizedFiles'] +$DeleteDeploymentStep = $OctopusParameters['DeleteDeploymentStep'] + +## -------------------------------------------------------------------------------------- +## Helpers +## -------------------------------------------------------------------------------------- +# Helper for validating input parameters +function Validate-Parameter([string]$foo, [string[]]$validInput, $parameterName) { + if (! $parameterName -contains \"Password\") + { + Write-Host \"${parameterName}: $foo\" + } + if (! $foo) { + throw \"No value was set for $parameterName, and it cannot be empty\" + } +} + +# A collection of functions that can be used by script steps to determine where packages installed +# by previous steps are located on the filesystem. +function Find-InstallLocations { + $result = @() + $OctopusParameters.Keys | foreach { + if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) { + $result += $OctopusParameters[$_] + } + } + return $result +} + +function Find-InstallLocation($stepName) { + $result = $OctopusParameters.Keys | where { + $_.Equals(\"Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath\", [System.StringComparison]::OrdinalIgnoreCase) + } | select -first 1 + + if ($result) { + return $OctopusParameters[$result] + } + + throw \"No install location found for step: $stepName\" +} + +function Find-SingleInstallLocation { + $all = @(Find-InstallLocations) + if ($all.Length -eq 1) { + return $all[0] + } + if ($all.Length -eq 0) { + throw \"No package steps found\" + } + throw \"Multiple package steps have run; please specify a single step\" +} + +# Session.FileTransferred event handler +function FileTransferred +{ + param($e) + + if ($e.Error -eq $Null) + { + Write-Host (\"Upload of {0} succeeded\" -f $e.FileName) + } + else + { + Write-Error (\"Upload of {0} failed: {1}\" -f $e.FileName, $e.Error) + } + + if ($e.Chmod -ne $Null) + { + if ($e.Chmod.Error -eq $Null) + { + Write-Host \"##octopus[stdout-verbose]\" + Write-Host (\"Permisions of {0} set to {1}\" -f $e.Chmod.FileName, $e.Chmod.FilePermissions) + Write-Host \"##octopus[stdout-default]\" + } + else + { + Write-Error (\"Setting permissions of {0} failed: {1}\" -f $e.Chmod.FileName, $e.Chmod.Error) + } + + } + else + { + Write-Host \"##octopus[stdout-verbose]\" + Write-Host (\"Permissions of {0} kept with their defaults\" -f $e.Destination) + Write-Host \"##octopus[stdout-default]\" + } + + if ($e.Touch -ne $Null) + { + if ($e.Touch.Error -eq $Null) + { + Write-Host \"##octopus[stdout-verbose]\" + Write-Host (\"Timestamp of {0} set to {1}\" -f $e.Touch.FileName, $e.Touch.LastWriteTime) + Write-Host \"##octopus[stdout-default]\" + } + else + { + Write-Error (\"Setting timestamp of {0} failed: {1}\" -f $e.Touch.FileName, $e.Touch.Error) + } + + } + else + { + # This should never happen during \"local to remote\" synchronization + Write-Host \"##octopus[stdout-verbose]\" + Write-Host (\"Timestamp of {0} kept with its default (current time)\" -f $e.Destination) + Write-Host \"##octopus[stdout-default]\" + } +} + +## -------------------------------------------------------------------------------------- +## Configuration +## -------------------------------------------------------------------------------------- +Validate-Parameter $PathToWinScp -parameterName \"Path to WinSCP .NET Assembly\" +Validate-Parameter $FtpHost -parameterName \"Host\" +Validate-Parameter $FtpUsername -parameterName \"Username\" +Validate-Parameter $FtpPassword -parameterName \"Password\" +Validate-Parameter $FtpRemoteDirectory -parameterName \"Remote directory\" +Validate-Parameter $FtpPackageStepName -parameterName \"Package step name\" +Validate-Parameter $FtpDeleteUnrecognizedFiles -parameterName \"Delete unrecognized files\" + +## -------------------------------------------------------------------------------------- +## Main script +## -------------------------------------------------------------------------------------- + +# Load WinSCP .NET assembly +$fullPathToWinScp = \"$PathToWinScp\\WinSCPnet.dll\" +if(-not (Test-Path $fullPathToWinScp)) +{ + throw \"$PathToWinScp does not contain the WinSCP .NET Assembly\" +} +Add-Type -Path $fullPathToWinScp + +$stepPath = \"\" +if (-not [string]::IsNullOrEmpty($FtpPackageStepName)) { + Write-Host \"Finding path to package step: $FtpPackageStepName\" + $stepPath = Find-InstallLocation $FtpPackageStepName +} else { + $stepPath = Find-SingleInstallLocation +} +Write-Host \"Package was installed to: $stepPath\" + +try +{ + $sessionOptions = New-Object WinSCP.SessionOptions + + # WinSCP defaults to SFTP, but it's good to ensure that's the case + if ($FtpHostKeyFingerprint -ne \"\") { + $sessionOptions.Protocol = [WinScp.Protocol]::Sftp + } + else { + $sessionOptions.Protocol = [WinSCP.Protocol]::Ftp + } + $sessionOptions.HostName = $FtpHost + $sessionOptions.UserName = $FtpUsername + + $sessionOptions.SshHostKeyFingerprint = $FtpHostKeyFingerprint + + # If there is a path to the private key, use that instead of a password + if ($FtpPasskey -ne \"\") { + Write-Host \"Attempting to use passkey instead of password\" + + # Check key exists + if (!(Test-Path $FtpPasskey)) { + throw \"Unable to locate passkey at: $FtpPasskey\" + } + + $sessionOptions.SshPrivateKeyPath = $FtpPasskey + + # If the key requires a passphrase to access + if ($FtpPasskeyPhrase -ne \"\") { + $sessionOptions.PrivateKeyPassphrase = $FtpPasskeyPhrase + } + } + else { + $sessionOptions.Password = $FtpPassword + } + + $session = New-Object WinSCP.Session + try + { + # Will continuously report progress of synchronization + $session.add_FileTransferred( { FileTransferred($_) } ) + + # Connect + $session.Open($sessionOptions) + + Write-Host \"Beginning synchronization between $stepPath and $FtpRemoteDirectory on $FtpHost\" + + if (-not $session.FileExists($FtpRemoteDirectory)) + { + Write-Host \"Remote directory not found, creating $FtpRemoteDirectory\" + $session.CreateDirectory($FtpRemoteDirectory); + } + + # Synchronize files + $synchronizationResult = $session.SynchronizeDirectories( + [WinSCP.SynchronizationMode]::Remote, $stepPath, $FtpRemoteDirectory, $FtpDeleteUnrecognizedFiles) + + # Throw on any error + $synchronizationResult.Check() + } + finally + { + # Disconnect, clean up + $session.Dispose() + + if ($DeleteDeploymentStep) { + Remove-Item -Path $stepPath -Recurse + } + } + + exit 0 +} +catch [Exception] +{ + throw $_.Exception.Message +} +", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "PathToWinScp", + "Label": "Path to WinScp", + "HelpText": "The directory where you extracted the WinSCP .NET Assembly.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "FtpHost", + "Label": "Host", + "HelpText": "The address of your FTP server. Example: `ftp.yourhost.com`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "FtpUsername", + "Label": "Username", + "HelpText": "If no username is specified, the well-known username `anonymous` will be used.", + "DefaultValue": "anonymous", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "FtpPassword", + "Label": "Password", + "HelpText": "If no password is specified, the well-known password `guest` will be used. + +If the password field is bound, the binding expression will be visible to other authorized users.", + "DefaultValue": "guest", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "FtpHostKeyFingerprint", + "Label": "Host Key Fingerprint(s)", + "HelpText": "By supplying a host key fingerprint (or a semicolon separated list), you will force the deployment into SFTP mode, and automatically accept the host certificates.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "FtpPasskey", + "Label": "Passkey", + "HelpText": "Path to the PPK passkey file, leave blank if using a Password", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "FtpPasskeyPhrase", + "Label": "Passkey Phrase", + "HelpText": "If your passkey is encrypted, please supply the pass phrase. + +If the password field is bound, the binding expression will be visible to other authorized users.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "FtpRemoteDirectory", + "Label": "Remote directory", + "HelpText": "The directory on your FTP server in which you want files to be placed. Example: `/site/wwwroot`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "FtpPackageStepName", + "Label": "Package step name", + "HelpText": "Name of the previously-deployed package step that contains the files that you want to deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Name": "FtpDeleteUnrecognizedFiles", + "Label": "Delete unrecognized files", + "HelpText": "Files can exist on the FTP server that do not exist in the NuGet package. Examples may be binaries from a previous release, or uploaded images in a CMS. Use this option to choose how to treat these files.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DeleteDeploymentStep", + "Label": "Delete Deployment Step?", + "HelpText": "Should this script delete the deployment - for example, if you are running this on the Octopus Server, you might want to clean up the output of the previous package deploy step when this has completed.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2018-07-05T15:12:39.219Z", + "LastModifiedBy": "Zhaph", + "$Meta": { + "ExportedAt": "2018-07-05T15:12:39.219Z", + "OctopusVersion": "2018.6.2", + "Type": "ActionTemplate" + }, + "Category": "winscp" +} diff --git a/step-templates/gcp-secret-manager-retrieve-secrets.json.human b/step-templates/gcp-secret-manager-retrieve-secrets.json.human new file mode 100644 index 000000000..0ffb84624 --- /dev/null +++ b/step-templates/gcp-secret-manager-retrieve-secrets.json.human @@ -0,0 +1,197 @@ +{ + "Id": "9f5a9e3c-76b1-462f-972a-ae91d5deaa05", + "Name": "GCP Secret Manager - Retrieve Secrets", + "Description": "This step retrieves one or more secrets from [Secret Manager](https://cloud.google.com/secret-manager) on Google Cloud Platform (GCP), and creates [sensitive output variables](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) for each value retrieved. These values can be used in other steps in your deployment or runbook process. + +It's recommended that you retrieve secrets with a specific version, and not the *latest* version. You can choose a custom output variable name for each secret, or one will be created dynamically. + +--- + +**Required:** +- Octopus Server **2021.2** or higher. +- PowerShell **5.1** or higher. +- The Google Cloud (`gcloud`) CLI, version **338.0.0** or higher installed on the target or worker. If the CLI can't be found, the step will fail. +- A Google account with permissions to retrieve secrets from Secret Manager on Google Cloud. Accessing a secret version requires the **Secret Manager Secret Accessor** role (`roles/secretmanager.secretAccessor`) on the secret, project, folder, or organization. + +Notes: + +- Tested on Octopus **2021.2**. +- Tested on both Windows Server 2019 and Ubuntu 20.04. + +", + "ActionType": "Octopus.GoogleCloudScripting", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.GoogleCloud.ImpersonateServiceAccount": "False", + "Octopus.Action.GoogleCloud.UseVMServiceAccount": "False", + "Octopus.Action.GoogleCloudAccount.Variable": "#{GCP.SecretManager.RetrieveSecrets.Account}", + "Octopus.Action.GoogleCloud.Project": "#{GCP.SecretManager.RetrieveSecrets.Project}", + "Octopus.Action.GoogleCloud.Region": "#{GCP.SecretManager.RetrieveSecrets.Region}", + "Octopus.Action.GoogleCloud.Zone": "#{GCP.SecretManager.RetrieveSecrets.Zone}", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +# Variables +$SecretNames = $OctopusParameters[\"GCP.SecretManager.RetrieveSecrets.SecretNames\"] +$PrintVariableNames = $OctopusParameters[\"GCP.SecretManager.RetrieveSecrets.PrintVariableNames\"] + +# GCP Project/Region/Zone +$Project = $OctopusParameters[\"GCP.SecretManager.RetrieveSecrets.Project\"] +$Region = $OctopusParameters[\"GCP.SecretManager.RetrieveSecrets.Region\"] +$Zone = $OctopusParameters[\"GCP.SecretManager.RetrieveSecrets.Zone\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($SecretNames)) { + throw \"Required parameter GCP.SecretManager.RetrieveSecrets.SecretNames not specified\" +} + +$Secrets = @() +$VariablesCreated = 0 +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +# Extract secret names +@(($SecretNames -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + Write-Verbose \"Working on: '$_'\" + $secretDefinition = ($_ -Split \"\\|\") + $secretName = $secretDefinition[0].Trim() + $secretNameAndVersion = ($secretName -Split \" \") + $secretVersion = \"latest\" + if ($secretNameAndVersion.Count -gt 1) { + $secretName = $secretNameAndVersion[0].Trim() + $secretVersion = $secretNameAndVersion[1].Trim() + } + if ([string]::IsNullOrWhiteSpace($secretName)) { + throw \"Unable to establish secret name from: '$($_)'\" + } + $secret = [PsCustomObject]@{ + Name = $secretName + SecretVersion = $secretVersion + VariableName = if (![string]::IsNullOrWhiteSpace($secretDefinition[1])) { $secretDefinition[1].Trim() } else { \"\" } + } + $Secrets += $secret + } +} + +Write-Verbose \"GCP Default Project: $Project\" +Write-Verbose \"GCP Default Region: $Region\" +Write-Verbose \"GCP Default Zone: $Zone\" +Write-Verbose \"Secrets to retrieve: $($Secrets.Count)\" +Write-Verbose \"Print variables: $PrintVariableNames\" + +# Retrieve Secrets +foreach ($secret in $secrets) { + $name = $secret.Name + $secretVersion = $secret.SecretVersion + $variableName = $secret.VariableName + if ([string]::IsNullOrWhiteSpace($variableName)) { + $variableName = \"$($name.Trim())-$secretVersion\" + } + Write-Host \"Retrieving Secret '$name' (version: $secretVersion)\" + if ($secretVersion -ieq \"latest\") { + Write-Host \"Note: Retrieving the 'latest' version for secret '$name' isn't recommended. Consider choosing a specific version to retrieve.\" + } + + $secretValue = (gcloud secrets versions access $secretVersion --secret=\"$name\") -Join \"`n\" + + if ([string]::IsNullOrWhiteSpace($secretValue)) { + throw \"Error: Secret '$name' (version: $secretVersion) not found or has no versions.\" + } + + Set-OctopusVariable -Name $variableName -Value $secretValue -Sensitive + + if ($PrintVariableNames -eq $True) { + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$variableName}\" + } + $VariablesCreated += 1 +} + +Write-Host \"Created $variablesCreated output variables\" +" + }, + "Parameters": [ + { + "Id": "29f8f138-c6a2-4906-a059-b220c6fd9dd2", + "Name": "GCP.SecretManager.RetrieveSecrets.Account", + "Label": "Google Cloud Account", + "HelpText": "A Google Cloud account with permissions to access secrets from Secret Manager.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "GoogleCloudAccount" + } + }, + { + "Id": "7ab1295b-52dc-432e-ac8f-14c8db41e7dd", + "Name": "GCP.SecretManager.RetrieveSecrets.Project", + "Label": "Google Cloud Project", + "HelpText": "Specify the default project. This sets the `CLOUDSDK_CORE_PROJECT` [environment variable](https://g.octopushq.com/GCPDefaultProject).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c75430a0-7cdd-462a-a302-1c05097b4570", + "Name": "GCP.SecretManager.RetrieveSecrets.Region", + "Label": "Google Cloud Region", + "HelpText": "Specify the default region. View the [GCP Regions and Zones](https://g.octopushq.com/GCPRegionsZones) documentation for a current list of the available region and zone codes. + +This sets the `CLOUDSDK_COMPUTE_REGION` [environment variable](https://g.octopushq.com/GCPDefaultRegionAndZone).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "18a683b7-8bc1-4005-bbef-2a88ab52b659", + "Name": "GCP.SecretManager.RetrieveSecrets.Zone", + "Label": "Google Cloud Zone", + "HelpText": "Specify the default zone. View the [GCP Regions and Zones](https://g.octopushq.com/GCPRegionsZones) documentation for a current list of the available region and zone codes. + +This sets the `CLOUDSDK_COMPUTE_ZONE` [environment variable](https://g.octopushq.com/GCPDefaultRegionAndZone).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "08056d0f-d0a3-42c3-ac4c-612c56b93d21", + "Name": "GCP.SecretManager.RetrieveSecrets.SecretNames", + "Label": "Secret names to retrieve", + "HelpText": "Specify the names of the secrets to be returned from Secret Manager in Google Cloud, in the format: + +`SecretName SecretVersion | OutputVariableName` where: + +- `SecretName` is the name of the secret to retrieve. +- `SecretVersion` is the version of the secret to retrieve. *If this value isn't specified, the latest version will be retrieved*. +- `OutputVariableName` is the _optional_ Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) name to store the secret's value in. *If this value isn't specified, an output name will be generated dynamically*. + +**Note:** Multiple fields can be retrieved by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "517e1f61-418e-4e97-9d07-54cac58f03a2", + "Name": "GCP.SecretManager.RetrieveSecrets.PrintVariableNames", + "Label": "Print output variable names", + "HelpText": "Write out the Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) names to the task log. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2023-05-01T16:37:53.788Z", + "OctopusVersion": "2023.2.9229", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "google-cloud", + "MinimumServerVersion": "2021.2.0" +} diff --git a/step-templates/ghostinspector-runsuite.json.human b/step-templates/ghostinspector-runsuite.json.human new file mode 100644 index 000000000..9f7d1b93f --- /dev/null +++ b/step-templates/ghostinspector-runsuite.json.human @@ -0,0 +1,135 @@ +{ + "Id": "8b84e760-a6ca-412c-9b83-1129ca239f32", + "Name": "Run GhostInspector", + "Description": "Runs GhostInspector smoke tests asynchronously, returning immediately", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "\r +$ErrorActionPreference = \"Stop\" \r +\r +function Get-Param($Name, [switch]$Required, $Default) {\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + $variable = Get-Variable $Name -EA SilentlyContinue \r + if ($variable -ne $null) {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null -or $result -eq \"\") {\r + if ($Required) {\r + throw \"Missing parameter value $Name\"\r + } else {\r + $result = $Default\r + }\r + }\r +\r + return $result\r +}\r +\r +& {\r + param(\r +\t\t[string]$suiteId,\r +\t\t[string]$apiKey,\r +\t\t[string]$siteUrl,\r +\t\t[string]$httpAuthUser,\r +\t\t[string]$httpAuthPass\r + ) \r +\r +\t$apiUrl = \"https://api.ghostinspector.com/v1/suites/$suiteId/execute/?immediate=1&apiKey=\" + $apiKey\r +\r +\tif(-! ([string]::IsNullOrEmpty($siteUrl)))\r +\t{\r +\t\t$apiUrl = $apiUrl + '&startUrl=' + $siteUrl\r +\t}\r +\t\r +\tif(-! ([string]::IsNullOrEmpty($httpAuthUser) -and [string]::IsNullOrEmpty($httpAuthPass)))\r +\t{\r +\t\t$apiUrl = $apiUrl + '&httpAuthUsername=' + $httpAuthUser + '&httpAuthPassword=' + $httpAuthPass\r +\t}\r +\r +\tWrite-Output \"Invoking API url: $apiUrl\" \r +\t\r + try {\r +\t\tInvoke-WebRequest $apiUrl -UseBasicParsing\r + } catch [Exception] {\r + Write-Host \"There was a problem invoking Url\"\r + Write-Host $_.Exception|format-list -force;\r + }\r + Write-Output $(\"Test Output can be viewed here: https://app.ghostinspector.com/suites/{0} -f $suiteId\")\r +\r + } `\r + (Get-Param 'suiteId' -Required) `\r + (Get-Param 'apiKey' - Required) `\r + (Get-Param 'siteUrl') `\r + (Get-Param 'httpAuthUser') `\r + (Get-Param 'httpAuthPass')", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "suiteId", + "Label": "Suite ID", + "HelpText": "Suite ID from Ghostinspector. Can be found in the suite's URL e.g.: +https://app.ghostinspector.com/suites/", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "siteUrl", + "Label": "Site Base URL", + "HelpText": "The site's base URL. All smoke tests will be run starting from this page.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "httpAuthUser", + "Label": "Username", + "HelpText": "The username for use with HTTP Auth (can be left blank for open sites)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "httpAuthPass", + "Label": "Password", + "HelpText": "The password for use with HTTP Auth (can be left blank for open sites)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "apiKey", + "Label": "GhostInspector API Key", + "HelpText": "GhostInspector API key. Can be found here: https://app.ghostinspector.com/account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2016-06-20T06:48:33.000+00:00", + "OctopusVersion": "3.3.11", + "Type": "ActionTemplate" + }, + "Category": "ghostinspector" +} diff --git a/step-templates/git-clone-copy-push-another-repo.json.human b/step-templates/git-clone-copy-push-another-repo.json.human new file mode 100644 index 000000000..d24f41721 --- /dev/null +++ b/step-templates/git-clone-copy-push-another-repo.json.human @@ -0,0 +1,387 @@ +{ + "Id": "6db15b08-f6c6-4a6e-833c-773eb38ec0f0", + "Name": "Git - Clone, copy, push to another repo", + "Description": "Clones both a source and destination repository, copies files from the `Source Path` to the `Destination Path` then commits to the destination repository.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Function Invoke-Git +{ +\t# Define parameters + param ( + \t$GitRepositoryUrl, + $GitFolder, + $GitUsername, + $GitPassword, + $GitCommand, + $AdditionalArguments + ) + + # Get current work folder + $workDirectory = Get-Location + +\t# Check to see if GitFolder exists + if (![String]::IsNullOrWhitespace($GitFolder) -and (Test-Path -Path $GitFolder) -eq $false) + { + \t# Create the folder + New-Item -Path $GitFolder -ItemType \"Directory\" -Force | Out-Null + + # Set the location to the new folder + Set-Location -Path $GitFolder + } + + # Create arguments array + $gitArguments = @() + $gitArguments += $GitCommand + + # Check for url + if (![string]::IsNullOrWhitespace($GitRepositoryUrl)) + { + # Convert url to URI object + $gitUri = [System.Uri]$GitRepositoryUrl + $gitUrl = \"{0}://{1}:{2}@{3}:{4}{5}\" -f $gitUri.Scheme, $GitUsername, $GitPassword, $gitUri.Host, $gitUri.Port, $gitUri.PathAndQuery + $gitArguments += $gitUrl + + # Get the newly created folder name + $gitFolderName = $GitRepositoryUrl.SubString($GitRepositoryUrl.LastIndexOf(\"/\") + 1) + if ($gitFolderName.Contains(\".git\")) + { + $gitFolderName = $gitFolderName.SubString(0, $gitFolderName.IndexOf(\".\")) + } + } + + + # Check for additional arguments + if ($null -ne $AdditionalArguments) + { + \t\t# Add the additional arguments + $gitArguments += $AdditionalArguments + } + + # Execute git command + $results = Execute-Command -commandPath \"git\" -commandArguments $gitArguments -workingDir $GitFolder + + Write-Host $results.stdout + Write-Host $results.stderr + + # Return the foldername + Set-Location -Path $workDirectory + + # Check to see if GitFolder is null + if ($null -ne $GitFolder) + { + \treturn Join-Path -Path $GitFolder -ChildPath $gitFolderName + } +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +Function Copy-Files +{ +\t# Define parameters + param ( + \t$SourcePath, + $DestinationPath + ) + + # Copy the items from source path to destination path + $copyArguments = @{} + $copyArguments.Add(\"Path\", $SourcePath) + $copyArguments.Add(\"Destination\", $DestinationPath) + + # Check to make sure destination exists + if ((Test-Path -Path $DestinationPath) -eq $false) + { + \t# Create the destination path + New-Item -Path $DestinationPath -ItemType \"Directory\" | Out-Null + } + + # Check for wildcard + if ($SourcePath.EndsWith(\"/*\") -or $SourcePath.EndsWith(\"\\*\")) + { +\t\t# Add recurse argument +\t\t$copyArguments.Add(\"Recurse\", $true) + } + + # Force overwrite + $copyArguments.Add(\"Force\", $true) + + # Copy files + Copy-Item @copyArguments +} + +Function Execute-Command +{ +\tparam ( + \t$commandPath, + $commandArguments, + $workingDir + ) + +\t$gitExitCode = 0 + $executionResults = $null + + Try { + $pinfo = New-Object System.Diagnostics.ProcessStartInfo + $pinfo.FileName = $commandPath + $pinfo.WorkingDirectory = $workingDir + $pinfo.RedirectStandardError = $true + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + $pinfo.Arguments = $commandArguments + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $pinfo + $p.Start() | Out-Null + $executionResults = [pscustomobject]@{ + stdout = $p.StandardOutput.ReadToEnd() + stderr = $p.StandardError.ReadToEnd() + ExitCode = $null + } + $p.WaitForExit() + $gitExitCode = [int]$p.ExitCode + $executionResults.ExitCode = $gitExitCode + + if ($gitExitCode -ge 2) + { +\t\t# Fail the step + throw + } + + return $executionResults + } + Catch { + # Check exit code + Write-Error -Message \"$($executionResults.stderr)\" -ErrorId $gitExitCode + exit $gitExitCode + } + +} + +Function Get-GitExecutable +{ +\t# Define parameters + param ( + \t$WorkingDirectory + ) + + # Define variables + $gitExe = \"PortableGit-2.41.0.3-64-bit.7z.exe\" + $gitDownloadUrl = \"https://github.com/git-for-windows/git/releases/download/v2.41.0.windows.3/$gitExe\" + $gitDownloadArguments = @{} + $gitDownloadArguments.Add(\"Uri\", $gitDownloadUrl) + $gitDownloadArguments.Add(\"OutFile\", \"$WorkingDirectory/git/$gitExe\") + + # This makes downloading faster + $ProgressPreference = 'SilentlyContinue' + + # Check to see if git subfolder exists + if ((Test-Path -Path \"$WorkingDirectory/git\") -eq $false) + { + \t# Create subfolder + New-Item -Path \"$WorkingDirectory/git\" -ItemType Directory + } + + # Check PowerShell version + if ($PSVersionTable.PSVersion.Major -lt 6) + { + \t# Use basic parsing is required + $gitDownloadArguments.Add(\"UseBasicParsing\", $true) + } + + # Download Git + Write-Host \"Downloading Git ...\" + Invoke-WebRequest @gitDownloadArguments + + # Extract Git + $gitExtractArguments = @() + $gitExtractArguments += \"-o\" + $gitExtractArguments += \"$WorkingDirectory\\git\" + $gitExtractArguments += \"-y\" + $gitExtractArguments += \"-bd\" + + Write-Host \"Extracting Git download ...\" + & \"$WorkingDirectory\\git\\$gitExe\" $gitExtractArguments + + # Wait until unzip action is complete + while ($null -ne (Get-Process | Where-Object {$_.ProcessName -eq ($gitExe.Substring(0, $gitExe.LastIndexOf(\".\")))})) + { + Start-Sleep 5 + } + + # Add bin folder to path + $env:PATH = \"$WorkingDirectory\\git\\bin$([IO.Path]::PathSeparator)\" + $env:PATH + + # Disable promopt for credential helper + Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"--system\", \"--unset\", \"credential.helper\") +} + +# Get variables +$sourceGitUrl = $OctopusParameters['Template.Git.Source.Repo.Url'] +$destinationGitUrl = $OctopusParameters['Template.Git.Destination.Repo.Url'] +$gitSourceUser = $OctopusParameters['Template.Git.Source.User.Name'] +$gitSourcePassword = $OctopusParameters['Template.Git.Source.User.Password'] +$gitDestinationUser = $OctopusParameters['Template.Git.Destination.User.Name'] +$gitDestinationPassword = $OctopusParameters['Template.Git.Destination.User.Password'] +$sourceItems = $OctopusParameters['Template.Git.Source.Path'] +$destinationPath = $OctopusParameters['Template.Git.Destination.Path'] +$gitTag = $OctopusParameters['Template.Git.Tag'] +$gitSource = $null +$gitDestination = $null + +# Check to see if it's Windows +if ($IsWindows -and $OctopusParameters['Octopus.Workerpool.Name'] -eq \"Hosted Windows\") +{ +\t# Dynamic worker don't have git, download portable version and add to path for execution + Write-Host \"Detected usage of Windows Dynamic Worker ...\" + Get-GitExecutable -WorkingDirectory $PWD +} + +# Clone destination repository +$destinationFolderName = Invoke-Git -GitRepositoryUrl $destinationGitUrl -GitUsername $gitDestinationUser -GitPassword $gitDestinationPassword -GitCommand \"clone\" -GitFolder \"$($PWD)/destination/default\" + +# Check for tag +if (![String]::IsNullOrWhitespace($gitTag)) +{ + $sourceFolderName = Invoke-Git -GitRepositoryUrl $sourceGitUrl -GitUsername $gitSourceUser -GitPassword $gitSourcePassword -GitCommand \"clone\" -GitFolder \"$($PWD)/source/tags/$gitTag\" -AdditionalArguments @(\"-b\", \"$gitTag\") +} +else +{ +\t$sourceFolderName = Invoke-Git -GitRepositoryUrl $sourceGitUrl -GitUsername $gitSourceUser -GitPassword $gitSourcePassword -GitCommand \"clone\" -GitFolder \"$($PWD)/source/default\" +} + +# Copy files from source to destination +Copy-Files -SourcePath \"$($sourceFolderName)$($sourceItems)\" -DestinationPath \"$($destinationFolderName)$($destinationPath)\" + +# Set user +$gitAuthorName = $OctopusParameters['Octopus.Deployment.CreatedBy.DisplayName'] +$gitAuthorEmail = $OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress'] + +# Check to see if user is system +if ([string]::IsNullOrWhitespace($gitAuthorEmail) -and $gitAuthorName -eq \"System\") +{ +\t# Initiated by the Octopus server via automated process, put something in for the email address + $gitAuthorEmail = \"system@octopus.local\" +} + +Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"user.name\", $gitAuthorName) -GitFolder \"$($destinationFolderName)\" | Out-Null +Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"user.email\", $gitAuthorEmail) -GitFolder \"$($destinationFolderName)\" | Out-Null + +# Commit changes +Invoke-Git -GitCommand \"add\" -GitFolder \"$destinationFolderName\" -AdditionalArguments @(\".\") | Out-Null +Invoke-Git -GitCommand \"commit\" -GitFolder \"$destinationFolderName\" -AdditionalArguments @(\"-m\", \"`\"Commit from #{Octopus.Project.Name} release version #{Octopus.Release.Number}`\"\") | Out-Null + +# Push the changes back to git +Invoke-Git -GitCommand \"push\" -GitFolder \"$destinationFolderName\" | Out-Null + +" + }, + "Parameters": [ + { + "Id": "674d5325-aa93-4779-a734-cee8e5690f17", + "Name": "Template.Git.Source.Repo.Url", + "Label": "Source Git Repository URL", + "HelpText": "The URL used for the `git clone` operation.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f2d07c9c-85fc-485e-8057-5577efd9a26d", + "Name": "Template.Git.Source.User.Name", + "Label": "Source Git Username", + "HelpText": "Username of the credentials to use to log into git.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "597ae9a5-1ef4-4062-a435-2d9bb1fb16a2", + "Name": "Template.Git.Source.User.Password", + "Label": "Source Git User Password", + "HelpText": "Password for the git credential.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "5c080713-029f-4f9b-8037-234c7dd579bc", + "Name": "Template.Git.Source.Path", + "Label": "Source Path", + "HelpText": "Relative path to the folder or items to copy. This field can take wildcards, eg - `/MyPath/*`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5fa438c9-2aa7-476f-ae79-bd77cdc22ccc", + "Name": "Template.Git.Tag", + "Label": "Tag", + "HelpText": "**(Optional)** Checkout the code for `SourcePath` from a specific tag.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6d3f8f4f-8e02-48bc-b473-817693aaf835", + "Name": "Template.Git.Destination.Repo.Url", + "Label": "Destination Git Repository URL", + "HelpText": "The destination repository to copy and push files to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "59590dd8-6698-46f8-884c-78ad9f4b7c2e", + "Name": "Template.Git.Destination.User.Name", + "Label": "Destination Git Username", + "HelpText": "Username of the credentials to log into destination git repo.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d81e0a3d-5eb9-4dd3-a197-e5bf0c48cdf1", + "Name": "Template.Git.Destination.User.Password", + "Label": "Destination Git User Password", + "HelpText": "Password for the destination git credential.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "f9a2141a-bcfa-4e53-862b-429b4f9892d9", + "Name": "Template.Git.Destination.Path", + "Label": "Destination Path", + "HelpText": "Relative path to the folder to copy items to. This is the folder name only.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-09-12T15:32:07.199Z", + "OctopusVersion": "2023.4.2661", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "git" +} diff --git a/step-templates/git-clone-copy-push-package.json.human b/step-templates/git-clone-copy-push-package.json.human new file mode 100644 index 000000000..f7db70d2d --- /dev/null +++ b/step-templates/git-clone-copy-push-package.json.human @@ -0,0 +1,361 @@ +{ + "Id": "4d541683-4731-4f43-a342-5b9f6c915c4d", + "Name": "Git - Clone, copy, push from a Package", + "Description": "Clones a repository, copies files from the `Source Package` to the `Destination Path` then commits to the repository.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "88b1ab11-c878-4c7f-8227-610c3784311b", + "Name": "Template.Package.Reference", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "Template.Package.Reference", + "Purpose": "" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Function Invoke-Git +{ +\t# Define parameters + param ( + \t$GitRepositoryUrl, + $GitFolder, + $GitUsername, + $GitPassword, + $GitCommand, + $AdditionalArguments + ) + + # Get current work folder + $workDirectory = Get-Location + +\t# Check to see if GitFolder exists + if (![String]::IsNullOrWhitespace($GitFolder) -and (Test-Path -Path $GitFolder) -eq $false) + { + \t# Create the folder + New-Item -Path $GitFolder -ItemType \"Directory\" -Force | Out-Null + + # Set the location to the new folder + Set-Location -Path $GitFolder + } + + # Create arguments array + $gitArguments = @() + $gitArguments += $GitCommand + + # Check for url + if (![string]::IsNullOrWhitespace($GitRepositoryUrl)) + { + # Convert url to URI object + $gitUri = [System.Uri]$GitRepositoryUrl + $gitUrl = \"{0}://{1}:{2}@{3}:{4}{5}\" -f $gitUri.Scheme, $GitUsername, $GitPassword, $gitUri.Host, $gitUri.Port, $gitUri.PathAndQuery + $gitArguments += $gitUrl + + # Get the newly created folder name + $gitFolderName = $GitRepositoryUrl.SubString($GitRepositoryUrl.LastIndexOf(\"/\") + 1) + if ($gitFolderName.Contains(\".git\")) + { + $gitFolderName = $gitFolderName.SubString(0, $gitFolderName.IndexOf(\".\")) + } + } + + + # Check for additional arguments + if ($null -ne $AdditionalArguments) + { + \t\t# Add the additional arguments + $gitArguments += $AdditionalArguments + } + + # Execute git command + $results = Execute-Command -commandPath \"git\" -commandArguments $gitArguments -workingDir $GitFolder + + Write-Host $results.stdout + Write-Host $results.stderr + + # Return the foldername + Set-Location -Path $workDirectory + + # Check to see if GitFolder is null + if ($null -ne $GitFolder) + { + \treturn Join-Path -Path $GitFolder -ChildPath $gitFolderName + } +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +Function Copy-Files +{ +\t# Define parameters + param ( + \t$SourcePath, + $DestinationPath + ) + + # Copy the items from source path to destination path + $copyArguments = @{} + $copyArguments.Add(\"Path\", $SourcePath) + $copyArguments.Add(\"Destination\", $DestinationPath) + + # Check to make sure destination exists + if ((Test-Path -Path $DestinationPath) -eq $false) + { + \t# Create the destination path + New-Item -Path $DestinationPath -ItemType \"Directory\" | Out-Null + } + + # Check for wildcard + if ($SourcePath.EndsWith(\"/*\") -or $SourcePath.EndsWith(\"\\*\")) + { +\t\t# Add recurse argument +\t\t$copyArguments.Add(\"Recurse\", $true) + } + + $copyArguments.Add(\"Force\", $true) + + # Copy files + Copy-Item @copyArguments +} + +Function Execute-Command +{ +\tparam ( + \t$commandPath, + $commandArguments, + $workingDir + ) + +\t$gitExitCode = 0 + $executionResults = $null + + Try { + $pinfo = New-Object System.Diagnostics.ProcessStartInfo + $pinfo.FileName = $commandPath + $pinfo.WorkingDirectory = $workingDir + $pinfo.RedirectStandardError = $true + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + $pinfo.Arguments = $commandArguments + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $pinfo + $p.Start() | Out-Null + $executionResults = [pscustomobject]@{ + stdout = $p.StandardOutput.ReadToEnd() + stderr = $p.StandardError.ReadToEnd() + ExitCode = $null + } + $p.WaitForExit() + $gitExitCode = [int]$p.ExitCode + $executionResults.ExitCode = $gitExitCode + + if ($gitExitCode -ge 2) + { +\t\t# Fail the step + throw + } + + return $executionResults + } + Catch { + # Check exit code + Write-Error -Message \"$($executionResults.stderr)\" -ErrorId $gitExitCode + exit $gitExitCode + } + +} + +Function Get-GitExecutable +{ +\t# Define parameters + param ( + \t$WorkingDirectory + ) + + # Define variables + $gitExe = \"PortableGit-2.41.0.3-64-bit.7z.exe\" + $gitDownloadUrl = \"https://github.com/git-for-windows/git/releases/download/v2.41.0.windows.3/$gitExe\" + $gitDownloadArguments = @{} + $gitDownloadArguments.Add(\"Uri\", $gitDownloadUrl) + $gitDownloadArguments.Add(\"OutFile\", \"$WorkingDirectory/git/$gitExe\") + + # This makes downloading faster + $ProgressPreference = 'SilentlyContinue' + + # Check to see if git subfolder exists + if ((Test-Path -Path \"$WorkingDirectory/git\") -eq $false) + { + \t# Create subfolder + New-Item -Path \"$WorkingDirectory/git\" -ItemType Directory + } + + # Check PowerShell version + if ($PSVersionTable.PSVersion.Major -lt 6) + { + \t# Use basic parsing is required + $gitDownloadArguments.Add(\"UseBasicParsing\", $true) + } + + # Download Git + Write-Host \"Downloading Git ...\" + Invoke-WebRequest @gitDownloadArguments + + # Extract Git + $gitExtractArguments = @() + $gitExtractArguments += \"-o\" + $gitExtractArguments += \"$WorkingDirectory\\git\" + $gitExtractArguments += \"-y\" + $gitExtractArguments += \"-bd\" + + Write-Host \"Extracting Git download ...\" + & \"$WorkingDirectory\\git\\$gitExe\" $gitExtractArguments + + # Wait until unzip action is complete + while ($null -ne (Get-Process | Where-Object {$_.ProcessName -eq ($gitExe.Substring(0, $gitExe.LastIndexOf(\".\")))})) + { + Start-Sleep 5 + } + + # Add bin folder to path + $env:PATH = \"$WorkingDirectory\\git\\bin$([IO.Path]::PathSeparator)\" + $env:PATH + + # Disable promopt for credential helper + Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"--system\", \"--unset\", \"credential.helper\") +} + +# Get variables +$gitUrl = $OctopusParameters['Template.Git.Repo.Url'] +$gitUser = $OctopusParameters['Template.Git.User.Name'] +$gitPassword = $OctopusParameters['Template.Git.User.Password'] +$sourceItems = $OctopusParameters['Octopus.Action.Package[Template.Package.Reference].ExtractedPath'] +$destinationPath = $OctopusParameters['Template.Git.Destination.Path'] +$gitSource = $null +$gitDestination = $null + +# Check to see if it's Windows +if ($IsWindows -and $OctopusParameters['Octopus.Workerpool.Name'] -eq \"Hosted Windows\") +{ +\t# Dynamic worker don't have git, download portable version and add to path for execution + Write-Host \"Detected usage of Windows Dynamic Worker ...\" + Get-GitExecutable -WorkingDirectory $PWD +} + +# Clone repository +$folderName = Invoke-Git -GitRepositoryUrl $gitUrl -GitUsername $gitUser -GitPassword $gitPassword -GitCommand \"clone\" -GitFolder \"$($PWD)/default\" + +$gitSource = $sourceItems +$gitDestination = $folderName + +# Copy files from source to destination +Copy-Files -SourcePath \"$($gitSource)/*\" -DestinationPath \"$($gitDestination)$($destinationPath)\" + +# Set user +$gitAuthorName = $OctopusParameters['Octopus.Deployment.CreatedBy.DisplayName'] +$gitAuthorEmail = $OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress'] + +# Check to see if user is system +if ([string]::IsNullOrWhitespace($gitAuthorEmail) -and $gitAuthorName -eq \"System\") +{ +\t# Initiated by the Octopus server via automated process, put something in for the email address + $gitAuthorEmail = \"system@octopus.local\" +} + +Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"user.name\", $gitAuthorName) -GitFolder \"$($folderName)\" | Out-Null +Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"user.email\", $gitAuthorEmail) -GitFolder \"$($folderName)\" | Out-Null + +# Commit changes +Invoke-Git -GitCommand \"add\" -GitFolder \"$folderName\" -AdditionalArguments @(\".\") | Out-Null +Invoke-Git -GitCommand \"commit\" -GitFolder \"$folderName\" -AdditionalArguments @(\"-m\", \"`\"Commit from #{Octopus.Project.Name} release version #{Octopus.Release.Number}`\"\") | Out-Null + +# Push the changes back to git +Invoke-Git -GitCommand \"push\" -GitFolder \"$folderName\" | Out-Null + +", + "Octopus.Action.EnabledFeatures": "Octopus.Features.JsonConfigurationVariables", + "Octopus.Action.Package.JsonConfigurationVariablesTargets": "#{Template.VariableReplacement.Paths}" + }, + "Parameters": [ + { + "Id": "674d5325-aa93-4779-a734-cee8e5690f17", + "Name": "Template.Git.Repo.Url", + "Label": "Git Repository URL", + "HelpText": "The URL used for the `git clone` operation.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f2d07c9c-85fc-485e-8057-5577efd9a26d", + "Name": "Template.Git.User.Name", + "Label": "Git Username", + "HelpText": "Username of the credentials to use to log into git.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "597ae9a5-1ef4-4062-a435-2d9bb1fb16a2", + "Name": "Template.Git.User.Password", + "Label": "Git User Password", + "HelpText": "Password for the git credential.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "6d6b0662-22bd-4fbc-b0dd-15799b158abe", + "Name": "Template.Package.Reference", + "Label": "Package", + "HelpText": "Choose the package to copy files from into the Git repository.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "f9a2141a-bcfa-4e53-862b-429b4f9892d9", + "Name": "Template.Git.Destination.Path", + "Label": "Destination Path", + "HelpText": "Relative path to the folder to copy items to. This is the folder name only.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e382b0db-0ea2-47bb-968b-2dae9a8b861d", + "Name": "Template.VariableReplacement.Paths", + "Label": "Structured Configuration Variables", + "HelpText": "Target files need to be new line seperated, relative to the package contents. Extended wildcard syntax is supported. E.g., appsettings.json, Config\\*.xml, **\\specific-folder\\*.yaml. Learn more about Structured Configuration Variables and view examples.", + "DefaultValue": " ", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-09-12T15:31:36.900Z", + "OctopusVersion": "2023.4.2661", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "git" +} diff --git a/step-templates/git-clone-copy-push.json.human b/step-templates/git-clone-copy-push.json.human new file mode 100644 index 000000000..6492340bb --- /dev/null +++ b/step-templates/git-clone-copy-push.json.human @@ -0,0 +1,356 @@ +{ + "Id": "492dd5fe-26f5-4d12-800d-ad2cce008de6", + "Name": "Git - Clone, copy, push", + "Description": "Clones a repository, copies files from the `Source Path` to the `Destination Path` then commits to the repository.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Function Invoke-Git +{ +\t# Define parameters + param ( + \t$GitRepositoryUrl, + $GitFolder, + $GitUsername, + $GitPassword, + $GitCommand, + $AdditionalArguments + ) + + # Get current work folder + $workDirectory = Get-Location + +\t# Check to see if GitFolder exists + if (![String]::IsNullOrWhitespace($GitFolder) -and (Test-Path -Path $GitFolder) -eq $false) + { + \t# Create the folder + New-Item -Path $GitFolder -ItemType \"Directory\" -Force | Out-Null + + # Set the location to the new folder + Set-Location -Path $GitFolder + } + + # Create arguments array + $gitArguments = @() + $gitArguments += $GitCommand + + # Check for url + if (![string]::IsNullOrWhitespace($GitRepositoryUrl)) + { + # Convert url to URI object + $gitUri = [System.Uri]$GitRepositoryUrl + $gitUrl = \"{0}://{1}:{2}@{3}:{4}{5}\" -f $gitUri.Scheme, $GitUsername, $GitPassword, $gitUri.Host, $gitUri.Port, $gitUri.PathAndQuery + $gitArguments += $gitUrl + + # Get the newly created folder name + $gitFolderName = $GitRepositoryUrl.SubString($GitRepositoryUrl.LastIndexOf(\"/\") + 1) + if ($gitFolderName.Contains(\".git\")) + { + $gitFolderName = $gitFolderName.SubString(0, $gitFolderName.IndexOf(\".\")) + } + } + + + # Check for additional arguments + if ($null -ne $AdditionalArguments) + { + \t\t# Add the additional arguments + $gitArguments += $AdditionalArguments + } + + # Execute git command + $results = Execute-Command -commandPath \"git\" -commandArguments $gitArguments -workingDir $GitFolder + + Write-Host $results.stdout + Write-Host $results.stderr + + # Return the foldername + Set-Location -Path $workDirectory + + # Check to see if GitFolder is null + if ($null -ne $GitFolder) + { + \treturn Join-Path -Path $GitFolder -ChildPath $gitFolderName + } +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +Function Copy-Files +{ +\t# Define parameters + param ( + \t$SourcePath, + $DestinationPath + ) + + # Copy the items from source path to destination path + $copyArguments = @{} + $copyArguments.Add(\"Path\", $SourcePath) + $copyArguments.Add(\"Destination\", $DestinationPath) + + # Check to make sure destination exists + if ((Test-Path -Path $DestinationPath) -eq $false) + { + \t# Create the destination path + New-Item -Path $DestinationPath -ItemType \"Directory\" | Out-Null + } + + # Check for wildcard + if ($SourcePath.EndsWith(\"/*\") -or $SourcePath.EndsWith(\"\\*\")) + { +\t\t# Add recurse argument +\t\t$copyArguments.Add(\"Recurse\", $true) + } + + # Force overwrite + $copyArguments.Add(\"Force\", $true) + + # Copy files + Copy-Item @copyArguments +} + +Function Execute-Command +{ +\tparam ( + \t$commandPath, + $commandArguments, + $workingDir + ) + +\t$gitExitCode = 0 + $executionResults = $null + + Try { + $pinfo = New-Object System.Diagnostics.ProcessStartInfo + $pinfo.FileName = $commandPath + $pinfo.WorkingDirectory = $workingDir + $pinfo.RedirectStandardError = $true + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + $pinfo.Arguments = $commandArguments + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $pinfo + $p.Start() | Out-Null + $executionResults = [pscustomobject]@{ + stdout = $p.StandardOutput.ReadToEnd() + stderr = $p.StandardError.ReadToEnd() + ExitCode = $null + } + $p.WaitForExit() + $gitExitCode = [int]$p.ExitCode + $executionResults.ExitCode = $gitExitCode + + if ($gitExitCode -ge 2) + { +\t\t# Fail the step + throw + } + + return $executionResults + } + Catch { + # Check exit code + Write-Error -Message \"$($executionResults.stderr)\" -ErrorId $gitExitCode + exit $gitExitCode + } + +} + +Function Get-GitExecutable +{ +\t# Define parameters + param ( + \t$WorkingDirectory + ) + + # Define variables + $gitExe = \"PortableGit-2.41.0.3-64-bit.7z.exe\" + $gitDownloadUrl = \"https://github.com/git-for-windows/git/releases/download/v2.41.0.windows.3/$gitExe\" + $gitDownloadArguments = @{} + $gitDownloadArguments.Add(\"Uri\", $gitDownloadUrl) + $gitDownloadArguments.Add(\"OutFile\", \"$WorkingDirectory/git/$gitExe\") + + # This makes downloading faster + $ProgressPreference = 'SilentlyContinue' + + # Check to see if git subfolder exists + if ((Test-Path -Path \"$WorkingDirectory/git\") -eq $false) + { + \t# Create subfolder + New-Item -Path \"$WorkingDirectory/git\" -ItemType Directory + } + + # Check PowerShell version + if ($PSVersionTable.PSVersion.Major -lt 6) + { + \t# Use basic parsing is required + $gitDownloadArguments.Add(\"UseBasicParsing\", $true) + } + + # Download Git + Write-Host \"Downloading Git ...\" + Invoke-WebRequest @gitDownloadArguments + + # Extract Git + $gitExtractArguments = @() + $gitExtractArguments += \"-o\" + $gitExtractArguments += \"$WorkingDirectory\\git\" + $gitExtractArguments += \"-y\" + $gitExtractArguments += \"-bd\" + + Write-Host \"Extracting Git download ...\" + & \"$WorkingDirectory\\git\\$gitExe\" $gitExtractArguments + + # Wait until unzip action is complete + while ($null -ne (Get-Process | Where-Object {$_.ProcessName -eq ($gitExe.Substring(0, $gitExe.LastIndexOf(\".\")))})) + { + Start-Sleep 5 + } + + # Add bin folder to path + $env:PATH = \"$WorkingDirectory\\git\\bin$([IO.Path]::PathSeparator)\" + $env:PATH + + # Disable promopt for credential helper + Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"--system\", \"--unset\", \"credential.helper\") +} + +# Get variables +$gitUrl = $OctopusParameters['Template.Git.Repo.Url'] +$gitUser = $OctopusParameters['Template.Git.User.Name'] +$gitPassword = $OctopusParameters['Template.Git.User.Password'] +$sourceItems = $OctopusParameters['Template.Git.Source.Path'] +$destinationPath = $OctopusParameters['Template.Git.Destination.Path'] +$gitTag = $OctopusParameters['Template.Git.Tag'] +$gitSource = $null +$gitDestination = $null + +# Check to see if it's Windows +if ($IsWindows -and $OctopusParameters['Octopus.Workerpool.Name'] -eq \"Hosted Windows\") +{ +\t# Dynamic worker don't have git, download portable version and add to path for execution + Write-Host \"Detected usage of Windows Dynamic Worker ...\" + Get-GitExecutable -WorkingDirectory $PWD +} + +# Clone repository +$folderName = Invoke-Git -GitRepositoryUrl $gitUrl -GitUsername $gitUser -GitPassword $gitPassword -GitCommand \"clone\" -GitFolder \"$($PWD)/default\" + +# Check for tag +if (![String]::IsNullOrWhitespace($gitTag)) +{ +\t$gitDestination = $folderName + $gitSource = Invoke-Git -GitRepositoryUrl $gitUrl -GitUsername $gitUser -GitPassword $gitPassword -GitCommand \"clone\" -GitFolder \"$($PWD)/tags/$gitTag\" -AdditionalArguments @(\"-b\", \"$gitTag\") +} +else +{ +\t$gitSource = $folderName + $gitDestination = $folderName +} + +# Copy files from source to destination +Copy-Files -SourcePath \"$($gitSource)$($sourceItems)\" -DestinationPath \"$($gitDestination)$($destinationPath)\" + +# Set user +$gitAuthorName = $OctopusParameters['Octopus.Deployment.CreatedBy.DisplayName'] +$gitAuthorEmail = $OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress'] + +# Check to see if user is system +if ([string]::IsNullOrWhitespace($gitAuthorEmail) -and $gitAuthorName -eq \"System\") +{ +\t# Initiated by the Octopus server via automated process, put something in for the email address + $gitAuthorEmail = \"system@octopus.local\" +} + +Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"user.name\", $gitAuthorName) -GitFolder \"$($folderName)\" | Out-Null +Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"user.email\", $gitAuthorEmail) -GitFolder \"$($folderName)\" | Out-Null + +# Commit changes +Invoke-Git -GitCommand \"add\" -GitFolder \"$folderName\" -AdditionalArguments @(\".\") | Out-Null +Invoke-Git -GitCommand \"commit\" -GitFolder \"$folderName\" -AdditionalArguments @(\"-m\", \"`\"Commit from #{Octopus.Project.Name} release version #{Octopus.Release.Number}`\"\") | Out-Null + +# Push the changes back to git +Invoke-Git -GitCommand \"push\" -GitFolder \"$folderName\" | Out-Null + +" + }, + "Parameters": [ + { + "Id": "674d5325-aa93-4779-a734-cee8e5690f17", + "Name": "Template.Git.Repo.Url", + "Label": "Git Repository URL", + "HelpText": "The URL used for the `git clone` operation.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f2d07c9c-85fc-485e-8057-5577efd9a26d", + "Name": "Template.Git.User.Name", + "Label": "Git Username", + "HelpText": "Username of the credentials to use to log into git.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "597ae9a5-1ef4-4062-a435-2d9bb1fb16a2", + "Name": "Template.Git.User.Password", + "Label": "Git User Password", + "HelpText": "Password for the git credential.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "5c080713-029f-4f9b-8037-234c7dd579bc", + "Name": "Template.Git.Source.Path", + "Label": "Source Path", + "HelpText": "Relative path to the folder or items to copy. This field can take wildcards, eg - `/MyPath/*`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f9a2141a-bcfa-4e53-862b-429b4f9892d9", + "Name": "Template.Git.Destination.Path", + "Label": "Destination Path", + "HelpText": "Relative path to the folder to copy items to. This is the folder name only.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5fa438c9-2aa7-476f-ae79-bd77cdc22ccc", + "Name": "Template.Git.Tag", + "Label": "Tag", + "HelpText": "**(Optional)** Checkout the code for `SourcePath` from a specific tag.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-09-12T15:29:34.628Z", + "OctopusVersion": "2023.4.2661", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "git" +} diff --git a/step-templates/git-create-pull-request.json.human b/step-templates/git-create-pull-request.json.human new file mode 100644 index 000000000..254f6200e --- /dev/null +++ b/step-templates/git-create-pull-request.json.human @@ -0,0 +1,265 @@ +{ + "Id": "a24c6354-5612-4e2c-a0ff-9b5a329fc0e9", + "Name": "Git - Create Pull Request", + "Description": "Create a Pull or Merge Request for the repository", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +# Fix ANSI Color on PWSH Core issues when displaying objects +if ($PSEdition -eq \"Core\") { + $PSStyle.OutputRendering = \"PlainText\" +} + +# Get variables +$gitUrl = $OctopusParameters['Template.Git.Repo.Url'] +$gitUser = $OctopusParameters['Template.Git.User.Name'] +$gitPassword = $OctopusParameters['Template.Git.User.Password'] +$gitSourceBranch = $OctopusParameters['Template.Git.Source.Branch'] +$gitDestinationBranch = $OctopusParameters['Template.Git.Destination.Branch'] +$gitTech = $OctopusParameters['Template.Git.Repository.Technology'] + +# Convert url into uri object +$gitUri = [System.Uri]$gitUrl + +switch ($gitTech) +{ + \"ado\" + { + +\t\t# Parse url + $gitOrganization = $gitUri.AbsolutePath + $gitOrganization = $gitOrganization.Substring(1) +\t\t$gitOrganization = $gitOrganization.Substring(0, $gitOrganization.IndexOf(\"/\")) + + # Encode personal access token + $encodedPAT = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(\"`:$gitPassword\")) + + # Construct Headers + $header = @{ + \tAuthorization = \"Basic $encodedPAT\" + } + + $gitProject = $gitUri.AbsolutePath.Replace($gitOrganization, \"\").Replace(\"//\", \"\") + $gitProject = $gitProject.Substring(0, $gitProject.IndexOf(\"/\")) + +\t\t# Create pull request + $jsonBody = @{ + \tsourceRefName = \"refs/heads/\" + $gitSourceBranch + targetRefName = \"refs/heads/\" + $gitDestinationBranch + title = \"PR from Octopus Deploy\" + description = \"PR from #{Octopus.Project.Name} release version #{Octopus.Release.Number}\" + } + + # Construct API call + $adoApiUrl = \"{0}://{1}:{2}/{3}/{4}/_apis/git/repositories/{4}/pullrequests\" -f $gitUri.Scheme, $gitUri.Host, $gitUri.Port, $gitOrganization, $gitProject + Invoke-RestMethod -Method Post -Uri ($adoApiUrl + \"?api-version=7.0\") -Body ($jsonBody | ConvertTo-Json -Depth 10) -Headers $header -ContentType \"application/json\" + } + \"bitbucket\" + { +\t\t# Parse url + $gitOrganization = $gitUri.AbsolutePath + $gitOrganization = $gitOrganization.Substring(1) +\t\t$gitOrganization = $gitOrganization.Substring(0, $gitOrganization.IndexOf(\"/\")) + $gitProject = $gitUri.AbsolutePath.Replace($gitOrganization, \"\").Replace(\"//\", \"\") + + # Check to see if Repo Name ends with .git + if ($gitProject.EndsWith(\".git\")) + { + \t# Strip off the last part + $gitProject = $gitProject.Replace(\".git\", \"\") + } + + # Construct Headers + $header = @{ + \tAuthorization = \"Bearer $gitPassword\" + } + + # Construct API url + $bitbucketApiUrl = \"{0}://api.{1}:{2}/2.0/repositories/{3}/{4}/pullrequests\" -f $gitUri.Scheme, $gitUri.Host, $gitUri.Port, $gitOrganization, $gitProject + +\t\t# Construct json body + $jsonBody = @{ + \ttitle = \"PR from Octopus Deploy\" + source = @{ + \tbranch = @{ + \tname = $gitSourceBranch + } + } + destination = @{ + \tbranch = @{ + \tname = $gitDestinationBranch + } + } + } + + # Create PR + Invoke-RestMethod -Method Post -Uri $bitbucketApiUrl -Headers $header -Body ($jsonBody | ConvertTo-Json -Depth 10) -ContentType \"application/json\" + } + \"github\" + { + # Parse URL + $gitRepoOwner = $gitUri.AbsolutePath.Substring(1, $gitUri.AbsolutePath.LastIndexOf(\"/\") - 1) + $gitRepoName = $gitUri.AbsolutePath.Substring($gitUri.AbsolutePath.LastIndexOf(\"/\") + 1 ) + + # Check to see if Repo Name ends with .git + if ($gitRepoName.EndsWith(\".git\")) + { + \t# Strip off the last part + $gitRepoName = $gitRepoName.Replace(\".git\", \"\") + } + + # Construct API endpoint + $githubApiUrl = \"{0}://api.{1}:{2}/repos/{3}/{4}/pulls\" -f $gitUri.Scheme, $gitUri.Host, $gitUri.Port, $gitRepoOwner, $gitRepoName + + # Construct Headers + $header = @{ + \tAuthorization = \"Bearer $gitPassword\" + Accept = \"application/vnd.github+json\" + \"X-Github-Api-Version\" = \"2022-11-28\" + } + + # Construct body + $jsonBody = @{ + \ttitle = \"PR from Octopus Deploy\" + body = \"PR from #{Octopus.Project.Name} release version #{Octopus.Release.Number}\" + head = $gitSourceBranch + base = $gitDestinationBranch + } + + # Create the pull request + Invoke-RestMethod -Method Post -Uri $gitHubApiUrl -Headers $header -Body ($jsonBody | ConvertTo-Json -Depth 10) + } + \"gitlab\" + { +\t\t# Get project name + $gitlabProjectName = $gitUrl.SubString($gitUrl.LastIndexOf(\"/\") + 1) + + # Parse uri + $gitlabApiUrl = \"{0}://{1}:{2}/api/v4/users/{3}/projects\" -f $gitUri.Scheme, $gitUri.Host, $gitUri.Port, $gitUser + + # Check to see if it ends in .git + if ($gitlabProjectName.EndsWith(\".git\")) + { + \t# Strip that part off + $gitlabProjectName = $gitlabProjectName.Replace(\".git\", \"\") + } + + # Create header + $header = @{ \"PRIVATE-TOKEN\" = $gitPassword } + + # Get the project + $gitlabProject = (Invoke-RestMethod -Method Get -Uri $gitlabApiUrl -Headers $header) | Where-Object {$_.Name -eq $gitlabProjectName} + + # Create the merge request + $gitlabApiUrl = \"{0}://{1}:{2}/api/v4/projects/{3}/merge_requests?source_branch={4}&target_branch={5}&target_project_id={3}&title={6}\" -f $gitUri.Scheme, $gitUri.Host, $gitUri.Port, $gitlabProject.id, $gitSourceBranch, $gitDestinationBranch, \"PR from #{Octopus.Project.Name} release version #{Octopus.Release.Number}\" + Invoke-RestMethod -Method Post -Uri $gitlabApiUrl -Headers $header + } +} + +<# +# Set user +$gitAuthorName = $OctopusParameters['Octopus.Deployment.CreatedBy.DisplayName'] +$gitAuthorEmail = $OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress'] + +# Check to see if user is system +if ([string]::IsNullOrWhitespace($gitAuthorEmail) -and $gitAuthorName -eq \"System\") +{ +\t# Initiated by the Octopus server via automated process, put something in for the email address + $gitAuthorEmail = \"system@octopus.local\" +} + +# Configure user information +Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"user.name\", $gitAuthorName) #-GitFolder \"$($PWD)/$($folderName)\" +Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"user.email\", $gitAuthorEmail) #-GitFolder \"$($PWD)/$($folderName)\" + + +# Push the new tag +Invoke-Git -Gitcommand \"request-pull\" -AdditionalArguments @(\"$gitSourceBranch\", $gitUrl, \"$gitDestinationBranch\") -GitFolder \"$($PWD)/$($folderName)\" +#>" + }, + "Parameters": [ + { + "Id": "674d5325-aa93-4779-a734-cee8e5690f17", + "Name": "Template.Git.Repo.Url", + "Label": "Git Repository URL", + "HelpText": "The URL used for the `git clone` operation.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f2d07c9c-85fc-485e-8057-5577efd9a26d", + "Name": "Template.Git.User.Name", + "Label": "Git Username", + "HelpText": "Username of the credentials to use to log into git.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "597ae9a5-1ef4-4062-a435-2d9bb1fb16a2", + "Name": "Template.Git.User.Password", + "Label": "Git User Password", + "HelpText": "Password for the git credential.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "7cfa6ba5-76c6-4b15-a30c-593e2cd8f914", + "Name": "Template.Git.Source.Branch", + "Label": "Source Branch", + "HelpText": "The source branch name to compare the repository with.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2b0ee0d3-8542-4db2-87fc-52ee41e63cc6", + "Name": "Template.Git.Destination.Branch", + "Label": "Destination Branch", + "HelpText": "The destination branch to create the pull/merge request against.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "74d8fc6d-2a4c-49ad-b7f8-248dcf44fc0f", + "Name": "Template.Git.Repository.Technology", + "Label": "Source Control Technology", + "HelpText": "Select which source control technology to create the request on.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "ado|Azure DevOps +bitbucket|BitBucket +github|GitHub +gitlab|GitLab" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-09-12T15:32:53.416Z", + "OctopusVersion": "2023.4.2661", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "git" +} diff --git a/step-templates/git-pull-https.json.human b/step-templates/git-pull-https.json.human new file mode 100644 index 000000000..d08c200d0 --- /dev/null +++ b/step-templates/git-pull-https.json.human @@ -0,0 +1,122 @@ +{ + "Id": "5c08170d-e919-4afe-9da3-7616c797d42b", + "Name": "Git - Pull (HTTPS)", + "Description": "Gets the latest source for a Git repository.", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[System.Reflection.Assembly]::LoadWithPartialName(\"System.Web\") +function Format-UriWithCredentials($url, $username, $password) { + $uri = New-Object \"System.Uri\" $url + + $url = $uri.Scheme + \"://\" + if (-not [string]::IsNullOrEmpty($username)) { + $url = $url + [System.Web.HttpUtility]::UrlEncode($username) + + if (-not [string]::IsNullOrEmpty($password)) { + $url = $url + \":\" + [System.Web.HttpUtility]::UrlEncode($password) + } + + $url = $url + \"@\" + } elseif (-not [string]::IsNullOrEmpty($uri.UserInfo)) { + $url = $uri.UserInfo + \"@\" + } + + $url = $url + $uri.Host + $uri.PathAndQuery + return $url +} + +function Test-LastExit($cmd) { + if ($LastExitCode -ne 0) { + Write-Host \"##octopus[stderr-error]\" + write-error \"$cmd failed with exit code: $LastExitCode\" + } +} + +$tempDirectoryPath = $OctopusParameters['Octopus.Tentacle.Agent.ApplicationDirectoryPath'] +$tempDirectoryPath = join-path $tempDirectoryPath \"GitPull\" +$tempDirectoryPath = join-path $tempDirectoryPath $OctopusParameters['Octopus.Environment.Name'] +$tempDirectoryPath = join-path $tempDirectoryPath $OctopusParameters['Octopus.Project.Name'] +$tempDirectoryPath = join-path $tempDirectoryPath $OctopusParameters['Octopus.Action.Name'] + +Write-Host \"Repository will be cloned to: $tempDirectoryPath\" + +# Step 1: Ensure we have the latest version of the repository +mkdir $tempDirectoryPath -ErrorAction SilentlyContinue +cd $tempDirectoryPath + +Write-Host \"##octopus[stderr-progress]\" + +git init +Test-LastExit \"git init\" + +$url = Format-UriWithCredentials -url $OctopusParameters['GitHttpsUrl'] -username $OctopusParameters['Username'] -password $OctopusParameters['Password'] + +$branch = $OctopusParameters['GitHttpsBranchName'] + +# We might have already run before, so we need to reset the origin +git remote remove origin +git remote add origin $url +Test-LastExit \"git remote add origin\" + +Write-Host \"Fetching remote repository\" +git fetch origin +Test-LastExit \"git fetch origin\" + +Write-Host \"Check out branch $branch\" +git reset --hard \"origin/$branch\" +Test-LastExit \"git reset --hard\" + +Set-OctopusVariable -name \"RepositoryDirectory\" -value $tempDirectoryPath +Write-Verbose \"Directory path '$tempDirectoryPath' available in 'RepositoryDirectory' output variable\" +Write-Host \"Repository successfully cloned to: $tempDirectoryPath\"", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "GitHttpsUrl", + "Label": "Clone URL", + "HelpText": "'https://' URL to the repository that will be cloned from.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Username", + "Label": "Username", + "HelpText": "Username to use when authenticating with the HTTPS server.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Password", + "Label": "Password", + "HelpText": "Password to use when authenticating with the HTTPS server. You should create a sensitive variable in your project variables, and then bind this value.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "GitHttpsBranchName", + "Label": "Branch name", + "HelpText": "Name of the Git branch to clone", + "DefaultValue": "master", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-16T00:23:33.282+00:00", + "OctopusVersion": "2.4.4.43", + "Type": "ActionTemplate" + }, + "Category": "git" +} diff --git a/step-templates/git-push-https.json.human b/step-templates/git-push-https.json.human new file mode 100644 index 000000000..508041737 --- /dev/null +++ b/step-templates/git-push-https.json.human @@ -0,0 +1,199 @@ +{ + "Id": "85cc56f9-26cb-4b0e-8320-854e88a09a25", + "Name": "Git - Push (HTTPS)", + "Description": "Deploy a package using Git to a HTTPS server. Performs a clone, overwrites the repository with the files from your package, then pushes. Great for deploying to AppHarbor and Windows Azure websites.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[System.Reflection.Assembly]::LoadWithPartialName(\"System.Web\") + +# A collection of functions that can be used by script steps to determine where packages installed +# by previous steps are located on the filesystem. + +function Find-InstallLocations { + $result = @() + $OctopusParameters.Keys | foreach { + if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) { + $result += $OctopusParameters[$_] + } + } + return $result +} + +function Find-InstallLocation($stepName) { + $result = $OctopusParameters.Keys | where { + $_.Equals(\"Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath\", [System.StringComparison]::OrdinalIgnoreCase) + } | select -first 1 + + if ($result) { + return $OctopusParameters[$result] + } + + throw \"No install location found for step: $stepName\" +} + +function Find-SingleInstallLocation { + $all = @(Find-InstallLocations) + if ($all.Length -eq 1) { + return $all[0] + } + if ($all.Length -eq 0) { + throw \"No package steps found\" + } + throw \"Multiple package steps have run; please specify a single step\" +} + +function Format-UriWithCredentials($url, $username, $password) { + $uri = New-Object \"System.Uri\" $url + + $url = $uri.Scheme + \"://\" + if (-not [string]::IsNullOrEmpty($username)) { + $url = $url + [System.Web.HttpUtility]::UrlEncode($username) + + if (-not [string]::IsNullOrEmpty($password)) { + $url = $url + \":\" + [System.Web.HttpUtility]::UrlEncode($password) + } + + $url = $url + \"@\" + } elseif (-not [string]::IsNullOrEmpty($uri.UserInfo)) { + $url = $uri.UserInfo + \"@\" + } + + $url = $url + $uri.Host + $uri.PathAndQuery + return $url +} + +function Test-LastExit($cmd) { + if ($LastExitCode -ne 0) { + Write-Host \"##octopus[stderr-error]\" + write-error \"$cmd failed with exit code: $LastExitCode\" + } +} + +$tempDirectoryPath = $OctopusParameters['Octopus.Tentacle.Agent.ApplicationDirectoryPath'] +$tempDirectoryPath = join-path $tempDirectoryPath \"GitPush\" +$tempDirectoryPath = join-path $tempDirectoryPath $OctopusParameters['Octopus.Environment.Name'] +$tempDirectoryPath = join-path $tempDirectoryPath $OctopusParameters['Octopus.Project.Name'] +$tempDirectoryPath = join-path $tempDirectoryPath $OctopusParameters['Octopus.Action.Name'] + +$stepName = $OctopusParameters['GitHttpsPackageStepName'] + +$stepPath = \"\" +if (-not [string]::IsNullOrEmpty($stepName)) { + Write-Host \"Finding path to package step: $stepName\" + $stepPath = Find-InstallLocation $stepName +} else { + $stepPath = Find-SingleInstallLocation +} +Write-Host \"Package was installed to: $stepPath\" + +Write-Host \"Repository will be cloned to: $tempDirectoryPath\" + +# Step 1: Ensure we have the latest version of the repository +mkdir $tempDirectoryPath -ErrorAction SilentlyContinue +cd $tempDirectoryPath + +Write-Host \"##octopus[stderr-progress]\" + +git init +Test-LastExit \"git init\" + +$url = Format-UriWithCredentials -url $OctopusParameters['GitHttpsUrl'] -username $OctopusParameters['Username'] -password $OctopusParameters['Password'] + +$branch = $OctopusParameters['GitHttpsBranchName'] + +# We might have already run before, so we need to reset the origin +git remote remove origin +git remote add origin $url +Test-LastExit \"git remote add origin\" + +Write-Host \"Fetching remote repository\" +git fetch origin +Test-LastExit \"git fetch origin\" + +Write-Host \"Check out branch $branch\" +git reset --hard \"origin/$branch\" + +# Step 2: Overwrite the contents +write-host \"Synchronizing package contents with local git repository using Robocopy\" +& robocopy $stepPath $tempDirectoryPath /MIR /xd \".git\" +if ($lastexitcode -ge 5) { + write-error \"Unable to copy files from the package to the local cloned Git repository. See the Robocopy errors above for details.\" +} + +# Step 3: Push the results +$deploymentName = $OctopusParameters['Octopus.Deployment.Name'] +$releaseName = $OctopusParameters['Octopus.Release.Number'] +$projName = $OctopusParameters['Octopus.Project.Name'] + +git add . -A +Test-LastExit \"git add\" + +git diff-index --quiet HEAD +if ($lastexitcode -ne 0) { + git commit -m \"$projName release $releaseName - $deploymentName\" + Test-LastExit \"git commit\" +} + +git push origin $branch +Test-LastExit \"git push\" +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "GitHttpsUrl", + "Label": "Clone URL", + "HelpText": "`https://` URL to the repository that will be cloned from and pushed to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Username", + "Label": "Username", + "HelpText": "Username to use when authenticating with the HTTPS server.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Password", + "Label": "Password", + "HelpText": "Password to use when authenticating with the HTTPS server. You should create a sensitive variable in your project variables, and then bind this value.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "GitHttpsPackageStepName", + "Label": "Package step name", + "HelpText": "Name of the previously-deployed package step that contains the files that you want to push.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "GitHttpsBranchName", + "Label": "Branch name", + "HelpText": "Name of the Git branch to clone/push", + "DefaultValue": "master", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-10-24T14:59:45.295+00:00", + "OctopusVersion": "2.5.11.614", + "Type": "ActionTemplate" + }, + "Category": "git" +} diff --git a/step-templates/git-tag-repository.json.human b/step-templates/git-tag-repository.json.human new file mode 100644 index 000000000..b557126bf --- /dev/null +++ b/step-templates/git-tag-repository.json.human @@ -0,0 +1,269 @@ +{ + "Id": "dd76dee1-f5b1-4974-a5ae-bde643cf67af", + "Name": "Git - Tag repository", + "Description": "Tags a git repository with the specified tag", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Function Invoke-Git +{ +\t# Define parameters + param ( + \t$GitRepositoryUrl, + $GitFolder, + $GitUsername, + $GitPassword, + $GitCommand, + $AdditionalArguments, + $SupressOutput = $false + ) + + # Get current work folder + $workDirectory = Get-Location + + # Create arguments array + $gitArguments = @() + $gitArguments += $GitCommand + + # Check for url + if (![string]::IsNullOrWhitespace($GitRepositoryUrl)) + { + # Convert url to URI object + $gitUri = [System.Uri]$GitRepositoryUrl + $gitUrl = \"{0}://{1}:{2}@{3}:{4}{5}\" -f $gitUri.Scheme, $GitUsername, $GitPassword, $gitUri.Host, $gitUri.Port, $gitUri.PathAndQuery + $gitArguments += $gitUrl + + # Get the newly created folder name + $gitFolderName = $GitRepositoryUrl.SubString($GitRepositoryUrl.LastIndexOf(\"/\") + 1) + if ($gitFolderName.Contains(\".git\")) + { + $gitFolderName = $gitFolderName.SubString(0, $gitFolderName.IndexOf(\".\")) + } + } + + + # Check for additional arguments + if ($null -ne $AdditionalArguments) + { + \t\t# Add the additional arguments + $gitArguments += $AdditionalArguments + } + + # Execute git command + $results = Execute-Command \"git\" $gitArguments $GitFolder + + # Check to see if output is supposed to be suppressed + if ($SupressOutput -ne $true) + { + \tWrite-Host $results.stdout + } + +\t# Always display error messages + Write-Host $results.stderr + + # Store results into file + Add-Content -Path \"$PWD/$($GitCommand).txt\" -Value $results.stdout + + # Return the foldername + \treturn $gitFolderName +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +Function Execute-Command +{ +\tparam ( + \t$commandPath, + $commandArguments, + $workingDir + ) + +\t$gitExitCode = 0 + $executionResults = $null + + Try { + $pinfo = New-Object System.Diagnostics.ProcessStartInfo + $pinfo.FileName = $commandPath + $pinfo.WorkingDirectory = $workingDir + $pinfo.RedirectStandardError = $true + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + $pinfo.Arguments = $commandArguments + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $pinfo + $p.Start() | Out-Null + $executionResults = [pscustomobject]@{ + stdout = $p.StandardOutput.ReadToEnd() + stderr = $p.StandardError.ReadToEnd() + ExitCode = $null + } + $p.WaitForExit() + $gitExitCode = [int]$p.ExitCode + $executionResults.ExitCode = $gitExitCode + + if ($gitExitCode -ge 2) + { +\t\t# Fail the step + throw + } + + return $executionResults + } + Catch { + # Check exit code + Write-Error -Message \"$($executionResults.stderr)\" -ErrorId $gitExitCode + exit $gitExitCode + } + +} + + +# Get variables +$gitUrl = $OctopusParameters['Template.Git.Repo.Url'] +$gitUser = $OctopusParameters['Template.Git.User.Name'] +$gitPassword = $OctopusParameters['Template.Git.User.Password'] +$gitTag = $OctopusParameters['Template.Git.Tag'] +$gitAction = $OctopusParameters['Template.Git.Action'] + +# Clone repository +$folderName = Invoke-Git -GitRepositoryUrl $gitUrl -GitUsername $gitUser -GitPassword $gitPassword -GitCommand \"clone\" + +# Set user +$gitAuthorName = $OctopusParameters['Octopus.Deployment.CreatedBy.DisplayName'] +$gitAuthorEmail = $OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress'] + +# Check to see if user is system +if ([string]::IsNullOrWhitespace($gitAuthorEmail) -and $gitAuthorName -eq \"System\") +{ +\t# Initiated by the Octopus server via automated process, put something in for the email address + $gitAuthorEmail = \"system@octopus.local\" +} + +# Configure user information +Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"user.name\", $gitAuthorName) -GitFolder \"$($PWD)/$($folderName)\" +Invoke-Git -GitCommand \"config\" -AdditionalArguments @(\"user.email\", $gitAuthorEmail) -GitFolder \"$($PWD)/$($folderName)\" + +# Record existing tags, if any +Invoke-Git -GitCommand \"tag\" -GitFolder \"$($PWD)/$($folderName)\" -SupressOutput $true + +# Check the file +$existingTags = Get-Content \"$PWD/tag.txt\" + +if (![String]::IsNullOrWhitespace($existingTags)) +{ +\t# Parse + $existingTags = $existingTags.Split(\"`n\",[System.StringSplitOptions]::RemoveEmptyEntries) + + # Check to see if tag already exists + if ($null -ne ($existingTags | Where-Object {$_ -eq $gitTag})) + { +\t\t# Check the selected action + switch ($gitAction) + { + \t\"delete\" + { + # Delete the tag locally + Write-Host \"Deleting tag $gitTag from cloned repository ...\" + Invoke-Git -GitCommand \"tag\" -AdditionalArguments @(\"--delete\", \"$gitTag\") -GitFolder \"$($PWD)/$($folderName)\" + + # Delete the tag on remote + Write-Host \"Deleting tag from remote repository ...\" + Invoke-Git -GitCommand \"push\" -AdditionalArguments @(\":refs/tags/$gitTag\") -GitFolder \"$($PWD)/$($folderName)\" -GitRepositoryUrl $gitUrl -GitUsername $gitUser -GitPassword $gitPassword + + break + } + \"ignore\" + { + \t# Ignore and continue + Write-Host \"$gitTag already exists on $gitUrl. Selected action is Ignore, exiting.\" + + exit 0 + } + \"fail\" + { +\t\t\t\t# Error, tag already exists + \t\tWrite-Error \"Error: $gitTag already exists on $gitUrl!\" + } + } + } +} + +# Tag the repo +Invoke-Git -GitCommand \"tag\" -AdditionalArguments @(\"-a\", $gitTag, \"-m\", \"`\"Tag from #{Octopus.Project.Name} release version #{Octopus.Release.Number}`\"\") -GitFolder \"$($PWD)/$($folderName)\" + +# Push the new tag +Invoke-Git -Gitcommand \"push\" -AdditionalArguments @(\"--tags\") -GitFolder \"$($PWD)/$($folderName)\"" + }, + "Parameters": [ + { + "Id": "674d5325-aa93-4779-a734-cee8e5690f17", + "Name": "Template.Git.Repo.Url", + "Label": "Git Repository URL", + "HelpText": "The URL used for the `git clone` operation.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f2d07c9c-85fc-485e-8057-5577efd9a26d", + "Name": "Template.Git.User.Name", + "Label": "Git Username", + "HelpText": "Username of the credentials to use to log into git.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "597ae9a5-1ef4-4062-a435-2d9bb1fb16a2", + "Name": "Template.Git.User.Password", + "Label": "Git User Password", + "HelpText": "Password for the git credential.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "7cfa6ba5-76c6-4b15-a30c-593e2cd8f914", + "Name": "Template.Git.Tag", + "Label": "Tag", + "HelpText": "The tag name to tag the repository with.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b7ae407f-f926-4478-af72-74397025e4b6", + "Name": "Template.Git.Action", + "Label": "Action if tag already exists", + "HelpText": "Select the action if the tag already exsists.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "delete|Delete and recreate +fail|Fail +ignore|Ignore" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-09-12T15:33:15.238Z", + "OctopusVersion": "2023.4.2661", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "git" +} diff --git a/step-templates/github-create-repo.json.human b/step-templates/github-create-repo.json.human new file mode 100644 index 000000000..aff91a5bb --- /dev/null +++ b/step-templates/github-create-repo.json.human @@ -0,0 +1,327 @@ +{ + "Id": "493fa039-fd5c-47d2-b830-63cc32a19d04", + "Name": "GitHub - Create Repository", + "Description": "This step creates a new GitHub repository if it does not exist.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Python", + "Octopus.Action.Script.ScriptBody": "# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid +# having to use a regular user account. +import subprocess +import sys + +# Install our own dependencies +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt', '--disable-pip-version-check']) + +import json +import subprocess +import sys +import os +import urllib.request +import base64 +import re +import jwt +import time +import argparse +import platform +from urllib.request import urlretrieve + +# If this script is not being run as part of an Octopus step, setting variables is a noop +if 'set_octopusvariable' not in globals(): + def set_octopusvariable(variable, value): + pass + +# If this script is not being run as part of an Octopus step, return variables from environment variables. +# Periods are replaced with underscores, and the variable name is converted to uppercase +if \"get_octopusvariable\" not in globals(): + def get_octopusvariable(variable): + return os.environ[re.sub('\\\\.', '_', variable.upper())] + +# If this script is not being run as part of an Octopus step, print directly to std out. +if 'printverbose' not in globals(): + def printverbose(msg): + print(msg) + + +def printverbose_noansi(output): + \"\"\" + Strip ANSI color codes and print the output as verbose + :param output: The output to print + \"\"\" + output_no_ansi = re.sub(r'\\x1b\\[[0-9;]*m', '', output) + printverbose(output_no_ansi) + + +def get_octopusvariable_quiet(variable): + \"\"\" + Gets an octopus variable, or an empty string if it does not exist. + :param variable: The variable name + :return: The variable value, or an empty string if the variable does not exist + \"\"\" + try: + return get_octopusvariable(variable) + except: + return '' + + +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi, raise_on_non_zero=False, + append_to_path=None): + \"\"\" + The execute method provides the ability to execute external processes while capturing and returning the + output to std err and std out and exit code. + \"\"\" + + my_env = os.environ.copy() if env is None else env + + if append_to_path is not None: + my_env[\"PATH\"] = append_to_path + os.pathsep + my_env['PATH'] + + process = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=open(os.devnull), + text=True, + cwd=cwd, + env=my_env) + stdout, stderr = process.communicate() + retcode = process.returncode + + if not retcode == 0 and raise_on_non_zero: + raise Exception('command returned exit code ' + retcode) + + if print_args is not None: + print_output(' '.join(args)) + + if print_output is not None: + print_output(stdout) + print_output(stderr) + + return stdout, stderr, retcode + + +def init_argparse(): + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTION]', + description='Create a GitHub repo' + ) + parser.add_argument('--new-repo-name', action='store', + default=get_octopusvariable_quiet( + 'CreateGithubRepo.Git.Url.NewRepoName') or get_octopusvariable_quiet( + 'Git.Url.NewRepoName') or get_octopusvariable_quiet('Octopus.Project.Name')) + parser.add_argument('--new-repo-name-prefix', action='store', + default=get_octopusvariable_quiet( + 'CreateGithubRepo.Git.Url.NewRepoNamePrefix') or get_octopusvariable_quiet( + 'Git.Url.NewRepoNamePrefix')) + parser.add_argument('--git-organization', action='store', + default=get_octopusvariable_quiet( + 'CreateGithubRepo.Git.Url.Organization') or get_octopusvariable_quiet( + 'Git.Url.Organization')) + parser.add_argument('--github-app-id', action='store', + default=get_octopusvariable_quiet( + 'CreateGithubRepo.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id')) + parser.add_argument('--github-app-installation-id', action='store', + default=get_octopusvariable_quiet( + 'CreateGithubRepo.GitHub.App.InstallationId') or get_octopusvariable_quiet( + 'GitHub.App.InstallationId')) + parser.add_argument('--github-app-private-key', action='store', + default=get_octopusvariable_quiet( + 'CreateGithubRepo.GitHub.App.PrivateKey') or get_octopusvariable_quiet( + 'GitHub.App.PrivateKey')) + parser.add_argument('--github-access-token', action='store', + default=get_octopusvariable_quiet( + 'CreateGithubRepo.Git.Credentials.AccessToken') or get_octopusvariable_quiet( + 'Git.Credentials.AccessToken'), + help='The git password') + + return parser.parse_known_args() + + +def generate_github_token(github_app_id, github_app_private_key, github_app_installation_id): + # Generate the tokens used by git and the GitHub API + app_id = github_app_id + signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8')) + + payload = { + # Issued at time + 'iat': int(time.time()), + # JWT expiration time (10 minutes maximum) + 'exp': int(time.time()) + 600, + # GitHub App's identifier + 'iss': app_id + } + + # Create JWT + jwt_instance = jwt.JWT() + encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256') + + # Create access token + url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens' + headers = { + 'Authorization': 'Bearer ' + encoded_jwt, + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + } + request = urllib.request.Request(url, headers=headers, method='POST') + response = urllib.request.urlopen(request) + response_json = json.loads(response.read().decode()) + return response_json['token'] + + +def generate_auth_header(token): + auth = base64.b64encode(('x-access-token:' + token).encode('ascii')) + return 'Basic ' + auth.decode('ascii') + + +def verify_new_repo(token, cac_org, new_repo): + # Attempt to view the new repo + try: + url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo + headers = { + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer ' + token, + 'X-GitHub-Api-Version': '2022-11-28' + } + request = urllib.request.Request(url, headers=headers) + urllib.request.urlopen(request) + return True + except: + return False + + +def create_new_repo(token, cac_org, new_repo): + # If we could not view the repo, assume it needs to be created. + # https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#create-an-organization-repository + # Note you have to use the token rather than the JWT: + # https://stackoverflow.com/questions/39600396/bad-credentails-for-jwt-for-github-integrations-api + + headers = { + 'Authorization': 'token ' + token, + 'Content-Type': 'application/json', + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + } + + try: + # First try to create an organization repo: + # https://docs.github.com/en/free-pro-team@latest/rest/repos/repos#create-an-organization-repository + url = 'https://api.github.com/orgs/' + cac_org + '/repos' + body = {'name': new_repo} + request = urllib.request.Request(url, headers=headers, data=json.dumps(body).encode('utf-8')) + urllib.request.urlopen(request) + except urllib.error.URLError as ex: + # Then fall back to creating a repo for the user: + # https://docs.github.com/en/free-pro-team@latest/rest/repos/repos?apiVersion=2022-11-28#create-a-repository-for-the-authenticated-user + if ex.code == 404: + url = 'https://api.github.com/user/repos' + body = {'name': new_repo} + request = urllib.request.Request(url, headers=headers, data=json.dumps(body).encode('utf-8')) + urllib.request.urlopen(request) + else: + raise ValueError(\"Failed to create thew new repository. This could indicate bad credentials.\") from ex + + +def is_windows(): + return platform.system() == 'Windows' + + +parser, _ = init_argparse() + +if not parser.github_access_token.strip() and not ( + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()): + print(\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\") + sys.exit(1) + +if not parser.new_repo_name.strip(): + print(\"You must define the new repo name\") + sys.exit(1) + +# The access token is generated from a github app or supplied directly as an access token +token = generate_github_token(parser.github_app_id, parser.github_app_private_key, parser.github_app_installation_id) \\ + if not parser.github_access_token.strip() else parser.github_access_token.strip() + +cac_org = parser.git_organization.strip() +new_repo_custom_prefix = re.sub('[^a-zA-Z0-9-]', '_', parser.new_repo_name_prefix.strip()) +project_repo_sanitized = re.sub('[^a-zA-Z0-9-]', '_', parser.new_repo_name.strip()) + +# The prefix is optional +new_repo_prefix_with_separator = new_repo_custom_prefix + '_' if new_repo_custom_prefix else '' + +# The new repo name is the prefix + the name of thew new project +new_repo = new_repo_prefix_with_separator + project_repo_sanitized + +# This is the value of the forked git repo +set_octopusvariable('NewRepoUrl', 'https://github.com/' + cac_org + '/' + new_repo) +set_octopusvariable('NewRepo', new_repo) + +if not verify_new_repo(token, cac_org, new_repo): + create_new_repo(token, cac_org, new_repo) + print( + 'New repo was created at https://github.com/' + cac_org + '/' + new_repo) +else: + print('Repo at https://github.com/' + cac_org + '/' + new_repo + ' already exists and has not been modified') + +print('New repo URL is defined in the output variable \"NewRepoUrl\": #{Octopus.Action[' + + get_octopusvariable_quiet('Octopus.Step.Name') + '].Output.NewRepoUrl}') +print('New repo name is defined in the output variable \"NewRepo\": #{Octopus.Action[' + + get_octopusvariable_quiet('Octopus.Step.Name') + '].Output.NewRepo}') +" + }, + "Parameters": [ + { + "Id": "5845b0e2-d679-4cec-9022-c233031e6b35", + "Name": "CreateGithubRepo.Git.Url.NewRepoName", + "Label": "Repository Name", + "HelpText": "The name of the new GitHub repository.", + "DefaultValue": "#{Octopus.Project.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b85e4a76-bc9c-4a51-8ac0-653991824db2", + "Name": "CreateGithubRepo.Git.Url.NewRepoNamePrefix", + "Label": "Repository Name Prefix", + "HelpText": "An optional prefix to apply to the name of the new repository. The repository name will often be generated from the project name, and the prefix will be based on a tenant name (`#{Octopus.Deployment.Tenant.Name}`) to ensure each tenant has a unique repo. + + +This value can be left blank.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b0d86188-fbb8-475e-89de-4b994170f2b7", + "Name": "CreateGithubRepo.Git.Url.Organization", + "Label": "GitHub Owner or Organization", + "HelpText": "This is the GitHub owner or organisation where the new repo is created.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0ffd12dd-59ce-401a-b1df-cf3d6711e2eb", + "Name": "CreateGithubRepo.Git.Credentials.AccessToken", + "Label": "GitHub Access Token", + "HelpText": "The access token used to authenticate with GitHub. See the [GitHub documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) for more details.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-10-17T02:12:09.605Z", + "OctopusVersion": "2023.4.5667", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "github" +} diff --git a/step-templates/github-create-secret.json.human b/step-templates/github-create-secret.json.human new file mode 100644 index 000000000..b0bc57404 --- /dev/null +++ b/step-templates/github-create-secret.json.human @@ -0,0 +1,310 @@ +{ + "Id": "8d59cae3-3168-466e-9490-bf654c180660", + "Name": "GitHub - Create Repo Secret", + "Description": "This step creates a secret in a GitHub repo.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Python", + "Octopus.Action.Script.ScriptBody": "# https://gist.github.com/comdotlinux/9a53bb00767a16d6646464c4b8249094 + +# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid +# having to use a regular user account. +import subprocess +import sys + +# Install our own dependencies +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt', '--disable-pip-version-check']) +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pynacl', '--disable-pip-version-check']) + +import requests +import json +import sys +import os +import urllib.request +import base64 +import re +import jwt +import time +import argparse +import urllib3 +from base64 import b64encode +from typing import TypedDict +from nacl import public, encoding + +# Disable insecure http request warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# If this script is not being run as part of an Octopus step, setting variables is a noop +if 'set_octopusvariable' not in globals(): + def set_octopusvariable(variable, value): + pass + +# If this script is not being run as part of an Octopus step, return variables from environment variables. +# Periods are replaced with underscores, and the variable name is converted to uppercase +if \"get_octopusvariable\" not in globals(): + def get_octopusvariable(variable): + return os.environ[re.sub('\\\\.', '_', variable.upper())] + +# If this script is not being run as part of an Octopus step, print directly to std out. +if 'printverbose' not in globals(): + def printverbose(msg): + print(msg) + + +def printverbose_noansi(output): + \"\"\" + Strip ANSI color codes and print the output as verbose + :param output: The output to print + \"\"\" + output_no_ansi = re.sub(r'\\x1b\\[[0-9;]*m', '', output) + printverbose(output_no_ansi) + + +def get_octopusvariable_quiet(variable): + \"\"\" + Gets an octopus variable, or an empty string if it does not exist. + :param variable: The variable name + :return: The variable value, or an empty string if the variable does not exist + \"\"\" + try: + return get_octopusvariable(variable) + except: + return '' + + +def init_argparse(): + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTION]', + description='Fork a GitHub repo' + ) + + parser.add_argument('--secret-name', action='store', + default=get_octopusvariable_quiet( + 'CreateGitHubSecret.GitHub.Secret.Name') or get_octopusvariable_quiet( + 'GitHub.Secret.Name')) + parser.add_argument('--secret-value', action='store', + default=get_octopusvariable_quiet( + 'CreateGitHubSecret.GitHub.Secret.Value') or get_octopusvariable_quiet( + 'GitHub.Secret.Value')) + + parser.add_argument('--repo', action='store', + default=get_octopusvariable_quiet( + 'CreateGitHubSecret.Git.Url.Repo') or get_octopusvariable_quiet( + 'Git.Url.Repo') or get_octopusvariable_quiet('Octopus.Project.Name')) + parser.add_argument('--git-organization', action='store', + default=get_octopusvariable_quiet( + 'CreateGitHubSecret.Git.Url.Organization') or get_octopusvariable_quiet( + 'Git.Url.Organization')) + parser.add_argument('--github-app-id', action='store', + default=get_octopusvariable_quiet( + 'CreateGitHubSecret.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id')) + parser.add_argument('--github-app-installation-id', action='store', + default=get_octopusvariable_quiet( + 'CreateGitHubSecret.GitHub.App.InstallationId') or get_octopusvariable_quiet( + 'GitHub.App.InstallationId')) + parser.add_argument('--github-app-private-key', action='store', + default=get_octopusvariable_quiet( + 'CreateGitHubSecret.GitHub.App.PrivateKey') or get_octopusvariable_quiet( + 'GitHub.App.PrivateKey')) + parser.add_argument('--git-password', action='store', + default=get_octopusvariable_quiet( + 'CreateGitHubSecret.Git.Credentials.Password') or get_octopusvariable_quiet( + 'Git.Credentials.Password'), + help='The git password. This takes precedence over the --github-app-id, --github-app-installation-id, and --github-app-private-key') + + return parser.parse_known_args() + + +def generate_github_token(github_app_id, github_app_private_key, github_app_installation_id): + # Generate the tokens used by git and the GitHub API + app_id = github_app_id + signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8')) + + payload = { + # Issued at time + 'iat': int(time.time()), + # JWT expiration time (10 minutes maximum) + 'exp': int(time.time()) + 600, + # GitHub App's identifier + 'iss': app_id + } + + # Create JWT + jwt_instance = jwt.JWT() + encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256') + + # Create access token + url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens' + headers = { + 'Authorization': 'Bearer ' + encoded_jwt, + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + } + request = urllib.request.Request(url, headers=headers, method='POST') + response = urllib.request.urlopen(request) + response_json = json.loads(response.read().decode()) + return response_json['token'] + + +def verify_new_repo(token, cac_org, new_repo): + # Attempt to view the new repo + try: + url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo + headers = { + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer ' + token, + 'X-GitHub-Api-Version': '2022-11-28' + } + request = urllib.request.Request(url, headers=headers) + urllib.request.urlopen(request) + return True + except: + return False + + +def encrypt(public_key_for_repo: str, secret_value_input: str) -> str: + \"\"\"Encrypt a Unicode string using the public key.\"\"\" + sealed_box = public.SealedBox(public.PublicKey(public_key_for_repo.encode(\"utf-8\"), encoding.Base64Encoder())) + encrypted = sealed_box.encrypt(secret_value_input.encode(\"utf-8\")) + return b64encode(encrypted).decode(\"utf-8\") + + +def get_public_key(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str) -> (str, str): + public_key_endpoint: str = f\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/public-key\" + headers: TypedDict[str, str] = {\"Authorization\": f\"Bearer {gh_auth_token}\"} + response = requests.get(url=public_key_endpoint, headers=headers) + if response.status_code != 200: + raise IOError( + f\"Could not get public key for repository {gh_owner}/{gh_repo}. The Response code was {response.status_code}\") + + public_key_json = response.json() + return public_key_json['key_id'], public_key_json['key'] + + +def set_secret(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str, public_key_id: str, secret_key: str, + encrypted_secret_value: str): + secret_creation_url = f\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/{secret_key}\" + secret_creation_body = {\"key_id\": public_key_id, \"encrypted_value\": encrypted_secret_value} + headers: TypedDict[str, str] = {\"Authorization\": f\"Bearer {gh_auth_token}\", \"Content-Type\": \"application/json\"} + + secret_creation_response = requests.put(url=secret_creation_url, json=secret_creation_body, headers=headers) + if secret_creation_response.status_code == 201 or secret_creation_response.status_code == 204: + print(\"--Secret Created / Updated!--\") + else: + print(f\"-- Error creating / updating github secret, the reason was : {secret_creation_response.reason}\") + + +parser, _ = init_argparse() + +if not parser.git_password.strip() and not ( + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()): + print(\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\") + sys.exit(1) + +if not parser.git_organization.strip(): + print(\"You must define the organization\") + sys.exit(1) + +if not parser.repo.strip(): + print(\"You must define the repo name\") + sys.exit(1) + +token = generate_github_token(parser.github_app_id, parser.github_app_private_key, + parser.github_app_installation_id) if len( + parser.git_password.strip()) == 0 else parser.git_password.strip() + +if not parser.git_password.strip() and not ( + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()): + print(\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\") + sys.exit(1) + +if not parser.git_organization.strip(): + print(\"You must define the organization\") + sys.exit(1) + +if not parser.repo.strip(): + print(\"You must define the repo name\") + sys.exit(1) + +if not parser.secret_name.strip(): + print(\"You must define the secret name\") + sys.exit(1) + +if not verify_new_repo(token, parser.git_organization, parser.repo): + print(\"Could not find the repo\") + sys.exit(1) + +key_id, public_key = get_public_key('https://api.github.com/repos', parser.git_organization, parser.repo, + token) +encrypted_secret: str = encrypt(public_key_for_repo=public_key, secret_value_input=parser.secret_value) +set_secret(gh_base_url='https://api.github.com/repos', gh_owner=parser.git_organization, gh_repo=parser.repo, + gh_auth_token=token, public_key_id=key_id, secret_key=parser.secret_name, + encrypted_secret_value=encrypted_secret) +" + }, + "Parameters": [ + { + "Id": "6470d539-d137-42dd-bff2-9ebc1955b2a7", + "Name": "CreateGitHubSecret.GitHub.Secret.Name", + "Label": "Secret Name", + "HelpText": "The name of the GitHub secret.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2ead2e13-1a01-4360-9585-f920181880a1", + "Name": "CreateGitHubSecret.GitHub.Secret.Value", + "Label": "Secret Value", + "HelpText": "The secret value.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "643b6230-5cca-4ca2-8340-9624ed721b9b", + "Name": "CreateGitHubSecret.Git.Url.Repo", + "Label": "GitHub Repo Name", + "HelpText": "The GitHub repo name i.e. `myrepo` in the URL`https://github.com/owner/myrepo`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8c506ad7-f8b9-458c-a53b-23a19984c413", + "Name": "CreateGitHubSecret.Git.Url.Organization", + "Label": "Github Owner", + "HelpText": "The GitHub repo owner or organization i.e. `owner` in the URL `https://github.com/owner/myrepo`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a457faef-6cd8-4c1b-ad23-306b03419794", + "Name": "CreateGitHubSecret.Git.Credentials.Password", + "Label": "GitHub Access Token", + "HelpText": "The GitHub access token", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-10-19T01:59:46.768Z", + "OctopusVersion": "2023.4.6357", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "github" +} diff --git a/step-templates/github-fork-repo.json.human b/step-templates/github-fork-repo.json.human new file mode 100644 index 000000000..7b7de829b --- /dev/null +++ b/step-templates/github-fork-repo.json.human @@ -0,0 +1,504 @@ +{ + "Id": "f79befdd-9042-4e49-a4d9-164fc8daf8d6", + "Name": "GitHub - Fork Repo", + "Description": "Forks a repo in GitHub and returns the new repo URL in the output variable `NewRepo`. If the new repo already exists, it is not modified.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid +# having to use a regular user account. +import subprocess +import sys + +# Install our own dependencies +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt']) + +import json +import subprocess +import sys +import os +import urllib.request +import base64 +import re +import jwt +import time +import argparse +import platform +from urllib.request import urlretrieve + +# If this script is not being run as part of an Octopus step, setting variables is a noop +if 'set_octopusvariable' not in globals(): + def set_octopusvariable(variable, value): + pass + +# If this script is not being run as part of an Octopus step, return variables from environment variables. +# Periods are replaced with underscores, and the variable name is converted to uppercase +if \"get_octopusvariable\" not in globals(): + def get_octopusvariable(variable): + return os.environ[re.sub('\\\\.', '_', variable.upper())] + +# If this script is not being run as part of an Octopus step, print directly to std out. +if 'printverbose' not in globals(): + def printverbose(msg): + print(msg) + + +def printverbose_noansi(output): + \"\"\" + Strip ANSI color codes and print the output as verbose + :param output: The output to print + \"\"\" + output_no_ansi = re.sub('\\x1b\\[[0-9;]*m', '', output) + printverbose(output_no_ansi) + + +def get_octopusvariable_quiet(variable): + \"\"\" + Gets an octopus variable, or an empty string if it does not exist. + :param variable: The variable name + :return: The variable value, or an empty string if the variable does not exist + \"\"\" + try: + return get_octopusvariable(variable) + except: + return '' + + +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi, raise_on_non_zero=False): + \"\"\" + The execute method provides the ability to execute external processes while capturing and returning the + output to std err and std out and exit code. + \"\"\" + process = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=cwd, + env=env) + stdout, stderr = process.communicate() + retcode = process.returncode + + if not retcode == 0 and raise_on_non_zero: + raise Exception('command returned exit code ' + retcode) + + if print_args is not None: + print_output(' '.join(args)) + + if print_output is not None: + print_output(stdout) + print_output(stderr) + + return stdout, stderr, retcode + + +def init_argparse(): + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTION] [FILE]...', + description='Fork a GitHub repo' + ) + parser.add_argument('--new-repo-name', action='store', + default=get_octopusvariable_quiet( + 'ForkGithubRepo.Git.Url.NewRepoName') or get_octopusvariable_quiet( + 'Exported.Project.Name')) + parser.add_argument('--new-repo-name-prefix', action='store', + default=get_octopusvariable_quiet( + 'ForkGithubRepo.Git.Url.NewRepoNamePrefix') or get_octopusvariable_quiet( + 'Git.Url.NewRepoNamePrefix')) + parser.add_argument('--template-repo-name', action='store', + default=get_octopusvariable_quiet( + 'ForkGithubRepo.Git.Url.OriginalRepoName') or + re.sub('[^a-zA-Z0-9-]', '_', get_octopusvariable_quiet('Octopus.Project.Name'))) + parser.add_argument('--tenant-name', action='store', + default=get_octopusvariable_quiet('Octopus.Deployment.Tenant.Name')) + parser.add_argument('--git-organization', action='store', + default=get_octopusvariable_quiet( + 'ForkGithubRepo.Git.Url.Organization') or get_octopusvariable_quiet('Git.Url.Organization')) + parser.add_argument('--mainline-branch', + action='store', + default=get_octopusvariable_quiet( + 'ForkGiteaRepo.Git.Branch.MainLine') or get_octopusvariable_quiet('Git.Branch.MainLine'), + help='The branch name to use for the fork. Defaults to \"main\".') + parser.add_argument('--github-app-id', action='store', + default=get_octopusvariable_quiet( + 'ForkGithubRepo.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id')) + parser.add_argument('--github-app-installation-id', action='store', + default=get_octopusvariable_quiet( + 'ForkGithubRepo.GitHub.App.InstallationId') or get_octopusvariable_quiet( + 'GitHub.App.InstallationId')) + parser.add_argument('--github-app-private-key', action='store', + default=get_octopusvariable_quiet( + 'ForkGithubRepo.GitHub.App.PrivateKey') or get_octopusvariable_quiet( + 'GitHub.App.PrivateKey')) + parser.add_argument('--github-access-token', action='store', + default=get_octopusvariable_quiet( + 'ForkGithubRepo.GitHub.Credentials.AccessToken') or get_octopusvariable_quiet( + 'GitHub.Credentials.AccessToken'), + help='The GitHub access token. This takes precedence over the --github-app-id, --github-app-installation-id, and --github-app-private-key') + + return parser.parse_known_args() + + +def generate_github_token(github_app_id, github_app_private_key, github_app_installation_id): + # Generate the tokens used by git and the GitHub API + app_id = github_app_id + signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8')) + + payload = { + # Issued at time + 'iat': int(time.time()), + # JWT expiration time (10 minutes maximum) + 'exp': int(time.time()) + 600, + # GitHub App's identifier + 'iss': app_id + } + + # Create JWT + jwt_instance = jwt.JWT() + encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256') + + # Create access token + url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens' + headers = { + 'Authorization': 'Bearer ' + encoded_jwt, + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + } + request = urllib.request.Request(url, headers=headers, method='POST') + response = urllib.request.urlopen(request) + response_json = json.loads(response.read().decode()) + return response_json['token'] + + +def generate_auth_header(token): + auth = base64.b64encode(('x-access-token:' + token).encode('ascii')) + return 'Basic ' + auth.decode('ascii') + + +def verify_template_repo(token, cac_org, template_repo): + # Attempt to view the template repo + url = 'https://api.github.com/repos/' + cac_org + '/' + template_repo + try: + headers = { + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer ' + token, + 'X-GitHub-Api-Version': '2022-11-28' + } + request = urllib.request.Request(url, headers=headers) + urllib.request.urlopen(request) + except: + print('Could not find the template repo at ' + url) + print('Check that the repo exists, and that the authentication credentials are correct') + sys.exit(1) + + +def verify_new_repo(token, cac_org, new_repo): + # Attempt to view the new repo + try: + url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo + headers = { + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer ' + token, + 'X-GitHub-Api-Version': '2022-11-28' + } + request = urllib.request.Request(url, headers=headers) + urllib.request.urlopen(request) + return True + except: + return False + + +def create_new_repo(token, cac_org, new_repo): + # If we could not view the repo, assume it needs to be created. + # https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#create-an-organization-repository + # Note you have to use the token rather than the JWT: + # https://stackoverflow.com/questions/39600396/bad-credentails-for-jwt-for-github-integrations-api + + headers = { + 'Authorization': 'token ' + token, + 'Content-Type': 'application/json', + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + } + + try: + # First try to create an organization repo: + # https://docs.github.com/en/free-pro-team@latest/rest/repos/repos#create-an-organization-repository + url = 'https://api.github.com/orgs/' + cac_org + '/repos' + body = {'name': new_repo} + request = urllib.request.Request(url, headers=headers, data=json.dumps(body).encode('utf-8')) + urllib.request.urlopen(request) + except urllib.error.URLError as ex: + # Then fall back to creating a repo for the user: + # https://docs.github.com/en/free-pro-team@latest/rest/repos/repos?apiVersion=2022-11-28#create-a-repository-for-the-authenticated-user + if ex.code == 404: + url = 'https://api.github.com/user/repos' + body = {'name': new_repo} + request = urllib.request.Request(url, headers=headers, data=json.dumps(body).encode('utf-8')) + urllib.request.urlopen(request) + else: + raise ex + + +def fork_repo(git_executable, token, cac_org, new_repo, template_repo): + # Clone the repo and add the upstream repo + _, _, retcode = execute([git_executable, 'clone', 'https://' + 'x-access-token:' + token + '@' + + 'github.com/' + cac_org + '/' + new_repo + '.git']) + + if not retcode == 0: + print('Failed to clone repo ' + 'https://github.com/' + cac_org + '/' + new_repo + '.git.' + + ' Check the verbose logs for details.') + sys.exit(1) + + _, _, retcode = execute( + [git_executable, 'remote', 'add', 'upstream', 'https://' + 'x-access-token:' + token + '@' + + 'github.com/' + cac_org + '/' + template_repo + '.git'], + cwd=new_repo) + + if not retcode == 0: + print('Failed to add remote ' + 'https://github.com/' + cac_org + '/' + template_repo + '.git. ' + + 'Check the verbose logs for details.') + sys.exit(1) + + _, _, retcode = execute([git_executable, 'fetch', '--all'], cwd=new_repo) + + if not retcode == 0: + print('Failed to fetch. Check the verbose logs for details.') + sys.exit(1) + + _, _, retcode = execute(['git', 'checkout', '-b', 'upstream-' + branch, 'upstream/' + branch], cwd=new_repo) + + if not retcode == 0: + print('Failed to checkout branch ' + branch + '. Check the verbose logs for details.') + sys.exit(1) + + if branch != 'master' and branch != 'main': + _, _, retcode = execute(['git', 'checkout', '-b', branch, 'origin/' + branch], cwd=new_repo) + else: + _, _, retcode = execute(['git', 'checkout', branch], cwd=new_repo) + + if not retcode == 0: + print('Failed to checkout branch ' + branch + '. Check the verbose logs for details.') + sys.exit(1) + + # Hard reset it to the template main branch. + _, _, retcode = execute([git_executable, 'reset', '--hard', 'upstream/' + branch], cwd=new_repo) + + if not retcode == 0: + print( + 'Failed to perform a hard reset against branch upstream/' + branch + '.' + + ' Check the verbose logs for details.') + sys.exit(1) + + # Push the changes. + _, _, retcode = execute([git_executable, 'push', 'origin', branch], cwd=new_repo) + + if not retcode == 0: + print('Failed to push changes. Check the verbose logs for details.') + sys.exit(1) + + +def is_windows(): + return platform.system() == 'Windows' + + +def ensure_git_exists(): + if is_windows(): + print(\"Checking git is installed\") + try: + stdout, _, exit_code = execute(['git', 'version']) + printverbose(stdout) + if not exit_code == 0: + raise \"git not found\" + except: + print(\"Downloading git\") + urlretrieve('https://www.7-zip.org/a/7zr.exe', '7zr.exe') + urlretrieve( + 'https://github.com/git-for-windows/git/releases/download/v2.42.0.windows.2/PortableGit-2.42.0.2-64-bit.7z.exe', + 'PortableGit.7z.exe') + print(\"Installing git\") + print(\"Consider installing git on the worker or using a standard worker-tools image\") + execute(['7zr.exe', 'x', 'PortableGit.7z.exe', '-o' + os.getcwd() + '\\\\git', '-y']) + return os.getcwd() + '\\\\git\\\\bin\\\\git' + + return 'git' + + +git_executable = ensure_git_exists() +parser, _ = init_argparse() + +if not parser.github_access_token.strip() and not ( + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()): + print(\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\") + sys.exit(1) + +if not parser.template_repo_name.strip(): + print(\"You must supply the upstream (or template) repo\") + sys.exit(1) + +if not parser.tenant_name.strip() and not parser.new_repo_name_prefix.strip(): + print(\"You must define the new repo prefix or run this script against a tenant\") + sys.exit(1) + +# The access token is generated from a github app or supplied directly as an access token +token = generate_github_token(parser.github_app_id, parser.github_app_private_key, + parser.github_app_installation_id) if len( + parser.github_access_token.strip()) == 0 else parser.github_access_token.strip() + +# The process followed here is: +# 1. Verify the manually supplied upstream repo exists +# 2. Build the name of the new downstream repo with a prefix and the new project name. +# a. The prefix is either specified or assumed to be the name of a tenant +# b. The new project name is either specified or assumed to be the same as the upstream project name +# 3. Create a new downstream repo if it doesn't exist +# 4. Fork the upstream repo into the downstream repo with a hard git reset + +cac_org = parser.git_organization.strip() +template_repo = parser.template_repo_name.strip() +new_repo_custom_prefix = re.sub('[^a-zA-Z0-9-]', '_', parser.new_repo_name_prefix.strip()) +tenant_name_sanitized = re.sub('[^a-zA-Z0-9-]', '_', parser.tenant_name.strip()) +project_repo_sanitized = re.sub('[^a-zA-Z0-9-]', '_', + parser.new_repo_name.strip() if parser.new_repo_name.strip() else template_repo) + +# The new repo is prefixed either with the custom prefix or the tenant name if no custom prefix is defined +new_repo_prefix = new_repo_custom_prefix if len(new_repo_custom_prefix) != 0 else tenant_name_sanitized + +# The new repo name is the prefix + the name of thew new project +new_repo = new_repo_prefix + '_' + project_repo_sanitized if len(new_repo_prefix) != 0 else project_repo_sanitized + +# Assume the main branch if nothing else was specified +branch = parser.mainline_branch or 'main' + +# This is the value of the forked git repo +set_octopusvariable('NewRepo', 'https://github.com/' + cac_org + '/' + new_repo) + +verify_template_repo(token, cac_org, template_repo) + +if not verify_new_repo(token, cac_org, new_repo): + create_new_repo(token, cac_org, new_repo) + fork_repo(git_executable, token, cac_org, new_repo, template_repo) + print( + 'Repo was forked from ' + 'https://github.com/' + cac_org + '/' + template_repo + ' to ' + + 'https://github.com/' + cac_org + '/' + new_repo) +else: + print('Repo at https://github.com/' + cac_org + '/' + new_repo + ' already exists and has not been modified') + +print('New repo URL is defined in the output variable \"NewRepo\": #{Octopus.Action[' + + get_octopusvariable_quiet('Octopus.Step.Name') + '].Output.NewRepo}') +", + "Octopus.Action.Script.Syntax": "Python" + }, + "Parameters": [ + { + "Id": "d6b60ef3-0dc1-4c01-86b9-b3b65a266acf", + "Name": "ForkGithubRepo.Git.Url.OriginalRepoName", + "Label": "Original Repo Name", + "HelpText": "This is the name of the original repo that is forked. The full URL of the original repo is `http://github.com/organization/`. + +This value defaults to the name of the project if not defined.", + "DefaultValue": "#{Octopus.Project.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a4c8c416-591f-4bc5-bf76-041064d8993b", + "Name": "ForkGithubRepo.Git.Url.NewRepoName", + "Label": "Forked Repo Name", + "HelpText": "This value is used to generate the name of the forked repo. The full URL of the original repo is `http://github.com/organization/_`. + +The `Forked Repo Name` defaults to the `Original Repo Name` if it is left blank. + +The forked repo name is sanitized to replace non-alpha-numeric or dash characters with an underscore.", + "DefaultValue": "#{Octopus.Project.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f829046a-8f59-4c53-8122-8281b6329363", + "Name": "ForkGithubRepo.Git.Url.NewRepoNamePrefix", + "Label": "Forked Repo Prefix", + "HelpText": "This prefix is prepended to the name of the forked repo. The final name of the forked repo is `_`. If a prefix is not specified, it is assumed to be the tenant's name running this script. + +You must supply a prefix or run the script as a tenant. + +The forked repo prefix is sanitized to replace non-alpha-numeric or dash characters with an underscore.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3fcfc3ff-ffbe-4d9f-a078-bf4394d2cf3b", + "Name": "ForkGithubRepo.Git.Url.Organization", + "Label": "GitHub Organization", + "HelpText": "This is the GitHub Organization or username that the source and destination repos are found in. It is the field `organization` in the URL `https://github.com/organization`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3972a9b5-ff11-4336-afff-0ed92b7e111c", + "Name": "ForkGiteaRepo.Git.Branch.MainLine", + "Label": "Mainline Branch", + "HelpText": "The name of the branch to fork.", + "DefaultValue": "main", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e6c3dc14-af61-4c3f-acc9-75f77cef9a6a", + "Name": "ForkGithubRepo.GitHub.Credentials.AccessToken", + "Label": "GitHub Access Token", + "HelpText": "This is the [access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) used to authenticate with GitHub. Leave this blank and fill in the App ID, Installation ID, and Private Key to use a GitHub for authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "b19c2e88-1418-4a4c-8088-048007da5ffb", + "Name": "ForkGithubRepo.GitHub.App.Id", + "Label": "GitHub App ID", + "HelpText": "If a [GitHub App](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps) is used for authentication, this is the application ID. Leave this blank and supply the `GitHub Access Token` field to use regular token authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ce37e390-a6df-4782-9166-d7a22c8e6a50", + "Name": "ForkGithubRepo.GitHub.App.InstallationId", + "Label": "GitHub App Installation ID", + "HelpText": "If a [GitHub App](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps) is used for authentication, this is the installation ID. Leave this blank and supply the `GitHub Access Token` field to use regular token authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1b7ad3bb-f12c-40e2-b062-8fdbfc1259e6", + "Name": "ForkGithubRepo.GitHub.App.PrivateKey", + "Label": "GitHub App Private Key", + "HelpText": "If a [GitHub App](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps) is used for authentication, this is the private key. Leave this blank and supply the `GitHub Access Token` field to use regular token authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-09-18T21:46:58.212Z", + "OctopusVersion": "2023.4.2775", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "github" +} diff --git a/step-templates/github-push-yeoman-to-repo.json.human b/step-templates/github-push-yeoman-to-repo.json.human new file mode 100644 index 000000000..64b1eaa7f --- /dev/null +++ b/step-templates/github-push-yeoman-to-repo.json.human @@ -0,0 +1,631 @@ +{ + "Id": "14ffc4b7-1cab-4f81-a835-7da41fa47123", + "Name": "GitHub - Push Yeoman Generator", + "Description": "Clones a GitHub repo, runs Yeoman in the cloned directory, and pushes the changes. Note that the Yeoman generators can only use arguments or options, as prompts can not be provided.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "6bd0fbd2-442e-4ae9-9192-b93a041cfdd1", + "Name": "YeomanGenerator", + "AcquisitionLocation": "Server", + "FeedId": null, + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "PopulateGithubRepo.Yeoman.Generator.Package", + "Purpose": "" + } + } + ], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Python", + "Octopus.Action.Script.ScriptBody": "# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid +# having to use a regular user account. +import subprocess +import sys + +# Install our own dependencies +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt', '--disable-pip-version-check']) +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'requests', '--disable-pip-version-check']) +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'anyascii', '--disable-pip-version-check']) + +import requests +import json +import subprocess +import sys +import os +import urllib.request +import base64 +import re +import jwt +import time +import argparse +import platform +import zipfile +import lzma +import tarfile +import shutil +import urllib3 +from shlex import split +from anyascii import anyascii + +# Disable insecure http request warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# If this script is not being run as part of an Octopus step, setting variables is a noop +if 'set_octopusvariable' not in globals(): + def set_octopusvariable(variable, value): + pass + +# If this script is not being run as part of an Octopus step, return variables from environment variables. +# Periods are replaced with underscores, and the variable name is converted to uppercase +if \"get_octopusvariable\" not in globals(): + def get_octopusvariable(variable): + return os.environ[re.sub('\\\\.', '_', variable.upper())] + +# If this script is not being run as part of an Octopus step, print directly to std out. +if 'printverbose' not in globals(): + def printverbose(msg): + print(msg) + + +def printverbose_noansi(output): + \"\"\" + Strip ANSI color codes and print the output as verbose + :param output: The output to print + \"\"\" + output_no_ansi = re.sub(r'\\x1b\\[[0-9;]*m', '', output) + printverbose(output_no_ansi) + + +def get_octopusvariable_quiet(variable): + \"\"\" + Gets an octopus variable, or an empty string if it does not exist. + :param variable: The variable name + :return: The variable value, or an empty string if the variable does not exist + \"\"\" + try: + return get_octopusvariable(variable) + except Exception as inst: + return '' + + +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi, raise_on_non_zero=False, + append_to_path=None): + \"\"\" + The execute method provides the ability to execute external processes while capturing and returning the + output to std err and std out and exit code. + \"\"\" + + my_env = os.environ.copy() if env is None else env + + if append_to_path is not None: + my_env[\"PATH\"] = append_to_path + os.pathsep + my_env['PATH'] + + process = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=open(os.devnull), + text=True, + cwd=cwd, + env=my_env) + stdout, stderr = process.communicate() + retcode = process.returncode + + if not retcode == 0 and raise_on_non_zero: + raise Exception('command returned exit code ' + retcode) + + if print_args is not None: + print_output(' '.join(args)) + + if print_output is not None: + print_output(stdout) + print_output(stderr) + + return stdout, stderr, retcode + + +def init_argparse(): + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTION]', + description='Fork a GitHub repo' + ) + parser.add_argument('--generator', action='store', + default=get_octopusvariable_quiet( + 'PopulateGithubRepo.Yeoman.Generator.Name') or get_octopusvariable_quiet( + 'Yeoman.Generator.Name')) + parser.add_argument('--sub-generator', action='store', + default=get_octopusvariable_quiet( + 'PopulateGithubRepo.Yeoman.Generator.SubGenerator') or get_octopusvariable_quiet( + 'Yeoman.Generator.SubGenerator')) + parser.add_argument('--generator-arguments', action='store', + default=get_octopusvariable_quiet( + 'PopulateGithubRepo.Yeoman.Generator.Arguments') or get_octopusvariable_quiet( + 'Yeoman.Generator.Arguments'), + help='The arguments to pas to yo. Pass all arguments as a single string. This string is then parsed as if it were yo arguments.') + parser.add_argument('--repo', action='store', + default=get_octopusvariable_quiet( + 'PopulateGithubRepo.Git.Url.Repo') or get_octopusvariable_quiet( + 'Git.Url.Repo')) + parser.add_argument('--git-organization', action='store', + default=get_octopusvariable_quiet( + 'PopulateGithubRepo.Git.Url.Organization') or get_octopusvariable_quiet( + 'Git.Url.Organization')) + parser.add_argument('--github-app-id', action='store', + default=get_octopusvariable_quiet( + 'PopulateGithubRepo.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id')) + parser.add_argument('--github-app-installation-id', action='store', + default=get_octopusvariable_quiet( + 'PopulateGithubRepo.GitHub.App.InstallationId') or get_octopusvariable_quiet( + 'GitHub.App.InstallationId')) + parser.add_argument('--github-app-private-key', action='store', + default=get_octopusvariable_quiet( + 'PopulateGithubRepo.GitHub.App.PrivateKey') or get_octopusvariable_quiet( + 'GitHub.App.PrivateKey')) + parser.add_argument('--git-password', action='store', + default=get_octopusvariable_quiet( + 'PopulateGithubRepo.Git.Credentials.Password') or get_octopusvariable_quiet( + 'Git.Credentials.Password'), + help='The git password. This takes precedence over the --github-app-id, --github-app-installation-id, and --github-app-private-key') + parser.add_argument('--git-username', action='store', + default=get_octopusvariable_quiet( + 'PopulateGithubRepo.Git.Credentials.Username') or get_octopusvariable_quiet( + 'Git.Credentials.Username'), + help='The git username. This will be used for both the git authentication and the username associated with any commits.') + + return parser.parse_known_args() + + +def generate_github_token(github_app_id, github_app_private_key, github_app_installation_id): + # Generate the tokens used by git and the GitHub API + app_id = github_app_id + signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8')) + + payload = { + # Issued at time + 'iat': int(time.time()), + # JWT expiration time (10 minutes maximum) + 'exp': int(time.time()) + 600, + # GitHub App's identifier + 'iss': app_id + } + + # Create JWT + jwt_instance = jwt.JWT() + encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256') + + # Create access token + url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens' + headers = { + 'Authorization': 'Bearer ' + encoded_jwt, + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + } + request = urllib.request.Request(url, headers=headers, method='POST') + response = urllib.request.urlopen(request) + response_json = json.loads(response.read().decode()) + return response_json['token'] + + +def generate_auth_header(token): + auth = base64.b64encode(('x-access-token:' + token).encode('ascii')) + return 'Basic ' + auth.decode('ascii') + + +def verify_new_repo(token, cac_org, new_repo): + # Attempt to view the new repo + try: + url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo + headers = { + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer ' + token, + 'X-GitHub-Api-Version': '2022-11-28' + } + request = urllib.request.Request(url, headers=headers) + urllib.request.urlopen(request) + return True + except Exception as inst: + return False + + +def is_windows(): + return platform.system() == 'Windows' + + +def download_file(url, filename, verify_ssl=True): + r = requests.get(url, verify=verify_ssl) + with open(filename, 'wb') as file: + file.write(r.content) + + +def ensure_git_exists(): + if is_windows(): + print(\"Checking git is installed\") + try: + stdout, _, exit_code = execute(['git', 'version']) + printverbose(stdout) + if not exit_code == 0: + raise \"git not found\" + except: + print(\"Downloading git\") + download_file('https://www.7-zip.org/a/7zr.exe', '7zr.exe') + download_file( + 'https://github.com/git-for-windows/git/releases/download/v2.42.0.windows.2/PortableGit-2.42.0.2-64-bit.7z.exe', + 'PortableGit.7z.exe') + print(\"Installing git\") + print(\"Consider installing git on the worker or using a standard worker-tools image\") + execute(['7zr.exe', 'x', 'PortableGit.7z.exe', '-o' + os.path.join(os.getcwd(), 'git'), '-y']) + return os.path.join(os.getcwd(), 'git', 'bin', 'git') + + return 'git' + + +def install_npm_linux(): + print(\"Downloading node\") + download_file( + 'https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz', + 'node.tar.xz') + print(\"Installing node on Linux\") + with lzma.open(\"node.tar.xz\", \"r\") as lzma_ref: + with open(\"node.tar\", \"wb\") as fdst: + shutil.copyfileobj(lzma_ref, fdst) + with tarfile.open(\"node.tar\", \"r\") as tar_ref: + tar_ref.extractall(os.getcwd()) + + try: + _, _, exit_code = execute([os.getcwd() + '/node-v18.18.2-linux-x64/bin/npm', '--version'], + append_to_path=os.getcwd() + '/node-v18.18.2-linux-x64/bin') + if not exit_code == 0: + raise Exception(\"Failed to run npm\") + except Exception as ex: + print('Failed to install npm ' + str(ex)) + sys.exit(1) + return os.getcwd() + '/node-v18.18.2-linux-x64/bin/npm', os.getcwd() + '/node-v18.18.2-linux-x64/bin' + + +def install_npm_windows(): + print(\"Downloading node\") + download_file('https://nodejs.org/dist/v18.18.2/node-v18.18.2-win-x64.zip', 'node.zip', False) + print(\"Installing node on Windows\") + with zipfile.ZipFile(\"node.zip\", \"r\") as zip_ref: + zip_ref.extractall(os.getcwd()) + try: + _, _, exit_code = execute([os.path.join(os.getcwd(), 'node-v18.18.2-win-x64', 'npm.cmd'), '--version'], + append_to_path=os.path.join(os.getcwd(), 'node-v18.18.2-win-x64')) + if not exit_code == 0: + raise Exception(\"Failed to run npm\") + except Exception as ex: + print('Failed to install npm ' + str(ex)) + sys.exit(1) + + return (os.path.join(os.getcwd(), 'node-v18.18.2-win-x64', 'npm.cmd'), + os.path.join(os.getcwd(), 'node-v18.18.2-win-x64')) + + +def ensure_node_exists(): + try: + print(\"Checking node is installed\") + _, _, exit_code = execute(['npm', '--version']) + if not exit_code == 0: + raise Exception(\"npm not found\") + except: + if is_windows(): + return install_npm_windows() + else: + return install_npm_linux() + + return 'npm', None + + +def ensure_yo_exists(npm_executable, npm_path): + try: + print(\"Checking Yeoman is installed\") + _, _, exit_code = execute(['yo', '--version']) + if not exit_code == 0: + raise Exception(\"yo not found\") + except: + print('Installing Yeoman') + + _, _, retcode = execute([npm_executable, 'install', '-g', 'yo'], append_to_path=npm_path) + + if not retcode == 0: + print(\"Failed to set install Yeoman. Check the verbose logs for details.\") + sys.exit(1) + + npm_bin, _, retcode = execute([npm_executable, 'config', 'get', 'prefix'], append_to_path=npm_path) + + if not retcode == 0: + print(\"Failed to set get the npm prefix directory. Check the verbose logs for details.\") + sys.exit(1) + + try: + if is_windows(): + _, _, exit_code = execute([os.path.join(npm_bin.strip(), 'yo.cmd'), '--version'], + append_to_path=npm_path) + else: + _, _, exit_code = execute([os.path.join(npm_bin.strip(), 'bin', 'yo'), '--version'], + append_to_path=npm_path) + + if not exit_code == 0: + raise Exception(\"Failed to run yo\") + except Exception as ex: + print('Failed to install yo ' + str(ex)) + sys.exit(1) + + # Windows and Linux save NPM binaries in different directories + if is_windows(): + return os.path.join(npm_bin.strip(), 'yo.cmd') + + return os.path.join(npm_bin.strip(), 'bin', 'yo') + + return 'yo' + + +git_executable = ensure_git_exists() +npm_executable, npm_path = ensure_node_exists() +yo_executable = ensure_yo_exists(npm_executable, npm_path) +parser, _ = init_argparse() + +if not parser.git_password.strip() and not ( + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()): + print(\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\") + sys.exit(1) + +if not parser.git_organization.strip(): + print(\"You must define the organization\") + sys.exit(1) + +if not parser.repo.strip(): + print(\"You must define the repo name\") + sys.exit(1) + +if not parser.generator.strip(): + print(\"You must define the Yeoman generator\") + sys.exit(1) + +# Create a dir for the git clone +if os.path.exists('downstream'): + shutil.rmtree('downstream') + +os.mkdir('downstream') + +# Create a dir for yeoman to use +if os.path.exists('downstream-yeoman'): + shutil.rmtree('downstream-yeoman') + +os.mkdir('downstream-yeoman') +# Yeoman will use a less privileged user to write to this directory, so grant full access +if not is_windows(): + os.chmod('downstream-yeoman', 0o777) + +downstream_dir = os.path.join(os.getcwd(), 'downstream') +downstream_yeoman_dir = os.path.join(os.getcwd(), 'downstream-yeoman') + +# The access token is generated from a github app or supplied directly as an access token +token = generate_github_token(parser.github_app_id, parser.github_app_private_key, + parser.github_app_installation_id) if len( + parser.git_password.strip()) == 0 else parser.git_password.strip() + +if not verify_new_repo(token, parser.git_organization, parser.repo): + print('Repo at https://github.com/' + parser.git_organization + '/' + parser.repo + ' could not be accessed') + sys.exit(1) + +# We need to disable the credentials helper prompt, which will pop open a GUI prompt that we can never close +if is_windows(): + _, _, retcode = execute([git_executable, 'config', '--system', 'credential.helper', 'manager']) + + if not retcode == 0: + print(\"Failed to set the credential.helper setting. Check the verbose logs for details.\") + sys.exit(1) + + _, _, retcode = execute([git_executable, 'config', '--system', 'credential.modalprompt', 'false']) + + if not retcode == 0: + print(\"Failed to srt the credential.modalprompt setting. Check the verbose logs for details.\") + sys.exit(1) + + # We need to disable the credentials helper prompt, which will pop open a GUI prompt that we can never close + _, _, retcode = execute( + [git_executable, 'config', '--system', 'credential.microsoft.visualstudio.com.interactive', 'never']) + + if not retcode == 0: + print( + \"Failed to set the credential.microsoft.visualstudio.com.interactive setting. Check the verbose logs for details.\") + sys.exit(1) + +_, _, retcode = execute([git_executable, 'config', '--global', 'user.email', 'octopus@octopus.com']) + +if not retcode == 0: + print(\"Failed to set the user.email setting. Check the verbose logs for details.\") + sys.exit(1) + +_, _, retcode = execute([git_executable, 'config', '--global', 'core.autocrlf', 'input']) + +if not retcode == 0: + print(\"Failed to set the core.autocrlf setting. Check the verbose logs for details.\") + sys.exit(1) + +username = parser.git_username if len(parser.git_username) != 0 else 'Octopus' +_, _, retcode = execute([git_executable, 'config', '--global', 'user.name', username]) + +if not retcode == 0: + print(\"Failed to set the git username. Check the verbose logs for details.\") + sys.exit(1) + +_, _, retcode = execute([git_executable, 'config', '--global', 'credential.helper', 'cache']) + +if not retcode == 0: + print(\"Failed to set the git credential helper. Check the verbose logs for details.\") + sys.exit(1) + +print('Cloning repo') + +_, _, retcode = execute( + [git_executable, 'clone', + 'https://' + username + ':' + token + '@github.com/' + parser.git_organization + '/' + parser.repo + '.git', + 'downstream']) + +if not retcode == 0: + print(\"Failed to clone the git repo. Check the verbose logs for details.\") + sys.exit(1) + +print('Configuring Yeoman Generator') + +_, _, retcode = execute([npm_executable, 'install'], cwd=os.path.join(os.getcwd(), 'YeomanGenerator'), append_to_path=npm_path) + +if not retcode == 0: + print(\"Failed to install the generator dependencies. Check the verbose logs for details.\") + sys.exit(1) + +_, _, retcode = execute([npm_executable, 'link'], cwd=os.path.join(os.getcwd(), 'YeomanGenerator'), append_to_path=npm_path) + +if not retcode == 0: + print(\"Failed to link the npm module. Check the verbose logs for details.\") + sys.exit(1) + +print('Running Yeoman Generator') + +# Treat the string of yo arguments as a raw input and parse it again. The resulting list of unknown arguments +# is then passed to yo. We have to convert the incoming values from utf to ascii when parsing a second time. +yo_args = split(anyascii(parser.generator_arguments)) + +generator_name = parser.generator + ':' + parser.sub_generator if len(parser.sub_generator) != 0 else parser.generator + +yo_arguments = [yo_executable, generator_name, '--force', '--skip-install'] + +# Yeoman has issues running as root, which it will often do in a container. +# So we run Yeoman in its own directory, and then copy the changes to the git directory. +_, _, retcode = execute(yo_arguments + yo_args, cwd=downstream_yeoman_dir, append_to_path=npm_path) + +if not retcode == 0: + print(\"Failed to run Yeoman. Check the verbose logs for details.\") + sys.exit(1) + +shutil.copytree(downstream_yeoman_dir, downstream_dir, dirs_exist_ok=True) + +print('Adding changes to git') + +_, _, retcode = execute([git_executable, 'add', '.'], cwd=downstream_dir) + +if not retcode == 0: + print(\"Failed to add the git changes. Check the verbose logs for details.\") + sys.exit(1) + +# Check for pending changes +_, _, retcode = execute([git_executable, 'diff-index', '--quiet', 'HEAD'], cwd=downstream_dir) + +if not retcode == 0: + print('Committing changes to git') + _, _, retcode = execute([git_executable, 'commit', '-m', + 'Added files from Yeoman generator ' + parser.generator + ':' + parser.sub_generator], + cwd=downstream_dir) + + if not retcode == 0: + print(\"Failed to set commit the git changes. Check the verbose logs for details.\") + sys.exit(1) + + print('Pushing changes to git') + + _, _, retcode = execute([git_executable, 'push', 'origin', 'main'], cwd=downstream_dir) + + if not retcode == 0: + print(\"Failed to push the git changes. Check the verbose logs for details.\") + sys.exit(1) +" + }, + "Parameters": [ + { + "Id": "334f5b32-01a8-4687-af33-54906e53cdee", + "Name": "PopulateGithubRepo.Yeoman.Generator.Package", + "Label": "Yeoman Generator Package", + "HelpText": "The package containing the Yeoman package. Yeoman packages are usually distributed by npm, but they can also be packaged as zip files and pushed to the built-in feed or any other feed that supports zip files.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "d1c9bf99-5f1f-417b-a482-a72878065871", + "Name": "PopulateGithubRepo.Yeoman.Generator.Name", + "Label": "Yeoman Generator Name", + "HelpText": "The name of the Yeoman generator.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2c1dd445-c1d4-4e44-91a0-b7f02682711b", + "Name": "PopulateGithubRepo.Yeoman.Generator.SubGenerator", + "Label": "Yeoman Subgenerator Name", + "HelpText": "The optional name of the Yeoman subgenerator. Leave blank to use the default generator.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bac9200a-2fce-4390-9b07-40e2a14c2e44", + "Name": "PopulateGithubRepo.Yeoman.Generator.Arguments", + "Label": "Yeoman Arguments", + "HelpText": "The optional arguments pass to Yeoman to define options and arguments.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f5c1092a-70d8-40ba-84e9-feedf5911946", + "Name": "PopulateGithubRepo.Git.Url.Repo", + "Label": "GitHub Repo Name", + "HelpText": "The GitHub repo name i.e. `myrepo` in the URL`https://github.com/owner/myrepo`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ece2ee80-9572-4ca1-ab0a-65ffaec2edb7", + "Name": "PopulateGithubRepo.Git.Url.Organization", + "Label": "Github Owner", + "HelpText": "The GitHub repo owner or organization i.e. `owner` in the URL `https://github.com/owner/myrepo`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4795ec27-f38b-4f2f-8417-97c3cd74d2db", + "Name": "PopulateGithubRepo.Git.Credentials.Password", + "Label": "GitHub Access Token", + "HelpText": "The GitHub access token", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "300596fd-59b1-420e-b58f-305b7c83b54d", + "Name": "PopulateGithubRepo.Git.Credentials.Username", + "Label": "GitHub Username", + "HelpText": "This will appear in the commit logs. Leave blank to use the default username.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-10-16T23:49:40.031Z", + "OctopusVersion": "2023.4.6093", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "github" +} + diff --git a/step-templates/github-report-deployment.json.human b/step-templates/github-report-deployment.json.human new file mode 100644 index 000000000..4839050c8 --- /dev/null +++ b/step-templates/github-report-deployment.json.human @@ -0,0 +1,243 @@ +{ + "Id": "fb3137e5-f062-4dcd-9a56-b15321072a21", + "Name": "GitHub - Report Deployment", + "Description": "Creates or updates a deployment using [GitHub Deployments API](https://developer.github.com/v3/repos/deployments/)", + "ActionType": "Octopus.Script", + "Version": 45, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +function InputParameters-Check() { + If ([string]::IsNullOrEmpty($OctopusParameters[\"GitHubHash\"])) { + Write-Host \"No github hash was specified, there's nothing to report then.\" + exit + } + + If ([string]::IsNullOrEmpty($OctopusParameters[\"GitHubUserName\"])) { + Write-Host \"GitHubUserName is not set up, can't report without authentication.\" + exit + } + + If ([string]::IsNullOrEmpty($OctopusParameters[\"GitHubPassword\"])) { + Write-Host \"GitHubPassword is not set up, can't report without authentication.\" + exit + } + + If ([string]::IsNullOrEmpty($OctopusParameters[\"GitHubOwner\"])) { + Write-Host \"GitHubOwner is not set up, can't report without knowing the owner.\" + exit + } + + + If ([string]::IsNullOrEmpty($OctopusParameters[\"GitHubRepoName\"])) { + Write-Host \"GitHubRepoName is not set up, can't report without knowing the repo.\" + exit + } +} + +function Headers-Create ([String] $username, [String] $password) { + $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(\"${username}:${password}\")) + + $headers = @{ + Authorization=(\"Basic {0}\" -f $base64AuthInfo); + Accept=\"application/vnd.github.ant-man-preview+json, application/vnd.github.flash-preview+json\"; + } + return $headers +} + +function GithubDeployment-Create ([String] $owner, [String] $repository, [String] $gitHubHash, [String] $environment, [String] $username, [String] $password) { + $fullRepoName = $owner + \"/\" + $repository + $projectDeploymentsUrl = \"https://api.github.com/repos/$fullRepoName/deployments\" + Write-Host \"Creating a deployment on GitHub on behalf of $username for repo $fullRepoName\" + $headers = Headers-Create -username $username -password $password + + $deploymentCreatePayload = @{ + \"ref\"=$gitHubHash; + \"auto_merge\"=$False; + \"environment\"=$environment; + \"description\"=\"Octopus Deploy\"; + } + + Write-Host \"Calling $projectDeploymentsUrl\" + Write-Host \"Payload:\" + Write-Host ($deploymentCreatePayload | Format-Table | Out-String) + + $newDeployment = Invoke-RestMethod $projectDeploymentsUrl -Method Post -Body ($deploymentCreatePayload|ConvertTo-Json) -Headers $headers + $deploymentId=$newDeployment.id + return $deploymentId +} + +function GithubDeployment-UpdateStatus ([String] $owner, [String] $repository, [String] $deploymentId, [String] $environment, [String] $newStatus, [String] $logLink) { + $fullRepoName = $owner + \"/\" + $repository + $projectDeploymentsUrl = \"https://api.github.com/repos/$fullRepoName/deployments\" + Write-Host \"Setting statuses to $newStatus on $projectDeploymentsUrl\" + $headers = Headers-Create -username $username -password $password + + $statusUpdatePayload = @{ + \"environment\"=$environment; + \"state\"=$newStatus; + \"description\"=\"Octopus Deploy\"; + \"log_url\"=$logLink; + \"environment_url\"=$logLink; + \"auto_inactive\"=$True; + } + + $statusesUrl = \"$projectDeploymentsUrl/$deploymentId/statuses\" + + Write-Host \"Calling $statusesUrl\" + Write-Host \"Payload:\" + Write-Host ($statusUpdatePayload | Format-Table | Out-String) + + $statusResult = Invoke-RestMethod $statusesUrl -Method Post -Body ($statusUpdatePayload|ConvertTo-Json) -Headers $headers + + Write-Host \"Call result:\" + Write-Host ($statusResult | Format-Table | Out-String) + +} + +function Status-Create() { + If ([string]::IsNullOrEmpty($OctopusParameters[\"GitHubStatus\"])) { + $octopusError=$OctopusParameters[\"Octopus.Deployment.Error\"] + $octopusErrorDetail=$OctopusParameters[\"Octopus.Deployment.ErrorDetail\"] + + Write-Host \"Desired status is not specified. Reporting either success or error.\" + Write-Host \"Current Octopus.Deployment.Error is $octopusError\" + Write-Host \"Current Octopus.Deployment.ErrorDetail is $octopusErrorDetail\" + $newStatus = If ([string]::IsNullOrEmpty($octopusError)) { \"success\" } Else { \"failure\" } + Write-Host \"Based on that, the status is going to be $newStatus\" + return $newStatus + } + else { + Write-Host \"Desired status of $newStatus is specified, using it.\" + return $OctopusParameters[\"GitHubStatus\"] + } +} + +$repoOwner=$OctopusParameters[\"GitHubOwner\"] +$repoName=$OctopusParameters[\"GitHubRepoName\"] +$gitHubHash=$OctopusParameters[\"GitHubHash\"] +$username=$OctopusParameters[\"GitHubUserName\"] +$password=$OctopusParameters[\"GitHubPassword\"] +$environment=$OctopusParameters[\"Octopus.Environment.Name\"] +$deploymentLink=$OctopusParameters[\"Octopus.Web.DeploymentLink\"] +$serverUrl=$OctopusParameters[\"#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}\"] -replace \"http://\", \"https://\" +$fullDeploymentLink=\"$serverUrl$deploymentLink\" +$newStatus=$OctopusParameters[\"GitHubStatus\"] +$deploymentId=$OctopusParameters[\"GitHubDeploymentId\"] + +InputParameters-Check + +If ([string]::IsNullOrEmpty($deploymentId)) { + Write-Host \"No deployment id is provided.\" + $deploymentId = GithubDeployment-Create -owner $repoOwner -repository $repoName -gitHubHash $gitHubHash -environment $environment -username $username -password $password + + Write-Host \"Created a deployment on GitHub: #($deploymentId). Exported it as GitHubDeploymentId\" + Set-OctopusVariable -name \"GitHubDeploymentId\" -value $deploymentId +} + +Write-Host \"Using deployment id $deploymentId.\" + +$status = Status-Create +Write-Host \"\" +Write-Host \"\" + +GithubDeployment-UpdateStatus -owner $repoOwner -repository $repoName -deploymentId $deploymentId -environment $environment -newStatus $status -logLink $fullDeploymentLink +" + }, + "Parameters": [ + { + "Id": "51ba3ed4-5a81-46b8-af97-eec3de9fcec8", + "Name": "GitHubUserName", + "Label": "GitHub user", + "HelpText": "A username to be used to access GitHub.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7e0ae992-f878-473f-b51a-f53ead92ff4f", + "Name": "GitHubPassword", + "Label": "GitHub password", + "HelpText": "Password (access token) used to access GitHub. Use https://github.com/settings/tokens to generate a token. **repo_deployment** is the required scope for the token.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "230e8590-63d7-4125-aee8-71d25e66d7d1", + "Name": "GitHubRepoName", + "Label": "GitHub repository", + "HelpText": "Name of your GitHub repository.", + "DefaultValue": "#{Octopus.Project.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9a47c6db-11bd-489d-a904-cf31dc7fe0b2", + "Name": "GitHubOwner", + "Label": "GitHub owner", + "HelpText": "Owner of the repository (organization or user).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "56fca421-bb10-4bef-b10f-4a83c9ea1675", + "Name": "GitHubHash", + "Label": "GitHub hash", + "HelpText": "Commit hash used to create the deployment for.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c9cbf161-e537-46a1-8edf-5d4c34fb3744", + "Name": "GitHubDeploymentId", + "Label": "GitHub deployment id (optional)", + "HelpText": "Optional github deployment id. If not set, a new deployment will be created and exported", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a94f5a5b-4747-4738-8ab2-714b991a178d", + "Name": "GitHubStatus", + "Label": "GitHub deployment status (optional)", + "HelpText": "If not specified, will be either `error` or `success` depending on the current build status. + +The state of the status. Can be one of `error,` `failure`, `inactive`, `in_progress`, `queued`, `pending`, or `success`. + +See docu for details: https://developer.github.com/v3/repos/deployments/#parameters-2", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": " +inactive +pending +queued +in_progress +error +failure +success +" + } + } + ], + "$Meta": { + "ExportedAt": "2021-08-23T12:40:10.975Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "Category": "github", + "LastModifiedBy": "benjimac93" +} diff --git a/step-templates/github-tag-release.json.human b/step-templates/github-tag-release.json.human new file mode 100644 index 000000000..866dc2c6e --- /dev/null +++ b/step-templates/github-tag-release.json.human @@ -0,0 +1,125 @@ +{ + "Id": "7a6704f9-c675-4dd2-bb2d-5fba374fd439", + "Name": "GitHub - Create Release", + "Description": "Create a release for a Github Repository.", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$formattedVersionNumber = [string]::Format(\"v{0}\", $versionNumber) +$isDraft = [bool]::Parse($draft) +$isPrerelease = [bool]::Parse($preRelease) + +$releaseData = @{ + tag_name = $formattedVersionNumber; + target_commitish = $commitId; + name = $formattedVersionNumber; + body = $releaseNotes; + draft = $isDraft; + prerelease = $isPrerelease; +} +$json = (ConvertTo-Json $releaseData -Compress) +$releaseParams = @{ + Uri = \"https://api.github.com/repos/$gitHubUsername/$gitHubRepository/releases\"; + Method = 'POST'; + Headers = @{ + Authorization = 'Basic ' + [Convert]::ToBase64String( + [Text.Encoding]::ASCII.GetBytes($gitHubApiKey + \":x-oauth-basic\") + ); + } + ContentType = 'application/json; charset=utf-8'; + Body = [System.Text.Encoding]::UTF8.GetBytes($json) +} + +Write-Host \"Creating release $formattedVersionNumber for $commitId.\" +$result = Invoke-RestMethod @releaseParams + +Write-Host \"Release successfully created.\" +$result", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "versionNumber", + "Label": "Version Number", + "HelpText": "The version number for this release", + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "commitId", + "Label": "Commitish", + "HelpText": "Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Unused if the Git tag already exists.", + "DefaultValue": "master", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "releaseNotes", + "Label": "Release Notes", + "HelpText": "The notes to accompany this GitHub release. Defaults to the release notes of the release.", + "DefaultValue": "#{Octopus.Release.Notes}", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "gitHubUsername", + "Label": "Owner", + "HelpText": "The owner of the repository.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "gitHubRepository", + "Label": "Repository", + "HelpText": "The repository to create the release for.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "gitHubApiKey", + "Label": "Api Token", + "HelpText": "The GitHub [API key](https://github.com/blog/1509-personal-api-tokens)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "draft", + "Label": "Draft", + "HelpText": "Set to true to mark this as a draft release (not visible to users)", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "preRelease", + "Label": "PreRelease", + "HelpText": "Set to true to mark this as a pre-release version", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2020-06-09T08:00:00.000+00:00", + "LastModifiedBy": "dario-l", + "$Meta": { + "ExportedAt": "2018-02-28T02:37:27.534+00:00", + "OctopusVersion": "2018.2.6", + "Type": "ActionTemplate" + }, + "Category": "github" +} diff --git a/step-templates/gitlab-create-tag.json.human b/step-templates/gitlab-create-tag.json.human new file mode 100644 index 000000000..8530a1204 --- /dev/null +++ b/step-templates/gitlab-create-tag.json.human @@ -0,0 +1,185 @@ +{ + "Id": "4456a21f-56a6-41a4-9237-43338a49b160", + "Name": "Gitlab - Create Tag", + "Description": "Create an annotated tag in Gitlab repository. + +If you fill release description then tag become Gitlab release too +(gitlab release internally is a tag stored in git plus release info for this tag stored in gitlab db) + +Proposed usage is along with publishing package metadata + +It's recommended to use with GitlabTag variable set.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "#fix for bug with encode and powershell 4 +#https://stackoverflow.com/questions/43129163/powershell-invoke-webrequest-to-a-url-with-literal-2f-in-it +function fixUri($uri){ + $UnEscapeDotsAndSlashes = 0x2000000; + $SimpleUserSyntax = 0x20000; + + $type = $uri.GetType(); + $fieldInfo = $type.GetField(\"m_Syntax\", ([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic)); + + $uriParser = $fieldInfo.GetValue($uri); + $typeUriParser = $uriParser.GetType().BaseType; + $fieldInfo = $typeUriParser.GetField(\"m_Flags\", ([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::FlattenHierarchy)); + $uriSyntaxFlags = $fieldInfo.GetValue($uriParser); + + $uriSyntaxFlags = $uriSyntaxFlags -band (-bnot $UnEscapeDotsAndSlashes); + $uriSyntaxFlags = $uriSyntaxFlags -band (-bnot $SimpleUserSyntax); + $fieldInfo.SetValue($uriParser, $uriSyntaxFlags); +} + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +Add-Type -AssemblyName System.Web + +$projectIdOrProjectPathEncoded = [System.Web.HttpUtility]::UrlEncode($projectIdOrProjectPath) +$messageEncoded = [System.Web.HttpUtility]::UrlEncode($message) +$releaseDescriptionEncoded = [System.Web.HttpUtility]::UrlEncode($releaseDescription) + +$getTagUri = New-Object System.Uri \"$gitlabUrl/api/v4/projects/$projectIdOrProjectPathEncoded/repository/tags?tag_name=$tagName\" +fixUri $getTagUri +$getTagRequest = @{ + Uri = $getTagUri; + Method = 'GET'; + Headers = @{'PRIVATE-TOKEN' = $personalAccessToken; } + ContentType = 'application/json'; +} + +\"Checking if tag $tagName exists.\" +try { + $resultTag = Invoke-RestMethod @getTagRequest + Write-Host \"Tag info received.\" +} +catch { + $statusCode = $_.Exception.Response.StatusCode.value__ + Write-Host \"Error while tag info receive.\" + $_.Exception | Format-List -Force + throw +} +if ($resultTag.name -eq $tagName) { + Write-Host \"Tag already exists, skip creation\" + exit +} + +$createTagUri = New-Object System.Uri \"$gitlabUrl/api/v4/projects/$projectIdOrProjectPathEncoded/repository/tags?tag_name=$tagName&ref=$vcsReference&message=$messageEncoded&release_description=$releaseDescriptionEncoded\" +fixUri $createTagUri + +$createTagRequest = @{ + Uri = $createTagUri; + Method = 'POST'; + Headers = @{'PRIVATE-TOKEN' = $personalAccessToken; } + ContentType = 'application/json'; +} + +\"Creating tag $tagName for $vcsReference.\" + +$createTagRequestLines = $createTagRequest | Out-String -Width 1000 +$createTagRequestHeaderLines = $createTagRequest.Headers | Out-String -Width 1000 +Write-Host \"Request is $createTagRequestLines\" +Write-Host \"Headers is $createTagRequestHeaderLines\" + +try { + $result = Invoke-RestMethod @createTagRequest + Write-Host \"Tag successfully created.\" +} +catch { + Write-Host \"Error while tag creating.\" + $_.Exception | Format-List -Force + throw +} + + +$result", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "243c81b8-b346-4e85-8422-a7b31c6e05ad", + "Name": "gitlabUrl", + "Label": "Gitlab Url", + "HelpText": "Url of gitlab, e.g. https://gitlab.com", + "DefaultValue": "#{GitlabTag.GitlabUrl}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a4b118dc-4e14-4e4c-99a2-1296d1f37afb", + "Name": "personalAccessToken", + "Label": "Personal access token", + "HelpText": "The Gitlab [personal access token](https://docs.gitlab.com/ee/api/README.html#personal-access-tokens) +#{GitlabTag.PersonalAccessToken} as default value", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "b4008fa4-1c2c-4341-a34c-0f0e1fff0082", + "Name": "projectIdOrProjectPath", + "Label": "ProjectIdOrProjectPath", + "HelpText": "The ID {e.g. 123} or path {e.g. \"SomeCompany/SomeProject\") of the project owned by the authenticated user", + "DefaultValue": "#{GitlabTag.ProjectIdOrProjectPath}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "63338a80-6a9f-4f4f-ab9c-42cbbd188271", + "Name": "vcsReference", + "Label": "VcsReference", + "HelpText": "Create tag using this commit SHA, another tag name, or branch name. +Hint to get it from package metadate: + +#{each package in Octopus.Deployment.PackageBuildMetadata}#{if Octopus.Template.Each.First}#{package.VcsCommitNumber}#{/if}#{/each}", + "DefaultValue": "#{GitlabTag.VcsReference}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "38ac6285-9894-4ae8-b3ff-04bd68ccc5ca", + "Name": "tagName", + "Label": "Tag name", + "HelpText": "Name of new tag", + "DefaultValue": "#{GitlabTag.TagName}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5ff9c20a-12d8-46bb-a2a4-ec84690c06ba", + "Name": "message", + "Label": "Tag message (annotated git tag)", + "HelpText": null, + "DefaultValue": "#{GitlabTag.TagMessage}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1b84e625-1476-44f7-83e0-b5c0a7631840", + "Name": "releaseDescription", + "Label": "Release Description", + "HelpText": "Add release notes to the Git tag and store it in the GitLab database. +Also will be saved as message to annotated tag in git database.", + "DefaultValue": "#{GitlabTag.ReleaseDescription}", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2019-11-06T16:31:39.759Z", + "OctopusVersion": "2019.10.1", + "Type": "ActionTemplate" + }, + "Category": "gitlab" +} diff --git a/step-templates/google-chat-send-message.json.human b/step-templates/google-chat-send-message.json.human new file mode 100644 index 000000000..df1c4c49f --- /dev/null +++ b/step-templates/google-chat-send-message.json.human @@ -0,0 +1,77 @@ +{ + "Id": "6c4c6253-45de-404f-b725-c96e1d7e4958", + "Name": "Google Chat - Send message", + "Description": "Send a message to a [Google Chat](https://chat.google.com) space using a configured [chat webhook](https://developers.google.com/workspace/chat/quickstart/webhooks#python_2). + +Multi-line message content and [basic formatting](https://developers.google.com/workspace/chat/format-messages) are supported. + +**Note:** This script is written in python, and is required for this step to function correctly.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Python", + "Octopus.Action.Script.ScriptBody": "import subprocess +import sys + +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'httplib2', '--disable-pip-version-check']) + +# parameters +webhook_url = get_octopusvariable(\"GoogleChat.SendMessage.WebhookUrl\") +message_content = get_octopusvariable(\"GoogleChat.SendMessage.MessageContent\") + +if not webhook_url: + raise ValueError('Webhook url null or empty!') + +if not message_content: + raise ValueError('Message content null or empty!') + +from json import dumps +from httplib2 import Http + +app_message = {\"text\": message_content} +message_headers = {\"Content-Type\": \"application/json; charset=UTF-8\"} +http_obj = Http() +response = http_obj.request( + uri=webhook_url, + method=\"POST\", + headers=message_headers, + body=dumps(app_message), +) +printverbose('Google response:') +printverbose(response)" + }, + "Parameters": [ + { + "Id": "bb5767eb-bbfb-4379-917e-31f73cf56ad2", + "Name": "GoogleChat.SendMessage.WebhookUrl", + "Label": "Webhook URL", + "HelpText": "Provide the Google Chat [Webhook URL](https://developers.google.com/workspace/chat/quickstart/webhooks)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "cb6577d7-d160-4e3a-86ea-b536bf92133a", + "Name": "GoogleChat.SendMessage.MessageContent", + "Label": "Message Content", + "HelpText": "Provide the message to send. Multi-line values, and [basic formatting](https://developers.google.com/workspace/chat/format-messages) are supported.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2024-04-03T13:02:57.192Z", + "OctopusVersion": "2024.2.4001", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "google-chat" + } diff --git a/step-templates/grate-database-migration.json.human b/step-templates/grate-database-migration.json.human new file mode 100644 index 000000000..f618b644c --- /dev/null +++ b/step-templates/grate-database-migration.json.human @@ -0,0 +1,745 @@ +{ + "Id": "ca23d18f-ab03-403d-bfb8-3ff74d3ddab3", + "Name": "grate Database Migrations", + "Description": "Database migrations using [grate](https://github.com/erikbra/grate). +With this template you can either include grate with your package or use the `Download grate?` feature to download it at deploy time. If you're downloading, you can choose the version by specifying it in the `Version of grate`. + +NOTE: + - AWS EC2 IAM Role authentication requires the AWS CLI be installed. + - To run on Linux, the machine must have both PowerShell Core and .NET Core 3.1 installed.", + "ActionType": "Octopus.Script", + "Version": 9, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "e0d1bcb0-4a7e-41dc-ab53-2799d6f2b051", + "Name": "gratePackage", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "gratePackage", + "Purpose": "" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Configure template + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) +{ +\tWrite-Host \"Determining Operating System...\" + switch ([System.Environment]::OSVersion.Platform) + { + \t\"Win32NT\" + { + \t# Set variable + $IsWindows = $true + $IsLinux = $false + } + \"Unix\" + { + \t$IsWindows = $false + $IsLinux = $true + } + } +} + +if ($IsWindows) +{ +\t$ProgressPreference = 'SilentlyContinue' +} + +# Define parameters +$grateExecutable = \"\" +$grateOutputPath = [System.IO.Path]::Combine($OctopusParameters[\"Octopus.Action.Package[gratePackage].ExtractedPath\"], \"output\") +$grateSsl = [System.Convert]::ToBoolean($grateSsl) + +Function Get-LatestVersionDownloadUrl { + # Define parameters + param( + $Repository, + $Version + ) + + # Define local variables + $releases = \"https://api.github.com/repos/$Repository/releases\" + + # Get latest version + Write-Host \"Determining latest release of $Repository ...\" + + $tags = (Invoke-WebRequest $releases -UseBasicParsing | ConvertFrom-Json) + + if ($null -ne $Version) { + # Get specific version + $tags = ($tags | Where-Object { $_.tag_name.EndsWith($Version) }) + + # Check to see if nothing was returned + if ($null -eq $tags) { + # Not found + Write-Host \"No release found matching version $Version, getting highest version using Major.Minor syntax...\" + + # Get the tags + $tags = (Invoke-WebRequest $releases -UseBasicParsing | ConvertFrom-Json) + + # Parse the version number into a version object + $parsedVersion = [System.Version]::Parse($Version) + $partialVersion = \"$($parsedVersion.Major).$($parsedVersion.Minor)\" + + # Filter tags to ones matching only Major.Minor of version specified + $tags = ($tags | Where-Object { $_.tag_name.Contains(\"$partialVersion.\") -and $_.draft -eq $false }) + + # Grab the latest + if ($null -eq $tags) + { + \t# decrement minor version + $minorVersion = [int]$parsedVersion.Minor + $minorVersion -- + + # Check to make sure that minor version isn't negative + if ($minorVersion -ge 0) + { + \t# return the urls + \treturn (Get-LatestVersionDownloadUrl -Repository $Repository -Version \"$($parsedVersion.Major).$($minorVersion)\") + } + else + { + \t# Display error + Write-Error \"Unable to find a version within the major version of $($parsedVersion.Major)!\" + } + } + } + } + + # Find the latest version with a downloadable asset + foreach ($tag in $tags) { + if ($tag.assets.Count -gt 0) { + return $tag.assets.browser_download_url + } + } + + # Return the version + return $null +} + +# Change the location to the extract path +Set-Location -Path $OctopusParameters[\"Octopus.Action.Package[gratePackage].ExtractedPath\"] + +# Check to see if download is specified +if ([System.Boolean]::Parse($grateDownloadNuget)) +{ + # Set secure protocols + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + $downloadUrls = @() + +\t# Check to see if version number specified + if ([string]::IsNullOrWhitespace($grateNugetVersion)) + { + \t# Get the latest version number + $downloadUrls = Get-LatestVersionDownloadUrl -Repository \"erikbra/grate\" + } + else + { + \t# Get specific version + $downloadUrls = Get-LatestVersionDownloadUrl -Repository \"erikbra/grate\" -Version $grateNugetVersion + } + +\t# Check to make sure something was returned + if ($null -ne $downloadUrls -and $downloadUrls.Length -gt 0) +\t{ + + # Check for download folder + if ((Test-Path -Path \"$PSSCriptRoot/grate\") -eq $false) + { + # Create the folder + New-Item -ItemType Directory -Path \"$PSSCriptRoot/grate\" + } + + # Get URL of grate-dotnet-tool + $downloadUrl = $downloadUrls | Where-Object {$_.Contains(\"grate-dotnet-tool\")} + + # Check to see if something was returned + if ($null -eq $downloadUrl) + { + \t# Attempt to get nuget package + Write-Host \"An asset with grate-dotnet-tool was not found, attempting to locate nuget package ...\" + $downloadUrl = $downloadUrls | Where-Object {$_.Contains(\".nupkg\")} + + # Check to see if something was returned + if ($null -eq $downloadUrl) + { + \tWrite-Error \"Unable to find appropriate asset for download.\" + } + } + + # Download nuget package + Write-Output \"Downloading $downloadUrl ...\" + + # Get download file name + $downloadFile = $downloadUrl.Substring($downloadUrl.LastIndexOf(\"/\") + 1) + + # Download the file + Invoke-WebRequest -Uri $downloadUrl -OutFile \"$PSSCriptRoot/grate/$downloadFile\" + + # Check the extension + if ($downloadFile.EndsWith(\".zip\")) + { + # Extract the file + Write-Host \"Extracting $downloadFile ...\" + Expand-Archive -Path \"$PSSCriptRoot/grate/$downloadFile\" -Destination \"$PSSCriptRoot/grate\" + + # Delete the downloaded .zip + Remove-Item -Path \"$PSSCriptRoot/grate/$downloadFile\" + + # Get extracted files + $extractedFiles = Get-ChildItem -Path \"$PSSCriptRoot/grate\" + + # Check to see if what was extracted was simply a nuget file + if ($extractedFiles.Count -eq 1 -and $extractedFiles[0].Extension -eq \".nupkg\") + { + # Zip file contained a nuget package + Write-Host \"Archive contained a NuGet package, extracting package ...\" + $nugetPackage = $extractedFiles[0] + $nugetPackage | Rename-Item -NewName $nugetPackage.Name.Replace(\".nupkg\", \".zip\") + Expand-Archive -Path $nugetPackage.FullName.Replace(\".nupkg\", \".zip\") -Destination \"$PSSCriptRoot/grate\" + } + } + + if ($downloadFile.EndsWith(\".nupkg\")) + { + # Zip file contained a nuget package + $nugetPackage = Get-ChildItem -Path \"$PSSCriptRoot/grate/$($downloadFile)\" + $nugetPackage | Rename-Item -NewName $nugetPackage.Name.Replace(\".nupkg\", \".zip\") + Expand-Archive -Path \"$PSSCriptRoot/grate/$($downloadFile.Replace(\".nupkg\", \".zip\"))\" -Destination \"$PSSCriptRoot/grate\" + } + } + else + { + \tWrite-Error \"No download url returned!\" + } +} + + + +if ([string]::IsNullOrWhitespace($grateExecutable)) +{ +\t# Look for just grate.dll + $grateExecutable = Get-ChildItem -Path $PSSCriptRoot -Recurse | Where-Object {$_.Name -eq \"grate.dll\"} + + # Check for multiple results + if ($grateExecutable -is [array]) + { + # choose one that matches highest version of .net +\t\t$dotnetVersions = (dotnet --list-runtimes) | Where-Object {$_ -like \"*.NetCore*\"} + +\t\t$maxVersion = $null +\t\tforeach ($dotnetVersion in $dotnetVersions) +\t\t{ + \t\t$parsedVersion = $dotnetVersion.Split(\" \")[1] + \t\tif ($null -eq $maxVersion -or [System.Version]::Parse($parsedVersion) -gt [System.Version]::Parse($maxVersion)) + \t\t{ + \t\t$maxVersion = $parsedVersion + \t\t} +\t\t} + + $grateExecutable = $grateExecutable | Where-Object {$_.FullName -like \"*net$(([System.Version]::Parse($maxVersion).Major))*\"} + } +} + +if ([string]::IsNullOrWhitespace($grateExecutable)) +{ + # Couldn't find grate + Write-Error \"Couldn't find the grate executable!\" +} + +# Build the arguments +$grateSwitches = @() + +# Update the connection string based on authentication method +switch ($grateAuthenticationMethod) +{ + \"awsiam\" + { + # Region is part of the RDS endpoint, extract + $region = ($grateServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $grateUserPassword = (aws rds generate-db-auth-token --hostname $grateServerName --region $region --port $grateServerPort --username $grateUserName) + $grateUserInfo = \"Uid=$grateUserName;Pwd=$grateUserPassword;\" + + break + } +\t + \"azuremanagedidentity\" + { + \t# SQL Server driver doesn't assign password + if ($grateDatabaseServerType -ne \"sqlserver\") + { + # Get login token + Write-Host \"Generating Azure Managed Identity token ...\" + $token = Invoke-RestMethod -Method GET -Uri \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://ossrdbms-aad.database.windows.net\" -Headers @{\"MetaData\" = \"true\"} + + $grateUserPassword = $token.access_token + $grateUserInfo = \"Uid=$grateUserName;Pwd=$grateUserPassword;\" + } + + break + } + + \"gcpserviceaccount\" + { + # Define header + $header = @{ \"Metadata-Flavor\" = \"Google\"} + + # Retrieve service accounts + $serviceAccounts = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\" -Headers $header + + # Results returned in plain text format, get into array and remove empty entries + $serviceAccounts = $serviceAccounts.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + + # Retreive the specific service account assigned to the VM + $serviceAccount = $serviceAccounts | Where-Object {$_.Contains(\"iam.gserviceaccount.com\") } + +\t\tWrite-Host \"Generating GCP IAM token ...\" + # Retrieve token for account + $token = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/$serviceAccount/token\" -Headers $header + $grateUserPassword = $token.access_token + + # Append remaining portion of connection string + $grateUserInfo = \"Uid=$grateUserName;Pwd=$grateUserPassword;\" + } + + + \"usernamepassword\" + { + \t# Append remaining portion of connection string + $grateUserInfo = \"Uid=$grateUserName;Pwd=$grateUserPassword;\" + +\t\tbreak +\t} + + \"windowsauthentication\" + { + # Append remaining portion of connection string +\t $grateUserInfo = \"integrated security=true;\" + + # Append username (required for non + $grateUserInfo += \"Uid=$grateUserName;\" + } + +} + +# Configure connnection string based on technology +switch ($grateDatabaseServerType) +{ + \"sqlserver\" + { + # Check to see if port has been defined + if (![string]::IsNullOrEmpty($grateServerPort)) + { + # Append to servername + $grateServerName += \",$grateServerPort\" + + # Empty the port + $grateServerPort = [string]::Empty + } + } + \"mariadb\" + { + \t$grateServerPort = \"Port=$grateServerPort;Allow User Variables=true;\" + } + \"mysql\" + { + \t# Use the MySQL client + $grateDatabaseServerType = \"mariadb\" + $grateServerPort = \"Port=$grateServerPort;Allow User Variables=true;\" + } + \"oracle\" + { + \t# Oracle connection strings are built different than all others + $grateServerConnectionString = \"--connectionstring=`\"Data source=$($grateServerName):$($grateServerPort)/$grateDatabaseName;$($grateUserInfo.Replace(\"Uid\", \"User Id\").Replace(\"Pwd\", \"Password\")) \" + } + default + { + $grateServerPort = \"Port=$grateServerPort;\" + } +} + +# Build base connection string +if ([string]::IsNullOrWhitespace($grateServerConnectionString)) +{ +\t$grateServerConnectionString = \"--connectionstring=`\"Server=$grateServerName;$grateServerPort $grateUserInfo Database=$grateDatabaseName;\" +} + +# Check for SQL Server and Azure Managed Identity +if (($grateDatabaseServerType -eq \"sqlserver\") -and ($grateAuthenticationMethod -eq \"azuremanagedidentity\")) +{ +\t# Append AD component to connection string + $grateServerConnectionString += \"Authentication=Active Directory Default;\" +} + +if ($grateSsl -eq $true) +{ +\tif (($grateDatabaseServerType -eq \"mariadb\") -or ($grateDatabaseServerType -eq \"mysql\") -or ($grateDatabaseServerType -eq \"postgres\")) + { + \t# Add sslmode + $grateServerConnectionString += \"SslMode=Require;Trust Server Certificate=true;\" + } + elseif ($grateDatabaseServerType -eq \"sqlserver\") + { + \t$grateServerConnectionString += \"Trust Server Certificate=true;\" + } + else + { + \tWrite-Warning \"Invalid Database Server Type selection for SSL, ignoring setting.\" + } +} + +# Add terminating double quote to connection string +$grateServerConnectionString += \"`\"\" + +$grateSwitches += $grateServerConnectionString + +$grateSwitches += \"--databasetype=$grateDatabaseServerType\" +$grateSwitches += \"--silent\" + +if ([System.Boolean]::Parse($grateDryRun)) +{ + $grateSwitches += \"--dryrun\" +} + +if ([System.Boolean]::Parse($grateRecordOutput)) +{ + $grateSwitches += \"--outputPath=$grateOutputPath\" + + # Check to see if path exists + if ((Test-Path -Path $grateOutputPath) -eq $false) + { + \t# Create folder + New-Item -Path $grateOutputPath -ItemType \"Directory\" + } +} + +# Add transaction switch +$grateSwitches += \"--transaction=$($grateWithTransaction.ToLower())\" + +# Add Command Timeout +if (![string]::IsNullOrEmpty($grateCommandTimeout)){ + $grateSwitches += \"--commandtimeout=$([int]$grateCommandTimeout)\" +} + +# Add Baseline switch +if ([System.Boolean]::Parse($grateBaseline)) { + $grateSwitches += \"--baseline\" +} + +# Add SQL Files Directory parameter +if (![string]::IsNullOrEmpty($grateSqlScriptFolder)) { + # Add up folder + $grateSwitches += \"--sqlfilesdirectory=$grateSqlScriptFolder\" +} + +# Add log verbosity flag +if (![string]::IsNullOrEmpty($grateLogVerbosity)) { + # Add up folder + $grateSwitches += \"--verbosity=$grateLogVerbosity\" +} + + +# Check for version +if (![string]::IsNullOrEmpty($grateVersion)) +{ + # Add version + $grateSwitches += \"--version=$grateVersion\" +} + +# Set grate environment +if (![string]::IsNullOrEmpty($grateEnvironment)) +{ + # Add environment + $grateSwitches += \"--environment=$grateEnvironment\" +} + +# Set grate schema. Especially useful when migrating from RoundhousE +if (![string]::IsNullOrEmpty($grateSchema)) +{ + # Add schema + $grateSwitches += \"--schema=$grateSchema\" +} + +# Display what's going to be run +if (![string]::IsNullOrWhitespace($grateUserPassword)) +{ +\tWrite-Host \"Executing $($grateExecutable.FullName) with $($grateSwitches.Replace($grateUserPassword, \"****\"))\" +} +else +{ +\tWrite-Host \"Executing $($grateExecutable.FullName) with $($grateSwitches)\" +} + +# Execute grate +if ($grateExecutable.FullName.EndsWith(\".dll\")) +{ +\t& dotnet $grateExecutable.FullName $grateSwitches +} +else +{ +\t& $grateExecutable.FullName $grateSwitches +} + +# If the output path was specified, attach artifacts +if ([System.Boolean]::Parse($grateRecordOutput)) +{ + # Zip up output folder content + Add-Type -Assembly 'System.IO.Compression.FileSystem' + + $zipFile = \"$($OctopusParameters[\"Octopus.Action.Package[gratePackage].ExtractedPath\"])/output.zip\" + +\t[System.IO.Compression.ZipFile]::CreateFromDirectory($grateOutputPath, $zipFile) + New-OctopusArtifact -Path \"$zipFile\" -Name \"output.zip\" +} +" + }, + "Parameters": [ + { + "Id": "5d54012a-1f10-4b3c-bbfd-fe70bf843904", + "Name": "gratePackage", + "Label": "grate Package", + "HelpText": "The package containing the scripts for grate to deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "57a43e61-86c0-46a3-8446-aacbb67cf596", + "Name": "grateServerName", + "Label": "Database Server Name", + "HelpText": "Name or IP address of the server being deployed to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ba3705a1-e620-4fb1-b494-2b9e65fbb194", + "Name": "grateServerPort", + "Label": "Database Server Port", + "HelpText": "Port number for the database server. Uses default server port if left blank.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c3a7c802-babd-470b-8447-5f6159128b4c", + "Name": "grateAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the database server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +azuremanagedidentity|Azure Managed Identity +gcpserviceaccount|GCP Service Account +usernamepassword|Username\\Password +windowsauthentication|Windows Authentication" + } + }, + { + "Id": "c955ad8c-9686-419c-a1cf-129ffbe0acb4", + "Name": "grateDatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to deploy to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0438580a-8825-40db-a782-569a35144f4b", + "Name": "grateSsl", + "Label": "Force SSL", + "HelpText": "Check this box for force connection string to use SSL. Only applicable to MariaDB, MySQL, SQL Server, and PostgreSQL database types.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "e007d182-88f5-4e1f-906f-c85ff955c51e", + "Name": "grateVersion", + "Label": "Database Version", + "HelpText": "Version number of your database migration. Default value is the version of the grate package.", + "DefaultValue": "#{Octopus.Action.Package[gratePackage].PackageVersion}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "58ceec6b-f374-4c2e-83bb-fbd7ce26dc05", + "Name": "grateUsername", + "Label": "Database Username", + "HelpText": "Username of the account with sufficient permissions to execute scripts. (Leave blank for Integrated Authentication.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "29f6bb35-c076-4b5d-98cf-8ee8494cb38b", + "Name": "grateUserPassword", + "Label": "Database User Password", + "HelpText": "Password for the Database Username account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "5e5d68f1-fdc8-41ce-8cd3-ab082278218d", + "Name": "grateDatabaseServerType", + "Label": "Database Server Type", + "HelpText": "The database technology being deployed to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "mariadb|MariaDB +mysql|MySQL +oracle|Oracle +postgresql|PostgreSQL +sqlserver|SQL Server" + } + }, + { + "Id": "67c016c3-d779-4300-b3d1-e11463bb9fc4", + "Name": "grateWithTransaction", + "Label": "Use Transaction?", + "HelpText": "Check this box if you want all scripts to be run within the same transaction.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "29a4f568-23f7-4bb2-a5ba-910b922c5406", + "Name": "grateDryRun", + "Label": "Dry Run?", + "HelpText": "Check this box if you want to perform a dry run. Results are recorded and attached as deployment artifacts if you check Record Output.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "eaad7639-05d8-46ed-bab3-b72b8ad81d5a", + "Name": "grateRecordOutput", + "Label": "Record Output?", + "HelpText": "Check this box to record the output of the run. Useful for gathering what would be changed for approval purposes.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "84b46c25-6762-44c2-8ec6-4ba30ff599f5", + "Name": "grateCommandTimeout", + "Label": "Command Timeout", + "HelpText": "Customizable command timeout (in seconds). Default is 60", + "DefaultValue": "60", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3382a212-f467-409e-950b-317f05f59ea5", + "Name": "grateBaseline", + "Label": "Use Baseline", + "HelpText": "Check this box if you want grate to mark the scripts as run, but not to actually run anything against the database. [More information about this option can be found here](https://erikbra.github.io/grate/configuration-options/)", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "60dc1e2a-93ec-4bc9-abde-aacf6aeba0a7", + "Name": "grateSqlScriptFolder", + "Label": "SQL Script folder", + "HelpText": "Script location to use for grate (if not in the root of the package)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b792092e-e3c1-487b-8349-e17832506f12", + "Name": "grateLogVerbosity", + "Label": "Log Level", + "HelpText": "Configure log level when running Grate.", + "DefaultValue": "information", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "critical|Critical +debug|Debug +error|Error +information|Information +none|None +trace|Trace +warning|Warning" + } + }, + { + "Id": "b8a17064-900f-4d77-9354-e5de1d3057f0", + "Name": "grateDownloadNuget", + "Label": "Download grate?", + "HelpText": "Check this box if you want the template to download RoundhousE and use the downloaded version for deployment. Requires .NET Core be installed on the machine executing the deployment.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "409bd29d-79cf-4209-a005-ce768714eb64", + "Name": "grateNugetVersion", + "Label": "Version of grate", + "HelpText": "Version of grate to download (used with Download grate option), leave blank for latest.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d9660ee0-8376-4f9b-8d1d-99f3b4701a42", + "Name": "grateEnvironment", + "Label": "Grate environment", + "HelpText": "The environment specific scripts that grate will execute", + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a93b61a7-74ae-471e-ace3-d09cdfab8858", + "Name": "grateSchema", + "Label": "Grate schema for migration tables", + "HelpText": "Useful if migrating from RoundhousE.", + "DefaultValue": "grate", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2022-10-17T22:47:54.861Z", + "OctopusVersion": "2022.4.4910", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "farhanalam", + "Category": "grate" + } diff --git a/step-templates/hashicorp-vault-approle-get-wrapped-secretid.json.human b/step-templates/hashicorp-vault-approle-get-wrapped-secretid.json.human new file mode 100644 index 000000000..4639d9700 --- /dev/null +++ b/step-templates/hashicorp-vault-approle-get-wrapped-secretid.json.human @@ -0,0 +1,238 @@ +{ + "Id": "76827264-af27-46d0-913a-e093a4f0db48", + "Name": "HashiCorp Vault - AppRole Get Wrapped Secret ID", + "Description": "This step generates a response-wrapped Secret ID for an [AppRole](https://www.vaultproject.io/docs/auth/approle) from a HashiCorp Vault server. + +--- + +Two properties from the response will be made available as [sensitive Output variables](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables): + +- `wrap_info.token` named `WrappedToken` - This is the wrapped token used to retrieve the actual Secret ID from Vault. +- `wrap_info.creation_path` named `WrappedTokenCreationPath` - This is the creation path of the token to allow you to [validate no malfeasance](https://www.vaultproject.io/docs/concepts/response-wrapping#response-wrapping-token-validation) has occurred. + +This step template makes use of the [Rest API](https://www.vaultproject.io/api-docs/auth/approle#login-with-approle), so no other dependencies are needed. + +--- + +**Required:** +- The Vault server must be [unsealed](https://www.vaultproject.io/docs/concepts/seal). +- The full path where the AppRole auth method is mounted. +- A `RoleName`. +- A TTL for the wrapped token (Default: `120s`). +- The [Auth token](https://www.vaultproject.io/docs/auth/token) used to authenticate with Vault. This token should be of limited scope and should only retrieve wrapped SecretIDs. See the [HashiCorp documentation](https://learn.hashicorp.com/tutorials/vault/pattern-approle?in=vault/recommended-patterns#vault-returns-a-token) for further information. + +--- + +*Optional* +- A Vault [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. **Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) . + +--- + +Notes: + +- Tested on Vault Server `1.11.3`. +- Tested on both PowerShell Desktop and PowerShell Core.", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Variables +$VAULT_APPROLE_WRAPPED_SECRETID_ADDRESS = $OctopusParameters[\"Vault.AppRole.WrappedSecretID.VaultAddress\"] +$VAULT_APPROLE_WRAPPED_SECRETID_API_VERSION = $OctopusParameters[\"Vault.AppRole.WrappedSecretID.ApiVersion\"] +$VAULT_APPROLE_WRAPPED_SECRETID_NAMESPACE = $OctopusParameters[\"Vault.AppRole.WrappedSecretID.Namespace\"] +$VAULT_APPROLE_WRAPPED_SECRETID_PATH = $OctopusParameters[\"Vault.AppRole.WrappedSecretID.AppRolePath\"] +$VAULT_APPROLE_WRAPPED_SECRETID_ROLENAME = $OctopusParameters[\"Vault.AppRole.WrappedSecretID.RoleName\"] +$VAULT_APPROLE_WRAPPED_SECRETID_TTL = $OctopusParameters[\"Vault.AppRole.WrappedSecretID.TTL\"] +$VAULT_APPROLE_WRAPPED_SECRETID_TOKEN = $OctopusParameters[\"Vault.AppRole.WrappedSecretID.AuthToken\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_WRAPPED_SECRETID_ADDRESS)) { + throw \"Required parameter VAULT_APPROLE_WRAPPED_SECRETID_ADDRESS not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_WRAPPED_SECRETID_API_VERSION)) { +\tthrow \"Required parameter VAULT_APPROLE_WRAPPED_SECRETID_API_VERSION not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_WRAPPED_SECRETID_PATH)) { +\tthrow \"Required parameter VAULT_APPROLE_WRAPPED_SECRETID_AUTH_PATH not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_WRAPPED_SECRETID_ROLENAME)) { +\tthrow \"Required parameter VAULT_APPROLE_WRAPPED_SECRETID_ROLENAME not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_WRAPPED_SECRETID_TOKEN)) { +\tthrow \"Required parameter VAULT_APPROLE_WRAPPED_SECRETID_TOKEN not specified\" +} + +# Helper functions +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $rawResponse = $reader.ReadToEnd() + $response = \"\" + try {$response = $rawResponse | ConvertFrom-Json} catch {$response=$rawResponse} + return $response + } + } + else { + return $RequestError.ErrorDetails.Message + } +} +############################################################################### + +$VAULT_APPROLE_WRAPPED_SECRETID_ADDRESS = $VAULT_APPROLE_WRAPPED_SECRETID_ADDRESS.TrimEnd('/') +$VAULT_APPROLE_WRAPPED_SECRETID_PATH = $VAULT_APPROLE_WRAPPED_SECRETID_PATH.TrimStart('/').TrimEnd('/') +$VAULT_APPROLE_WRAPPED_SECRETID_ROLENAME = $VAULT_APPROLE_WRAPPED_SECRETID_ROLENAME.TrimStart('/').TrimEnd('/') + +# Local variables +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +try +{ +\t$headers = @{ + \"X-Vault-Token\" = $VAULT_APPROLE_WRAPPED_SECRETID_TOKEN + \"X-Vault-Wrap-Ttl\" = $VAULT_APPROLE_WRAPPED_SECRETID_TTL + } + if (-not [string]::IsNullOrWhiteSpace($VAULT_APPROLE_WRAPPED_SECRETID_NAMESPACE)) { + Write-Verbose \"Setting 'X-Vault-Namespace' header to: $VAULT_APPROLE_WRAPPED_SECRETID_NAMESPACE\" + $Headers.Add(\"X-Vault-Namespace\", $VAULT_APPROLE_WRAPPED_SECRETID_NAMESPACE) + } + + $uri = \"$VAULT_APPROLE_WRAPPED_SECRETID_ADDRESS/$VAULT_APPROLE_WRAPPED_SECRETID_API_VERSION/$VAULT_APPROLE_WRAPPED_SECRETID_PATH/role/$([uri]::EscapeDataString($VAULT_APPROLE_WRAPPED_SECRETID_ROLENAME))/secret-id\" + Write-Verbose \"Making Put request to $uri\" + $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Put + + if($null -ne $response) { + \tSet-OctopusVariable -Name \"WrappedToken\" -Value $response.wrap_info.token -Sensitive + Set-OctopusVariable -Name \"WrappedTokenCreationPath\" -Value $response.wrap_info.creation_path -Sensitive + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.WrappedToken}\" + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.WrappedTokenCreationPath}\" + } + else { + \tWrite-Error \"Null or Empty response returned from Vault server\" -Category InvalidResult + } +} +catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred getting a wrapped secretid: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.errors) { + $AdditionalDetail = $ErrorBody.errors -Join \",\" + } + else { + $errorDetails = $null + try { $errorDetails = ConvertFrom-Json $ErrorBody } catch {} + $AdditionalDetail += if ($null -ne $errorDetails) { $errorDetails.errors -Join \",\" } else { $ErrorBody } + } + } + + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`n`tDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category ConnectionError +}" + }, + "Parameters": [ + { + "Id": "bbf1cf96-066a-43e8-8387-24131fa63feb", + "Name": "Vault.AppRole.WrappedSecretID.VaultAddress", + "Label": "Vault Server URL", + "HelpText": "The URL of the Vault instance you are connecting to. Port should be included (The default is `8200`). For example: + + +`https://myvault.local:8200/`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bd6ca2f2-2102-40e0-a1e0-36e526393203", + "Name": "Vault.AppRole.WrappedSecretID.ApiVersion", + "Label": "API version", + "HelpText": "All API routes are prefixed with a version e.g. `/v1/`. + +See the [API documentation](https://www.vaultproject.io/api-docs) for further details.", + "DefaultValue": "v1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "v1|v1" + } + }, + { + "Id": "30366827-71f8-42cc-ac0a-d05300ae6c8d", + "Name": "Vault.AppRole.WrappedSecretID.Namespace", + "Label": "Namespace (Optional)", + "HelpText": "The _optional_ [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. + +**Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) .", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "67c5d1c7-6373-4342-b02b-a7a6fa69402b", + "Name": "Vault.AppRole.WrappedSecretID.AppRolePath", + "Label": "App Role Path", + "HelpText": "The path where the approle auth method was mounted. The default path is `/auth/approle`. If the AppRole auth method was mounted at a different path, for example `my-path`, then specify `/my-path` instead.", + "DefaultValue": "/auth/approle", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4c8ab1b0-36e3-4d19-9a45-557300bb0f06", + "Name": "Vault.AppRole.WrappedSecretID.RoleName", + "Label": "Role Name", + "HelpText": "The role name of the [AppRole](https://www.vaultproject.io/api/auth/approle).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cc4afac6-6dc4-4c9f-bcb8-0ab9c2049523", + "Name": "Vault.AppRole.WrappedSecretID.TTL", + "Label": "Time-to-live (TTL)", + "HelpText": "The TTL of the [response-wrapping token](https://www.vaultproject.io/docs/concepts/response-wrapping#response-wrapping-tokens) itself. Enter a value in seconds. The default is: `120s`", + "DefaultValue": "120s", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "297871f1-a145-4dde-97ba-d7a9a0690cc6", + "Name": "Vault.AppRole.WrappedSecretID.AuthToken", + "Label": "Auth token", + "HelpText": "The [Auth token](https://www.vaultproject.io/docs/auth/token) used to authenticate with Vault to generate a [response-wrapped](https://www.vaultproject.io/docs/concepts/response-wrapping) Secret ID.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedAt": "2022-09-18T08:56:29.022Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2022-09-18T08:56:29.022Z", + "OctopusVersion": "2022.4.2266", + "Type": "ActionTemplate" + }, + "Category": "hashicorp-vault" + } diff --git a/step-templates/hashicorp-vault-approle-login.json.human b/step-templates/hashicorp-vault-approle-login.json.human new file mode 100644 index 000000000..9f9346f24 --- /dev/null +++ b/step-templates/hashicorp-vault-approle-login.json.human @@ -0,0 +1,215 @@ +{ + "Id": "e04a9cec-f04a-4da2-849b-1aed0fd408f0", + "Name": "HashiCorp Vault - AppRole Login", + "Description": "This step logs into a HashiCorp Vault server with the [AppRole](https://www.vaultproject.io/docs/auth/approle) auth method. + +The `client_token` from the response will be made available as a sensitive [Output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) named `AppRoleAuthToken` for use in other step templates. + +This step template makes use of the [Rest API](https://www.vaultproject.io/api-docs/auth/approle#login-with-approle), so no other dependencies are needed. + +**Required:** +- The Vault server must be [unsealed](https://www.vaultproject.io/docs/concepts/seal). +- You must supply the full path where the AppRole auth method is mounted. +- You must supply both a `RoleId` and `SecretId`. + +*Optional*: +- A Vault [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. **Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) . + +Notes: + +- Tested on Vault Server `1.11.3`. +- Tested on both PowerShell Desktop and PowerShell Core. +", + "ActionType": "Octopus.Script", + "Version": 8, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Variables +$VAULT_APPROLE_LOGIN_ADDRESS = $OctopusParameters[\"Vault.AppRole.Login.VaultAddress\"] +$VAULT_APPROLE_LOGIN_API_VERSION = $OctopusParameters[\"Vault.AppRole.Login.ApiVersion\"] +$VAULT_APPROLE_LOGIN_APPROLE_PATH = $OctopusParameters[\"Vault.AppRole.Login.AppRolePath\"] +$VAULT_APPROLE_LOGIN_ROLEID = $OctopusParameters[\"Vault.AppRole.Login.RoleID\"] +$VAULT_APPROLE_LOGIN_SECRETID = $OctopusParameters[\"Vault.AppRole.Login.SecretID\"] + +# Optional variables +$VAULT_APPROLE_LOGIN_NAMESPACE = $OctopusParameters[\"Vault.AppRole.Login.Namespace\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_LOGIN_ADDRESS)) { + throw \"Required parameter VAULT_APPROLE_LOGIN_ADDRESS not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_LOGIN_API_VERSION)) { + throw \"Required parameter VAULT_APPROLE_LOGIN_API_VERSION not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_LOGIN_APPROLE_PATH)) { + throw \"Required parameter VAULT_APPROLE_LOGIN_APPROLE_PATH not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_LOGIN_ROLEID)) { + throw \"Required parameter VAULT_APPROLE_LOGIN_ROLEID not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_LOGIN_SECRETID)) { + throw \"Required parameter VAULT_APPROLE_LOGIN_SECRETID not specified\" +} + +# Helper functions +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $rawResponse = $reader.ReadToEnd() + $response = \"\" + try { $response = $rawResponse | ConvertFrom-Json } catch { $response = $rawResponse } + return $response + } + } + else { + return $RequestError.ErrorDetails.Message + } +} +############################################################################### + +$VAULT_APPROLE_LOGIN_ADDRESS = $VAULT_APPROLE_LOGIN_ADDRESS.TrimEnd('/') +$VAULT_APPROLE_LOGIN_APPROLE_PATH = $VAULT_APPROLE_LOGIN_APPROLE_PATH.TrimStart('/').TrimEnd('/') + +# Local variables +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +try { + $payload = @{ + role_id = $VAULT_APPROLE_LOGIN_ROLEID + secret_id = $VAULT_APPROLE_LOGIN_SECRETID + } + + $Headers = @{} + if (-not [string]::IsNullOrWhiteSpace($VAULT_APPROLE_LOGIN_NAMESPACE)) { + Write-Verbose \"Setting 'X-Vault-Namespace' header to: $VAULT_APPROLE_LOGIN_NAMESPACE\" + $Headers.Add(\"X-Vault-Namespace\", $VAULT_APPROLE_LOGIN_NAMESPACE) + } + + $uri = \"$VAULT_APPROLE_LOGIN_ADDRESS/$VAULT_APPROLE_LOGIN_API_VERSION/$([uri]::EscapeDataString($VAULT_APPROLE_LOGIN_APPROLE_PATH))/login\" + Write-Verbose \"Making request to $uri\" + $response = Invoke-RestMethod -Method Post -Uri $uri -Body ($payload | ConvertTo-Json -Depth 10) -Headers $Headers + + if ($null -ne $response) { + Set-OctopusVariable -Name \"AppRoleAuthToken\" -Value $response.auth.client_token -Sensitive + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.AppRoleAuthToken}\" + } + else { + Write-Error \"Null or Empty response returned from Vault server\" -Category InvalidResult + } +} +catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred logging in with AppRole: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.errors) { + $AdditionalDetail = $ErrorBody.errors -Join \",\" + } + else { + $errorDetails = $null + try { $errorDetails = ConvertFrom-Json $ErrorBody } catch {} + $AdditionalDetail += if ($null -ne $errorDetails) { $errorDetails.errors -Join \",\" } else { $ErrorBody } + } + } + + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`n`tDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category ConnectionError +}" + }, + "Parameters": [ + { + "Id": "bbf1cf96-066a-43e8-8387-24131fa63feb", + "Name": "Vault.AppRole.Login.VaultAddress", + "Label": "Vault Server URL", + "HelpText": "The URL of the Vault instance you are connecting to. Port should be included (The default is `8200`). For example: + + +`https://myvault.local:8200/`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bd6ca2f2-2102-40e0-a1e0-36e526393203", + "Name": "Vault.AppRole.Login.ApiVersion", + "Label": "API version", + "HelpText": "All API routes are prefixed with a version e.g. `/v1/`. + +See the [API documentation](https://www.vaultproject.io/api-docs) for further details.", + "DefaultValue": "v1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "v1|v1" + } + }, + { + "Id": "49dc9994-778b-40ad-afe5-80ed34e45056", + "Name": "Vault.AppRole.Login.Namespace", + "Label": "Namespace (Optional)", + "HelpText": "The _optional_ [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. + +**Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) .", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "67c5d1c7-6373-4342-b02b-a7a6fa69402b", + "Name": "Vault.AppRole.Login.AppRolePath", + "Label": "App Role Path", + "HelpText": "The path where the approle auth method was mounted. The default path is `/auth/approle`. If the AppRole auth method was mounted at a different path, for example `my-path`, then specify `/my-path` instead.", + "DefaultValue": "/auth/approle", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "297871f1-a145-4dde-97ba-d7a9a0690cc6", + "Name": "Vault.AppRole.Login.RoleID", + "Label": "Role ID", + "HelpText": "The [RoleID](https://www.vaultproject.io/docs/auth/approle#roleid) of the AppRole.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "4c8ab1b0-36e3-4d19-9a45-557300bb0f06", + "Name": "Vault.AppRole.Login.SecretID", + "Label": "Secret ID", + "HelpText": "The [Secret ID](https://www.vaultproject.io/docs/auth/approle#secretid) of the AppRole.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedAt": "2022-09-16T19:37:08.177Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2022-09-16T19:37:08.177Z", + "OctopusVersion": "2022.4.2266", + "Type": "ActionTemplate" + }, + "Category": "hashicorp-vault" + } diff --git a/step-templates/hashicorp-vault-approle-unwrap-secretid-login.json.human b/step-templates/hashicorp-vault-approle-unwrap-secretid-login.json.human new file mode 100644 index 000000000..b8d4cc95a --- /dev/null +++ b/step-templates/hashicorp-vault-approle-unwrap-secretid-login.json.human @@ -0,0 +1,286 @@ +{ + "Id": "aa113393-e615-40ed-9c5a-f95f471d728f", + "Name": "HashiCorp Vault - AppRole Unwrap Secret ID and Login", + "Description": "This step combines two Vault operations into one: +1. It retrieves (and unwraps) a Secret ID for an [AppRole](https://www.vaultproject.io/docs/auth/approle) using a wrapped auth token from a HashiCorp Vault server. +2. It logs into the HashiCorp Vault server using a supplied Role ID and the unwrapped Secret ID. + +The `client_token` from the login response will be made available as a sensitive [Output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) named `AppRoleAuthToken` for use in other step templates. + +This step template makes use of the [Rest API](https://www.vaultproject.io/api-docs/auth/approle#login-with-approle), so no other dependencies are needed. + +--- + +**Required:** +- The Vault server must be [unsealed](https://www.vaultproject.io/docs/concepts/seal). +- The full path where the AppRole auth method is mounted. +- The [RoleID](https://www.vaultproject.io/docs/auth/approle#roleid) of the AppRole. +- The wrapped [auth token](https://www.vaultproject.io/docs/auth/token) used to retrieve the unwrapped Secret ID. + +--- + +*Optional* +- The creation path of the wrapped token. If this parameter value is provided, the step template will perform a [wrapping lookup](https://www.vaultproject.io/api-docs/system/wrapping-lookup) to [validate no malfeasance](https://www.vaultproject.io/docs/concepts/response-wrapping#response-wrapping-token-validation) has occurred. +- A Vault [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. **Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) . + +--- + +Notes: + + +- Tested on Vault Server `1.11.3`. +- Tested on both PowerShell Desktop and PowerShell Core. +- See the HashiCorp [AppRole patterns documentation](https://learn.hashicorp.com/tutorials/vault/pattern-approle?in=vault/recommended-patterns#vault-returns-a-token) for further information. +", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Variables +$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ADDRESS = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.Login.VaultAddress\"] +$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_API_VERSION = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.Login.ApiVersion\"] +$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_NAMESPACE = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.Login.Namespace\"] +$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_APPROLE_PATH = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.Login.AppRolePath\"] +$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ROLEID = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.Login.RoleID\"] +$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_TOKEN = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.Login.WrappedToken\"] + +# Optional Variables +$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_TOKEN_CREATION_PATH = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.Login.WrappedTokenCreationPath\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ADDRESS)) { + throw \"Required parameter VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ADDRESS not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_API_VERSION)) { + throw \"Required parameter VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_API_VERSION not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_APPROLE_PATH)) { + throw \"Required parameter VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_APPROLE_PATH not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ROLEID)) { + throw \"Required parameter VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ROLEID not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_TOKEN)) { + throw \"Required parameter VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_TOKEN not specified\" +} + +# Helper functions +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $rawResponse = $reader.ReadToEnd() + $response = \"\" + try { $response = $rawResponse | ConvertFrom-Json } catch { $response = $rawResponse } + return $response + } + } + else { + return $RequestError.ErrorDetails.Message + } +} +############################################################################### + +$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ADDRESS = $VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ADDRESS.TrimEnd('/') +$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_APPROLE_PATH = $VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_APPROLE_PATH.TrimStart('/').TrimEnd('/') + +# Local variables +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +try { + + Write-Verbose \"X-Vault-Namespace header: $VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_NAMESPACE\" + + # Should we validate lookup token's creation path? + if (![string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_TOKEN_CREATION_PATH)) { + $uri = \"$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ADDRESS/$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_API_VERSION/sys/wrapping/lookup\" + $payload = @{ + token = $VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_TOKEN + } + + $Headers = @{} + if (-not [string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_NAMESPACE)) { + $Headers.Add(\"X-Vault-Namespace\", $VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_NAMESPACE) + } + + Write-Verbose \"Making Post request to $uri\" + $response = Invoke-RestMethod -Uri $uri -Method Post -Body ($payload | ConvertTo-Json -Depth 10) -Headers $Headers + + if ($null -ne $response) { + Write-Verbose \"Validating Wrapped token creation path.\" + $Lookup_CreationPath = $response.data.creation_path + if ($VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_TOKEN_CREATION_PATH -ne $Lookup_CreationPath) { + throw \"Supplied Wrapped token creation path failed lookup validation. Check the creation path value and retry.\" + } + } + else { + Write-Error \"Null or Empty response returned from Vault server lookup\" -Category InvalidResult + } + } + + # Call to unwrap secret id from wrapped token. + $Headers = @{ + \"X-Vault-Token\" = $VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_TOKEN + } + + if (-not [string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_NAMESPACE)) { + $Headers.Add(\"X-Vault-Namespace\", $VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_NAMESPACE) + } + + $uri = \"$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ADDRESS/$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_API_VERSION/sys/wrapping/unwrap\" + Write-Verbose \"Making Post request to $uri\" + $response = Invoke-RestMethod -Uri $uri -Headers $Headers -Method Post + + if ($null -ne $response) { + + $payload = @{ + role_id = $VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ROLEID + secret_id = $response.data.secret_id + } + + $Headers = @{} + if (-not [string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_NAMESPACE)) { + $Headers.Add(\"X-Vault-Namespace\", $VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_NAMESPACE) + } + + $uri = \"$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_ADDRESS/$VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_API_VERSION/$([uri]::EscapeDataString($VAULT_APPROLE_UNWRAP_SECRETID_LOGIN_APPROLE_PATH))/login\" + Write-Verbose \"Making Post request to $uri\" + $login_response = Invoke-RestMethod -Method Post -Uri $uri -Body ($payload | ConvertTo-Json -Depth 10) -Headers $Headers + + if ($null -ne $login_response) { + Set-OctopusVariable -Name \"AppRoleAuthToken\" -Value $login_response.auth.client_token -Sensitive + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.AppRoleAuthToken}\" + } + else { + Write-Error \"Null or Empty response returned from Vault server\" -Category InvalidResult + } + } + else { + throw \"Null or Empty response returned from Vault server unwrap\" + } +} +catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred unwrapping secretid: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.errors) { + $AdditionalDetail = $ErrorBody.errors -Join \",\" + } + else { + $errorDetails = $null + try { $errorDetails = ConvertFrom-Json $ErrorBody } catch {} + $AdditionalDetail += if ($null -ne $errorDetails) { $errorDetails.errors -Join \",\" } else { $ErrorBody } + } + } + + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`n`tDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category ConnectionError +}" + }, + "Parameters": [ + { + "Id": "a0078913-4e0b-4b01-b8eb-f410138951e8", + "Name": "Vault.AppRole.UnwrapSecretID.Login.VaultAddress", + "Label": "Vault Server URL", + "HelpText": "The URL of the Vault instance you are connecting to. Port should be included (The default is `8200`). For example: + + +`https://myvault.local:8200/`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6b441222-26f8-4e68-bb96-fdabd0ebcc59", + "Name": "Vault.AppRole.UnwrapSecretID.Login.ApiVersion", + "Label": "API version", + "HelpText": "All API routes are prefixed with a version e.g. `/v1/`. + +See the [API documentation](https://www.vaultproject.io/api-docs) for further details.", + "DefaultValue": "v1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "v1|v1" + } + }, + { + "Id": "f973d44c-4840-4559-9331-6b2d91c130f3", + "Name": "Vault.AppRole.UnwrapSecretID.Login.Namespace", + "Label": "Namespace (Optional)", + "HelpText": "The _optional_ [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. + +**Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) .", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6fd4b775-5b97-43c7-bc53-5dc3e2353287", + "Name": "Vault.AppRole.UnwrapSecretID.Login.AppRolePath", + "Label": "App Role Path", + "HelpText": "The path where the approle auth method was mounted. The default path is `/auth/approle`. If the AppRole auth method was mounted at a different path, for example `my-path`, then specify `/my-path` instead.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a8c0824c-3e1a-4014-8060-96a703d52365", + "Name": "Vault.AppRole.UnwrapSecretID.Login.RoleID", + "Label": "Role ID", + "HelpText": "The [RoleID](https://www.vaultproject.io/docs/auth/approle#roleid) of the AppRole.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "8ba05c24-215f-4ca0-9edc-bca38c43d7b5", + "Name": "Vault.AppRole.UnwrapSecretID.Login.WrappedToken", + "Label": "Wrapped Token", + "HelpText": "This is the wrapped token used to retrieve the actual Secret ID from Vault.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "3d94ac28-e7b6-4da2-8ff4-af506d5ded95", + "Name": "Vault.AppRole.UnwrapSecretID.Login.WrappedTokenCreationPath", + "Label": "Token Creation Path", + "HelpText": "*Optional* - the creation path of the wrapped token. If this parameter value is provided, the step template will perform a [wrapping lookup](https://www.vaultproject.io/api-docs/system/wrapping-lookup) to [validate no malfeasance](https://www.vaultproject.io/docs/concepts/response-wrapping#response-wrapping-token-validation) has occurred.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedAt": "2022-09-18T09:07:46.536Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2022-09-18T09:07:46.536Z", + "OctopusVersion": "2022.4.2266", + "Type": "ActionTemplate" + }, + "Category": "hashicorp-vault" + } diff --git a/step-templates/hashicorp-vault-approle-unwrap-secretid.json.human b/step-templates/hashicorp-vault-approle-unwrap-secretid.json.human new file mode 100644 index 000000000..d359c8d78 --- /dev/null +++ b/step-templates/hashicorp-vault-approle-unwrap-secretid.json.human @@ -0,0 +1,236 @@ +{ + "Id": "c1f56030-0bcd-458d-bc70-b4f43ec0d30f", + "Name": "HashiCorp Vault - AppRole Unwrap Secret ID", + "Description": "This step retrieves (and unwraps) a Secret ID for an [AppRole](https://www.vaultproject.io/docs/auth/approle) using a wrapped auth token from a HashiCorp Vault server. + +--- + +One property from the response will be made available as a [sensitive Output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables): + +- `data.secret_id` - This is the unwrapped Secret ID from Vault. The output variable name will be `UnwrappedSecretID`. + +This step template makes use of the [Rest API](https://www.vaultproject.io/api-docs/auth/approle#login-with-approle), so no other dependencies are needed. + +--- + +**Required:** +- The Vault server must be [unsealed](https://www.vaultproject.io/docs/concepts/seal). +- The wrapped [auth token](https://www.vaultproject.io/docs/auth/token) used to retrieve the unwrapped Secret ID. + +--- + +*Optional* +- The creation path of the wrapped token. If this parameter value is provided, the step template will perform a [wrapping lookup](https://www.vaultproject.io/api-docs/system/wrapping-lookup) to [validate no malfeasance](https://www.vaultproject.io/docs/concepts/response-wrapping#response-wrapping-token-validation) has occurred. +- A Vault [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. **Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault). + +--- + +Notes: + +- Tested on Vault Server `1.11.3`. +- Tested on both PowerShell Desktop and PowerShell Core. +- See the HashiCorp [AppRole patterns documentation](https://learn.hashicorp.com/tutorials/vault/pattern-approle?in=vault/recommended-patterns#vault-returns-a-token) for further information.", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Variables +$VAULT_APPROLE_UNWRAP_SECRETID_ADDRESS = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.VaultAddress\"] +$VAULT_APPROLE_UNWRAP_SECRETID_API_VERSION = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.ApiVersion\"] +$VAULT_APPROLE_UNWRAP_SECRETID_TOKEN = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.WrappedToken\"] + +# Optional Variables +$VAULT_APPROLE_UNWRAP_SECRETID_NAMESPACE = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.Namespace\"] +$VAULT_APPROLE_UNWRAP_SECRETID_TOKEN_CREATION_PATH = $OctopusParameters[\"Vault.AppRole.UnwrapSecretID.WrappedTokenCreationPath\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_ADDRESS)) { + throw \"Required parameter VAULT_APPROLE_UNWRAP_SECRETID_ADDRESS not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_API_VERSION)) { + throw \"Required parameter VAULT_APPROLE_UNWRAP_SECRETID_API_VERSION not specified\" +} + +if ([string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_TOKEN)) { + throw \"Required parameter VAULT_APPROLE_UNWRAP_SECRETID_TOKEN not specified\" +} + +# Helper functions +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $rawResponse = $reader.ReadToEnd() + $response = \"\" + try { $response = $rawResponse | ConvertFrom-Json } catch { $response = $rawResponse } + return $response + } + } + else { + return $RequestError.ErrorDetails.Message + } +} +############################################################################### + +$VAULT_APPROLE_UNWRAP_SECRETID_ADDRESS = $VAULT_APPROLE_UNWRAP_SECRETID_ADDRESS.TrimEnd('/') + +# Local variables +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +try { + + Write-Verbose \"X-Vault-Namespace header: $VAULT_APPROLE_UNWRAP_SECRETID_NAMESPACE\" + + # Should we validate lookup token's creation path? + if (![string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_TOKEN_CREATION_PATH)) { + $uri = \"$VAULT_APPROLE_UNWRAP_SECRETID_ADDRESS/$VAULT_APPROLE_UNWRAP_SECRETID_API_VERSION/sys/wrapping/lookup\" + $payload = @{ + token = $VAULT_APPROLE_UNWRAP_SECRETID_TOKEN + } + + $Headers = @{} + if (-not [string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_NAMESPACE)) { + $Headers.Add(\"X-Vault-Namespace\", $VAULT_APPROLE_UNWRAP_SECRETID_NAMESPACE) + } + + Write-Verbose \"Making Post request to $uri\" + $response = Invoke-RestMethod -Uri $uri -Method Post -Body ($payload | ConvertTo-Json -Depth 10) -Headers $Headers + + if ($null -ne $response) { + Write-Verbose \"Validating Wrapped token creation path.\" + $Lookup_CreationPath = $response.data.creation_path + if ($VAULT_APPROLE_UNWRAP_SECRETID_TOKEN_CREATION_PATH -ne $Lookup_CreationPath) { + throw \"Supplied Wrapped token creation path failed lookup validation. Check the creation path value and retry.\" + } + } + else { + Write-Error \"Null or Empty response returned from Vault server lookup\" -Category InvalidResult + } + } + + # Call to unwrap secret id from wrapped token. + $Headers = @{ + \"X-Vault-Token\" = $VAULT_APPROLE_UNWRAP_SECRETID_TOKEN + } + + if (-not [string]::IsNullOrWhiteSpace($VAULT_APPROLE_UNWRAP_SECRETID_NAMESPACE)) { + $Headers.Add(\"X-Vault-Namespace\", $VAULT_APPROLE_UNWRAP_SECRETID_NAMESPACE) + } + + $uri = \"$VAULT_APPROLE_UNWRAP_SECRETID_ADDRESS/$VAULT_APPROLE_UNWRAP_SECRETID_API_VERSION/sys/wrapping/unwrap\" + Write-Verbose \"Making Post request to $uri\" + $response = Invoke-RestMethod -Uri $uri -Headers $Headers -Method Post + + if ($null -ne $response) { + Set-OctopusVariable -Name \"UnwrappedSecretID\" -Value $response.data.secret_id -Sensitive + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.UnwrappedSecretID}\" + } + else { + Write-Error \"Null or Empty response returned from Vault server unwrap\" -Category InvalidResult + } +} +catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred unwrapping secretid: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.errors) { + $AdditionalDetail = $ErrorBody.errors -Join \",\" + } + else { + $errorDetails = $null + try { $errorDetails = ConvertFrom-Json $ErrorBody } catch {} + $AdditionalDetail += if ($null -ne $errorDetails) { $errorDetails.errors -Join \",\" } else { $ErrorBody } + } + } + + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`n`tDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category ConnectionError +}" + }, + "Parameters": [ + { + "Id": "b30bafd3-a5b6-4c26-8eea-af938f5ba7da", + "Name": "Vault.AppRole.UnwrapSecretID.VaultAddress", + "Label": "Vault Server URL", + "HelpText": "The URL of the Vault instance you are connecting to. Port should be included (The default is `8200`). For example: + + +`https://myvault.local:8200/`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a810fce1-3180-4eb8-b342-8b2d76a39667", + "Name": "Vault.AppRole.UnwrapSecretID.ApiVersion", + "Label": "API version", + "HelpText": "All API routes are prefixed with a version e.g. `/v1/`. + +See the [API documentation](https://www.vaultproject.io/api-docs) for further details.", + "DefaultValue": "v1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "v1|v1" + } + }, + { + "Id": "e60b5c0b-dacc-4a99-8907-82e57eb9f573", + "Name": "Vault.AppRole.UnwrapSecretID.Namespace", + "Label": "Namespace (Optional)", + "HelpText": "The _optional_ [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. + +**Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) .", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6fd2fe01-9600-4ccc-a3f2-f733af79a255", + "Name": "Vault.AppRole.UnwrapSecretID.WrappedToken", + "Label": "Wrapped Token", + "HelpText": "This is the wrapped token used to retrieve the actual Secret ID from Vault.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "6dc8f829-c89c-403f-805c-350710d8d855", + "Name": "Vault.AppRole.UnwrapSecretID.WrappedTokenCreationPath", + "Label": "Token Creation Path", + "HelpText": "*Optional* - the creation path of the wrapped token. If this parameter value is provided, the step template will perform a [wrapping lookup](https://www.vaultproject.io/api-docs/system/wrapping-lookup) to [validate no malfeasance](https://www.vaultproject.io/docs/concepts/response-wrapping#response-wrapping-token-validation) has occurred.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedAt": "2022-09-18T09:22:26.156Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2022-09-18T09:22:26.156Z", + "OctopusVersion": "2022.4.2266", + "Type": "ActionTemplate" + }, + "Category": "hashicorp-vault" + } diff --git a/step-templates/hashicorp-vault-generate-jwt.json.human b/step-templates/hashicorp-vault-generate-jwt.json.human new file mode 100644 index 000000000..e8a5790d4 --- /dev/null +++ b/step-templates/hashicorp-vault-generate-jwt.json.human @@ -0,0 +1,673 @@ +{ + "Id": "e72fd23a-3bfd-4758-a720-2462d5206f65", + "Name": "HashiCorp Vault - Generate JWT", + "Description": "This step template generates a [Json Web Token (JWT)](https://en.wikipedia.org/wiki/JSON_Web_Token) for use with HashiCorp Vault. + +The step is based on the existing [JWT - Generate JSON Web Token](https://library.octopus.com/step-templates/1ca0401c-dfca-420e-81ca-1f4b7cf02d2d/actiontemplate-jwt-generate-json-web-token) step template. + +However, it differs as it offers less flexibility in choosing the fields to use in the generated JWT and is opinionated towards support for Vault [entities and groups](https://learn.hashicorp.com/tutorials/vault/identity). + +The resulting JWT will be stored as a [sensitive output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) called **JWT**. + +A private key needs to be provided that will sign the combined JWT header and payload. + +Currently, the following three signing algorithms are supported: + +1. `RS256` - RSASSA-PKCS1-v1_5 using SHA-256 +2. `RS384` - RSASSA-PKCS1-v1_5 using SHA-384 +3. `RS512` - RSASSA-PKCS1-v1_5 using SHA-512 + +The default is `RS256`. + +**Notes:** +- Tested on Windows and Linux (PowerShell Core) +- Tested with Octopus **2020.1**", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +# Helper functions +############################################################################### + +function ConvertTo-JwtBase64 { + param ( + $Value + ) + if ($Value -is [string]) { + $ConvertedValue = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Value)) -replace '\\+', '-' -replace '/', '_' -replace '=' + } + elseif ($Value -is [byte[]]) { + $ConvertedValue = [Convert]::ToBase64String($Value) -replace '\\+', '-' -replace '/', '_' -replace '=' + } + + return $ConvertedValue +} + +function ConvertTo-Subject { + param ([string]$Subject, [string]$IdentityPrefix) + return ConvertTo-Identity -IdentityValue $Subject -IdentityPrefix $IdentityPrefix +} + +function ConvertTo-Groups { + param ([string]$Groups, [string]$IdentityPrefix) + return ConvertTo-Identity -IdentityValue $Groups -IdentityPrefix $IdentityPrefix +} + +function ConvertTo-Identity { + param ( + [string]$IdentityValue, + [string]$IdentityPrefix + ) + $ServerUri = $OctopusParameters[\"Octopus.Web.ServerUri\"].ToLower() -Replace \"http://\" -Replace \"https://\" + $ProjectGroup = $OctopusParameters[\"Octopus.ProjectGroup.Name\"].ToLower() -Replace \" \", \"-\" + $Project = $OctopusParameters[\"Octopus.Project.Name\"].ToLower() -Replace \" \", \"-\" + $Environment = $OctopusParameters[\"Octopus.Environment.Name\"].ToLower() -Replace \" \", \"-\" + $identity = \"\" + switch ($VAULT_GENERATE_JWT_SUBJECT) { + \"serveruri\" { + $identity = $ServerUri + } + \"projectgroup\" { + $identity = \"$ServerUri/$ProjectGroup\" + } + \"project\" { + $identity = \"$ServerUri/$ProjectGroup/$Project\" + } + \"environment\" { + $identity = \"$ServerUri/$ProjectGroup/$Project/$Environment\" + } + } + if (![string]::IsNullOrWhiteSpace($IdentityPrefix)) { + $identity = \"$IdentityPrefix$identity\" + } + return $identity +} + +############################################################################### + +# Variables +$VAULT_GENERATE_JWT_PRIVATE_KEY = $OctopusParameters[\"Vault.Generate.JWT.PrivateKey\"] +$VAULT_GENERATE_JWT_ALGORITHM = $OctopusParameters[\"Vault.Generate.JWT.Signing.Algorithm\"] +$VAULT_GENERATE_JWT_EXPIRES_MINS = $OctopusParameters[\"Vault.Generate.JWT.ExpiresAfterMinutes\"] +$VAULT_GENERATE_JWT_ISSUER = $OctopusParameters[\"Vault.Generate.JWT.Issuer\"] +$VAULT_GENERATE_JWT_SUBJECT = $OctopusParameters[\"Vault.Generate.JWT.Subject\"] +$VAULT_GENERATE_JWT_GROUPS = $OctopusParameters[\"Vault.Generate.JWT.Groups\"] +$VAULT_GENERATE_JWT_AUDIENCE = $OctopusParameters[\"Vault.Generate.JWT.Audience\"] + +# Optional +$VAULT_GENERATE_JWT_IDENTITY_PREFIX = $OctopusParameters[\"Vault.Generate.JWT.IdentityPrefix\"] + +$subject = ConvertTo-Subject -Groups $VAULT_GENERATE_JWT_GROUPS -IdentityPrefix $VAULT_GENERATE_JWT_IDENTITY_PREFIX +$groups = ConvertTo-Groups -Groups $VAULT_GENERATE_JWT_GROUPS -IdentityPrefix $VAULT_GENERATE_JWT_IDENTITY_PREFIX + +$audiences = @() +if (![string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_AUDIENCE)) { + @(($VAULT_GENERATE_JWT_AUDIENCE -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + $audiences += $_ + } + } +} + +# Validation +if ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_PRIVATE_KEY)) { + throw \"Required parameter Vault.Generate.JWT.PrivateKey not specified.\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_ALGORITHM)) { + throw \"Required parameter Vault.Generate.JWT.Signing.Algorithm not specified.\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_EXPIRES_MINS)) { + throw \"Required parameter Vault.Generate.JWT.ExpiresAfterMinutes not specified.\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_ISSUER)) { + throw \"Required parameter Vault.Generate.JWT.Issuer not specified.\" +} +if ($audiences.Length -le 0) { + throw \"Required parameter Vault.Generate.JWT.Audience not specified.\" +} +if ([string]::IsNullOrWhiteSpace($subject)) { + throw \"Required parameter Vault.Generate.JWT.Subject not specified.\" +} +if ([string]::IsNullOrWhiteSpace($groups)) { + throw \"Required parameter Vault.Generate.JWT.Groups not specified.\" +} + + +# Signing functions +############################################################################### + +$RsaPrivateKey_Header = \"-----BEGIN RSA PRIVATE KEY-----\" +$RsaPrivateKey_Footer = \"-----END RSA PRIVATE KEY-----\" +$Pkcs8_PrivateKey_Header = \"-----BEGIN PRIVATE KEY-----\" +$Pkcs8_PrivateKey_Footer = \"-----END PRIVATE KEY-----\" + +function ExtractPemData { + param ( + [string]$Pem, + [string]$Header, + [string]$Footer + ) + + $Start = $Pem.IndexOf($Header) + $Header.Length + $End = $Pem.IndexOf($Footer, $Start) - $Start + $EncodedPem = ($Pem.Substring($Start, $End).Trim()) -Replace \" \", \"`n\" + + $PemData = [Convert]::FromBase64String($EncodedPem) + return [byte[]]$PemData +} + +function DecodeIntSize { + param ( + [System.IO.BinaryReader]$BinaryReader + ) + + [byte]$byteValue = $BinaryReader.ReadByte() + + # If anything other than 0x02, an ASN.1 integer follows. + if ($byteValue -ne 0x02) { + return 0; + } + + $byteValue = $BinaryReader.ReadByte() + # 0x81 == Data size in next byte. + if ($byteValue -eq 0x81) { + $size = $BinaryReader.ReadByte() + } + # 0x82 == Data size in next 2 bytes. + else { + if ($byteValue -eq 0x82) { + [byte]$high = $BinaryReader.ReadByte() + [byte]$low = $BinaryReader.ReadByte() + $byteValues = [byte[]]@($low, $high, 0x00, 0x00) + $size = [System.BitConverter]::ToInt32($byteValues, 0) + } + else { + # Otherwise, data size has already been read above. + $size = $byteValue + } + } + # Remove high-order zeros in data + $byteValue = $BinaryReader.ReadByte() + while ($byteValue -eq 0x00) { + $byteValue = $BinaryReader.ReadByte() + $size -= 1 + } + + $BinaryReader.BaseStream.Seek(-1, [System.IO.SeekOrigin]::Current) | Out-Null + return $size +} + +function PadByteArray { + param ( + [byte[]]$Bytes, + [int]$Size + ) + + if ($Bytes.Length -eq $Size) { + return $Bytes + } + if ($Bytes.Length -gt $Size) { + throw \"Specified size '$Size' to pad is too small for byte array of size '$($Bytes.Length)'.\" + } + + [byte[]]$PaddedBytes = New-Object Byte[] $Size + [System.Array]::Copy($Bytes, 0, $PaddedBytes, $Size - $bytes.Length, $bytes.Length) | Out-Null + return $PaddedBytes +} + +function Compare-ByteArrays { + param ( + [byte[]]$First, + [byte[]]$Second + ) + if ($First.Length -ne $Second.Length) { + return $False + } + [int]$i = 0 + foreach ($byte in $First) { + if ($byte -ne $Second[$i]) { + return $False + } + $i = $i + 1 + } + return $True +} + +function CreateRSAFromPkcs8 { + param ( + [byte[]]$KeyBytes + ) + Write-Verbose \"Reading RSA Pkcs8 private key bytes\" + + # The encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = \"1.2.840.113549.1.1.1\" + # this byte[] includes the sequence byte and terminal encoded null + [byte[]]$SeqOID = 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 + [byte[]]$Seq = New-Object byte[] 15 + + # Have to wrap $KeyBytes in another array :| + $MemoryStream = New-Object System.IO.MemoryStream(, $KeyBytes) + $reader = New-Object System.IO.BinaryReader($MemoryStream) + $StreamLength = [int]$MemoryStream.Length + + try { + [UInt16]$Bytes = $reader.ReadUInt16() + + if ($Bytes -eq 0x8130) { + $reader.ReadByte() | Out-Null + } + elseif ($Bytes -eq 0x8230) { + $reader.ReadInt16() | Out-Null + } + else { + return $null + } + + [byte]$byteValue = $reader.ReadByte() + + if ($byteValue -ne 0x02) { + return $null + } + + $Bytes = $reader.ReadUInt16() + + if ($Bytes -ne 0x0001) { + return $null + } + + # Read the Sequence OID + $Seq = $reader.ReadBytes(15) + $SequenceMatches = Compare-ByteArrays -First $Seq -Second $SeqOID + if ($SequenceMatches -eq $False) { + Write-Verbose \"Sequence OID doesnt match\" + return $null + } + + $byteValue = $reader.ReadByte() + # Next byte should be a Octet string + if ($byteValue -ne 0x04) { + return $null + } + # Read next byte / 2 bytes. + # Should be either: 0x81 or 0x82; otherwise it's the byte count. + $byteValue = $reader.ReadByte() + if ($byteValue -eq 0x81) { + $reader.ReadByte() | Out-Null + } + else { + if ($byteValue -eq 0x82) { + $reader.ReadUInt16() | Out-Null + } + } + + # Remaining sequence *should* be the RSA Pkcs1 private Key bytes + [byte[]]$RsaKeyBytes = $reader.ReadBytes([int]($StreamLength - $MemoryStream.Position)) + Write-Verbose \"Attempting to create RSA object from remaining Pkcs1 bytes\" + $rsa = CreateRSAFromPkcs1 -KeyBytes $RsaKeyBytes + return $rsa + } + catch { + Write-Warning \"CreateRSAFromPkcs8: Exception occurred - $($_.Exception.Message)\" + return $null + } + finally { + if ($null -ne $reader) { $reader.Close() } + if ($null -ne $MemoryStream) { $MemoryStream.Close() } + } +} + +function CreateRSAFromPkcs1 { + param ( + [byte[]]$KeyBytes + ) + Write-Verbose \"Reading RSA Pkcs1 private key bytes\" + # Have to wrap $KeyBytes in another array :| + $MemoryStream = New-Object System.IO.MemoryStream(, $KeyBytes) + $reader = New-Object System.IO.BinaryReader($MemoryStream) + try { + + [UInt16]$Bytes = $reader.ReadUInt16() + + if ($Bytes -eq 0x8130) { + $reader.ReadByte() | Out-Null + } + elseif ($Bytes -eq 0x8230) { + $reader.ReadInt16() | Out-Null + } + else { + return $null + } + + $Bytes = $reader.ReadUInt16() + if ($Bytes -ne 0x0102) { + return $null + } + + [byte]$byteValue = $reader.ReadByte() + if ($byteValue -ne 0x00) { + return $null + } + + # Private key parameters are integer sequences. + # For a summary of the RSA Parameters fields, + # See https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.rsaparameters#summary-of-fields + + $Modulus_Size = DecodeIntSize -BinaryReader $reader + $Modulus = $reader.ReadBytes($Modulus_Size) + + $E_Size = DecodeIntSize -BinaryReader $reader + $E = $reader.ReadBytes($E_Size) + + $D_Size = DecodeIntSize -BinaryReader $reader + $D = $reader.ReadBytes($D_Size) + + $P_Size = DecodeIntSize -BinaryReader $reader + $P = $reader.ReadBytes($P_Size) + + $Q_Size = DecodeIntSize -BinaryReader $reader + $Q = $reader.ReadBytes($Q_Size) + + $DP_Size = DecodeIntSize -BinaryReader $reader + $DP = $reader.ReadBytes($DP_Size) + + $DQ_Size = DecodeIntSize -BinaryReader $reader + $DQ = $reader.ReadBytes($DQ_Size) + + $IQ_Size = DecodeIntSize -BinaryReader $reader + $IQ = $reader.ReadBytes($IQ_Size) + + $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider + $rsaParameters = New-Object System.Security.Cryptography.RSAParameters + $rsaParameters.Modulus = $Modulus + $rsaParameters.Exponent = $E + $rsaParameters.P = $P + $rsaParameters.Q = $Q + # Some RSAParameter values dont play well with byte buffers having leading zeroes removed. + $rsaParameters.D = PadByteArray -Bytes $D -Size $Modulus.Length + $rsaParameters.DP = PadByteArray -Bytes $DP -Size $P.Length + $rsaParameters.DQ = PadByteArray -Bytes $DQ -Size $Q.Length + $rsaParameters.InverseQ = PadByteArray -Bytes $IQ -Size $Q.Length + $rsa.ImportParameters($rsaParameters) + + Write-Verbose \"Completed RSA object creation\" + return $rsa + } + catch { + Write-Warning \"CreateRSA-FromPkcs1: Exception occurred - $($_.Exception.Message)\" + return $null + } + finally { + if ($null -ne $reader) { $reader.Close() } + if ($null -ne $MemoryStream) { $MemoryStream.Close() } + } +} + +function CreateSigningKey { + param ( + [string]$Key + ) + try { + $Key = $Key.Trim() + switch -Wildcard($Key) { + \"$Pkcs8_PrivateKey_Header*\" { + $KeyBytes = ExtractPemData -PEM $Key -Header $Pkcs8_PrivateKey_Header -Footer $Pkcs8_PrivateKey_Footer + $SigningKey = CreateRSAFromPkcs8 -KeyBytes $KeyBytes + return $SigningKey + } + \"$RsaPrivateKey_Header*\" { + $KeyBytes = ExtractPemData -PEM $Key -Header $RsaPrivateKey_Header -Footer $RsaPrivateKey_Footer + $SigningKey = CreateRSAFromPkcs1 -KeyBytes $KeyBytes + return $SigningKey + } + default { + Write-Verbose \"The PEM header could not be found. Accepted headers: 'BEGIN PRIVATE KEY', 'BEGIN RSA PRIVATE KEY'\" + return $null + } + } + } + catch { + Write-Warning \"Couldn't create signing key: $($_.Exception.Message)\" + return $null + } +} + +############################################################################### + +# Local variables +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] +$OutputVariableName = \"JWT\" + +Write-Verbose \"Vault.Generate.JWT.Signing.Algorithm: $VAULT_GENERATE_JWT_ALGORITHM\" +Write-Verbose \"Vault.Generate.JWT.ExpiresAfterMinutes: $VAULT_GENERATE_JWT_EXPIRES_MINS\" +Write-Verbose \"Vault.Generate.JWT.Issuer: $VAULT_GENERATE_JWT_ISSUER\" +Write-Verbose \"Vault.Generate.JWT.Audience(s): $($audiences -Join \",\")\" +Write-Verbose \"Vault.Generate.JWT.Subject: $subject\" +Write-Verbose \"Vault.Generate.JWT.Groups: $groups\" +Write-Verbose \"Vault.Generate.JWT.IdentityPrefix: $VAULT_GENERATE_JWT_IDENTITY_PREFIX\" +Write-Verbose \"Step Name: $StepName\" + +try { + + # Created + Expires + $Created = (Get-Date).ToUniversalTime() + $Expires = $Created.AddMinutes([int]$VAULT_GENERATE_JWT_EXPIRES_MINS) + + $createDate = [Math]::Floor([decimal](Get-Date($Created) -UFormat \"%s\")) + $expiryDate = [Math]::Floor([decimal](Get-Date($Expires) -UFormat \"%s\")) + + $JwtHeader = @{ + alg = $VAULT_GENERATE_JWT_ALGORITHM; + typ = \"JWT\"; + } | ConvertTo-Json -Compress + + $JwtPayload = [Ordered]@{ + iat = [long]$createDate; + exp = [long]$expiryDate; + iss = $VAULT_GENERATE_JWT_ISSUER; + aud = $audiences; + sub = $subject; + groups = $groups; + } + + $JwtPayload = $JwtPayload | ConvertTo-Json -Compress + + $base64Header = ConvertTo-JwtBase64 -Value $JwtHeader + $base64Payload = ConvertTo-JwtBase64 -Value $JwtPayload + + $Jwt = $base64Header + '.' + $base64Payload + + $JwtBytes = [System.Text.Encoding]::UTF8.GetBytes($Jwt) + $JwtSignature = $null + + switch ($VAULT_GENERATE_JWT_ALGORITHM) { + \"RS256\" { + try { + + $rsa = CreateSigningKey -Key $VAULT_GENERATE_JWT_PRIVATE_KEY + if ($null -eq $rsa) { + throw \"Couldn't create RSA object\" + } + $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1) + $JwtSignature = ConvertTo-JwtBase64 -Value $Signature + } + catch { throw \"Signing with SHA256 and Pkcs1 padding failed using private key: $($_.Exception.Message)\" } + finally { if ($null -ne $rsa) { $rsa.Dispose() } } + + } + \"RS384\" { + try { + $rsa = CreateSigningKey -Key $VAULT_GENERATE_JWT_PRIVATE_KEY + if ($null -eq $rsa) { + throw \"Couldn't create RSA object\" + } + $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA384, [Security.Cryptography.RSASignaturePadding]::Pkcs1) + $JwtSignature = ConvertTo-JwtBase64 -Value $Signature + } + catch { throw \"Signing with SHA384 and Pkcs1 padding failed using private key: $($_.Exception.Message)\" } + finally { if ($null -ne $rsa) { $rsa.Dispose() } } + } + \"RS512\" { + try { + $rsa = CreateSigningKey -Key $VAULT_GENERATE_JWT_PRIVATE_KEY + if ($null -eq $rsa) { + throw \"Couldn't create RSA object\" + } + $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA512, [Security.Cryptography.RSASignaturePadding]::Pkcs1) + $JwtSignature = ConvertTo-JwtBase64 -Value $Signature + } + catch { throw \"Signing with SHA512 and Pkcs1 padding failed using private key: $($_.Exception.Message)\" } + finally { if ($null -ne $rsa) { $rsa.Dispose() } } + } + default { + throw \"The algorithm is not one of the supported: 'RS256', 'RS384', 'RS512'\" + } + } + if ([string]::IsNullOrWhiteSpace($JwtSignature) -eq $True) { + throw \"JWT signature empty.\" + } + + $Jwt = \"$Jwt.$JwtSignature\" + Set-OctopusVariable -Name $OutputVariableName -Value $Jwt -Sensitive + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$OutputVariableName}\" +} +catch { + $ExceptionMessage = $_.Exception.Message + $Message = \"An error occurred generating a JWT: $ExceptionMessage\" + Write-Error $Message -Category InvalidResult +}" + }, + "Parameters": [ + { + "Id": "71bacc82-21d7-4d08-a944-15dacd0a3405", + "Name": "Vault.Generate.JWT.PrivateKey", + "Label": "JWT Private signing key", + "HelpText": "Provide the private key in PEM format to be used to sign the JWT. + +Accepted headers: + +- `----BEGIN RSA PRIVATE KEY-----` +- `-----BEGIN PRIVATE KEY----` + +**Note:** It's recommended to use a sensitive variable to provide this value. If you instead enter the private key directly, the step template will attempt to detect this by replacing spaces with new lines ``n`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "84759ecc-a3f6-4822-a678-833269a8bf0d", + "Name": "Vault.Generate.JWT.Signing.Algorithm", + "Label": "JWT signing algorithm", + "HelpText": "The JWA [algorithm](https://datatracker.ietf.org/doc/html/rfc7518#section-3.1) to use when signing the JWT. The default is `RS256` (RSASSA-PKCS1-v1_5 using SHA-256). + +This value is also used for the `alg` [algorithm header](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1).", + "DefaultValue": "RS256", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "RS256|RS256: RSASSA-PKCS1-v1_5 using SHA-256 +RS384|RS384: RSASSA-PKCS1-v1_5 using SHA-384 +RS512|RS512: RSASSA-PKCS1-v1_5 using SHA-512" + } + }, + { + "Id": "76d289c5-f6f9-4cfb-b876-13515745f03f", + "Name": "Vault.Generate.JWT.ExpiresAfterMinutes", + "Label": "JWT expiry time in minutes", + "HelpText": "The number of minutes after the JWT is created that it expires. This value is used for the `exp` [expiration time claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4). The default is `20` minutes.", + "DefaultValue": "20", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "20d57320-15e4-4777-916b-8563192a326d", + "Name": "Vault.Generate.JWT.Issuer", + "Label": "JWT Issuer claim", + "HelpText": "The `iss` [isuer claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1) identifies the principal that issued the JWT.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "12a68971-8e0b-4b90-9a27-4c81413d0fb7", + "Name": "Vault.Generate.JWT.Audience", + "Label": "JWT Audience claim", + "HelpText": "The `aud` [audience claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3) identifies the recipients that the JWT is intended for. This is typically the base address of the resource being accessed, such as `https://example-domain.com`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "e7026958-60cb-4652-9fe0-2ef2965de951", + "Name": "Vault.Generate.JWT.Subject", + "Label": "JWT Subject claim", + "HelpText": "Choose from a defined list of values for the `sub` [subject claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2) of the JWT. + +Available options start from least granular to most granular: + +- Octopus Server Uri +- Octopus Server Uri, and Project Group name +- Octopus Server Uri, Project Group, and Project name +- Octopus Server Uri, Project Group, Project and Environment name + +*Note:* If the **Server Uri** property has not been set for the Octopus instance, the `Octopus.Web.ServerUri` variable will resolve to an empty string. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "serveruri|Octopus Server Uri +projectgroup|Octopus Server Uri, and Project Group +project|Octopus Server Uri, Project Group, and Project +environment|Octopus Server Uri, Project Group, Project, and Environment" + } + }, + { + "Id": "1bd2294c-64a3-4dad-bb9f-1a4cce5eaff5", + "Name": "Vault.Generate.JWT.Groups", + "Label": "JWT Groups claim", + "HelpText": "Choose from a defined list of values for the `groups` [private claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.3) that identifies groups associated with this JWT. + +Available options start from least granular to most granular: + +- Octopus Server Uri +- Octopus Server Uri, and Project Group name +- Octopus Server Uri, Project Group, and Project name +- Octopus Server Uri, Project Group, Project and Environment name + +*Note:* If the **Server Uri** property has not been set for the Octopus instance, the `Octopus.Web.ServerUri` variable will resolve to an empty string.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "serveruri|Octopus Server Uri +projectgroup|Octopus Server Uri, and Project Group +project|Octopus Server Uri, Project Group, and Project +environment|Octopus Server Uri, Project Group, Project, and Environment" + } + }, + { + "Id": "88298719-27ac-48c6-8c13-c40a3aaceb6c", + "Name": "Vault.Generate.JWT.IdentityPrefix", + "Label": "JWT Identity Prefix (optional)", + "HelpText": "*Optional* - A prefix to be added to the values used in **both** the `sub` and `groups` claims. + +If no value is provided, the prefix is not included.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-08-10T12:55:30.088Z", + "OctopusVersion": "2021.2.7090", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "hashicorp-vault" + } diff --git a/step-templates/hashicorp-vault-jwt-login.json.human b/step-templates/hashicorp-vault-jwt-login.json.human new file mode 100644 index 000000000..c9fc2b2f5 --- /dev/null +++ b/step-templates/hashicorp-vault-jwt-login.json.human @@ -0,0 +1,215 @@ +{ + "Id": "d49bc861-cd36-4624-960c-77613a54b139", + "Name": "HashiCorp Vault - JWT Login", + "Description": "This step logs into a HashiCorp Vault server using the [JWT](https://www.vaultproject.io/docs/auth/jwt) auth method. + +The `client_token` from the response will be made available as a sensitive [Output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) named `JWTAuthToken` for use in other step templates. + +This step template makes use of the [Rest API](https://www.vaultproject.io/api/auth/ldap#login-with-ldap-user), so no other dependencies are needed. + +**Required:** +- The Vault server must be [unsealed](https://www.vaultproject.io/docs/concepts/seal). +- You must supply the full path where the JWT auth method is mounted. +- You must supply a valid Vault [JWT role](https://www.vaultproject.io/api/auth/jwt#create-role). +- You must supply a valid, non-expired [JWT token](https://en.wikipedia.org/wiki/JSON_Web_Token). + +*Optional*: +- A Vault [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. **Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) . + +Notes: + +- Tested on Vault Server `1.11.3`. +- Tested on both PowerShell Desktop and PowerShell Core.", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Variables +$VAULT_JWT_LOGIN_ADDRESS = $OctopusParameters[\"Vault.JWT.Login.VaultAddress\"] +$VAULT_JWT_LOGIN_API_VERSION = $OctopusParameters[\"Vault.JWT.Login.ApiVersion\"] +$VAULT_JWT_LOGIN_AUTH_PATH = $OctopusParameters[\"Vault.JWT.Login.AuthPath\"] +$VAULT_JWT_LOGIN_ROLE = $OctopusParameters[\"Vault.JWT.Login.Role\"] +$VAULT_JWT_LOGIN_TOKEN = $OctopusParameters[\"Vault.JWT.Token\"] + +# Optional +$VAULT_JWT_LOGIN_NAMESPACE = $OctopusParameters[\"Vault.JWT.Login.Namespace\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($VAULT_JWT_LOGIN_ADDRESS)) { + throw \"Required parameter Vault.JWT.Login.VaultAddress not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_JWT_LOGIN_API_VERSION)) { + throw \"Required parameter Vault.JWT.Login.ApiVersion not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_JWT_LOGIN_AUTH_PATH)) { + throw \"Required parameter Vault.JWT.Login.AuthPath not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_JWT_LOGIN_ROLE)) { + throw \"Required parameter Vault.JWT.Login.Role not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_JWT_LOGIN_TOKEN)) { + throw \"Required parameter Vault.JWT.Token not specified\" +} + +# Helper functions +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $rawResponse = $reader.ReadToEnd() + $response = \"\" + try { $response = $rawResponse | ConvertFrom-Json } catch { $response = $rawResponse } + return $response + } + } + else { + return $RequestError.ErrorDetails.Message + } +} +############################################################################### + +$VAULT_JWT_LOGIN_ADDRESS = $VAULT_JWT_LOGIN_ADDRESS.TrimEnd('/') +$VAULT_JWT_LOGIN_AUTH_PATH = $VAULT_JWT_LOGIN_AUTH_PATH.TrimStart('/').TrimEnd('/') + +# Local variables +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +try { + $payload = @{ + role = $VAULT_JWT_LOGIN_ROLE; + jwt = $VAULT_JWT_LOGIN_TOKEN; + } + + $Headers = @{} + if (-not [string]::IsNullOrWhiteSpace($VAULT_JWT_LOGIN_NAMESPACE)) { + Write-Verbose \"Setting 'X-Vault-Namespace' header to: $VAULT_JWT_LOGIN_NAMESPACE\" + $Headers.Add(\"X-Vault-Namespace\", $VAULT_JWT_LOGIN_NAMESPACE) + } + + $uri = \"$VAULT_JWT_LOGIN_ADDRESS/$VAULT_JWT_LOGIN_API_VERSION/$VAULT_JWT_LOGIN_AUTH_PATH/login\" + Write-Verbose \"Making request to $uri\" + $response = Invoke-RestMethod -Method Post -Uri $uri -Body ($payload | ConvertTo-Json -Depth 10) -Headers $Headers + if ($null -ne $response) { + Set-OctopusVariable -Name \"JWTAuthToken\" -Value $response.auth.client_token -Sensitive + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.JWTAuthToken}\" + } + else { + Write-Error \"Null or Empty response returned from Vault server\" -Category InvalidResult + } +} +catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred logging in with JWT: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.errors) { + $AdditionalDetail = $ErrorBody.errors -Join \",\" + } + else { + $errorDetails = $null + try { $errorDetails = ConvertFrom-Json $ErrorBody } catch {} + $AdditionalDetail += if ($null -ne $errorDetails) { $errorDetails.errors -Join \",\" } else { $ErrorBody } + } + } + + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`n`tDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category ConnectionError +}" + }, + "Parameters": [ + { + "Id": "37bec45d-aac8-4962-a8e8-1ccf38ab3038", + "Name": "Vault.JWT.Login.VaultAddress", + "Label": "Vault Server URL", + "HelpText": "The URL of the Vault instance you are connecting to. Port should be included (The default is `8200`). For example: + + +`https://myvault.local:8200/`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c4dc7abc-d659-42c3-9bc5-99c12049b9a6", + "Name": "Vault.JWT.Login.ApiVersion", + "Label": "API version", + "HelpText": "All API routes are prefixed with a version e.g. `/v1/`. + +See the [API documentation](https://www.vaultproject.io/api-docs) for further details.", + "DefaultValue": "v1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "v1|v1" + } + }, + { + "Id": "d2a19b73-36fe-445d-be9b-13d0b54adbfa", + "Name": "Vault.JWT.Login.Namespace", + "Label": "Namespace (Optional)", + "HelpText": "The _optional_ [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. + +**Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) .", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "16272d7f-aac5-49c4-86f3-36725b05c750", + "Name": "Vault.JWT.Login.AuthPath", + "Label": "JWT Auth Login path", + "HelpText": "The path that the JWT method is mounted at. The default is `/auth/jwt`. If the JWT auth method was enabled at a different path, for example `my-path`, then specify `/my-path` instead. +", + "DefaultValue": "/auth/jwt", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "379393d6-98c8-4114-af56-4ecfe29517f3", + "Name": "Vault.JWT.Login.Role", + "Label": "JWT role", + "HelpText": "The Vault [JWT role](https://www.vaultproject.io/api/auth/jwt#create-role).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "75e6eb74-180c-4aa0-bc47-f7a33b0e9fb4", + "Name": "Vault.JWT.Token", + "Label": "JWT Token", + "HelpText": "The signed [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) to login with.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedAt": "2022-09-21T19:20:21.100Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2022-09-21T19:20:21.100Z", + "OctopusVersion": "2022.4.2266", + "Type": "ActionTemplate" + }, + "Category": "hashicorp-vault" + } diff --git a/step-templates/hashicorp-vault-keyvalue-v1-retrieve-secrets.json.human b/step-templates/hashicorp-vault-keyvalue-v1-retrieve-secrets.json.human new file mode 100644 index 000000000..c553acda4 --- /dev/null +++ b/step-templates/hashicorp-vault-keyvalue-v1-retrieve-secrets.json.human @@ -0,0 +1,495 @@ +{ + "Id": "9aab9522-25e0-4539-841c-8b726e6b1520", + "Name": "HashiCorp Vault - Key Value (v1) retrieve secrets", + "Description": "This step retrieves one or more secrets in a v1 Key/Value secrets engine stored within a HashiCorp Vault server using a previously obtained authentication token. + +This step template uses the [Rest API](https://www.vaultproject.io/api-docs/secret/kv/kv-v1), so no other dependencies are needed. + +--- + +**Authentication Tokens** + +Octopus recommends using one of the pre-existing Vault step templates to obtain an auth token, such as: +- The [AppRole Login](https://library.octopus.com/step-templates/e04a9cec-f04a-4da2-849b-1aed0fd408f0/actiontemplate-hashicorp-vault-approle-l) or +- The most secure [AppRole Get Wrapped SecretID](https://library.octopus.com/step-templates/76827264-af27-46d0-913a-e093a4f0db48/actiontemplate-hashicorp-vault-approle-get-wrapped-secret-id) in conjunction with the [AppRole Unwrap SecretID and Login](https://library.octopus.com/step-templates/aa113393-e615-40ed-9c5a-f95f471d728f/actiontemplate-hashicorp-vault-approle-unwrap-secret-id-and-login) template. + +--- +**Secrets Path:** + +Specify the full path to the secret(s) you want to retrieve. e.g.`/secret/config`. + +This value should contain: +- The location where the [secrets engine has been enabled](https://www.vaultproject.io/api-docs/secret/kv/kv-v1). +- The path to the secret(s) you want to retrieve. + +For example, if the secrets engine was enabled at `/my-secrets` and you wanted to retrieve the secret(s) from the path `/config`, then the value you would enter is: + +`/my-secrets/config` + +--- + +**Retrieval methods:** + +The step template operates in one of 2 retrieval modes that control how many Vault Key values are returned. The options are: +- `Single vault key` - a single key is returned. If this option is selected, the path specified will be assumed to be an individual vault key. This performs the equivalent of a `vault kv get` command using the [Get](https://www.vaultproject.io/api-docs/secret/kv/kv-v1#read-secret) method. +- `Multiple vault keys` - multiple keys can be returned. If this option is selected, the path specified will be assumed to be able to be enumerated. This performs the equivalent of a `vault kv list` command using the [List](https://www.vaultproject.io/api-docs/secret/kv/kv-v1#list-secrets) method. + +The default is `Single vault key`. + +--- + +**Optional field names:** + +Choose specific fields to be returned from the Vault key(s) found in the Key/value secrets engine, in the format `FieldName | OutputVariableName` where: + +- `FieldName` is the name of the field to retrieve from the key +- `OutputVariableName` is the _optional_ Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) name to store the secret's value in. + +If this parameter is not set, all fields found from secret keys will be returned. + +**Note:** Multiple fields can be retrieved by entering each one on a new line. + +--- + +**Sensitive output variables:** + +For each vault key's field values, an Octopus [sensitive output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) will be created for use in other steps. + +--- + +**Required:** +- The Vault server must be [unsealed](https://www.vaultproject.io/docs/concepts/seal). +- An authentication [token](https://www.vaultproject.io/docs/auth/token). + +*Optional*: +- A Vault [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. **Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault). + +Notes: + +- Tested on Vault Server `1.11.3`. +- Tested on both PowerShell Desktop and PowerShell Core.", + "ActionType": "Octopus.Script", + "Version": 6, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "### Set TLS 1.2 +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Required Variables +$VAULT_RETRIEVE_KV_V1_SECRETS_ADDRESS = $OctopusParameters[\"Vault.Retrieve.KV.V1.Secrets.VaultAddress\"] +$VAULT_RETRIEVE_KV_V1_SECRETS_API_VERSION = $OctopusParameters[\"Vault.Retrieve.KV.V1.Secrets.ApiVersion\"] +$VAULT_RETRIEVE_KV_V1_SECRETS_TOKEN = $OctopusParameters[\"Vault.Retrieve.KV.V1.Secrets.AuthToken\"] +$VAULT_RETRIEVE_KV_V1_SECRETS_PATH = $OctopusParameters[\"Vault.Retrieve.KV.V1.Secrets.SecretsPath\"] +$VAULT_RETRIEVE_KV_V1_SECRETS_METHOD = $OctopusParameters[\"Vault.Retrieve.KV.V1.Secrets.RetrievalMethod\"] +$VAULT_RETRIEVE_KV_V1_SECRETS_RECURSIVE = $OctopusParameters[\"Vault.Retrieve.KV.V1.Secrets.RecursiveSearch\"] +$VAULT_RETRIEVE_KV_V1_PRINT_VARIABLE_NAMES = $OctopusParameters[\"Vault.Retrieve.KV.V1.Secrets.PrintVariableNames\"] + +# Optional variables +$VAULT_RETRIEVE_KV_V1_SECRETS_FIELD_VALUES = $OctopusParameters[\"Vault.Retrieve.KV.V1.Secrets.FieldValues\"] +$VAULT_RETRIEVE_KV_V1_SECRETS_NAMESPACE = $OctopusParameters[\"Vault.Retrieve.KV.V1.Secrets.Namespace\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V1_SECRETS_ADDRESS)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V1_SECRETS_ADDRESS not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V1_SECRETS_API_VERSION)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V1_SECRETS_API_VERSION not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V1_SECRETS_TOKEN)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V1_SECRETS_TOKEN not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V1_SECRETS_PATH)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V1_SECRETS_PATH not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V1_SECRETS_METHOD)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V1_SECRETS_METHOD not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V1_SECRETS_RECURSIVE)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V1_SECRETS_RECURSIVE not specified\" +} + +# Helper functions +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $rawResponse = $reader.ReadToEnd() + $response = \"\" + try {$response = $rawResponse | ConvertFrom-Json} catch {$response=$rawResponse} + return $response + } + } + else { + return $RequestError.ErrorDetails.Message + } +} + +function Get-VaultSecret { + param ( + [string]$SecretEnginePath, + [string]$SecretPath, + $Fields + ) + try { + # Local variables + $VariablesCreated = 0 + $FieldsSpecified = ($Fields.Count -gt 0) + $SecretPath = $SecretPath.TrimStart(\"/\") + $WorkingPath = \"$($SecretEnginePath)/$($SecretPath)\" + $RequestPath = \"$SecretEnginePath/$($SecretPath)\" + + $uri = \"$VAULT_RETRIEVE_KV_V1_SECRETS_ADDRESS/$VAULT_RETRIEVE_KV_V1_SECRETS_API_VERSION/$([uri]::EscapeDataString($RequestPath))\" + $Headers = @{\"X-Vault-Token\" = $VAULT_RETRIEVE_KV_V1_SECRETS_TOKEN } + + if (-not [string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V1_SECRETS_NAMESPACE)) { + $Headers.Add(\"X-Vault-Namespace\", $VAULT_RETRIEVE_KV_V1_SECRETS_NAMESPACE) + } + + $response = Invoke-RestMethod -Uri $uri -Headers $Headers -Method GET + + if ($null -ne $response) { + if ($FieldsSpecified -eq $True) { + foreach ($field in $Fields) { + $fieldName = $field.Name + $fieldVariableName = $field.VariableName + $fieldValue = $response.data.$fieldName + + if ($null -ne $fieldValue) { + if ([string]::IsNullOrWhiteSpace($fieldVariableName)) { + $fieldVariableName = \"$($WorkingPath.Replace(\"/\",\".\")).$($fieldName.Trim())\" + } + + Set-OctopusVariable -Name $fieldVariableName -Value $fieldValue -Sensitive + if($VAULT_RETRIEVE_KV_V1_PRINT_VARIABLE_NAMES -eq $True) { + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$fieldVariableName}\" + } + $VariablesCreated += 1 + } + } + } + # No fields specified, iterate through each one. + else { + $secretFieldNames = $response.data | Get-Member | Where-Object { $_.MemberType -eq \"NoteProperty\" } | Select-Object -ExpandProperty \"Name\" + foreach ($fieldName in $secretFieldNames) { + $fieldVariableName = \"$($WorkingPath.Replace(\"/\",\".\")).$($fieldName.Trim())\" + $fieldValue = $response.data.$fieldName + + Set-OctopusVariable -Name $fieldVariableName -Value $fieldValue -Sensitive + if($VAULT_RETRIEVE_KV_V1_PRINT_VARIABLE_NAMES -eq $True) { + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$fieldVariableName}\" + } + $VariablesCreated += 1 + } + } + return $VariablesCreated + } + } + catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred logging in with AppRole: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.errors) { + $AdditionalDetail = $ErrorBody.errors -Join \",\" + } + else { + $errorDetails = $null + try { $errorDetails = ConvertFrom-Json $ErrorBody } catch {} + $AdditionalDetail += if ($null -ne $errorDetails) { $errorDetails.errors -Join \",\" } else { $ErrorBody } + } + } + + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`n`tDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category ConnectionError + } +} + +function List-VaultSecrets { + param ( + [string]$SecretEnginePath, + [string]$SecretPath + ) + try { + $SecretPath = $SecretPath.TrimStart(\"/\") + $RequestPath = \"$SecretEnginePath/$SecretPath\" + + # Vault uses the 'LIST' HTTP verb, which is only supported in PowerShell 6.0+ using -CustomMethod. + # Adding ?list=true will allow support for Windows Desktop PowerShell. + # See https://www.vaultproject.io/api#api-operations for further details/ + $uri = \"$VAULT_RETRIEVE_KV_V1_SECRETS_ADDRESS/$VAULT_RETRIEVE_KV_V1_SECRETS_API_VERSION/$([uri]::EscapeDataString($RequestPath))?list=true\" + $Headers = @{\"X-Vault-Token\" = $VAULT_RETRIEVE_KV_V1_SECRETS_TOKEN } + + if (-not [string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V1_SECRETS_NAMESPACE)) { + $Headers.Add(\"X-Vault-Namespace\", $VAULT_RETRIEVE_KV_V1_SECRETS_NAMESPACE) + } + + $response = Invoke-RestMethod -Uri $uri -Headers $Headers -Method GET + + return $response + } + catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred logging in with AppRole: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.errors) { + $AdditionalDetail = $ErrorBody.errors -Join \",\" + } + else { + $errorDetails = $null + try { $errorDetails = ConvertFrom-Json $ErrorBody } catch {} + $AdditionalDetail += if ($null -ne $errorDetails) { $errorDetails.errors -Join \",\" } else { $ErrorBody } + } + } + + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`n`tDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category ConnectionError + } +} + +function Recursive-GetVaultSecrets { + param( + [string]$SecretEnginePath, + [string]$SecretPath + ) + $VariablesCreated = 0 + $SecretPath = $SecretPath.TrimStart(\"/\") + $SecretPath = $SecretPath.TrimEnd(\"/\") + + Write-Verbose \"Executing Recursive-GetVaultSecrets\" + + # Get list of secrets for path + $VaultKeysResponse = List-VaultSecrets -SecretEnginePath $SecretEnginePath -SecretPath $SecretPath + + if ($null -ne $VaultKeysResponse) { + $keys = $VaultKeysResponse.data.keys + if ($null -ne $keys) { + $secretKeys = $keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) -and !$_.EndsWith(\"/\") } + foreach ($secretKey in $secretKeys) { + $secretKeyPath = \"$($SecretPath)/$secretKey\" + $variablesCreated += Get-VaultSecret -SecretEnginePath $SecretEnginePath -SecretPath $secretKeyPath -Fields $Fields + } + + if ($VAULT_RETRIEVE_KV_V1_SECRETS_RECURSIVE -eq $True) { + $folderKeys = $keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) -and $_.EndsWith(\"/\") } + foreach ($folderKey in $folderKeys) { + $Depth = $Depth += 1 + $folderPath = \"$($SecretPath)/$folderKey\" + $VariablesCreated += Recursive-GetVaultSecrets -SecretEnginePath $SecretEnginePath -SecretPath $folderPath + } + } + } + } + return $VariablesCreated +} + +############################################################################### +$VAULT_RETRIEVE_KV_V1_SECRETS_ADDRESS = $VAULT_RETRIEVE_KV_V1_SECRETS_ADDRESS.TrimEnd('/') +$VAULT_RETRIEVE_KV_V1_SECRETS_PATH = $VAULT_RETRIEVE_KV_V1_SECRETS_PATH.TrimStart('/') + +# Local variables +$RetrieveMultipleKeys = $VAULT_RETRIEVE_KV_V1_SECRETS_METHOD.ToUpper().Trim() -ne \"GET\" +$SecretPathItems = ($VAULT_RETRIEVE_KV_V1_SECRETS_PATH -Split \"/\") +$SecretEnginePath = ($SecretPathItems | Select-Object -First 1) +$SecretPath = ($SecretPathItems | Select-Object -Skip 1) -Join \"/\" +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +$Fields = @() +$VariablesCreated = 0 + +if (![string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V1_SECRETS_FIELD_VALUES)) { + + @(($VAULT_RETRIEVE_KV_V1_SECRETS_FIELD_VALUES -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + Write-Verbose \"Working on: '$_'\" + $fieldDefinition = ($_ -Split \"\\|\") + $name = $fieldDefinition[0].Trim() + if([string]::IsNullOrWhiteSpace($name)) { + throw \"Unable to establish fieldname from: '$($_)'\" + } + $field = [PsCustomObject]@{ + Name = $name + VariableName = if (![string]::IsNullOrWhiteSpace($fieldDefinition[1])) { $fieldDefinition[1].Trim() } else { \"\" } + } + $Fields += $field + } + } +} +$FieldsSpecified = ($Fields.Count -gt 0) + +Write-Verbose \"VAULT_RETRIEVE_KV_V1_SECRETS_ADDRESS: $VAULT_RETRIEVE_KV_V1_SECRETS_ADDRESS\" +Write-Verbose \"VAULT_RETRIEVE_KV_V1_SECRETS_API_VERSION: $VAULT_RETRIEVE_KV_V1_SECRETS_API_VERSION\" +Write-Verbose \"VAULT_RETRIEVE_KV_V1_SECRETS_TOKEN: '********'\" +Write-Verbose \"VAULT_RETRIEVE_KV_V1_SECRETS_PATH: $VAULT_RETRIEVE_KV_V1_SECRETS_PATH\" +Write-Verbose \"VAULT_RETRIEVE_KV_V1_SECRETS_METHOD: $VAULT_RETRIEVE_KV_V1_SECRETS_METHOD\" +Write-Verbose \"VAULT_RETRIEVE_KV_V1_SECRETS_RECURSIVE: $VAULT_RETRIEVE_KV_V1_SECRETS_RECURSIVE\" +Write-Verbose \"VAULT_RETRIEVE_KV_V1_SECRETS_NAMESPACE: $VAULT_RETRIEVE_KV_V1_SECRETS_NAMESPACE\" +Write-Verbose \"RetrieveMultipleKeys: $RetrieveMultipleKeys\" +Write-Verbose \"Fields Specified: $($FieldsSpecified)\" +Write-Verbose \"Engine Path: $SecretEnginePath\" +Write-Verbose \"Secret Path: $SecretPath\" + +$variablesCreated = 0 + +if ($RetrieveMultipleKeys -eq $false) { + $variablesCreated += Get-VaultSecret -SecretEnginePath $SecretEnginePath -SecretPath $SecretPath -Fields $Fields +} +else { + $variablesCreated = Recursive-GetVaultSecrets -SecretEnginePath $SecretEnginePath -SecretPath $SecretPath -Depth 0 +} +Write-Host \"Created $variablesCreated output variables\"" + }, + "Parameters": [ + { + "Id": "13d0b003-63ef-45d0-969d-e032ba5b41ee", + "Name": "Vault.Retrieve.KV.V1.Secrets.VaultAddress", + "Label": "Vault Server URL", + "HelpText": "The URL of the Vault instance you are connecting to. Port should be included (The default is `8200`). For example: + + +`https://myvault.local:8200/`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e167ab26-3959-4e51-93fb-39c4a1ac76db", + "Name": "Vault.Retrieve.KV.V1.Secrets.ApiVersion", + "Label": "API version", + "HelpText": "All API routes are prefixed with a version e.g. `/v1/`. + +See the [API documentation](https://www.vaultproject.io/api-docs) for further details.", + "DefaultValue": "v1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "v1|v1" + } + }, + { + "Id": "5f904ca6-299e-4591-a47b-a998c5aa9a9c", + "Name": "Vault.Retrieve.KV.V1.Secrets.Namespace", + "Label": "Namespace (Optional)", + "HelpText": "The _optional_ [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. + +**Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) .", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "66a8d06e-08ac-4c9f-9e3a-a1727f2e0898", + "Name": "Vault.Retrieve.KV.V1.Secrets.AuthToken", + "Label": "Auth Token", + "HelpText": "The [Auth Token](https://www.vaultproject.io/docs/auth/token) used to authenticate to retrieve secrets. + +Octopus recommends using one of the pre-existing Vault step templates to obtain an auth token, such as: +- The [AppRole Login](https://library.octopus.com/step-templates/e04a9cec-f04a-4da2-849b-1aed0fd408f0/actiontemplate-hashicorp-vault-approle-l) or +- The most secure [AppRole Get Wrapped SecretID](https://library.octopus.com/step-templates/76827264-af27-46d0-913a-e093a4f0db48/actiontemplate-hashicorp-vault-approle-get-wrapped-secret-id) in conjunction with the [AppRole Unwrap SecretID and Login](https://library.octopus.com/step-templates/aa113393-e615-40ed-9c5a-f95f471d728f/actiontemplate-hashicorp-vault-approle-unwrap-secret-id-and-login) template.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "2534a01a-db65-4bc5-9766-68c6567ed5f6", + "Name": "Vault.Retrieve.KV.V1.Secrets.SecretsPath", + "Label": "Secrets Path", + "HelpText": "The full path to the secret(s) you want to retrieve. e.g.`/secret/config`. + +**This value should contain:** +- The location where the [secrets engine has been enabled](https://www.vaultproject.io/api-docs/secret/kv/kv-v1). +- The path to the secret(s) you want to retrieve. + +For example, if the secrets engine was enabled at `/my-secrets` and you wanted to retrieve the secret(s) from the path `/config` then the value you would enter is: + +`/my-secrets/config`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "01ca54f6-269a-447a-9193-d7de9195b164", + "Name": "Vault.Retrieve.KV.V1.Secrets.RetrievalMethod", + "Label": "Secrets retrieval method", + "HelpText": "This controls how many Vault Key values are returned. The options are: +- A single key is returned. If this option is selected, the path specified will be assumed to be an individual vault key. This performs the equivalent of a `vault kv get` command using the [Get](https://www.vaultproject.io/api-docs/secret/kv/kv-v1#read-secret) method. +- Multiple keys can be returned. If this option is selected, the path specified will be assumed to be able to be enumerated. This performs the equivalent of a `vault kv list` command using the [List](https://www.vaultproject.io/api-docs/secret/kv/kv-v2#list-secrets) method. + +The default is `Single vault key`.", + "DefaultValue": "Get", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Get|Single vault key +List|Multiple vault keys" + } + }, + { + "Id": "ab4e36f5-96d9-4969-a8a8-7c1c7a2de9ff", + "Name": "Vault.Retrieve.KV.V1.Secrets.RecursiveSearch", + "Label": "Recursive retrieval", + "HelpText": "If the path is being enumerated, should any secrets included in sub-folders also be retrieved? The default is: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "20ae7130-dd7c-4abf-af2a-675c039df7a5", + "Name": "Vault.Retrieve.KV.V1.Secrets.FieldValues", + "Label": "Field names", + "HelpText": "Choose specific fields to be returned from the Vault key(s) found in the Key/value secrets engine, in the format `FieldName | OutputVariableName` where: + +- `FieldName` is the name of the field to retrieve from the key +- `OutputVariableName` is the _optional_ Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) name to store the secret's value in. + +If this value is not present, any fields found within secrets from the specified path will be retrieved. + +**Note:** Multiple fields can be retrieved by entering each one on a new line. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "3885d50e-3be5-4bc2-bb97-6e2b23459b07", + "Name": "Vault.Retrieve.KV.V1.Secrets.PrintVariableNames", + "Label": "Print output variable names", + "HelpText": "Write out the Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) names to the task log. Default: `False`", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedAt": "2022-09-21T17:01:25.405Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2022-09-21T17:01:25.405Z", + "OctopusVersion": "2022.4.2266", + "Type": "ActionTemplate" + }, + "Category": "hashicorp-vault" + } diff --git a/step-templates/hashicorp-vault-keyvalue-v2-retrieve-secrets.json.human b/step-templates/hashicorp-vault-keyvalue-v2-retrieve-secrets.json.human new file mode 100644 index 000000000..ec838d3fa --- /dev/null +++ b/step-templates/hashicorp-vault-keyvalue-v2-retrieve-secrets.json.human @@ -0,0 +1,530 @@ +{ + "Id": "337f1b67-cdb0-4f33-9e08-6bf804f672d2", + "Name": "HashiCorp Vault - Key Value (v2) retrieve secrets", + "Description": "This step retrieves one or more secrets in a v2 Key/Value secrets engine stored within a HashiCorp Vault server. + +This step template uses the [Rest API](https://www.vaultproject.io/api-docs/secret/kv/kv-v2), so no other dependencies are needed. + +--- + +**Authentication Tokens** + +Octopus recommends using one of the pre-existing Vault step templates to obtain an auth token, such as: +- The [AppRole Login](https://library.octopus.com/step-templates/e04a9cec-f04a-4da2-849b-1aed0fd408f0/actiontemplate-hashicorp-vault-approle-l) or +- The most secure [AppRole Get Wrapped SecretID](https://library.octopus.com/step-templates/76827264-af27-46d0-913a-e093a4f0db48/actiontemplate-hashicorp-vault-approle-get-wrapped-secret-id) in conjunction with the [AppRole Unwrap SecretID and Login](https://library.octopus.com/step-templates/aa113393-e615-40ed-9c5a-f95f471d728f/actiontemplate-hashicorp-vault-approle-unwrap-secret-id-and-login) template. + +--- +**Secrets Path:** + +Specify the full path to the secret(s) you want to retrieve. e.g.`/secret/config`. + +This value should contain: +- The location where the [secrets engine has been enabled](https://www.vaultproject.io/api-docs/secret/kv/kv-v2). +- The path to the secret(s) you want to retrieve. + +For example, if the secrets engine was enabled at `/my-secrets` and you wanted to retrieve the secret(s) from the path `/config`, then the value you would enter is: + +`/my-secrets/config` + +--- + +**Retrieval methods:** + +The step template operates in one of 2 retrieval modes that control how many Vault Key values are returned. The options are: +- `Single vault key` - a single key is returned. If this option is selected, the path specified will be assumed to be an individual vault key. This performs the equivalent of a `vault kv get` command using the [Get](https://www.vaultproject.io/api-docs/secret/kv/kv-v1#read-secret) method. +- `Multiple vault keys` - multiple keys can be returned. If this option is selected, the path specified will be assumed to be able to be enumerated. This performs the equivalent of a `vault kv list` command using the [List](https://www.vaultproject.io/api-docs/secret/kv/kv-v2#list-secrets) method. + +The default is `Single vault key`. + +--- + +**Optional field names:** + +Choose specific fields to be returned from the Vault key(s) found in the Key/value secrets engine, in the format `FieldName | OutputVariableName` where: + +- `FieldName` is the name of the field to retrieve from the key +- `OutputVariableName` is the _optional_ Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) name to store the secret's value in. + +If this parameter is not set, all fields found from secret keys will be returned. + +**Note:** Multiple fields can be retrieved by entering each one on a new line. + +--- + +**Optional: Specific version of secret:** + +_Optional:_ The default behavior of this step is to retrieve the latest version of secrets. + +However, to retrieve a specific version of a secret: +- Include the version of the secret you wish to retrieve. For example, if you want version 2 of all field values in a secret, enter `2`. + +Note: This parameter only works with the `Secrets Retrieval method` = `Single Vault key`. + +To retrieve multiple vault keys with specific versions, use this step template in multiple steps. + +--- + +**Sensitive output variables:** + +For each vault key's field values, an Octopus [sensitive output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) will be created for use in other steps. + +--- + +**Required:** +- The Vault server must be [unsealed](https://www.vaultproject.io/docs/concepts/seal). +- An authentication [token](https://www.vaultproject.io/docs/auth/token). + +*Optional*: +- A Vault [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. **Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault). + +Notes: + + +- Tested on Vault Server `1.11.3`. +- Tested on both PowerShell Desktop and PowerShell Core.", + "ActionType": "Octopus.Script", + "Version": 9, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "### Set TLS 1.2 +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Required Variables +$VAULT_RETRIEVE_KV_V2_SECRETS_ADDRESS = $OctopusParameters[\"Vault.Retrieve.KV.V2.Secrets.VaultAddress\"] +$VAULT_RETRIEVE_KV_V2_SECRETS_API_VERSION = $OctopusParameters[\"Vault.Retrieve.KV.V2.Secrets.ApiVersion\"] +$VAULT_RETRIEVE_KV_V2_SECRETS_TOKEN = $OctopusParameters[\"Vault.Retrieve.KV.V2.Secrets.AuthToken\"] +$VAULT_RETRIEVE_KV_V2_SECRETS_PATH = $OctopusParameters[\"Vault.Retrieve.KV.V2.Secrets.SecretsPath\"] +$VAULT_RETRIEVE_KV_V2_SECRETS_METHOD = $OctopusParameters[\"Vault.Retrieve.KV.V2.Secrets.RetrievalMethod\"] +$VAULT_RETRIEVE_KV_V2_SECRETS_RECURSIVE = $OctopusParameters[\"Vault.Retrieve.KV.V2.Secrets.RecursiveSearch\"] +$VAULT_RETRIEVE_KV_V2_PRINT_VARIABLE_NAMES = $OctopusParameters[\"Vault.Retrieve.KV.V2.Secrets.PrintVariableNames\"] + +# Optional variables +$VAULT_RETRIEVE_KV_V2_SECRETS_FIELD_VALUES = $OctopusParameters[\"Vault.Retrieve.KV.V2.Secrets.FieldValues\"] +$VAULT_RETRIEVE_KV_V2_SECRETS_SECRET_VERSION = $OctopusParameters[\"Vault.Retrieve.KV.V2.Secrets.SecretVersion\"] +$VAULT_RETRIEVE_KV_V2_SECRETS_NAMESPACE = $OctopusParameters[\"Vault.Retrieve.KV.V2.Secrets.Namespace\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V2_SECRETS_ADDRESS)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V2_SECRETS_ADDRESS not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V2_SECRETS_API_VERSION)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V2_SECRETS_API_VERSION not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V2_SECRETS_TOKEN)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V2_SECRETS_TOKEN not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V2_SECRETS_PATH)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V2_SECRETS_PATH not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V2_SECRETS_METHOD)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V2_SECRETS_METHOD not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V2_SECRETS_RECURSIVE)) { + throw \"Required parameter VAULT_RETRIEVE_KV_V2_SECRETS_RECURSIVE not specified\" +} + +# Helper functions +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $rawResponse = $reader.ReadToEnd() + $response = \"\" + try { $response = $rawResponse | ConvertFrom-Json } catch { $response = $rawResponse } + return $response + } + } + else { + return $RequestError.ErrorDetails.Message + } +} + +function Get-VaultSecret { + param ( + [string]$SecretEnginePath, + [string]$SecretPath, + $Fields + ) + try { + # Local variables + $VariablesCreated = 0 + $FieldsSpecified = ($Fields.Count -gt 0) + $SecretPath = $SecretPath.TrimStart(\"/\") + $WorkingPath = \"$($SecretEnginePath)/$($SecretPath)\" + $RequestPath = \"$SecretEnginePath/data/$($SecretPath)\" + + $uri = \"$VAULT_RETRIEVE_KV_V2_SECRETS_ADDRESS/$VAULT_RETRIEVE_KV_V2_SECRETS_API_VERSION/$([uri]::EscapeDataString($RequestPath))\" + if (![string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V2_SECRETS_SECRET_VERSION) -and $RetrieveMultipleKeys -eq $False) { + $uri = \"$($uri)?version=$VAULT_RETRIEVE_KV_V2_SECRETS_SECRET_VERSION\" + } + + $headers = @{\"X-Vault-Token\" = $VAULT_RETRIEVE_KV_V2_SECRETS_TOKEN } + + if (-not [string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V2_SECRETS_NAMESPACE)) { + $Headers.Add(\"X-Vault-Namespace\", $VAULT_RETRIEVE_KV_V2_SECRETS_NAMESPACE) + } + + Write-Verbose \"Making request to $uri\" + $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET + + if ($null -ne $response) { + if ($FieldsSpecified -eq $True) { + foreach ($field in $Fields) { + $fieldName = $field.Name + $fieldVariableName = $field.VariableName + $fieldValue = $response.data.data.$fieldName + + if ($null -ne $fieldValue) { + if ([string]::IsNullOrWhiteSpace($fieldVariableName)) { + $fieldVariableName = \"$($WorkingPath.Replace(\"/\",\".\")).$($fieldName.Trim())\" + } + + Set-OctopusVariable -Name $fieldVariableName -Value $fieldValue -Sensitive + if ($VAULT_RETRIEVE_KV_V2_PRINT_VARIABLE_NAMES -eq $True) { + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$fieldVariableName}\" + } + $VariablesCreated += 1 + } + } + } + # No fields specified, iterate through each one. + else { + $secretFieldNames = $response.data.data | Get-Member | Where-Object { $_.MemberType -eq \"NoteProperty\" } | Select-Object -ExpandProperty \"Name\" + foreach ($fieldName in $secretFieldNames) { + $fieldVariableName = \"$($WorkingPath.Replace(\"/\",\".\")).$($fieldName.Trim())\" + $fieldValue = $response.data.data.$fieldName + + Set-OctopusVariable -Name $fieldVariableName -Value $fieldValue -Sensitive + if ($VAULT_RETRIEVE_KV_V2_PRINT_VARIABLE_NAMES -eq $True) { + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$fieldVariableName}\" + } + $VariablesCreated += 1 + } + } + return $VariablesCreated + } + } + catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred logging in with AppRole: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.errors) { + $AdditionalDetail = $ErrorBody.errors -Join \",\" + } + else { + $errorDetails = $null + try { $errorDetails = ConvertFrom-Json $ErrorBody } catch {} + $AdditionalDetail += if ($null -ne $errorDetails) { $errorDetails.errors -Join \",\" } else { $ErrorBody } + } + } + + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`n`tDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category ConnectionError + } +} + +function List-VaultSecrets { + param ( + [string]$SecretEnginePath, + [string]$SecretPath + ) + try { + $SecretPath = $SecretPath.TrimStart(\"/\") + $RequestPath = \"$SecretEnginePath/metadata/$SecretPath\" + + # Vault uses the 'LIST' HTTP verb, which is only supported in PowerShell 6.0+ using -CustomMethod. + # Adding ?list=true will allow support for Windows Desktop PowerShell. + # See https://www.vaultproject.io/api#api-operations for further details/ + $uri = \"$VAULT_RETRIEVE_KV_V2_SECRETS_ADDRESS/$VAULT_RETRIEVE_KV_V2_SECRETS_API_VERSION/$([uri]::EscapeDataString($RequestPath))?list=true\" + $headers = @{\"X-Vault-Token\" = $VAULT_RETRIEVE_KV_V2_SECRETS_TOKEN } + if (-not [string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V2_SECRETS_NAMESPACE)) { + $Headers.Add(\"X-Vault-Namespace\", $VAULT_RETRIEVE_KV_V2_SECRETS_NAMESPACE) + } + + Write-Verbose \"Making request to $uri\" + $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET + + return $response + } + catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred logging in with AppRole: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.errors) { + $AdditionalDetail = $ErrorBody.errors -Join \",\" + } + else { + $errorDetails = $null + try { $errorDetails = ConvertFrom-Json $ErrorBody } catch {} + $AdditionalDetail += if ($null -ne $errorDetails) { $errorDetails.errors -Join \",\" } else { $ErrorBody } + } + } + + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`n`tDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category ConnectionError + } +} + +function Recursive-GetVaultSecrets { + param( + [string]$SecretEnginePath, + [string]$SecretPath + ) + $VariablesCreated = 0 + $SecretPath = $SecretPath.TrimStart(\"/\") + $SecretPath = $SecretPath.TrimEnd(\"/\") + + Write-Verbose \"Executing Recursive-GetVaultSecrets\" + + # Get list of secrets for path + $VaultKeysResponse = List-VaultSecrets -SecretEnginePath $SecretEnginePath -SecretPath $SecretPath + + if ($null -ne $VaultKeysResponse) { + $keys = $VaultKeysResponse.data.keys + if ($null -ne $keys) { + $secretKeys = $keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) -and !$_.EndsWith(\"/\") } + foreach ($secretKey in $secretKeys) { + $secretKeyPath = \"$($SecretPath)/$secretKey\" + $variablesCreated += Get-VaultSecret -SecretEnginePath $SecretEnginePath -SecretPath $secretKeyPath -Fields $Fields + } + + if ($VAULT_RETRIEVE_KV_V2_SECRETS_RECURSIVE -eq $True) { + $folderKeys = $keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) -and $_.EndsWith(\"/\") } + foreach ($folderKey in $folderKeys) { + $Depth = $Depth += 1 + $folderPath = \"$($SecretPath)/$folderKey\" + $VariablesCreated += Recursive-GetVaultSecrets -SecretEnginePath $SecretEnginePath -SecretPath $folderPath + } + } + } + } + return $VariablesCreated +} + +############################################################################### +$VAULT_RETRIEVE_KV_V2_SECRETS_ADDRESS = $VAULT_RETRIEVE_KV_V2_SECRETS_ADDRESS.TrimEnd('/') +$VAULT_RETRIEVE_KV_V2_SECRETS_PATH = $VAULT_RETRIEVE_KV_V2_SECRETS_PATH.TrimStart('/') + +# Local variables +$RetrieveMultipleKeys = $VAULT_RETRIEVE_KV_V2_SECRETS_METHOD.ToUpper().Trim() -ne \"GET\" +$SecretPathItems = ($VAULT_RETRIEVE_KV_V2_SECRETS_PATH -Split \"/\") +$SecretEnginePath = ($SecretPathItems | Select-Object -First 1) +$SecretPath = ($SecretPathItems | Select-Object -Skip 1) -Join \"/\" +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +$Fields = @() +$VariablesCreated = 0 + +if (![string]::IsNullOrWhiteSpace($VAULT_RETRIEVE_KV_V2_SECRETS_FIELD_VALUES)) { + + @(($VAULT_RETRIEVE_KV_V2_SECRETS_FIELD_VALUES -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + Write-Verbose \"Working on: '$_'\" + $fieldDefinition = ($_ -Split \"\\|\") + $name = $fieldDefinition[0].Trim() + if ([string]::IsNullOrWhiteSpace($name)) { + throw \"Unable to establish fieldname from: '$($_)'\" + } + $field = [PsCustomObject]@{ + Name = $name + VariableName = if (![string]::IsNullOrWhiteSpace($fieldDefinition[1])) { $fieldDefinition[1].Trim() } else { \"\" } + } + $Fields += $field + } + } +} +$FieldsSpecified = ($Fields.Count -gt 0) + +Write-Verbose \"VAULT_RETRIEVE_KV_V2_SECRETS_ADDRESS: $VAULT_RETRIEVE_KV_V2_SECRETS_ADDRESS\" +Write-Verbose \"VAULT_RETRIEVE_KV_V2_SECRETS_API_VERSION: $VAULT_RETRIEVE_KV_V2_SECRETS_API_VERSION\" +Write-Verbose \"VAULT_RETRIEVE_KV_V2_SECRETS_TOKEN: '********'\" +Write-Verbose \"VAULT_RETRIEVE_KV_V2_SECRETS_PATH: $VAULT_RETRIEVE_KV_V2_SECRETS_PATH\" +Write-Verbose \"VAULT_RETRIEVE_KV_V2_SECRETS_METHOD: $VAULT_RETRIEVE_KV_V2_SECRETS_METHOD\" +Write-Verbose \"VAULT_RETRIEVE_KV_V2_SECRETS_RECURSIVE: $VAULT_RETRIEVE_KV_V2_SECRETS_RECURSIVE\" +Write-Verbose \"VAULT_RETRIEVE_KV_V2_SECRETS_SECRET_VERSION: $VAULT_RETRIEVE_KV_V2_SECRETS_SECRET_VERSION\" +Write-Verbose \"VAULT_RETRIEVE_KV_V2_SECRETS_NAMESPACE: $VAULT_RETRIEVE_KV_V2_SECRETS_NAMESPACE\" +Write-Verbose \"RetrieveMultipleKeys: $RetrieveMultipleKeys\" +Write-Verbose \"Fields Specified: $($FieldsSpecified)\" +Write-Verbose \"Engine Path: $SecretEnginePath\" +Write-Verbose \"Secret Path: $SecretPath\" + +$variablesCreated = 0 + +if ($RetrieveMultipleKeys -eq $false) { + $variablesCreated += Get-VaultSecret -SecretEnginePath $SecretEnginePath -SecretPath $SecretPath -Fields $Fields +} +else { + $variablesCreated = Recursive-GetVaultSecrets -SecretEnginePath $SecretEnginePath -SecretPath $SecretPath -Depth 0 +} +Write-Host \"Created $variablesCreated output variables\"" + }, + "Parameters": [ + { + "Id": "a38edf0b-0e90-4421-97d3-688391887f9c", + "Name": "Vault.Retrieve.KV.V2.Secrets.VaultAddress", + "Label": "Vault Server URL", + "HelpText": "The URL of the Vault instance you are connecting to. Port should be included (The default is `8200`). For example: + + +`https://myvault.local:8200/`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0ab319fd-2d90-4f61-b529-cd682bc590a3", + "Name": "Vault.Retrieve.KV.V2.Secrets.ApiVersion", + "Label": "API version", + "HelpText": "All API routes are prefixed with a version e.g. `/v1/`. + +See the [API documentation](https://www.vaultproject.io/api-docs) for further details.", + "DefaultValue": "v1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "v1|v1" + } + }, + { + "Id": "25fec34d-3627-4240-9532-6caf4558ac39", + "Name": "Vault.Retrieve.KV.V2.Secrets.Namespace", + "Label": "Namespace (Optional)", + "HelpText": "The _optional_ [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. + +**Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) .", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b4922053-38da-4092-ac45-fc1551c65254", + "Name": "Vault.Retrieve.KV.V2.Secrets.AuthToken", + "Label": "Auth Token", + "HelpText": "The [Auth Token](https://www.vaultproject.io/docs/auth/token) used to authenticate to retrieve secrets. + +Octopus recommends using one of the pre-existing Vault step templates to obtain an auth token, such as: +- The [AppRole Login](https://library.octopus.com/step-templates/e04a9cec-f04a-4da2-849b-1aed0fd408f0/actiontemplate-hashicorp-vault-approle-l) or +- The most secure [AppRole Get Wrapped SecretID](https://library.octopus.com/step-templates/76827264-af27-46d0-913a-e093a4f0db48/actiontemplate-hashicorp-vault-approle-get-wrapped-secret-id) in conjunction with the [AppRole Unwrap SecretID and Login](https://library.octopus.com/step-templates/aa113393-e615-40ed-9c5a-f95f471d728f/actiontemplate-hashicorp-vault-approle-unwrap-secret-id-and-login) template.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "5e62f123-d034-490b-a16f-c0ab7176974a", + "Name": "Vault.Retrieve.KV.V2.Secrets.SecretsPath", + "Label": "Secrets Path", + "HelpText": "The full path to the secret(s) you want to retrieve. e.g.`/secret/config`. + +**This value should contain:** +- The location where the [secrets engine has been enabled](https://www.vaultproject.io/api-docs/secret/kv/kv-v2). +- The path to the secret(s) you want to retrieve. + +For example, if the secrets engine was enabled at `/my-secrets` and you wanted to retrieve the secret(s) from the path `/config` then the value you would enter is: + +`/my-secrets/config`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "01c2b07e-40e5-4251-b246-1be0a514a09c", + "Name": "Vault.Retrieve.KV.V2.Secrets.RetrievalMethod", + "Label": "Secrets retrieval method", + "HelpText": "This controls how many Vault Key values are returned. The options are: +- A single key is returned. If this option is selected, the path specified will be assumed to be an individual vault key. This performs the equivalent of a `vault kv get` command using the [Get](https://www.vaultproject.io/api-docs/secret/kv/kv-v2#read-secret) method. +- Multiple keys can be returned. If this option is selected, the path specified will be assumed to be able to be enumerated. This performs the equivalent of a `vault kv list` command using the [List](https://www.vaultproject.io/api-docs/secret/kv/kv-v2#list-secrets) method. + +The default is `Single vault key`.", + "DefaultValue": "Get", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Get|Single vault key +List|Multiple vault keys" + } + }, + { + "Id": "f997fbcc-bfc3-4a38-9571-58957062873d", + "Name": "Vault.Retrieve.KV.V2.Secrets.RecursiveSearch", + "Label": "Recursive retrieval", + "HelpText": "If the path is being enumerated (indicated by `Secrets retrieval method = Multiple`), should any secrets included in sub-folders also be retrieved? The default is: `False`. + +Note: This value is ignored when `Secrets retrieval method = Single`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "b9db3c54-f1f8-4241-b997-c6440c12489d", + "Name": "Vault.Retrieve.KV.V2.Secrets.SecretVersion", + "Label": "Secret Version", + "HelpText": "_Optional_ version specifier of a secret to retrieve. For example, if you wanted version 2 of all field values in a secret, enter the value `2` + +Note: This parameter only works with the `Secrets Retrieval method` = `Single Vault key`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b446cb03-d57a-4869-8b9e-a9c12ed5646a", + "Name": "Vault.Retrieve.KV.V2.Secrets.FieldValues", + "Label": "Field names", + "HelpText": "Choose specific fields to be returned from the Vault key(s) found in the Key/value secrets engine, in the format `FieldName | OutputVariableName` where: + +- `FieldName` is the name of the field to retrieve from the key +- `OutputVariableName` is the _optional_ Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) name to store the secret's value in. + +If this value is not present, any fields found within secrets from the specified path will be retrieved. + +**Note:** Multiple fields can be retrieved by entering each one on a new line. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "52630c04-8fb3-4afb-a02e-ed7e2d20e34e", + "Name": "Vault.Retrieve.KV.V2.Secrets.PrintVariableNames", + "Label": "Print output variable names", + "HelpText": "Write out the Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) names to the task log. Default: `False`", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedAt": "2022-09-21T17:01:25.405Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2022-09-21T17:01:25.405Z", + "OctopusVersion": "2022.4.2266", + "Type": "ActionTemplate" + }, + "Category": "hashicorp-vault" + } diff --git a/step-templates/hashicorp-vault-ldap-login.json.human b/step-templates/hashicorp-vault-ldap-login.json.human new file mode 100644 index 000000000..29dfc1df0 --- /dev/null +++ b/step-templates/hashicorp-vault-ldap-login.json.human @@ -0,0 +1,211 @@ +{ + "Id": "de807003-3b05-4649-9af3-11a2c7722b3f", + "Name": "HashiCorp Vault - LDAP Login", + "Description": "This step logs into a HashiCorp Vault server using the [LDAP](https://www.vaultproject.io/docs/auth/ldap) auth method. + +The `client_token` from the response will be made available as a sensitive [Output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) named `LDAPAuthToken` for use in other step templates. + +This step template makes use of the [Rest API](https://www.vaultproject.io/api/auth/ldap#login-with-ldap-user), so no other dependencies are needed. + +**Required:** +- The Vault server must be [unsealed](https://www.vaultproject.io/docs/concepts/seal). +- You must supply the full path where the LDAP auth method is mounted. +- You must supply both a `Username` and `Password`. + +*Optional*: +- A Vault [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. **Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) . + +Notes: + +- Tested on Vault Server `1.11.3`. +- Tested on both PowerShell Desktop and PowerShell Core.", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Variables +$VAULT_LDAP_LOGIN_ADDRESS = $OctopusParameters[\"Vault.LDAP.Login.VaultAddress\"] +$VAULT_LDAP_LOGIN_API_VERSION = $OctopusParameters[\"Vault.LDAP.Login.ApiVersion\"] +$VAULT_LDAP_LOGIN_NAMESPACE = $OctopusParameters[\"Vault.LDAP.Login.Namespace\"] +$VAULT_LDAP_LOGIN_AUTH_PATH = $OctopusParameters[\"Vault.LDAP.Login.AuthPath\"] +$VAULT_LDAP_LOGIN_USERNAME = $OctopusParameters[\"Vault.LDAP.Login.Username\"] +$VAULT_LDAP_LOGIN_PASSWORD = $OctopusParameters[\"Vault.LDAP.Login.Password\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($VAULT_LDAP_LOGIN_ADDRESS)) { + throw \"Required parameter VAULT_LDAP_LOGIN_ADDRESS not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_LDAP_LOGIN_API_VERSION)) { + throw \"Required parameter VAULT_LDAP_LOGIN_API_VERSION not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_LDAP_LOGIN_AUTH_PATH)) { + throw \"Required parameter VAULT_LDAP_LOGIN_AUTH_PATH not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_LDAP_LOGIN_USERNAME)) { + throw \"Required parameter VAULT_LDAP_LOGIN_USERNAME not specified\" +} +if ([string]::IsNullOrWhiteSpace($VAULT_LDAP_LOGIN_PASSWORD)) { + throw \"Required parameter VAULT_LDAP_LOGIN_PASSWORD not specified\" +} + +# Helper functions +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $rawResponse = $reader.ReadToEnd() + $response = \"\" + try { $response = $rawResponse | ConvertFrom-Json } catch { $response = $rawResponse } + return $response + } + } + else { + return $RequestError.ErrorDetails.Message + } +} +############################################################################### + +$VAULT_LDAP_LOGIN_ADDRESS = $VAULT_LDAP_LOGIN_ADDRESS.TrimEnd('/') +$VAULT_LDAP_LOGIN_AUTH_PATH = $VAULT_LDAP_LOGIN_AUTH_PATH.TrimStart('/').TrimEnd('/') + +# Local variables +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +try { + $payload = @{ + password = $VAULT_LDAP_LOGIN_PASSWORD + } + + $Headers = @{} + if (-not [string]::IsNullOrWhiteSpace($VAULT_LDAP_LOGIN_NAMESPACE)) { + Write-Verbose \"Setting 'X-Vault-Namespace' header to: $VAULT_LDAP_LOGIN_NAMESPACE\" + $Headers.Add(\"X-Vault-Namespace\", $VAULT_LDAP_LOGIN_NAMESPACE) + } + + $uri = \"$VAULT_LDAP_LOGIN_ADDRESS/$VAULT_LDAP_LOGIN_API_VERSION/$VAULT_LDAP_LOGIN_AUTH_PATH/login/$([uri]::EscapeDataString($VAULT_LDAP_LOGIN_USERNAME))\" + Write-Verbose \"Making request to $uri\" + $response = Invoke-RestMethod -Method Post -Uri $uri -Body ($payload | ConvertTo-Json -Depth 10) -Headers $Headers + if ($null -ne $response) { + Set-OctopusVariable -Name \"LDAPAuthToken\" -Value $response.auth.client_token -Sensitive + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.LDAPAuthToken}\" + } + else { + Write-Error \"Null or Empty response returned from Vault server\" -Category InvalidResult + } +} +catch { + $ExceptionMessage = $_.Exception.Message + $ErrorBody = Get-WebRequestErrorBody -RequestError $_ + $Message = \"An error occurred logging in with LDAP: $ExceptionMessage\" + $AdditionalDetail = \"\" + if (![string]::IsNullOrWhiteSpace($ErrorBody)) { + if ($null -ne $ErrorBody.errors) { + $AdditionalDetail = $ErrorBody.errors -Join \",\" + } + else { + $errorDetails = $null + try { $errorDetails = ConvertFrom-Json $ErrorBody } catch {} + $AdditionalDetail += if ($null -ne $errorDetails) { $errorDetails.errors -Join \",\" } else { $ErrorBody } + } + } + + if (![string]::IsNullOrWhiteSpace($AdditionalDetail)) { + $Message += \"`n`tDetail: $AdditionalDetail\" + } + + Write-Error $Message -Category ConnectionError +}" + }, + "Parameters": [ + { + "Id": "d0e95468-4f9e-4272-9dfa-d964c610a279", + "Name": "Vault.LDAP.Login.VaultAddress", + "Label": "Vault Server URL", + "HelpText": "The URL of the Vault instance you are connecting to. Port should be included (The default is `8200`). For example: + + +`https://myvault.local:8200/`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "98d2a264-0466-4e0d-b5a3-4d172c81f405", + "Name": "Vault.LDAP.Login.ApiVersion", + "Label": "API version", + "HelpText": "All API routes are prefixed with a version e.g. `/v1/`. + +See the [API documentation](https://www.vaultproject.io/api-docs) for further details.", + "DefaultValue": "v1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "v1|v1" + } + }, + { + "Id": "74fa0a75-0ab4-4a9d-a417-c9b9b391c640", + "Name": "Vault.LDAP.Login.Namespace", + "Label": "Namespace (Optional)", + "HelpText": "The _optional_ [namespace](https://www.vaultproject.io/docs/enterprise/namespaces) to use. Nested namespaces can also be supplied, e.g. `ns1/ns2`. + +**Note:** This field is only supported on [Vault Enterprise](https://www.hashicorp.com/products/vault) .", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c56afe5a-6506-497f-99b0-ae8776f03269", + "Name": "Vault.LDAP.Login.AuthPath", + "Label": "LDAP Auth Login path", + "HelpText": "The path that the LDAP method is mounted at. The default is `/auth/ldap`. If the LDAP auth method was enabled at a different path, for example `my-path`, then specify `/my-path` instead. +", + "DefaultValue": "/auth/ldap", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7803e08c-026f-4d59-b902-f75a0992be48", + "Name": "Vault.LDAP.Login.Username", + "Label": "Username", + "HelpText": "The LDAP Username.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7a00c426-43f1-4229-a036-37b018d089a6", + "Name": "Vault.LDAP.Login.Password", + "Label": "LDAP Password", + "HelpText": "The LDAP Password.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedAt": "2022-09-18T08:25:57.132Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2022-09-18T08:25:57.132Z", + "OctopusVersion": "2022.4.2266", + "Type": "ActionTemplate" + }, + "Category": "hashicorp-vault" + } diff --git a/step-templates/hg-get-changelog.json.human b/step-templates/hg-get-changelog.json.human new file mode 100644 index 000000000..31e75d2ff --- /dev/null +++ b/step-templates/hg-get-changelog.json.human @@ -0,0 +1,64 @@ +{ + "Id": "1f76dc61-c7ec-47a8-bd27-fb135851e9c5", + "Name": "HG - Get Changelog", + "Description": "Generate exact changelog from Mercurial commit history. It is stored in the output variable \"Changelog\". + +Requirement: each release must have been labeled in the repository as \"release-OctopusReleaseNumber\" (for instance using VCS labeling feature of TeamCity). + +See http://hgbook.red-bean.com/read/customizing-the-output-of-mercurial.html for template format.", + "ActionType": "Octopus.Script", + "Version": 17, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "If ($OctopusParameters[\"Octopus.Release.CurrentForEnvironment.Number\"]) { + $prm = @('log', + \t'-r',\"ancestors('release-$($OctopusParameters[\"Octopus.Release.Number\"])') - ancestors('release-$($OctopusParameters[\"Octopus.Release.CurrentForEnvironment.Number\"])')\", + \t'-T',$Template, + \t'--repository',$HgRepository) + Write-Host Getting changelog on $prm[6] '[' $prm[2] ']' + $changelog = & hg $prm +} +Else { + $changelog = \"
  • (no changelog available)
  • \" +} +Write-Verbose $changelog +Set-OctopusVariable -name \"Changelog\" -value $changelog", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "9a308d93-915c-4216-a0a6-cbe8de108064", + "Name": "HgRepository", + "Label": "Repository Path", + "HelpText": "The Mercurial repository to use for generating the changelog. + +The repo path needs to be local to where the step is executed because Mercurial does not support remote log listing.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "57f225d5-4579-442f-ac3c-725743952f09", + "Name": "Template", + "Label": "", + "HelpText": "Default template generates HTML <li> elements for inclusion in a <ul> (not part of the step output).", + "DefaultValue": "
  • {date|shortdate} ({date|age} in {branch|escape}): {desc|strip|escape|addbreaks}
  • ", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2016-09-19T18:10:00.000+00:00", + "LastModifiedBy": "avonwyss", + "$Meta": { + "ExportedAt": "2016-09-19T18:10:27.463+00:00", + "OctopusVersion": "3.4.9", + "Type": "ActionTemplate" + }, + "Category": "mercurial" +} diff --git a/step-templates/hipchat-notify-api-v1.json.human b/step-templates/hipchat-notify-api-v1.json.human new file mode 100644 index 000000000..e58e21a02 --- /dev/null +++ b/step-templates/hipchat-notify-api-v1.json.human @@ -0,0 +1,94 @@ +{ + "Id": "c37ccd22-2b31-4263-8389-9e63868519b4", + "Name": "HipChat - Notify (API v1)", + "Description": "Send a success notification when this step is executed.", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$message = if ($OctopusParameters['HipChatMessage']) { $OctopusParameters['HipChatMessage'] } else { \"(successful) \" + $OctopusParameters['Octopus.Project.Name'] + \" [v$($OctopusParameters['Octopus.Release.Number'])] deployed to $($OctopusParameters['Octopus.Environment.Name']) on $($OctopusParameters['Octopus.Machine.Name'])\" } \r +#---------\r +$apitoken = $OctopusParameters['HipChatAuthToken']\r +$roomid = $OctopusParameters['HipChatRoomId']\r +$from = $OctopusParameters['HipChatFrom']\r +$colour = $OctopusParameters['HipChatColor']\r +\r +Try \r +{\r +\t#Do the HTTP POST to HipChat\r +\t$post = \"auth_token=$apitoken&room_id=$roomid&from=$from&color=$colour&message=$message¬ify=1&message_format=text\"\r +\t$webRequest = [System.Net.WebRequest]::Create(\"https://api.hipchat.com/v1/rooms/message\")\r +\t$webRequest.ContentType = \"application/x-www-form-urlencoded\"\r +\t$postStr = [System.Text.Encoding]::UTF8.GetBytes($post)\r +\t$webrequest.ContentLength = $postStr.Length\r +\t$webRequest.Method = \"POST\"\r +\t$requestStream = $webRequest.GetRequestStream()\r +\t$requestStream.Write($postStr, 0,$postStr.length)\r +\t$requestStream.Close()\r +\t\r +\t[System.Net.WebResponse] $resp = $webRequest.GetResponse();\r +\t$rs = $resp.GetResponseStream();\r +\t[System.IO.StreamReader] $sr = New-Object System.IO.StreamReader -argumentList $rs;\r +\t$sr.ReadToEnd();\t\t\t\t\t\r +}\r +catch [Exception] {\r +\t\"Woah!, wasn't expecting to get this exception. `r`n $_.Exception.ToString()\"\r +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "HipChatRoomId", + "Label": "Room Id", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HipChatAuthToken", + "Label": "Auth token", + "HelpText": "For API version 1.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HipChatFrom", + "Label": "From name", + "HelpText": null, + "DefaultValue": "Octopus Deploy", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HipChatColor", + "Label": "Color", + "HelpText": "HipChat message color", + "DefaultValue": "green", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HipChatMessage", + "Label": "Message", + "HelpText": "You can use variables here. Leave blank for the default build notification format.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-06-11T02:21:16.787+00:00", + "OctopusVersion": "2.4.7.85", + "Type": "ActionTemplate" + }, + "Category": "hipchat" +} diff --git a/step-templates/hipchat-notify.json.human b/step-templates/hipchat-notify.json.human new file mode 100644 index 000000000..029c3f46b --- /dev/null +++ b/step-templates/hipchat-notify.json.human @@ -0,0 +1,100 @@ +{ + "Id": "6a6ce997-c91b-4e06-b237-4484417efc89", + "Name": "HipChat - Notify", + "Description": "Notifies a HipChat room of a deployment outcome.", + "ActionType": "Octopus.Script", + "Version": 0, + "Properties": { + "Octopus.Action.Script.ScriptBody": "#--------- Notify-Hipchat\r +$apitoken = $OctopusParameters['AuthToken']\r +$roomid = $OctopusParameters['RoomId']\r +$messageText = \"(successful)\"\r +$color = 'green'\r +\r +if ($OctopusParameters['Octopus.Deployment.Error']) {\r + $messageText = \"(failed)\"\r + $color = 'red'\r +}\r +\r +$messageValue = \"$messageText $($OctopusParameters['Octopus.Project.Name']) [v$($OctopusParameters['Octopus.Release.Number'])] deployed to $($OctopusParameters['Octopus.Environment.Name']) on $($OctopusParameters['Octopus.Machine.Name'])\"\r +\r +if ($OctopusParameters['NotificationText']) {\r + $messageValue = $OctopusParameters['NotificationText']\r + $color = $OctopusParameters['NotificationColor']\r +}\r +\r +$message = New-Object PSObject \r +$message | Add-Member -MemberType NoteProperty -Name color -Value $color\r +$message | Add-Member -MemberType NoteProperty -Name message -Value $messageValue\r +$message | Add-Member -MemberType NoteProperty -Name notify -Value $false\r +$message | Add-Member -MemberType NoteProperty -Name message_format -Value text\r +\r +#Do the HTTP POST to HipChat\r +$uri = \"https://api.hipchat.com/v2/room/$roomid/notification?auth_token=$apitoken\"\r +$postBody = ConvertTo-Json -InputObject $message\r +$postStr = [System.Text.Encoding]::UTF8.GetBytes($postBody)\r +\r +$webRequest = [System.Net.WebRequest]::Create($uri)\r +$webRequest.ContentType = \"application/json\"\r +$webrequest.ContentLength = $postStr.Length\r +$webRequest.Method = \"POST\"\r +\r +$requestStream = $webRequest.GetRequestStream()\r +$requestStream.Write($postStr, 0,$postStr.length)\r +$requestStream.Close()\r +\r +[System.Net.WebResponse] $resp = $webRequest.GetResponse()\r +$rs = $resp.GetResponseStream()\r +\r +[System.IO.StreamReader] $sr = New-Object System.IO.StreamReader -argumentList $rs\r +$sr.ReadToEnd()", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "AuthToken", + "Label": "API Auth Token", + "HelpText": "HipChat authentication token for a user who can post notifications to rooms", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "RoomId", + "Label": "Room", + "HelpText": "The room name that you wish to post a notification to", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NotificationText", + "Label": "Notification Text", + "HelpText": "An optional text override for the notification. Default is: <(succeeded) or (failed)> (Project) v(Release) deployed to (Environment) on (Machine)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NotificationColor", + "Label": "Notification Color", + "HelpText": "The color for the notification for the room. Default messages will receive green success, and red failure.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2014-07-29T01:53:13.319+00:00", + "LastModifiedBy": "stephengodbold", + "$Meta": { + "ExportedAt": "2014-07-29T01:56:04.693+00:00", + "OctopusVersion": "2.5.5.318", + "Type": "ActionTemplate" + }, + "Category": "hipchat" +} diff --git a/step-templates/hockeyapp-upload-mobile-app.json.human b/step-templates/hockeyapp-upload-mobile-app.json.human new file mode 100644 index 000000000..3af00cfbf --- /dev/null +++ b/step-templates/hockeyapp-upload-mobile-app.json.human @@ -0,0 +1,572 @@ +{ + "Id": "5667710e-60b8-4067-bfa5-87196faafdda", + "Name": "HockeyApp - Upload Mobile App", + "Description": "This script uploads a new version of an existing app package to the [HockeyApp](http://hockeyapp.net/features/) services.", + "ActionType": "Octopus.Script", + "Version": 20, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Hockey App Upload script\r +#\r +# Uploads a mobile platform application package to Hockey App from a Nuget file\r +# extracted in a previous Octopus Deploy step. Allows a variety of parameters.\r +#\r +# v0.5 - Turns out Invoke-WebRequest was a memory hog, casuing high memory usage and\r +# out-of-memory errors. Switched to dot net native web request and streams.\r +# v0.4 - Package location search is now recursive, as required by *.nuspec example.\r +# Added default description to pass along nuget version to notes.\r +# v0.3 - Now supports windows .appx packages\r +# v0.2 - Added extra parameters\r +# v0.1 - Initial version, basic upload\r +# \r +#\r +# The following *.nuspec example will package ALL matching Ipa, Apk (signed), and Appx files.\r +# The upload script requires exactly one match (or specifying the exact file)\r +# \r +# Specify specific package path relative to the nuspec file location (or overriden basepath)\r +#\r +# https://docs.nuget.org/create/nuspec-reference#file-element-examples\r +#\r +# In some cases the ID, Version, and Description may need manually specified.\r +#\r +\r +<#\r +\r + \r + \r + \r + $id$\r + $id$\r + $version$\r + Mobile project packaged for Octopus deploy. $description$\r + \r + \r + \r +\r + \r + \r +\r + \r + \r +\r + \r + \r + \r + \r +\r +#>\r +\r +# Hockey App API reference\r +#\r +# General API reference: http://support.hockeyapp.net/kb/api\r +# Auth reference (tokens): http://support.hockeyapp.net/kb/api/api-basics-and-authentication\r +# Upload App Version reference: http://support.hockeyapp.net/kb/api/api-versions#upload-version\r +\r +#############################\r +# Debug Parameter Overrides #\r +#############################\r +\r +# These values are set explicitly durring debugging so that the script can\r +# be run in the editor.\r +# For local debugging, uncomment these values and fill in appropriately.\r +\r +<#\r +\r +$OctopusParameters = @{\r +\"HockeyAppApiToken\" = \"YourApiKeyhere\";\r +\"HockeyAppAppID\" = \"YourAppIdHere\";\r +\"PackageFileName\" = \"MyAppFile-1.2.3.4.ipa\"; # app file name\r +\"HockeyAppNotify\" = \"1\";\r +\"HockeyAppStatus\" = \"2\";\r +}\r +\r +# debug folder with app files\r +$stepPath = \"C:\\Temp\\HockeyAppScript\\\"\r +\r +\r +# #>\r +\r +###################################\r +# Octopus Deploy common functions #\r +###################################\r +\r +# A collection of functions that can be used by script steps to determine where packages installed\r +# by previous steps are located on the filesystem.\r + \r +function Find-InstallLocations {\r + $result = @()\r + $OctopusParameters.Keys | foreach {\r + if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) {\r + $result += $OctopusParameters[$_]\r + }\r + }\r + return $result\r +}\r + \r +function Find-InstallLocation($stepName) {\r + $result = $OctopusParameters.Keys | where {\r + $_.Equals(\"Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath\", [System.StringComparison]::OrdinalIgnoreCase)\r + } | select -first 1\r + \r + if ($result) {\r + return $OctopusParameters[$result]\r + }\r + \r + throw \"No install location found for step: $stepName\"\r +}\r +\r +function Find-SingleInstallLocation {\r + $all = @(Find-InstallLocations)\r + if ($all.Length -eq 1) {\r + return $all[0]\r + }\r + if ($all.Length -eq 0) {\r + throw \"No package steps found\"\r + }\r + throw \"Multiple package steps have run; please specify a single step\"\r +}\r +\r +#####################\r +# Utility functions #\r +#####################\r +\r +function Get-ExactlyOneMobilePackageFileInfo($searchPath)\r +{\r + $apkFiles = Get-ChildItem -Path $searchPath -Recurse -Filter *.apk #Android\r + $ipaFiles = Get-ChildItem -Path $searchPath -Recurse -Filter *.ipa #iOS\r + $appxFiles = Get-ChildItem -Path $searchPath -Recurse -Filter *.appx # windows\r +\r + $apkCount = $apkFiles.count\r +\r + $ipaCount = $ipaFiles.count\r +\r + $appxCount = $appxFiles.count\r +\r + $totalCount = $apkCount + $ipaCount + $appxCount\r +\r + if($totalCount -ne 1)\r + {\r + throw \"Did not find exactly one (1) mobile application package. Found $apkCount APK file(s), $ipaCount IPA file(s), and $appxCount Appx file(s).\"\r + }\r +\r + if($apkCount -eq 1)\r + {\r + return $apkFiles\r + }\r +\r + if($ipaCount -eq 1)\r + {\r + return $ipaFiles\r + }\r +\r + if($appxCount -eq 1)\r + {\r + return $appxFiles\r + }\r +\r + throw \"Unable to find mobile application packages (fallback error - not expected)\"\r +}\r +\r +function AddToHashIfExists([HashTable]$table, $value, $name)\r +{\r + if(-not [String]::IsNullOrWhiteSpace($value))\r + {\r + $table.Add($name, $value)\r + }\r +}\r +\r +function GetMultipartFormSectionString($key,$value)\r +{\r + return @\"\r +Content-Disposition: form-data; name=\"$key\"\r +\r +$value\r +\"@\r +}\r +\r +####################\r +# Basic Parameters #\r +####################\r +\r +$apiToken = $OctopusParameters['HockeyAppApiToken']\r +$appId = $OctopusParameters['HockeyAppAppID']\r +\r +$octopusFilePathOverride = $OctopusParameters['PackageFileName']\r +\r +$stepName = $OctopusParameters['MobileAppPackageStepName']\r +\r +# set step path, if not already set\r +If([string]::IsNullOrEmpty($stepPath))\r +{\r + if (![string]::IsNullOrEmpty($stepName)) {\r + Write-Host \"Finding path to package step: $stepName\"\r + $stepPath = Find-InstallLocation $stepName\r + } else {\r + $stepPath = Find-SingleInstallLocation\r + }\r +}\r +\r +Write-Host \"Package is located in folder: $stepPath\"\r +Write-Host \"##octopus[stderr-progress]\"\r +\r +# if we were not provided a file name, search for a single package file\r +if([string]::IsNullOrWhiteSpace($octopusFilePathOverride))\r +{\r + $appFileInfo = Get-ExactlyOneMobilePackageFileInfo $stepPath\r + $appFullFilePath = $appFileInfo.FullName\r +}\r +else\r +{\r + $appFullFilePath = Join-Path $stepPath $octopusFilePathOverride\r +}\r +\r +$fileName = [System.IO.Path]::GetFileName($appFullFilePath)\r +\r +$apiUploadUri = \"https://rink.hockeyapp.net/api/2/apps/$appId/app_versions/upload\"\r +\r +# Request token details\r +$uniqueBoundaryToken = [Guid]::NewGuid().ToString()\r +\r +$contentType = \"multipart/form-data; boundary=$uniqueBoundaryToken\"\r +\r +################################\r +# Set up Hockey App parameters #\r +################################\r +\r +$HockeyAppParameters = @{} # parameters are a hash table.\r +\r +# add parameters that have values - See docs at http://support.hockeyapp.net/kb/api/api-versions#upload-version\r +\r +AddToHashIfExists $HockeyAppParameters $OctopusParameters['HockeyAppNotes'] \"notes\"\r +AddToHashIfExists $HockeyAppParameters $OctopusParameters['HockeyAppNotesType'] \"notes_type\"\r +AddToHashIfExists $HockeyAppParameters $OctopusParameters['HockeyAppNotify'] \"notify\"\r +AddToHashIfExists $HockeyAppParameters $OctopusParameters['HockeyAppStatus'] \"status\"\r +AddToHashIfExists $HockeyAppParameters $OctopusParameters['HockeyAppTags'] \"tags\"\r +AddToHashIfExists $HockeyAppParameters $OctopusParameters['HockeyAppTeams'] \"teams\"\r +AddToHashIfExists $HockeyAppParameters $OctopusParameters['HockeyAppUsers'] \"users\"\r +AddToHashIfExists $HockeyAppParameters $OctopusParameters['HockeyAppMandatory'] \"mandatory\"\r +AddToHashIfExists $HockeyAppParameters $OctopusParameters['HockeyAppCommitSha'] \"commit_sha\"\r +AddToHashIfExists $HockeyAppParameters $OctopusParameters['HockeyAppBuildServerUrl'] \"build_server_url\"\r +AddToHashIfExists $HockeyAppParameters $OctopusParameters['HockeyAppRepositoryUrl'] \"repository_url\"\r +\r +$formSectionSeparator = @\"\r +\r +--$uniqueBoundaryToken\r +\r +\"@\r +\r +if($HockeyAppParameters.Count -gt 0)\r +{\r + $parameterSectionsString = [String]::Join($formSectionSeparator,($HockeyAppParameters.GetEnumerator() | %{GetMultipartFormSectionString $_.Key $_.Value}))\r +}\r +\r +############################\r +# Prepare request wrappers #\r +############################\r +\r +# Standard for multipart form data\r +# http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4\r +\r +$stringEncoding = [System.Text.Encoding]::ASCII\r +\r +# Note the hard-coded \"ipa\" name here is per HockeyApp API documentation\r +# and it applies to ALL platform application files.\r +\r +$preFileBytes = $stringEncoding.GetBytes(\r +$parameterSectionsString + \r +$formSectionSeparator +\r +@\"\r +Content-Disposition: form-data; name=\"ipa\"; filename=\"$fileName\"\r +Content-Type: application/octet-stream\r +\r +\r +\"@)\r +\r +# file bytes will go in between\r +\r +$postFileBytes = $stringEncoding.GetBytes(@\"\r +\r +--$uniqueBoundaryToken--\r +\"@)\r +\r +######################\r +# Invoke the request #\r +######################\r +\r +# Note, previous approach was Invoke-RestMethod based. It worked, but was NOT memory\r +# efficient, leading to high memory usage and \"out of memory\" errors.\r +\r +# Based on examples from\r +# http://stackoverflow.com/questions/566462/upload-files-with-httpwebrequest-multipart-form-data\r +# and \r +# https://gist.github.com/nolim1t/271018\r +\r +# Uses a dot net WebRequest and streaming to limit memory usage\r +\r +$WebRequest = [System.Net.WebRequest]::Create(\"$apiUploadUri\")\r +\r +$WebRequest.ContentType = $contentType\r +$WebRequest.Method = \"POST\"\r +$WebRequest.KeepAlive = $true;\r +$WebRequest.Headers.Add(\"X-HockeyAppToken\",$apiToken)\r +\r +$RequestStream = $WebRequest.GetRequestStream()\r +\r +# before file bytes\r +$RequestStream.Write($preFileBytes, 0, $preFileBytes.Length);\r +\r +#files bytes\r +\r +$fileMode = [System.IO.FileMode]::Open\r +$fileAccess = [System.IO.FileAccess]::Read\r +\r +$fileStream = New-Object IO.FileStream $appFullFilePath,$fileMode,$fileAccess\r +$bufferSize = 4096 # 4k at a time\r +$byteBuffer = New-Object Byte[] ($bufferSize)\r +\r +# read bytes. While bytes are read...\r +while(($bytesRead = $fileStream.Read($byteBuffer,0,$byteBuffer.Length)) -ne 0)\r +{\r + # write those byes to the request stream\r + $RequestStream.Write($byteBuffer, 0, $bytesRead)\r +}\r +\r +$fileStream.Close()\r +\r +# after file bytes\r +$RequestStream.Write($postFileBytes, 0, $postFileBytes.Length);\r +\r +$RequestStream.Close()\r +\r +$response = $WebRequest.GetResponse();\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "HockeyAppApiToken", + "Label": "HockeyApp Api Token", + "HelpText": "HockeyApp requires an access token for their API as show in the [HockeyApp API Authentication Documentation]( http://support.hockeyapp.net/kb/api/api-basics-and-authentication#authentication). Logged in users can generate tokens under [API Tokens](https://rink.hockeyapp.net/manage/auth_tokens) in the account menu. + +You should generate an application specific token for this purpose.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppAppID", + "Label": "HockeyApp App ID", + "HelpText": "The ID of your App in HockeyApp. This is visible on the [Manage Apps](https://rink.hockeyapp.net/manage/apps) management page for your target app.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MobileAppPackageStepName", + "Label": "Package Step Name", + "HelpText": "Name of the previously-deployed package step that contains the App that you want to deploy.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Name": "PackageFileName", + "Label": "Package File Name", + "HelpText": "The value is optional. + +If no value is provided the scrip will search for exactly one *.apk, *.ipa, or *.appx file in the nupgk package files. Zero or multiple matches will result in an error. + +If a value is provided, it will be used directly instead of searching. Use this to specify a mobile app to upload in the case of multiple apps, such as a signed and unsigned apk, or an apk and ipa in a single Nuget package. + +This value must be the path and filename relative to the root of the nupkg file. You may use octopus parameters if needed, but the passed value must be the combined relative path with full filename for the package. + +For creating the nupkg, the following *.nuspec example will package ALL matching Ipa, Apk (signed), and Appx files. If you have a single package as build output, the Nuget package should work by default. + +\t +\t +\t +\t\t$id$ +\t\t$id$ +\t\t$version$ +\t\tMobile project packaged for Octopus deploy. $description$ +\t +\t +\t\t + +\t\t +\t\t + +\t\t +\t\t + +\t\t +\t\t +\t +\t", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppNotes", + "Label": "HockeyApp Notes", + "HelpText": "optional, release notes as Textile or Markdown + +As an example, you could use the Octopus variables from the Nuget package extract step as like this: + +Deployed from Nuget Package #{Octopus.Action[Nuget Package Extract].Package.NuGetPackageId}, Version #{Octopus.Action[Nuget Package Extract].Package.NuGetPackageVersion} + +And get Hockey App notes like: + +Deployed from Nuget Package MyiOSPackage, Version 1.2.3.4 + +See [Upload API Documentation](http://support.hockeyapp.net/kb/api/api-versions#upload-version)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppNotesType", + "Label": "HockeyApp Notes Type", + "HelpText": "optional, type of release notes: + +- 0 - Textile +- 1 - Markdown + +See [Upload API Documentation](http://support.hockeyapp.net/kb/api/api-versions#upload-version)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppNotify", + "Label": "HockeyApp Notify", + "HelpText": "optional, notify testers (can only be set with full-access tokens): + +- 0 - Don't notify testers +- 1 - Notify all testers that can install this app +- 2 - Notify all testers + +See [Upload API Documentation](http://support.hockeyapp.net/kb/api/api-versions#upload-version)", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppStatus", + "Label": "HockeyApp Status", + "HelpText": "optional, download status (can only be set with full-access tokens): + +- 1: Don't allow users to download or install the version +- 2: Available for download or installation + +See [Upload API Documentation](http://support.hockeyapp.net/kb/api/api-versions#upload-version)", + "DefaultValue": "2", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppTags", + "Label": "HockeyApp Tags", + "HelpText": "optional, restrict download to comma-separated list of tags + +See [Upload API Documentation](http://support.hockeyapp.net/kb/api/api-versions#upload-version)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppTeams", + "Label": "HockeyApp Teams", + "HelpText": "optional, restrict download to comma-separated list of team IDs; example: + +teams=12,23,42 with 12, 23, and 42 being the database IDs of your teams + +See [Upload API Documentation](http://support.hockeyapp.net/kb/api/api-versions#upload-version)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppUsers", + "Label": "HockeyApp Users", + "HelpText": "optional, restrict download to comma-separated list of user IDs; example: + + 1224,5678 +with 1224 and 5678 being the database IDs of your users + +See [Upload API Documentation](http://support.hockeyapp.net/kb/api/api-versions#upload-version)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppMandatory", + "Label": "HockeyApp Mandatory", + "HelpText": "optional, set version as mandatory: + +- 0 - no +- 1 - yes + +See [Upload API Documentation](http://support.hockeyapp.net/kb/api/api-versions#upload-version)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppCommitSha", + "Label": "HockeyApp Commit Sha", + "HelpText": "optional, set to the git commit sha for this build + +See [Upload API Documentation](http://support.hockeyapp.net/kb/api/api-versions#upload-version)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppBuildServerUrl", + "Label": "HockeyApp Build Server Url", + "HelpText": "optional, set to the URL of the build job on your build server + +See [Upload API Documentation](http://support.hockeyapp.net/kb/api/api-versions#upload-version)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HockeyAppRepositoryUrl", + "Label": "HockeyApp Repository Url", + "HelpText": "optional, set to your source repository + +See [Upload API Documentation](http://support.hockeyapp.net/kb/api/api-versions#upload-version)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-12-01T00:11:50.815+00:00", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "hockeyapp" +} diff --git a/step-templates/http-invoke-url.json.human b/step-templates/http-invoke-url.json.human new file mode 100644 index 000000000..92c3aa5ee --- /dev/null +++ b/step-templates/http-invoke-url.json.human @@ -0,0 +1,80 @@ +{ + "Id": "bb2a8fef-1407-405b-9251-a259c1868bad", + "Name": "HTTP - Invoke URL", + "Description": "Invoke HTTP Get request using provided url. Doesn't throw exception when request fails.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\r +param(\r + [string]$url,\r + [switch]$whatIf\r +) \r +\r +$ErrorActionPreference = \"Stop\" \r +\r +function Get-Param($Name, [switch]$Required, $Default) {\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + $variable = Get-Variable $Name -EA SilentlyContinue \r + if ($variable -ne $null) {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null -or $result -eq \"\") {\r + if ($Required) {\r + throw \"Missing parameter value $Name\"\r + } else {\r + $result = $Default\r + }\r + }\r +\r + return $result\r +}\r +\r +& {\r + param(\r + [string]$url\r + ) \r +\r + Write-Host \"Invoke Url: $url\"\r +\r + try {\r + \r + Invoke-WebRequest -Uri $url -Method Get -UseBasicParsing\r +\r + } catch {\r + Write-Host \"There was a problem invoking Url\" \r + }\r +\r + } `\r + (Get-Param 'url' -Required)", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "url", + "Label": "Url", + "HelpText": "Web request Url", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-08-31T12:06:43.681+00:00", + "LastModifiedBy": "jmalczak", + "$Meta": { + "ExportedAt": "2015-08-31T12:25:19.923+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "http" +} diff --git a/step-templates/http-post-form.json.human b/step-templates/http-post-form.json.human new file mode 100644 index 000000000..eee5cfa80 --- /dev/null +++ b/step-templates/http-post-form.json.human @@ -0,0 +1,42 @@ +{ + "$Meta": { + "ExportedAt": "2015-11-13T21:57:26.839+00:00", + "OctopusVersion": "3.1.1", + "Type": "ActionTemplate" + }, + "ActionType": "Octopus.Script", + "Description": "Execute a simple form POST via HTTP. The script will construct a body in \"application/x-www-form-urlencoded\" format by extracting Octopus variables with a specified prefix (from the \"HTTP.PostForm.Prefix\" parameter). Variable names and values are encoded accordingly. + +For instance, if \"HTTP.PostForm.Prefix\" is \"foo.\", then the Octopus variable \"foo.Bar\"=\"baz\" will be translated to \"Bar=baz\" in the request body.", + "Id": "3c7a59bd-d66e-4dc6-9509-895b4a5d98c8", + "LastModifiedBy": "kburdett", + "LastModifiedOn": "2015-11-13T22:01:45.000+00:00", + "Name": "HTTP - Post Form", + "Parameters": [ + { + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "HelpText": "Specifies the destination of the POST.", + "Label": "URI", + "Name": "HTTP.PostForm.URI" + }, + { + "DefaultValue": "HTTP.Parameter.", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "HelpText": "Specifies the variable prefix for composing the body of the POST. Any variables in the current context that begin with this prefix will be extracted and added to the body.", + "Label": "Prefix", + "Name": "HTTP.PostForm.Prefix" + } + ], + "Properties": { + "Octopus.Action.Script.ScriptBody": "try {\r $uri = $OctopusParameters[\"HTTP.PostForm.URI\"]\r $prefix = $OctopusParameters[\"HTTP.PostForm.Prefix\"]\r $body = \"\"\r\r # Ensure URI is populated\r if ([string]::IsNullOrEmpty($uri)) {\r Write-Error \"HTTP.PostForm.URI is required and cannot be empty\"\r return\r }\r\r # Construct the body\r if (![string]::IsNullOrEmpty($prefix)) {\r $params = $OctopusParameters.Keys |\r Where-Object { $_.StartsWith($prefix) } |\r Foreach-Object {\r $key = [uri]::EscapeDataString($_.Substring($prefix.Length))\r $val = [uri]::EscapeDataString($OctopusParameters[$_])\r Write-Verbose \"Found parameter ${key}=${val}\"\r \"${key}=${val}\"\r }\r \r if ($params.Length -gt 0) {\r $body = $params -join \"&\"\r }\r }\r \r # Execute the request\r Write-Host \"Executing trigger: ${uri}\"\r Write-Host \"Body: ${body}\"\r Invoke-WebRequest -Method Post -Uri $uri -Body $body -ContentType \"application/x-www-form-urlencoded\"\r} catch {\r $ErrorMessage = $_.Exception.Message\r Write-Error $ErrorMessage\r}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Version": 2, + "Category": "http" +} diff --git a/step-templates/http-test-url.json.human b/step-templates/http-test-url.json.human new file mode 100644 index 000000000..d72d64cf5 --- /dev/null +++ b/step-templates/http-test-url.json.human @@ -0,0 +1,247 @@ +{ + "Id": "f5cebc0a-cc16-4876-9f72-bfbd513e6fdd", + "Name": "HTTP - Test URL", + "Description": "Makes a GET request to a HTTP(S) end point and verifies that a particular status code and (optional) response is returned within a specified period of time.", + "ActionType": "Octopus.Script", + "Version": 18, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$uri = $OctopusParameters['Uri'] +$customHostHeader = $OctopusParameters['CustomHostHeader'] +$expectedCode = [int] $OctopusParameters['ExpectedCode'] +$timeoutSeconds = [int] $OctopusParameters['TimeoutSeconds'] +$Username = $OctopusParameters['AuthUsername'] +$Password = $OctopusParameters['AuthPassword'] +$UseWindowsAuth = [System.Convert]::ToBoolean($OctopusParameters['UseWindowsAuth']) +$ExpectedResponse = $OctopusParameters['ExpectedResponse'] +$securityProtocol = $OctopusParameters['SecurityProtocol'] + +Write-Host \"Starting verification request to $uri\" +if ($customHostHeader) +{ + Write-Host \"Using custom host header $customHostHeader\" +} + +Write-Host \"Expecting response code $expectedCode.\" +Write-Host \"Expecting response: $ExpectedResponse.\" + +if ($securityProtocol) +{ + Write-Host \"Using security protocol $securityProtocol\" + [Net.ServicePointManager]::SecurityProtocol = [Enum]::parse([Net.SecurityProtocolType], $securityProtocol) +} + +$timer = [System.Diagnostics.Stopwatch]::StartNew() +$success = $false +do +{ + try + { + if ($Username -and $Password -and $UseWindowsAuth) + { + Write-Host \"Making request to $uri using windows authentication for user $Username\" + $request = [system.Net.WebRequest]::Create($uri) + $Credential = New-Object System.Management.Automation.PSCredential -ArgumentList $Username, $(ConvertTo-SecureString -String $Password -AsPlainText -Force) + $request.Credentials = $Credential + + if ($customHostHeader) + { + $request.Host = $customHostHeader + } + + try + { + $response = $request.GetResponse() + } + catch [System.Net.WebException] + { + Write-Host \"Request failed :-( System.Net.WebException\" + Write-Host $_.Exception + $response = $_.Exception.Response + } + + } +\t\telseif ($Username -and $Password) + { + Write-Host \"Making request to $uri using basic authentication for user $Username\" + $Credential = New-Object System.Management.Automation.PSCredential -ArgumentList $Username, $(ConvertTo-SecureString -String $Password -AsPlainText -Force) + if ($customHostHeader) + { + $response = Invoke-WebRequest -Uri $uri -Method Get -UseBasicParsing -Credential $Credential -Headers @{\"Host\" = $customHostHeader} -TimeoutSec $timeoutSeconds + } + else + { + $response = Invoke-WebRequest -Uri $uri -Method Get -UseBasicParsing -Credential $Credential -TimeoutSec $timeoutSeconds + } + } +\t\telse + { + Write-Host \"Making request to $uri using anonymous authentication\" + if ($customHostHeader) + { + $response = Invoke-WebRequest -Uri $uri -Method Get -UseBasicParsing -Headers @{\"Host\" = $customHostHeader} -TimeoutSec $timeoutSeconds + } + else + { + $response = Invoke-WebRequest -Uri $uri -Method Get -UseBasicParsing -TimeoutSec $timeoutSeconds + } + } + + $code = $response.StatusCode + $body = $response.Content; + Write-Host \"Recieved response code: $code\" + Write-Host \"Recieved response: $body\" + + if($response.StatusCode -eq $expectedCode) + { + $success = $true + } + if ($success -and $ExpectedResponse) + { + $success = ($ExpectedResponse -eq $body) + } + } + catch + { + # Anything other than a 200 will throw an exception so + # we check the exception message which may contain the + # actual status code to verify + + Write-Host \"Request failed :-(\" + Write-Host $_.Exception + + if($_.Exception -like \"*($expectedCode)*\") + { + $success = $true + } + } + + if(!$success) + { + Write-Host \"Trying again in 5 seconds...\" + Start-Sleep -s 5 + } +} +while(!$success -and $timer.Elapsed -le (New-TimeSpan -Seconds $timeoutSeconds)) + +$timer.Stop() + +# Verify result + +if(!$success) +{ + throw \"Verification failed - giving up.\" +} + +Write-Host \"Sucesss! Found status code $expectedCode\"", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "ca7c3e92-c243-4115-a326-7693eb830214", + "Name": "Uri", + "Label": "URI", + "HelpText": "The full Uri of the endpoint", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0d760ed5-81ed-46d9-b833-74f75df08bfc", + "Name": "CustomHostHeader", + "Label": "Custom HOST header", + "HelpText": "An optional custom HOST header which will be passed with the request", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b4e57984-01df-4438-96e5-75e74c2c6188", + "Name": "SecurityProtocol", + "Label": "Security Protocol", + "HelpText": "The optional security protocol version to use for HTTPS requests.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "SystemDefault +Ssl3 +Tls +Tls11 +Tls12" + } + }, + { + "Id": "8f47469e-3b6d-4915-a710-3a601debeb8a", + "Name": "ExpectedCode", + "Label": "Expected code", + "HelpText": "The expected HTTP status code", + "DefaultValue": "200", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7c96b2b3-53dd-4281-ae1a-ef67cbc0eb72", + "Name": "TimeoutSeconds", + "Label": "Timeout (Seconds)", + "HelpText": "The number of seconds before the step fails and times out", + "DefaultValue": "60", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ac0b303c-0c59-4776-be21-d93ebe9e28e7", + "Name": "AuthUsername", + "Label": "Username", + "HelpText": "Username for authentication. Leave blank to use Anonymous.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "38eae17a-3098-48df-b8dc-96c3185f9f40", + "Name": "AuthPassword", + "Label": "Password", + "HelpText": "Password for authentication. Leave blank for Anonymous.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "60fe87fe-e96d-448e-94fa-f2ce4bfbaf3a", + "Name": "UseWindowsAuth", + "Label": "Use Windows Authentication", + "HelpText": "Should the request be made passing windows authentication (kerberos) credentials otherwise uses basic authentication", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "47c779b5-c515-49f9-a122-f692b0f12ff7", + "Name": "ExpectedResponse", + "Label": "Expected Response", + "HelpText": "The response should be this text", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2020-06-16T08:03:12.574Z", + "OctopusVersion": "2020.2.13", + "Type": "ActionTemplate" + }, + "Category": "http" +} diff --git a/step-templates/hydra-update-octopus-tentacle.json.human b/step-templates/hydra-update-octopus-tentacle.json.human new file mode 100644 index 000000000..da4eab417 --- /dev/null +++ b/step-templates/hydra-update-octopus-tentacle.json.human @@ -0,0 +1,47 @@ +{ + "Id": "d4fb1945-f0a8-4de4-9045-8441e14057fa", + "Name": "Hydra - Update Octopus Tentacle", + "Description": "Performs an automatic update for a 2.6 Tentacle to a 3.0 Tentacle.", + "ActionType": "Octopus.TentaclePackage", + "Version": 8, + "Properties": { + "Octopus.Action.Package.NuGetFeedId": "feeds-builtin", + "Octopus.Action.EnabledFeatures": "Octopus.Features.CustomScripts", + "Octopus.Action.Package.AutomaticallyRunConfigurationTransformationFiles": "False", + "Octopus.Action.Package.AutomaticallyUpdateAppSettingsAndConnectionStrings": "False", + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.Package.NuGetPackageId": "OctopusDeploy.Hydra", + "Octopus.Action.CustomScripts.PostDeploy.ps1": "if ([System.String]::IsNullOrEmpty($ServerMapping)) { + & .\\Hydra.exe --defer +} else { + $cleanServerMapping = $ServerMapping.Replace(\" \",\"\") + & .\\Hydra.exe --defer --servers=$cleanServerMapping +} +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ServerMapping", + "Label": "Server Mapping", + "HelpText": "Optional mapping to new Octopus server location required for polling Tentacles. +Either a single IP address or DNS name with a port (e.g. `http://new.server:10943`) +Or a mapping in the form `https://old.server:10943/=>https://new.server:10943/`, with multiple mappings separated by a comma (`,`) or semicolon (`;`) + +_Note: If using the mapping format, the final `/` is required._", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-07-01T05:56:48.370+00:00", + "LastModifiedBy": "Damovisa", + "$Meta": { + "ExportedAt": "2015-07-01T05:57:02.674+00:00", + "OctopusVersion": "2.6.5.1581", + "Type": "ActionTemplate" + }, + "Category": "octopus", + "MaximumServerVersion": "3.0.0" +} diff --git a/step-templates/iis-add-isapicgirestrictionexception.json.human b/step-templates/iis-add-isapicgirestrictionexception.json.human new file mode 100644 index 000000000..7d24a5c09 --- /dev/null +++ b/step-templates/iis-add-isapicgirestrictionexception.json.human @@ -0,0 +1,123 @@ +{ + "Id": "47e1a39c-65f3-43aa-ad5e-ea1946bbf368", + "Name": "IIS - Add an Allow ISAPI And CGI Restriction Exception", + "Description": "Adds an IIS Server Allow exception to the ISAPI/CGI Restrictions in IIS 7 and above.", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Param\r +(\r + [Parameter(Position = 0)]\r + [ValidateNotNullOrEmpty()]\r + [string] $cgiIsapiExtensionPath,\r + [Parameter(Position = 1)]\r + [string] $description = [string]::Empty\r +)\r +\r +$ErrorActionPrefrence = \"Stop\"\r +\r +\r +function Get-Param($Name, [switch]$Required, $Default) \r +{\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) \r + {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) \r + {\r + $variable = Get-Variable $Name -EA SilentlyContinue \r + if ($variable -ne $null) \r + {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null) \r + {\r + if ($Required) \r + {\r + throw \"Missing parameter value $Name\"\r + } \r + else \r + {\r + $result = $Default\r + }\r + }\r +\r + return $result\r +}\r +\r +& {\r + Param\r + (\r + [Parameter(Mandatory=$True, Position = 0)]\r + [ValidateNotNullOrEmpty()]\r + [string] $cgiIsapiExtensionPath,\r + [Parameter(Position = 1)]\r + [string] $description = [string]::Empty\r + )\r +\r + Import-Module \"WebAdministration\"\r +\r + $cgiIsapiConfiguration = Get-WebConfiguration -Filter \"/system.webServer/security/isapiCgiRestriction/add\" -PSPath \"IIS:\\\"\r +\r + $cgiIsapiExtensionFullPath = [System.Environment]::ExpandEnvironmentVariables($cgiIsapiExtensionPath)\r + $cgiIsapiExtensionFullPath = Resolve-Path -Path $cgiIsapiExtensionFullPath\r +\r + $restrictionFound = $false\r + $cgiIsapiConfiguration | ForEach-Object {\r + $itemFullPath = [System.Environment]::ExpandEnvironmentVariables($_.path)\r + $itemFullPath = Resolve-Path -Path $itemFullPath\r +\r + if ($itemFullPath.Path -eq $cgiIsapiExtensionFullPath.Path)\r + {\r + $restrictionFound = $true\r + }\r + }\r +\r + if ($restrictionFound -eq $false)\r + {\r + Add-WebConfiguration -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter \"system.webServer/security/isapiCgiRestriction\" -value @{description=\"$description\";path=\"$cgiIsapiExtensionPath\";allowed='True'}\r + }\r + else\r + {\r + Write-Host \"Allowed CGI/ISAPI Restriction for '$cgiIsapiExtensionPath' already exists.\"\r + }\r +} `\r +(Get-Param 'cgiIsapiExtensionPath' -Required) `\r +(Get-Param 'description')", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "cgiIsapiExtensionPath", + "Label": "CGI/ISAPI Extension Path", + "HelpText": "The full path to the ISAPI/CGI extension to allow.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "description", + "Label": "Description", + "HelpText": "A description for the ISAPI/CGI Restriction allowance exception.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2016-01-13T21:28:56.040+00:00", + "LastModifiedBy": "ekrapfl", + "$Meta": { + "ExportedAt": "2016-01-13T21:28:56.040+00:00", + "OctopusVersion": "3.2.14", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-app-create.json.human b/step-templates/iis-app-create.json.human new file mode 100644 index 000000000..09c1878fa --- /dev/null +++ b/step-templates/iis-app-create.json.human @@ -0,0 +1,473 @@ +{ + "Id": "5b3e7576-44f8-4852-ae09-a45bd985c549", + "Name": "IIS Application - Create", + "Description": "Create an IIS virtual application (a virtual directory with an application pool)", + "ActionType": "Octopus.Script", + "Version": 37, + "Properties": { + "Octopus.Action.Script.ScriptBody": "## --------------------------------------------------------------------------------------\r +## Input\r +## --------------------------------------------------------------------------------------\r +\r +$virtualPath = $OctopusParameters['VirtualPath']\r +$physicalPath = $OctopusParameters['PhysicalPath']\r +$applicationPoolName = $OctopusParameters['ApplicationPoolName']\r +$setApplicationPoolSettings = [boolean]::Parse($OctopusParameters['SetApplicationPoolSettings'])\r +$appPoolFrameworkVersion = $OctopusParameters[\"ApplicationPoolFrameworkVersion\"]\r +$applicationPoolIdentityType = $OctopusParameters[\"ApplicationPoolIdentityType\"]\r +$applicationPoolUsername = $OctopusParameters[\"ApplicationPoolUsername\"]\r +$applicationPoolPassword = $OctopusParameters[\"ApplicationPoolPassword\"]\r +\r +$parentSite = $OctopusParameters['ParentSite']\r +$bindingProtocols = $OctopusParameters['BindingProtocols']\r +$authentication = $OctopusParameters['AuthenticationType']\r +$requireSSL = $OctopusParameters['RequireSSL']\r +$clientCertificate = $OctopusParameters['ClientCertificate']\r +\r +$preloadEnabled = [boolean]::Parse($OctopusParameters['PreloadEnabled'])\r +$enableAnonymous = [boolean]::Parse($OctopusParameters['EnableAnonymous'])\r +$enableBasic = [boolean]::Parse($OctopusParameters['EnableBasic'])\r +$enableWindows = [boolean]::Parse($OctopusParameters['EnableWindows'])\r +\r +## --------------------------------------------------------------------------------------\r +## Helpers\r +## --------------------------------------------------------------------------------------\r +# Helper for validating input parameters\r +function Validate-Parameter($foo, [string[]]$validInput, $parameterName) {\r + Write-Host \"${parameterName}: ${foo}\"\r + if (! $foo) {\r + throw \"$parameterName cannot be empty, please specify a value\"\r + }\r + \r + if ($validInput) {\r + @($foo) | % { \r + if ($validInput -notcontains $_) {\r + throw \"'$_' is not a valid input for '$parameterName'\"\r + }\r + } \r + } \r +}\r +\r +# Helper to run a block with a retry if things go wrong\r +$maxFailures = 5\r +$sleepBetweenFailures = Get-Random -minimum 1 -maximum 4\r +function Execute-WithRetry([ScriptBlock] $command) {\r + $attemptCount = 0\r + $operationIncomplete = $true\r +\r + while ($operationIncomplete -and $attemptCount -lt $maxFailures) {\r + $attemptCount = ($attemptCount + 1)\r +\r + if ($attemptCount -ge 2) {\r + Write-Output \"Waiting for $sleepBetweenFailures seconds before retrying...\"\r + Start-Sleep -s $sleepBetweenFailures\r + Write-Output \"Retrying...\"\r + }\r +\r + try {\r + & $command\r +\r + $operationIncomplete = $false\r + } catch [System.Exception] {\r + if ($attemptCount -lt ($maxFailures)) {\r + Write-Output (\"Attempt $attemptCount of $maxFailures failed: \" + $_.Exception.Message)\r + \r + }\r + else {\r + throw \"Failed to execute command\"\r + }\r + }\r + }\r +}\r +\r +## --------------------------------------------------------------------------------------\r +## Configuration\r +## --------------------------------------------------------------------------------------\r +Validate-Parameter $virtualPath -parameterName \"Virtual path\"\r +Validate-Parameter $physicalPath -parameterName \"Physical path\"\r +Validate-Parameter $applicationPoolName -parameterName \"Application pool\"\r +Validate-Parameter $parentSite -parameterName \"Parent site\"\r +\r +\r +Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue\r +Import-Module WebAdministration -ErrorAction SilentlyContinue\r +\r +\r +## --------------------------------------------------------------------------------------\r +## Run\r +## --------------------------------------------------------------------------------------\r +\r +Write-Host \"Getting web site $parentSite\"\r +# Workaround to bug in Get-WebSite cmdlet which would return all sites\r +# See http://forums.iis.net/p/1167298/1943273.aspx / http://stackoverflow.com/a/6832577/785750\r +$site = Get-WebSite | where { $_.Name -eq $parentSite }\r +if (!$site) {\r + throw \"The web site '$parentSite' does not exist. Please create the site first.\"\r +}\r +\r +$path = $site.PhysicalPath;\r +$parts = $virtualPath -split \"[/\\\\]\"\r +$name = \"\"\r +\r +for ($i = 0; $i -lt $parts.Length; $i++) {\r + $name = $name + \"/\" + $parts[$i]\r + $name = $name.TrimStart('/').TrimEnd('/')\r + if ($i -eq $parts.Length - 1) {\r + \r + }\r + elseif ([string]::IsNullOrEmpty($name) -eq $false -and $name -ne \"\") {\r + Write-Host \"Ensuring parent exists: $name\"\r + \r + $path = [IO.Path]::Combine($path, $parts[$i])\r + $app = Get-WebApplication -Name $name -Site $parentSite\r +\r + if (!$app) {\r + $vdir = Get-WebVirtualDirectory -Name $name -site $parentSite\r + if (!$vdir) {\r + Write-Verbose \"The application or virtual directory '$name' does not exist\"\r + if([IO.Directory]::Exists([System.Environment]::ExpandEnvironmentVariables($path)) -eq $true)\r + {\r + Write-Verbose \"Using physical path '$path' as parent\"\r + }\r + else\r + {\r + throw \"Failed to ensure parent\"\r + }\r + }\r + else\r + {\r + $path = $vdir.PhysicalPath\r + }\r + }\r + else\r + {\r + $path = $app.PhysicalPath\r + }\r + }\r +}\r +\r +$existing = Get-WebApplication -site $parentSite -Name $name\r +\r +# Set App Pool\r +Execute-WithRetry { \r +\tWrite-Verbose \"Loading Application pool\"\r +\t$pool = Get-Item \"IIS:\\AppPools\\$ApplicationPoolName\" -ErrorAction SilentlyContinue\r +\tif (!$pool) { \r +\t\tWrite-Host \"Application pool `\"$ApplicationPoolName`\" does not exist, creating...\" \r +\t\tnew-item \"IIS:\\AppPools\\$ApplicationPoolName\" -confirm:$false\r +\t\t$pool = Get-Item \"IIS:\\AppPools\\$ApplicationPoolName\"\r +\t} else {\r +\t\tWrite-Host \"Application pool `\"$ApplicationPoolName`\" already exists\"\r +\t}\r +}\r +\r +# Set App Pool Identity\r +Execute-WithRetry { \r +\tif($setApplicationPoolSettings)\r + {\r + Write-Host \"Set application pool identity: $applicationPoolIdentityType\"\r + if ($applicationPoolIdentityType -eq \"SpecificUser\") {\r + Set-ItemProperty \"IIS:\\AppPools\\$ApplicationPoolName\" -name processModel -value @{identitytype=\"SpecificUser\"; username=\"$applicationPoolUsername\"; password=\"$applicationPoolPassword\"}\r + } else {\r + Set-ItemProperty \"IIS:\\AppPools\\$ApplicationPoolName\" -name processModel -value @{identitytype=\"$applicationPoolIdentityType\"}\r + }\r + }\r +}\r +\r +# Set .NET Framework\r +Execute-WithRetry { \r + if($setApplicationPoolSettings)\r + {\r + Write-Host \"Set .NET framework version: $appPoolFrameworkVersion\" \r + if($appPoolFrameworkVersion -eq \"No Managed Code\")\r + {\r + Set-ItemProperty \"IIS:\\AppPools\\$ApplicationPoolName\" managedRuntimeVersion \"\"\r + }\r + else\r + {\r + Set-ItemProperty \"IIS:\\AppPools\\$ApplicationPoolName\" managedRuntimeVersion $appPoolFrameworkVersion\r + }\r + }\r +}\r +\r +Execute-WithRetry { \r + ## Check if the physical path exits\r + if(!(Test-Path -Path $physicalPath)) {\r + Write-Host \"Creating physical path '$physicalPath'\"\r + New-Item -ItemType directory -Path $physicalPath\r + }\r +\r + if (!$existing) {\r + Write-Host \"Creating web application '$name'\"\r + New-WebApplication -Site $parentSite -Name $name -ApplicationPool $applicationPoolName -PhysicalPath $physicalPath\r + Write-Host \"Web application created\"\r + } else {\r + Write-Host \"The web application '$name' already exists. Updating physical path:\"\r +\r + Set-ItemProperty IIS:\\\\Sites\\\\$parentSite\\\\$name -name physicalPath -value $physicalPath\r + Write-Host \"Physical path changed to: $physicalPath\"\r +\r + Set-ItemProperty IIS:\\\\Sites\\\\$parentSite\\\\$name -Name applicationPool -Value $applicationPoolName\r + Write-Output \"ApplicationPool changed to: $applicationPoolName\"\r + }\r + \r + Write-Host \"Enabling '$bindingProtocols' protocols\"\r + Set-ItemProperty IIS:\\\\Sites\\\\$parentSite\\\\$name -name enabledProtocols -value $bindingProtocols\r +\r + $enabledIisAuthenticationOptions = $Authentication -split '\\\\s*[,;]\\\\s*'\r +\r + try {\r +\r + Execute-WithRetry { \r + Write-Output \"Anonymous authentication enabled: $enableAnonymous\"\r + Set-WebConfigurationProperty -filter /system.webServer/security/authentication/anonymousAuthentication -name enabled -value \"$enableAnonymous\" -PSPath IIS:\\\\ -location $parentSite/$virtualPath\r + } \r + \r + Execute-WithRetry { \r + Write-Output \"Windows authentication enabled: $enableWindows\"\r + Set-WebConfigurationProperty -filter /system.WebServer/security/authentication/windowsAuthentication -name enabled -value \"$enableWindows\" -PSPath IIS:\\\\ -location $parentSite/$virtualPath\r + }\r +\r + Execute-WithRetry { \r + Write-Output \"Basic authentication enabled: $enableBasic\"\r + Set-WebConfigurationProperty -filter /system.webServer/security/authentication/basicAuthentication -name enabled -value \"$enableBasic\" -PSPath IIS:\\\\ -location $parentSite/$virtualPath\r + }\r +\r + } catch [System.Exception] {\r + Write-Output \"Authentication options could not be set. This can happen when there is a problem with your application's web.config. For example, you might be using a section that requires an extension that is not installed on this web server (such as URL Rewriting). It can also happen when you have selected an authentication option and the appropriate IIS module is not installed (for example, for Windows authentication, you need to enable the Windows Authentication module in IIS/Windows first)\"\r + throw\r + }\r +\r + Set-WebConfiguration -value \"None\" -filter \"system.webserver/security/access\" -location $parentSite/$virtualPath -PSPath IIS:\\\\ \r + if ($requireSSL -ieq \"True\")\r + {\r + Write-Output \"Require SSL enabled: $requireSSL\"\r + Set-WebConfiguration -value \"Ssl\" -filter \"system.webserver/security/access\" -location $parentSite/$virtualPath -PSPath IIS:\\\\ \r + Write-Output \"Client certificate mode: $clientCertificate\"\r + if ($clientCertificate -ieq \"Accept\") {\r + Set-WebConfigurationProperty -filter \"system.webServer/security/access\" -location $parentSite/$virtualPath -PSPath IIS:\\\\ -name \"sslFlags\" -value \"Ssl,SslNegotiateCert\"\r + }\r + if ($clientCertificate -ieq \"Require\") {\r + Set-WebConfigurationProperty -filter \"system.webServer/security/access\" -location $parentSite/$virtualPath -PSPath IIS:\\\\ -name \"sslFlags\" -value \"Ssl,SslNegotiateCert,SslRequireCert\"\r + }\r + }\r + \r + try {\r + Set-ItemProperty IIS:\\\\Sites\\\\$parentSite\\\\$name -name preloadEnabled -value $preloadEnabled\r + Write-Output \"Preload Enabled: $preloadEnabled\"\r + } catch [System.Exception] {\r + if ($preloadEnabled) {\r + Write-Output \"Preload Enabled: $preloadEnabled Could not be set. You may to install the Application Initialization feature\"\r + throw\r + }\r + }\r +}\r +", + + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "679da415-996c-4ad3-87d5-bb8ce4e1f8d0", + "Name": "VirtualPath", + "Label": "Virtual path", + "HelpText": "The name of the application to create. For example, to serve an application that will be available at `/myapp`, enter `myapp`. To create an application under a parent virtual directory or application, separate with slashes - for example: `/applications/myapp`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "3eecef84-fe72-4efc-98bf-42aa41a2d488", + "Name": "PhysicalPath", + "Label": "Physical path", + "HelpText": "Physical folder that the application will serve files from. Example: `C:\\MyApp`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f2b3a7ef-4d83-4692-a328-fc18cb85fd3e", + "Name": "ParentSite", + "Label": "Parent site", + "HelpText": "The name of the IIS web site to attach the application to. For example, to put the application under the default web site, enter: + + Default Web Site", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "3cb9971e-a7f2-47e7-8201-fb25c3080bd0", + "Name": "ApplicationPoolName", + "Label": "Application pool", + "HelpText": "The name of the application pool that the application will run under.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2a5d18bf-50ad-43c4-882b-9314ee2551b4", + "Name": "BindingProtocols", + "Label": "Protocols", + "HelpText": "The protocols to use for the application", + "DefaultValue": "http", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "07d979ef-c343-4c9c-8681-d3c06f451539", + "Name": "RequireSSL", + "Label": "Require SSL", + "HelpText": "Web site SSL settings", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "3a3a8fe8-9fb0-4db5-85c9-04141f33f32d", + "Name": "ClientCertificate", + "Label": "Client certificate", + "HelpText": "_(Require SSL)_ Defines how to handle client certificates if SSL is required.", + "DefaultValue": "Ignore", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Ignore +Accept +Require" + }, + "Links": {} + }, + { + "Id": "174299e5-38dc-4fe9-b25a-6a5bdad05b34", + "Name": "PreloadEnabled", + "Label": "Preload Enabled", + "HelpText": "If true, sets the application to enable preloading.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "1641d3e2-a1e5-4933-b993-6528f1764557", + "Name": "EnableAnonymous", + "Label": "Enable Anonymous authentication", + "HelpText": "Whether IIS should allow anonymous authentication", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "3a52b977-3ea6-482b-8e55-62efd0198b14", + "Name": "EnableBasic", + "Label": "Enable Basic authentication", + "HelpText": "Whether IIS should allow basic authentication with a 401 challenge.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "085a3183-30bc-499e-90bf-9b5bcdf0842a", + "Name": "EnableWindows", + "Label": "Enable Windows authentication", + "HelpText": "Whether IIS should allow integrated Windows authentication with a 401 challenge.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "de7b8dee-bf5a-48ea-99bc-f5b86fcdf118", + "Name": "SetApplicationPoolSettings", + "Label": "Set Application Pool Settings", + "HelpText": "If true, this will allow you to set the Application Pool CLR Version, identity using the .NET CLR Version, Identity, Username, and Password parameters. If false, the other four parameters will be ignored.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "dd301881-0cd6-40bf-98b4-36e13a587fe2", + "Name": "ApplicationPoolFrameworkVersion", + "Label": "Application Pool .NET CLR Version", + "HelpText": "The version of the .NET common language runtime that this application pool will use.", + "DefaultValue": "v4.0", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "v2.0|CLR v2.0 (.NET 2.0, 3.0, 3.5) +v4.0|CLR v4.0 (.NET 4.0, 4.5, 4.6) +No Managed Code" + }, + "Links": {} + }, + { + "Id": "92cb9de4-f65f-4855-bc88-d89e6a748f3c", + "Name": "ApplicationPoolIdentityType", + "Label": "Application Pool Identity", + "HelpText": "Which built-in account will the application pool run under.", + "DefaultValue": "ApplicationPoolIdentity", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "ApplicationPoolIdentity|Application Pool Identity +LocalService|Local Service +LocalSystem|Local System +NetworkService|Network Service +SpecificUser|Custom user ..." + }, + "Links": {} + }, + { + "Id": "7cbeec84-7fad-46dd-98de-ed61531f8bb3", + "Name": "ApplicationPoolUsername", + "Label": "Application Pool Username", + "HelpText": "The Windows/domain account of the custom user that the application pool will run under. Example: YOURDOMAIN\\\\YourAccount. You will need to ensure that this user has permissions to run as an application pool.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "748a99a2-a9f4-46b2-b9b5-283a864171d3", + "Name": "ApplicationPoolPassword", + "Label": "Application Pool Password", + "HelpText": "The password for the custom account given above.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2017-04-10T15:13:10.653Z", + "OctopusVersion": "3.12.1", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-app-remove.json.human b/step-templates/iis-app-remove.json.human new file mode 100644 index 000000000..82568eb75 --- /dev/null +++ b/step-templates/iis-app-remove.json.human @@ -0,0 +1,151 @@ +{ + "Id": "a3edf679-e65a-4758-88a8-ec45a6c77563", + "Name": "IIS Application - Remove", + "Description": "Removes an IIS virtual application (a virtual directory with an application pool)", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "## --------------------------------------------------------------------------------------\r +## Input\r +## --------------------------------------------------------------------------------------\r +\r +$virtualPath = $OctopusParameters['VirtualPath']\r +$parentSite = $OctopusParameters['ParentSite']\r +\r +## --------------------------------------------------------------------------------------\r +## Helpers\r +## --------------------------------------------------------------------------------------\r +# Helper for validating input parameters\r +function Validate-Parameter([string]$foo, [string[]]$validInput, $parameterName) {\r + Write-Host \"${parameterName}: $foo\"\r + if (! $foo) {\r + throw \"No value was set for $parameterName, and it cannot be empty\"\r + }\r + \r + if ($validInput) {\r + if (! $validInput -contains $foo) {\r + throw \"'$input' is not a valid input for '$parameterName'\"\r + }\r + }\r + \r +}\r +\r +# Helper to run a block with a retry if things go wrong\r +$maxFailures = 5\r +$sleepBetweenFailures = Get-Random -minimum 1 -maximum 4\r +function Execute-WithRetry([ScriptBlock] $command) {\r +\t$attemptCount = 0\r +\t$operationIncomplete = $true\r +\r +\twhile ($operationIncomplete -and $attemptCount -lt $maxFailures) {\r +\t\t$attemptCount = ($attemptCount + 1)\r +\r +\t\tif ($attemptCount -ge 2) {\r +\t\t\tWrite-Output \"Waiting for $sleepBetweenFailures seconds before retrying...\"\r +\t\t\tStart-Sleep -s $sleepBetweenFailures\r +\t\t\tWrite-Output \"Retrying...\"\r +\t\t}\r +\r +\t\ttry {\r +\t\t\t& $command\r +\r +\t\t\t$operationIncomplete = $false\r +\t\t} catch [System.Exception] {\r +\t\t\tif ($attemptCount -lt ($maxFailures)) {\r +\t\t\t\tWrite-Output (\"Attempt $attemptCount of $maxFailures failed: \" + $_.Exception.Message)\r +\t\t\t\r +\t\t\t}\r +\t\t\telse {\r +\t\t\t throw \"Failed to execute command\"\r +\t\t\t}\r +\t\t}\r +\t}\r +}\r +\r +## --------------------------------------------------------------------------------------\r +## Configuration\r +## --------------------------------------------------------------------------------------\r +Validate-Parameter $virtualPath -parameterName \"Virtual path\"\r +Validate-Parameter $parentSite -parameterName \"Parent site\"\r +\r +Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue\r +Import-Module WebAdministration -ErrorAction SilentlyContinue\r +\r +## --------------------------------------------------------------------------------------\r +## Run\r +## --------------------------------------------------------------------------------------\r +\r +Write-Host \"Getting web site $parentSite\"\r +$site = Get-Website -name $parentSite\r +if (!$site) {\r + throw \"The web site '$parentSite' does not exist. Please create the site first.\"\r +}\r +\r +$parts = $virtualPath -split \"[/\\\\]\"\r +$name = \"\"\r +\r +for ($i = 0; $i -lt $parts.Length; $i++) {\r + $name = $name + \"/\" + $parts[$i]\r + $name = $name.TrimStart('/').TrimEnd('/')\r + if ($i -eq $parts.Length - 1) {\r + \r + }\r + elseif ([string]::IsNullOrEmpty($name) -eq $false -and $name -ne \"/\") {\r + Write-Host \"Ensuring parent exists: $name\"\r +\r + $app = Get-WebApplication -Name $name -Site $parentSite\r +\r + if (!$app) {\r + $vdir = Get-WebVirtualDirectory -Name $name -site $parentSite\r + if (!$vdir) {\r + throw \"The application or virtual directory '$name' does not exist\"\r + }\r + }\r + }\r +}\r +\r +$existing = Get-WebApplication -site $parentSite -Name $name\r +\r +Execute-WithRetry { \r + if ($existing) {\r + Write-Host \"Removing web application '$name'\"\r +\t\tRemove-WebApplication -Name $name -Site $parentSite\r + Write-Host \"Web application removed\"\r + } else {\r + Write-Host \"Web application doesn't exist, nothing to remove.\"\r + }\r +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "VirtualPath", + "Label": "Virtual path", + "HelpText": "The name of the application to remove. For example, if the application that will be removed is at `/myapp`, enter `myapp`. To remove an application under a parent virtual directory or application, separate with slashes - for example: `/applications/myapp`", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ParentSite", + "Label": "Parent site", + "HelpText": "The name of the IIS web site to remove the application from. For example, to remove the application under the default web site, enter: + + Default Web Site", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-05-20T20:25:09.714+00:00", + "LastModifiedBy": "lukerogers", + "$Meta": { + "ExportedAt": "2015-05-20T20:27:33.262+00:00", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-app-set-connect-as-credential.json.human b/step-templates/iis-app-set-connect-as-credential.json.human new file mode 100644 index 000000000..adcdfb3c4 --- /dev/null +++ b/step-templates/iis-app-set-connect-as-credential.json.human @@ -0,0 +1,141 @@ +{ + "Id": "1a482cf6-95ae-442f-92a4-c1e79d5c8e4b", + "Name": "IIS Application - Set Connect As credential", + "Description": "Sets the credential for the Connect As of an IIS application", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Import the WebAdministration module +Import-Module WebAdministration + +# Get reference to the site +$iisSite = Get-IISSite | Where-Object {$_.Name -ieq $IISSiteName} + +# Check to make sure the site was found +if ($null -eq $iisSite) +{ +\t# Throw an error + throw \"$IISSiteName was not found.\" +} + +# Check to see if $IISApplicationName starts with a / +if ($IISApplicationName.StartsWith(\"/\")) +{ +\t# Remove the beginning slash + $IISApplicationName = $IISApplicationName.SubString(1) +} + +# Get reference to the application +$application = $iisSite.Applications | Where-Object {$_.Path -ieq \"/$IISApplicationName\"} + +# Check to see if the application was found +if ($null -eq $application) +{ +\t# Throw an error + throw \"$IISApplicationName was not found.\" +} + +# retrieve existing values +$currentUserName = Get-WebConfigurationProperty \"system.applicationHost/sites/site[@name='$($iisSite.Name)']/application[@path='$($application.Path)']/virtualDirectory[@path='/']\" -name username +$currentPassword = Get-WebConfigurationProperty \"system.applicationHost/sites/site[@name='$($iisSite.Name)']/application[@path='$($application.Path)']/virtualDirectory[@path='/']\" -name password + +# Check the value of $ApplicationUserName +if ([string]::IsNullOrEmpty($ApplicationUserName)) +{ + # Ensure $ApplicationUserName is an empty string + $ApplicationUserName = [string]::Empty +} + +# Check the value of $ApplicationPassword +if ([string]::IsNullOrEmpty($ApplicationUserPassword) -or $ApplicationUserName.EndsWith('$')) # Usernames ending in $ are an indication that an MSA is being used +{ +\t# Ensure $ApplicationPassword is empty string + $ApplicationPassword = [string]::Empty +} + +# Compare username values +if ($ApplicationUserName -ne $currentUserName.Value) +{ +\t# Display message + Write-Output \"Updating username for $IISSiteName/$IISApplicationName to $ApplicationUserName Connect As property\" + +\t# Update the property + Set-WebConfigurationProperty \"system.applicationHost/sites/site[@name='$($iisSite.Name)']/application[@path='$($application.Path)']/virtualDirectory[@path='/']\" -name username -value \"$ApplicationUserName\" +} +else +{ +\t# Display message + Write-Output \"User already set to $ApplicationUserName for $IISSiteName/$IISApplicationName Connect As property\" +} + +# Compare password values +if ($ApplicationUserPassword -ne $currentPassword.Value) +{ + # Display message + Write-Output \"Updating password for $IISSiteName/$IISApplicationName Connect As property\" + + # Set password property +\tSet-WebConfigurationProperty \"system.applicationHost/sites/site[@name='$($iisSite.Name)']/application[@path='$($application.Path)']/virtualDirectory[@path='/']\" -name password -value \"$ApplicationUserPassword\" +} +else +{ +\t# Display message + Write-Output \"Password does not need updating for $IISSiteName/$IISApplicationName Connect As property\" +} +" + }, + "Parameters": [ + { + "Id": "cca187a4-2e55-4b71-98e8-87bf47b9744d", + "Name": "IISSiteName", + "Label": "Site Name", + "HelpText": "Name of the IIS site.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "381050b6-975b-4a10-b79c-a6fee9383cf2", + "Name": "IISApplicationName", + "Label": "Application Name", + "HelpText": "Name of the application to set the credential on.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8b9f0aff-93d7-4bc0-b375-b30179835e17", + "Name": "ApplicationUserName", + "Label": "User name", + "HelpText": "Name of the credential to apply, leave blank to set to Pass-through authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "220c4021-727a-4f10-935b-101a3cc8a3bb", + "Name": "ApplicationUserPassword", + "Label": "Password", + "HelpText": "Password of the credential to set.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedOn": "2018-12-04T20:21:52.123Z", + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2018-12-04T20:21:52.123Z", + "OctopusVersion": "2018.9.12", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-app-update-property.json.human b/step-templates/iis-app-update-property.json.human new file mode 100644 index 000000000..f697a3f23 --- /dev/null +++ b/step-templates/iis-app-update-property.json.human @@ -0,0 +1,139 @@ +{ + "Id": "4eb08b60-142c-4443-92cf-2e069727f438", + "Name": "IIS App - Update Property", + "Description": "Updates property for specified application", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +param( + [string]$webSiteName, + [string]$applicationName, + [string]$propertyName, + [object]$propertyValue, + [switch]$whatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if ($result -eq $null -or $result -eq \"\") { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +& { + param( + [string]$webSiteName, + [string]$applicationName, + [string]$propertyName, + [object]$propertyValue + ) + + Write-Host \"Setting $webSiteName\\$applicationName property $propertyName to $propertyValue\" + + try { + Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue + Import-Module WebAdministration -ErrorAction SilentlyContinue + + $oldValue = Get-ItemProperty \"IIS:\\Sites\\$webSiteName\\$applicationName\" -Name $propertyName + $oldValueString = \"\" + + if ($oldValue.GetType() -eq [Microsoft.IIs.PowerShell.Framework.ConfigurationAttribute]) + { + $oldValueString = ($oldValue | Select-Object -ExpandProperty \"Value\") + } + elseif ($oldValue.GetType() -eq [System.String]) + { + $oldValueString = $oldValue + } + elseif ($oldValue.GetType() -eq [System.Management.Automation.PSCustomObject]) + { + $oldValueString = ($oldValue | Select-Object -ExpandProperty $propertyName) + } + + Write-Host \"Old value $oldValueString\" + Set-ItemProperty \"IIS:\\Sites\\$webSiteName\\$applicationName\" -Name $propertyName -Value $propertyValue + Write-Host \"New value $propertyValue\" + Write-Host \"Done\" + } catch { + Write-Host $_.Exception|format-list -force + Write-Host \"There was a problem setting property\" + } + + } ` + (Get-Param 'webSiteName' -Required) (Get-Param 'applicationName' -Required) (Get-Param 'propertyName' -Required) (Get-Param 'propertyValue' -Required) +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "41b1663e-f84a-4e8a-981a-fb86ec4176b9", + "Name": "webSiteName", + "Label": "Web site name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1d7a25f9-0900-4c5c-b76b-5afbb427249b", + "Name": "applicationName", + "Label": "Aplication name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b074c934-c903-45e3-a96b-6c5282840d60", + "Name": "propertyName", + "Label": "Name of the property to set", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cdc37d1e-8a16-44bd-b80b-5c83c9d3022f", + "Name": "propertyValue", + "Label": "Value of the property to set", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2018-12-03T17:04:14.758Z", + "LastModifiedBy": "micdenny", + "$Meta": { + "ExportedAt": "2018-12-03T17:04:14.758Z", + "OctopusVersion": "2018.9.12", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-apppool-create.json.human b/step-templates/iis-apppool-create.json.human new file mode 100644 index 000000000..681e99b19 --- /dev/null +++ b/step-templates/iis-apppool-create.json.human @@ -0,0 +1,402 @@ +{ + "Name": "IIS AppPool - Create", + "Description": "Creates or Reconfigures an IIS Application Pool", + "ActionType": "Octopus.Script", + "Version": 14, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function Validate-Parameter { + Param( + [Parameter(Position=0)][string]$Parameter, + [Parameter(Mandatory=$true, Position=1)][string]$ParameterName + ) + if (!$ParameterName -contains 'Password') { + Write-Host ('{0}: {1}' -f ${ParameterName},$Parameter) + } + if (!$Parameter) { + Write-Error ('No value was set for {0}, and it cannot be empty' -f $ParameterName) + } +} + +function Execute-Retry { + Param( + [Parameter(Mandatory=$true, Position=0)][ScriptBlock]$Command + ) +\t$attemptCount = 0 +\t$operationIncomplete = $true + $maxFailures = 5 + $sleepBetweenFailures = Get-Random -minimum 1 -maximum 4 +\twhile ($operationIncomplete -and $attemptCount -lt $maxFailures) { +\t\t$attemptCount = ($attemptCount + 1) +\t\tif ($attemptCount -ge 2) { +\t\t\tWrite-Output ('Waiting for {0} seconds before retrying ...' -f $sleepBetweenFailures) +\t\t\tStart-Sleep -s $sleepBetweenFailures +\t\t\tWrite-Output 'Retrying ...' +\t\t} +\t\ttry { +\t\t\t& $Command +\t\t\t$operationIncomplete = $false +\t\t} catch [System.Exception] { +\t\t\tif ($attemptCount -lt ($maxFailures)) { +\t\t\t\tWrite-Output ('Attempt {0} of {1} failed: {2}' -f $attemptCount,$maxFailures,$_.Exception.Message) +\t\t\t} +\t\t\telse { + Write-Host 'Failed to execute command' +\t\t\t} +\t\t} +\t} +} + +function Get-ScheduledTimes { + Param( + [Parameter(Position=0)][string]$Schedule + ) + if (!$Schedule) { + return @() + } + $minutes = $Schedule.Split(',') + $minuteArrayList = New-Object System.Collections.ArrayList(,$minutes) + return $minuteArrayList +} + +[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.Web.Administration') +Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue +Import-Module WebAdministration -ErrorAction SilentlyContinue + +$appPoolName = $OctopusParameters['AppPoolName'] +$appPoolIdentityType = $OctopusParameters['AppPoolIdentityType'] +if ($appPoolIdentityType -eq 3) { + $appPoolIdentityUser = $OctopusParameters['AppPoolIdentityUser'] + $appPoolIdentityPassword = $OctopusParameters['AppPoolIdentityPassword'] +} +$appPoolLoadUserProfile = [boolean]::Parse($OctopusParameters['AppPoolLoadUserProfile']) +$appPoolAutoStart = [boolean]::Parse($OctopusParameters['AppPoolAutoStart']) +$appPoolEnable32BitAppOnWin64 = [boolean]::Parse($OctopusParameters['AppPoolEnable32BitAppOnWin64']) +$appPoolManagedRuntimeVersion = $OctopusParameters['AppPoolManagedRuntimeVersion'] +$appPoolManagedPipelineMode = $OctopusParameters['AppPoolManagedPipelineMode'] +$appPoolIdleTimeout = [TimeSpan]::FromMinutes($OctopusParameters['AppPoolIdleTimeoutMinutes']) +$appPoolPeriodicRecycleTime = $OctopusParameters['AppPoolPeriodicRecycleTime'] +$appPoolMaxProcesses = [int]$OctopusParameters['AppPoolMaxProcesses'] +$appPoolRegularTimeInterval = [TimeSpan]::FromMinutes($OctopusParameters['AppPoolRegularTimeInterval']) +$appPoolQueueLength = [int]$OctopusParameters['AppPoolQueueLength'] +$appPoolStartMode = $OctopusParameters['AppPoolStartMode'] +$appPoolCpuAction = $OctopusParameters['AppPoolCpuLimitAction'] +$appPoolCpuLimit = [int]$OctopusParameters['AppPoolCpuLimit'] + +Validate-Parameter -Parameter $appPoolName -ParameterName 'Application Pool Name' +Validate-Parameter -Parameter $appPoolIdentityType -ParameterName 'Identity Type' +if ($appPoolIdentityType -eq 3) { + Validate-Parameter -Parameter $appPoolIdentityUser -ParameterName 'Identity UserName' + # If using Group Managed Serice Accounts, the password should be allowed to be empty +} +Validate-Parameter -Parameter $appPoolLoadUserProfile -parameterName 'Load User Profile' +Validate-Parameter -Parameter $appPoolAutoStart -ParameterName 'AutoStart' +Validate-Parameter -Parameter $appPoolEnable32BitAppOnWin64 -ParameterName 'Enable 32-Bit Apps on 64-bit Windows' +Validate-Parameter -Parameter $appPoolManagedRuntimeVersion -ParameterName 'Managed Runtime Version' +Validate-Parameter -Parameter $appPoolManagedPipelineMode -ParameterName 'Managed Pipeline Mode' +Validate-Parameter -Parameter $appPoolIdleTimeout -ParameterName 'Process Idle Timeout' +Validate-Parameter -Parameter $appPoolMaxProcesses -ParameterName 'Maximum Worker Processes' +Validate-Parameter -Parameter $appPoolStartMode -parameterName 'Start Mode' +Validate-Parameter -Parameter $appPoolCpuAction -parameterName 'CPU Limit Action' +Validate-Parameter -Parameter $appPoolCpuLimit -parameterName 'CPU Limit (percent)' + +$iis = (New-Object Microsoft.Web.Administration.ServerManager) +$pool = $iis.ApplicationPools | Where-Object {$_.Name -eq $appPoolName} | Select-Object -First 1 +if ($pool -eq $null) { + Write-Output ('Creating Application Pool {0}' -f $appPoolName) + Execute-Retry { + $iis = (New-Object Microsoft.Web.Administration.ServerManager) + $iis.ApplicationPools.Add($appPoolName) + $iis.CommitChanges() + } +} +else { + Write-Output ('Application Pool {0} already exists, reconfiguring ...' -f $appPoolName) +} +$list = Get-ScheduledTimes -Schedule $appPoolPeriodicRecycleTime +Execute-Retry { + $iis = (New-Object Microsoft.Web.Administration.ServerManager) + $pool = $iis.ApplicationPools | Where-Object {$_.Name -eq $appPoolName} | Select-Object -First 1 + Write-Output ('Setting: AutoStart = {0}' -f $appPoolAutoStart) + $pool.AutoStart = $appPoolAutoStart + Write-Output ('Setting: Enable32BitAppOnWin64 = {0}' -f $appPoolEnable32BitAppOnWin64) + $pool.Enable32BitAppOnWin64 = $appPoolEnable32BitAppOnWin64 + Write-Output ('Setting: IdentityType = {0}' -f $appPoolIdentityType) + $pool.ProcessModel.IdentityType = $appPoolIdentityType + if ($appPoolIdentityType -eq 3) { + Write-Output ('Setting: UserName = {0}' -f $appPoolIdentityUser) + $pool.ProcessModel.UserName = $appPoolIdentityUser + if (!$appPoolIdentityPassword) { + Write-Output ('Setting: Password = [empty]') + } + else { + Write-Output ('Setting: Password = [Omitted For Security]') + } + $pool.ProcessModel.Password = $appPoolIdentityPassword + } +\tWrite-Output ('Setting: LoadUserProfile = {0}' -f $appPoolLoadUserProfile) + $pool.ProcessModel.LoadUserProfile = $appPoolLoadUserProfile + Write-Output ('Setting: ManagedRuntimeVersion = {0}' -f $appPoolManagedRuntimeVersion) + if ($appPoolManagedRuntimeVersion -eq 'No Managed Code') { + $pool.ManagedRuntimeVersion = '' + } + else { + $pool.ManagedRuntimeVersion = $appPoolManagedRuntimeVersion + } + Write-Output ('Setting: ManagedPipelineMode = {0}' -f $appPoolManagedPipelineMode) + $pool.ManagedPipelineMode = $appPoolManagedPipelineMode + Write-Output ('Setting: IdleTimeout = {0}' -f $appPoolIdleTimeout) + $pool.ProcessModel.IdleTimeout = $appPoolIdleTimeout + Write-Output ('Setting: MaxProcesses = {0}' -f $appPoolMaxProcesses) + $pool.ProcessModel.MaxProcesses = $appPoolMaxProcesses + Write-Output ('Setting: RegularTimeInterval = {0}' -f $appPoolRegularTimeInterval) + $pool.Recycling.PeriodicRestart.Time = $appPoolRegularTimeInterval + Write-Output ('Setting: QueueLength = {0}' -f $appPoolQueueLength) + $pool.QueueLength = $appPoolQueueLength + Write-Output ('Setting: CPU Limit (percent) = {0}' -f $appPoolCpuLimit) + ## Limit is stored in 1/1000s of one percent + $pool.Cpu.Limit = $appPoolCpuLimit * 1000 + Write-Output ('Setting: CPU Limit Action = {0}' -f $appPoolCpuAction) + $pool.Cpu.Action = $appPoolCpuAction + Write-Output ('Setting: Schedule = {0}' -f $appPoolPeriodicRecycleTime) + $pool.Recycling.PeriodicRestart.Schedule.Clear() + foreach($timestamp in $list) { + $pool.Recycling.PeriodicRestart.Schedule.Add($timestamp) + } + if (Get-Member -InputObject $pool -Name StartMode -MemberType Properties) + { + Write-Output ('Setting: StartMode = {0}' -f $appPoolStartMode) + $pool.StartMode = $appPoolStartMode + } + else + { + Write-Output ('IIS does not support StartMode property, skipping this property...') + } + $iis.CommitChanges() +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Id": "bd870787-9020-49e8-8add-ad9f5fb65125", + "Name": "AppPoolName", + "Label": "Application pool name", + "HelpText": "The name of the application pool that the application will run under.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "7d8d63e7-5f6e-4a21-a58f-0f0787e1654f", + "Name": "AppPoolIdentityType", + "Label": "Identity Type", + "HelpText": "The type of identity that the application pool will be using.", + "DefaultValue": "3", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "0|Local System +1|Local Service +2|Network Service +3|Specific User... +4|Application Pool Identity" + }, + "Links": {} + }, + { + "Id": "4aabb8a8-df82-4462-be11-5f225466fa22", + "Name": "AppPoolIdentityUser", + "Label": "Specific User Name", + "HelpText": "_(Specific User)_ The user name to use with the application pool identity.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "701c812c-902c-4e2f-a888-cbf5d2fbf83d", + "Name": "AppPoolIdentityPassword", + "Label": "Specific User Password", + "HelpText": "_(Specific User)_ The password for the specific user to use with the application pool identity.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "63663052-6e0d-4ba8-ba95-f4213ba8aed2", + "Name": "AppPoolLoadUserProfile", + "Label": "Load User Profile", + "HelpText": "This setting specifies whether IIS loads the user profile for an application pool identity.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "c5c7d104-2d65-46da-a93f-bc3aea5ba1ab", + "Name": "AppPoolEnable32BitAppOnWin64", + "Label": "Enable 32-Bit Applications", + "HelpText": "Allows the application pool to run 32-bit applications when running on 64-bit windows.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "0af4b8ba-587f-4e92-a344-44a3408bad96", + "Name": "AppPoolAutoStart", + "Label": "Start Automatically", + "HelpText": "Automatically start the application pool when the application pool is created or whenever IIS is started.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "8eaf280a-7e17-4f03-90a1-29f66ca87b1a", + "Name": "AppPoolManagedRuntimeVersion", + "Label": "Managed Runtime Version", + "HelpText": "Specifies the CLR version to be used by the application pool.", + "DefaultValue": "v4.0", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "v1.1|CLR v1.1 (.NET 1.0, 1.1) +v2.0|CLR v2.0 (.NET 2.0, 3.0, 3.5) +v4.0|CLR v4.0 (.NET 4.0, 4.5, 4.6) +No Managed Code|No Managed Code (ASP.NET Core)" + }, + "Links": {} + }, + { + "Id": "bae7e84e-b17c-49bb-aba1-44d8f0326881", + "Name": "AppPoolManagedPipelineMode", + "Label": "Managed Pipeline Mode", + "HelpText": "Specifies the request-processing mode that is used to process requests for managed content.", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "0|Integrated +1|Classic" + }, + "Links": {} + }, + { + "Id": "cf5acda5-5261-40b9-ae6c-4067b35b9857", + "Name": "AppPoolIdleTimeoutMinutes", + "Label": "Process Idle Timeout", + "HelpText": "Amount of time (in minutes) a worker process will remain idle before it shuts down. A value of 0 means the process does not shut down after an idle timeout.", + "DefaultValue": "20", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "67c8fc44-00c6-4f0e-9b54-48acfe5792f9", + "Name": "AppPoolMaxProcesses", + "Label": "Maximum Worker Processes", + "HelpText": "Maximum number of worker processes permitted to service requests for the application pool.", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "63bc9198-49b3-451d-b082-9faf38f759e2", + "Name": "AppPoolRegularTimeInterval", + "Label": "Regular Time Interval", + "HelpText": "Period of time (in minutes) after which an application pool will recycle. A value of 0 means the application pool does not recycle on a regular interval.", + "DefaultValue": "1740", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "3b5cadcd-3092-4eb7-aeb6-2b7896a22e10", + "Name": "AppPoolPeriodicRecycleTime", + "Label": "Application pool periodic recycle time", + "HelpText": "A specific local time, in minutes after midnight, when the application pool is recycled. Separate multiple times by using a ,\ +\ +Example: \\\"00:30:00\\\" for half an hour past midnight. or \\\"06:00:00\\\" for midnight and 6 am.", + "DefaultValue": "03:00:00", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "cf466a9a-40a1-4fe9-ab2d-f99eb92d1593", + "Name": "AppPoolQueueLength", + "Label": "Queue Length", + "HelpText": "Maximum number of requests that HTTP.sys will queue for the application pool. When the queue is full, new requests receive a 504 \"Service Unavailable\" response.", + "DefaultValue": "1000", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "cbf99ca9-7282-4bb2-99b8-82c1355c4bc0", + "Name": "AppPoolStartMode", + "Label": "Start Mode", + "HelpText": "Specifies whether the application pool should run in On Demand Mode or Always Running Mode.", + "DefaultValue": "OnDemand", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "OnDemand|On Demand +AlwaysRunning|Always Running" + }, + "Links": {} + }, + { + "Id": "7ef971c4-6493-481f-9711-91cf902a2bca", + "Name": "AppPoolCpuLimit", + "Label": "CPU Limit (percent)", + "HelpText": "Configures the maximum percentage of CPU time (in percent) that the worker processes in an application pool are allowed to consume over a period of time as indicated by the resetInterval attribute. If the limit set by the limit attribute is exceeded, an event is written to the event log and an optional set of events can be triggered. These optional events are determined by the action attribute. + +The default value is 0, which disables CPU limiting.", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b5b33393-cd44-4af4-b1e2-0a090f42576e", + "Name": "AppPoolCpuLimitAction", + "Label": "CPU Limit Action", + "HelpText": "Configures the action that IIS takes when a worker process exceeds its configured CPU limit. The action attribute is configured on a per-application pool basis. + +The action attribute can be one of the following possible values. The default value is NoAction.", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "0|NoAction +1|KillW3wp +2|Throttle +3|ThrottleUnderLoad" + }, + "Links": {} + } + ], + "LastModifiedBy": "droorda", + "$Meta": { + "ExportedAt": "2018-09-18T04:42:42.009Z", + "OctopusVersion": "2018.7.14", + "Type": "ActionTemplate" + }, + "Id": "70a293d6-ee6a-4755-8e06-5f13d7e51fff", + "Category": "iis" +} diff --git a/step-templates/iis-apppool-delete.json.human b/step-templates/iis-apppool-delete.json.human new file mode 100644 index 000000000..cdf1f4ae3 --- /dev/null +++ b/step-templates/iis-apppool-delete.json.human @@ -0,0 +1,83 @@ +{ + "Id": "a7120f3e-6676-4bbb-b848-1413b03a35e8", + "Name": "IIS AppPool - Delete", + "Description": "Deletes an IIS Application Pool. +CAUTION: Delete will proceed even if applications are using the Application Pool.", + "ActionType": "Octopus.Script", + "Version": 14, + "Properties": { + "Octopus.Action.Script.ScriptBody": "## --------------------------------------------------------------------------------------\r +## Input\r +## --------------------------------------------------------------------------------------\r +\r +$appPoolName = $OctopusParameters['AppPoolName']\r +\r +## --------------------------------------------------------------------------------------\r +## Helpers\r +## --------------------------------------------------------------------------------------\r +# Helper for validating input parameters\r +function Validate-Parameter([string]$foo, [string[]]$validInput, $parameterName) {\r + IF (! $parameterName -contains \"Password\") \r + { \r + Write-Host \"${parameterName}: $foo\" \r + }\r + if (! $foo) {\r + throw \"No value was set for $parameterName, and it cannot be empty\"\r + }\r +}\r +\r +## --------------------------------------------------------------------------------------\r +## Configuration\r +## --------------------------------------------------------------------------------------\r +Validate-Parameter $appPoolName -parameterName \"Application Pool Name\"\r +\r +#Load Web Admin DLL\r +[System.Reflection.Assembly]::LoadFrom( \"C:\\windows\\system32\\inetsrv\\Microsoft.Web.Administration.dll\" )\r +\r +Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue\r +Import-Module WebAdministration -ErrorAction SilentlyContinue\r +\r +\r +## --------------------------------------------------------------------------------------\r +## Run\r +## --------------------------------------------------------------------------------------\r +\r +$iis = (New-Object Microsoft.Web.Administration.ServerManager)\r +\r +$appPool = $iis.ApplicationPools | Where {$_.Name -eq $appPoolName} | Select-Object -First 1\r +\r +IF ($appPool -eq $null)\r +{\r + Write-Output \"Could not find an Application Pool named '$appPoolName'\"\r +}\r +ELSE\r +{\r + Write-Output \"Removing Application Pool '$appPoolName'\"\r + $iis.ApplicationPools.Remove($appPool)\r + $iis.CommitChanges()\r +}\r +\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "AppPoolName", + "Label": "Application pool name", + "HelpText": "The name of the application pool that will be deleted.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2014-07-29T17:01:39.105+00:00", + "LastModifiedBy": "zagrophyte", + "$Meta": { + "ExportedAt": "2014-07-29T17:06:59.269+00:00", + "OctopusVersion": "2.5.5.318", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-apppool-restart.json.human b/step-templates/iis-apppool-restart.json.human new file mode 100644 index 000000000..3d2c3daa8 --- /dev/null +++ b/step-templates/iis-apppool-restart.json.human @@ -0,0 +1,49 @@ +{ + "Id": "de4a85ca-38cc-4a30-8244-64612e3a7921", + "Name": "IIS AppPool - Restart", + "Description": "Starts or Restarts an IIS Application Pool", + "ActionType": "Octopus.Script", + "Version": 9, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Load IIS module: +Import-Module WebAdministration + +# Get AppPool Name +$appPoolName = $OctopusParameters['appPoolName'] + +# Check if exists +if (Test-Path IIS:\\AppPools\\$appPoolName){ + # Start App Pool if stopped else restart +if ((Get-WebAppPoolState($appPoolName)).Value -eq \"Stopped\"){ + Write-Output \"Starting IIS app pool $appPoolName\" + Start-WebAppPool $appPoolName +} +else { + Write-Output \"Restarting IIS app pool $appPoolName\" + Restart-WebAppPool $appPoolName +} +} +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "AppPoolName", + "Label": "Application pool name", + "HelpText": "The name of the application pool in IIS.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-16T06:09:22.242+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-apppool-set-managed-pipeline-mode.json.human b/step-templates/iis-apppool-set-managed-pipeline-mode.json.human new file mode 100644 index 000000000..da9465899 --- /dev/null +++ b/step-templates/iis-apppool-set-managed-pipeline-mode.json.human @@ -0,0 +1,56 @@ +{ + "Id": "e87b057b-bbe0-49a4-bbed-5cfe0413667a", + "Name": "IIS AppPool - Set Managed Pipeline Mode", + "Description": "Sets an IIS Application Pool's Managed Pipeline Mode. +I.E. Classic or Integrated", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$AppPoolName = $OctopusParameters[\"AppPoolName\"]\r +$Mode = $OctopusParameters[\"PiplelineMode\"]\r +\r +Import-Module WebAdministration\r +\r +Get-ChildItem IIS:\\AppPools | ?{$_.Name -eq $AppPoolName} | Select-Object -ExpandProperty PSPath | %{ Set-ItemProperty $_ managedPipelineMode $Mode -Verbose}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "b850425c-dba9-45f9-9c00-78298cbd8e82", + "Name": "AppPoolName", + "Label": "Application Pool Name", + "HelpText": "The Application Pool Name to update.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f6ad588b-a41c-4e14-bd15-bb4d8830acc2", + "Name": "PiplelineMode", + "Label": "Managed Pipeline Mode", + "HelpText": "The Managed Pipleline mode to be set to", + "DefaultValue": "Classic", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Classic|Classic +Integrated|Integrated" + }, + "Links": {} + } + ], + "LastModifiedOn": "2017-02-14T00:00:00.386+00:00", + "LastModifiedBy": "drobison", + "$Meta": { + "ExportedAt": "2017-02-14T21:55:44.314Z", + "OctopusVersion": "3.5.1", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-apppool-start.json.human b/step-templates/iis-apppool-start.json.human new file mode 100644 index 000000000..68cae1f66 --- /dev/null +++ b/step-templates/iis-apppool-start.json.human @@ -0,0 +1,41 @@ +{ + "Id": "9db77671-0fe3-4aef-a014-551bf1e5e7ab", + "Name": "IIS AppPool - Start", + "Description": "Starts an IIS Application Pool", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Load IIS module: +Import-Module WebAdministration + +# Get AppPool Name +$appPoolName = $OctopusParameters['appPoolName'] + +Write-Output \"Starting IIS app pool $appPoolName\" +Start-WebAppPool $appPoolName + + +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "AppPoolName", + "Label": "Application pool name", + "HelpText": "The name of the application pool in IIS.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-16T06:09:53.938+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-apppool-stop.json.human b/step-templates/iis-apppool-stop.json.human new file mode 100644 index 000000000..406dc72b5 --- /dev/null +++ b/step-templates/iis-apppool-stop.json.human @@ -0,0 +1,88 @@ +{ + "Id": "3aaf34a5-90eb-4ea1-95db-15ec93c1e54d", + "Name": "IIS AppPool - Stop", + "Description": "Stops an IIS Application Pool", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Load IIS module: +Import-Module WebAdministration + +# Get AppPool Name +$appPoolName = $OctopusParameters['appPoolName'] +# Get the number of retries +$retries = $OctopusParameters['appPoolCheckRetries'] +# Get the number of attempts +$delay = $OctopusParameters['appPoolCheckDelay'] + +# Check if exists +if(Test-Path IIS:\\AppPools\\$appPoolName) { + + # Stop App Pool if not already stopped + if ((Get-WebAppPoolState $appPoolName).Value -ne \"Stopped\") { + Write-Output \"Stopping IIS app pool $appPoolName\" + Stop-WebAppPool $appPoolName + + $state = (Get-WebAppPoolState $appPoolName).Value + $counter = 1 + + # Wait for the app pool to the \"Stopped\" before proceeding + do{ + $state = (Get-WebAppPoolState $appPoolName).Value + Write-Output \"$counter/$retries Waiting for IIS app pool $appPoolName to shut down completely. Current status: $state\" + $counter++ + Start-Sleep -Milliseconds $delay + } + while($state -ne \"Stopped\" -and $counter -le $retries) + + # Throw an error if the app pool is not stopped + if($counter -gt $retries) { + throw \"Could not shut down IIS app pool $appPoolName. `nTry to increase the number of retries ($retries) or delay between attempts ($delay milliseconds).\" } + } + else { + Write-Output \"$appPoolName already Stopped\" + } +} +else { + Write-Output \"IIS app pool $appPoolName doesn't exist\" +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "Parameters": [ + { + "Name": "AppPoolName", + "Label": "Application pool name", + "HelpText": "The name of the application pool in IIS.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AppPoolCheckDelay", + "Label": "Status check interval", + "HelpText": "The delay, in milliseconds, between each attempt to query the application pool to see if its status is \"Stopped\"", + "DefaultValue": "500", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AppPoolCheckRetries", + "Label": "Status check retries", + "HelpText": "The number of retries before an error is thrown.", + "DefaultValue": "20", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2017-04-10T18:41:25+00:00", + "OctopusVersion": "3.12.1", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-apppool-update-property.json.human b/step-templates/iis-apppool-update-property.json.human new file mode 100644 index 000000000..e62e8d4de --- /dev/null +++ b/step-templates/iis-apppool-update-property.json.human @@ -0,0 +1,122 @@ +{ + "Id": "183c1676-cb8e-44e8-a348-bbcb2b77536e", + "Name": "IIS AppPool - Update Property", + "Description": "Updates property for specified AppPool, useful for example to change startMode to AlwaysRunning.", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +param( + [string]$appPoolName, + [string]$propertyName, + [object]$propertyValue, + [switch]$whatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if ($result -eq $null -or $result -eq \"\") { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +& { + param( + [string]$appPoolName, + [string]$propertyName, + [object]$propertyValue + ) + + Write-Host \"Setting $appPoolName property $propertyName to $propertyValue\" + + try { + Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue + Import-Module WebAdministration -ErrorAction SilentlyContinue + + $oldValue = Get-ItemProperty \"IIS:\\AppPools\\$appPoolName\" -Name $propertyName + $oldValueString = \"\" + + if ($oldValue.GetType() -eq [Microsoft.IIs.PowerShell.Framework.ConfigurationAttribute]) + { + $oldValueString = ($oldValue | Select-Object -ExpandProperty \"Value\"); + $convertedValue = $propertyValue -as $oldValueString.GetType(); + } + else + { + $oldValueString = $oldValue; + $convertedValue = $propertyValue -as $oldValue.GetType(); + } + + Write-Host \"Old value $oldValueString\" + Set-ItemProperty \"IIS:\\AppPools\\$appPoolName\" -Name $propertyName -Value $convertedValue + Write-Host \"New value $propertyValue\" + Write-Host \"Done\" + } catch { + Write-Host $_.Exception|format-list -force + Write-Host \"There was a problem setting property\" + } + + } ` + (Get-Param 'appPoolName' -Required) (Get-Param 'propertyName' -Required) (Get-Param 'propertyValue' -Required) +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "appPoolName", + "Label": "Application pool name", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "propertyName", + "Label": "Name of the property to set", + "HelpText": "", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "propertyValue", + "Label": "Value of the property to set", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2019-09-05T00:59:58.662+00:00", + "LastModifiedBy": "olsh", + "$Meta": { + "ExportedAt": "2015-10-23T02:15:10.732+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-apppool-update-recycle-settings.json.human b/step-templates/iis-apppool-update-recycle-settings.json.human new file mode 100644 index 000000000..460c17c5e --- /dev/null +++ b/step-templates/iis-apppool-update-recycle-settings.json.human @@ -0,0 +1,211 @@ +{ + "Id": "5d771fd0-710c-41a7-9969-10bc75d00307", + "Name": "IIS AppPool - Update Recycle Settings", + "Description": "Update the worker process and app pool timeout/recycle times.", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Import-Module WebAdministration + +function Update-IISAppPool-PeriodicRestart($appPool, $periodicRestart) { + Write-Output \"Setting worker process periodic restart time to $periodicRestart for AppPool $appPoolName.\" + $appPool.Recycling.PeriodicRestart.Time = [TimeSpan]::FromMinutes($periodicRestart) + $appPool | Set-Item +} + +function Update-IISAppPool-IdleTimeout($appPool, $appPoolName, $idleTimeout) { + Write-Output \"Setting worker process idle timeout to $idleTimeout for AppPool $appPoolName.\" + $appPool.ProcessModel.IdleTimeout = [TimeSpan]::FromMinutes($idleTimeout) + $appPool | Set-Item +} + +function Update-IISAppPool-ScheduledTimes($appPool, $appPoolName, $schedule) { + $minutes = $periodicRecycleTimes.Split(\",\") + $minuteArrayList = New-Object System.Collections.ArrayList + + foreach ($minute in $minutes) { + $minute = $minute.trim() + + if ($minute -eq \"-1\") { + break + } + if ($minute -lt 0) { + continue + } + + $temp = $minuteArrayList.Add([TimeSpan]::FromMinutes($minute)) + } + + Write-Output \"Setting worker process scheduled restart times to $minuteArrayList for AppPool $appPoolName.\" + + $settingName = \"recycling.periodicRestart.schedule\" + Clear-ItemProperty $appPool.PSPath -Name $settingName + + $doneOne = $false + foreach ($minute in $minuteArrayList) { + if ($doneOne -eq $false) { + Set-ItemProperty $appPool.PSPath -Name $settingName -Value @{value=$minute} + $doneOne = $true + } + else { + New-ItemProperty $appPool.PSPath -Name $settingName -Value @{value=$minute} + } + } +} + +function Update-IISAppPool-RecycleEventsToLog($appPool, $appPoolName, $events) { + $settingName = \"Recycling.logEventOnRecycle\" + Write-Output \"Setting $settingName for AppPool $appPoolName to $events.\" + + Clear-ItemProperty $appPool.PSPath -Name $settingName + if ($events -ne \"-\") { + Set-ItemProperty $appPool.PSPath -Name $settingName -Value $events + } +} + +function Run { + $OctopusParameters = $OctopusParameters + if ($OctopusParameters -eq $null) { + write-host \"Using test values\" + $OctopusParameters = New-Object \"System.Collections.Hashtable\" + $OctopusParameters[\"ApplicationPoolName\"]=\"DefaultAppPool\" + $OctopusParameters[\"IdleTimeoutMinutes\"]=\"\" + $OctopusParameters[\"RegularTimeIntervalMinutes\"]=\"10\" + $OctopusParameters[\"PeriodicRecycleTime\"]=\"14,15,16\" + $OctopusParameters[\"RecycleEventsToLog\"]=\"Time, Requests, Schedule, Memory, IsapiUnhealthy, OnDemand, ConfigChange, PrivateMemory\" + $OctopusParameters[\"EmptyClearsValue\"]=$true + } + + $applicationPoolName = $OctopusParameters[\"ApplicationPoolName\"] + $idleTimeout = $OctopusParameters[\"IdleTimeoutMinutes\"] + $periodicRestart = $OctopusParameters[\"RegularTimeIntervalMinutes\"] + $periodicRecycleTimes = $OctopusParameters[\"PeriodicRecycleTime\"] + $recycleEventsToLog = $OctopusParameters[\"RecycleEventsToLog\"] + $emptyClearsValue = $OctopusParameters[\"EmptyClearsValue\"] + + if ([string]::IsNullOrEmpty($applicationPoolName)) { + throw \"Application pool name is required.\" + } + + $appPool = Get-Item IIS:\\AppPools\\$applicationPoolName + + if ($emptyClearsValue -eq $true) { + Write-Output \"Empty values will reset to default\" + if ([string]::IsNullOrEmpty($idleTimeout)) { + $idleTimeout = \"0\" + } + if ([string]::IsNullOrEmpty($periodicRestart)) { + $periodicRestart = \"0\" + } + if ([string]::IsNullOrEmpty($periodicRecycleTimes)) { + $periodicRecycleTimes = \"-1\" + } + if ([string]::IsNullOrEmpty($recycleEventsToLog)) { + $recycleEventsToLog = \"-\" + } + } + + if (![string]::IsNullOrEmpty($periodicRestart)) { + Update-IISAppPool-PeriodicRestart -appPool $appPool -appPoolName $appPool.Name -PeriodicRestart $periodicRestart + } + if (![string]::IsNullOrEmpty($idleTimeout)) { + Update-IISAppPool-IdleTimeout -appPool $appPool -appPoolName $appPool.Name -idleTimeout $idleTimeout + } + if (![string]::IsNullOrEmpty($periodicRecycleTimes)) { + Update-IISAppPool-ScheduledTimes -appPool $appPool -appPoolName $appPool.Name -Schedule $periodicRecycleTimes + } + if(![string]::IsNullOrEmpty($recycleEventsToLog)){ + Update-IISAppPool-RecycleEventsToLog -appPool $appPool -appPoolName $appPool.Name -Events $recycleEventsToLog + } +} + +Run +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "0fd90579-79b4-4086-8c7c-602cf21f9f10", + "Name": "ApplicationPoolName", + "Label": "Application pool", + "HelpText": "The name of the application pool to modify. The application pool must already exist.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "4c1c4df5-4282-43f4-9119-e514016b4230", + "Name": "IdleTimeoutMinutes", + "Label": "Process idle timeout", + "HelpText": "Amount of time (in minutes) a worker process will remain idle before it shuts down. A value of 0 means the process does not shut down after an idle timeout.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "516e588b-b9e3-43cc-8342-7f50c97e138b", + "Name": "RegularTimeIntervalMinutes", + "Label": "Application pool recycle time interval", + "HelpText": "Period of time (in minutes) after which the application pool will recycle. A value of 0 means the application pool does not recycle on a regular interval.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a8121270-e728-4fe5-bf1b-f75cb5075516", + "Name": "PeriodicRecycleTime", + "Label": "Application pool periodic recycle time", + "HelpText": "A specific local time, in minutes after midnight, when the application pool is recycled. Seperate multiple times by using a , + +Example: \"30\" for half an hour past midnight. or \"0, 360\" for midnight and 6 am.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "fd4fe130-1bed-41ee-a1b5-73a080cc3ca5", + "Name": "RecycleEventsToLog", + "Label": "Recycle Events To Log", + "HelpText": "Event Log entries can be generated when an application pool is recycled. Select the Recycling events to log. The Options are **Time**, **Requests**, **Schedule**, **Memory**, **IsapiUnhealthy**, **OnDemand**, **ConfigChange**, **PrivateMemory**. These should be entered in a comma separated list. + +Example: \"OnDemand,ConfigChange\"", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "bb164dea-1843-4655-a0e2-b29f990b44b7", + "Name": "EmptyClearsValue", + "Label": "Empty values reset", + "HelpText": "When checked, if the values are not set it will remove the setting from IIS.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2017-07-05T23:25:56.598Z", + "OctopusVersion": "3.14.1", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-autostartprovider-add.json.human b/step-templates/iis-autostartprovider-add.json.human new file mode 100644 index 000000000..172d8018b --- /dev/null +++ b/step-templates/iis-autostartprovider-add.json.human @@ -0,0 +1,114 @@ +{ + "Id": "20b8c2fd-9194-47e3-948e-badc6c2a30d5", + "Name": "IIS AutoStartProvider - Add", + "Description": "Add autostartprovider entry so type can be used to warm up applicaiton. Final changes in applicationHost.config look like that: + + + + + +where name and type are taken from parameters.", + "ActionType": "Octopus.Script", + "Version": 0, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\r +param(\r + [string]$serviceName,\r + [string]$serviceType,\r + [switch]$whatIf\r + ) \r +\r +$ErrorActionPreference = \"Stop\" \r +\r +function Get-Param($Name, [switch]$Required, $Default) {\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + $variable = Get-Variable $Name -EA SilentlyContinue \r + if ($variable -ne $null) {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null -or $result -eq \"\") {\r + if ($Required) {\r + throw \"Missing parameter value $Name\"\r + } else {\r + $result = $Default\r + }\r + }\r +\r + return $result\r +}\r +\r +& {\r + param(\r + [string]$serviceName,\r + [string]$serviceType\r + ) \r +\r + Write-Host \"Setting $serviceName, $serviceType service autostart provider\"\r +\r + try {\r + Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue\r + Import-Module WebAdministration -ErrorAction SilentlyContinue\r +\r + $oldValue = Get-WebConfiguration -filter /system.applicationHost/serviceAutoStartProviders/add | \r + Where-Object { $_.Name -eq $serviceName }\r +\r + if ($oldValue -eq $null) {\r + Write-Host \"Adding new service type provider $serviceName, $serviceType\"\r + Add-WebConfiguration -filter /system.applicationHost/serviceAutoStartProviders -Value @{name=$serviceName; type=$serviceType}\r + } \r + elseif ($oldValue.Type -eq $serviceType) { \r + Write-Host \"Service provider with the same name and type exists\"\r + } else {\r + $oldValueType = $oldValue.Type\r + Write-Host \"Replacing service type from $oldValueType to $serviceType\"\r + Set-WebConfiguration -filter \"/system.applicationHost/serviceAutoStartProviders/add[@name='$serviceName']\" -Value @{\"type\" = \"$serviceType\"}\r + }\r +\r + Write-Host \"Done\"\r + } catch {\r + Write-Host $_.Exception|format-list -force\r + Write-Host \"There was a problem setting property\" \r + }\r +} `\r +(Get-Param 'serviceName' -Required) (Get-Param 'serviceType' -Required) \r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "serviceName", + "Label": "Name of autostart service", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "serviceType", + "Label": "Type which implements IProcessHostPreloadClient", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-10-23T01:06:20.789+00:00", + "LastModifiedBy": "jmalczak", + "$Meta": { + "ExportedAt": "2015-10-23T02:23:21.661+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-backup-iis-configuration.json.human b/step-templates/iis-backup-iis-configuration.json.human new file mode 100644 index 000000000..242298f1b --- /dev/null +++ b/step-templates/iis-backup-iis-configuration.json.human @@ -0,0 +1,44 @@ +{ + "Id": "e1006e45-bbde-42e3-b6b9-16d804772684", + "Name": "IIS - Backup IIS Configuration", + "Description": "This backs up the entire IIS configuration for the server (does not include website files). Note that all prior configurations on that IIS server will be cleared out.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "<#\r +This script will take the existing configuration (does not include website files) and back it up on the IIS Server, which can then be later restored if needed.\r +\r +To view existing backups for restore operation, find the latest backup here:\r + $env:Windir\\System32\\inetsrv\\backup\r +\r +To restore, use the following commands:\r + Restore-WebConfiguration -Name \"IISConfigBackup\"\r + iisreset\r +\r +Reference Articles:\r +https://technet.microsoft.com/en-us/library/hh867851(v=wps.630).aspx\r +https://technet.microsoft.com/en-us/library/hh867862(v=wps.630).aspx\r +#>\r +\r +# clear all backed up configurations first\r +Remove-WebConfigurationBackup\r +\r +# perform backup\r +Backup-WebConfiguration -Name \"IISConfigBackup\"\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [], + "LastModifiedBy": "jjaffery", + "$Meta": { + "ExportedAt": "2016-11-23T21:07:51.416Z", + "OctopusVersion": "3.4.15", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-bind-ssl-certificate-with-sni-enabled.json.human b/step-templates/iis-bind-ssl-certificate-with-sni-enabled.json.human new file mode 100644 index 000000000..c93b6d757 --- /dev/null +++ b/step-templates/iis-bind-ssl-certificate-with-sni-enabled.json.human @@ -0,0 +1,55 @@ +{ + "Id": "4827a205-ff46-4109-a0aa-7c59a8688f7e", + "Name": "IIS - Bind SSL Certificate with SNI Enabled", + "Description": "Applies a https binding, with SNI enabled.", + "ActionType": "Octopus.Script", + "Version": 10, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$WebsiteName = $OctopusParameters['WebsiteName']\r +$SSLBindingHost = $OctopusParameters['SSLBindingHost']\r +$CertificateThumbPrint = $OctopusParameters['CertificateThumbPrint']\r +\r +new-webbinding -Name $WebsiteName -Protocol \"https\" -Port 443 -HostHeader $SSLBindingHost -SslFlags 1\r +netsh http add sslcert hostnameport=$($SSLBindingHost):443 certhash=$CertificateThumbPrint appid='{58ee6009-4e61-400b-80cf-dedc242faf63}' certstorename=MY\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "WebsiteName", + "Label": "Website Name", + "HelpText": "Name of the web site in IIS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SSLBindingHost", + "Label": "SSL Binding Host", + "HelpText": "The host name to bind (www.pancreaticcancer.org.uk)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "CertificateThumbPrint", + "Label": "Certificate Thumbprint", + "HelpText": "The SSL Thumbprint for the certificate. Do not include spaces", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-10-23T14:40:38.823+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-bindings-backup.json.human b/step-templates/iis-bindings-backup.json.human new file mode 100644 index 000000000..88bea64fa --- /dev/null +++ b/step-templates/iis-bindings-backup.json.human @@ -0,0 +1,116 @@ +{ + "Id": "0225e445-fe89-4cad-b495-4e26ba0b3ed0", + "Name": "IIS Bindings - Backup", + "Description": "Backs up IIS bindings for a given site so they can be restored in later steps. This is very useful if we we have any existing bindings on IIS server since Octopus wipes all existing bindings when create new ones.", + "ActionType": "Octopus.Script", + "Version": 13, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\r +param(\r + [string]$webSiteName,\r + [string]$backupFolder,\r + [switch]$whatIf\r +) \r +\r +$ErrorActionPreference = \"Stop\" \r +\r +function Get-Param($Name, [switch]$Required, $Default) {\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + $variable = Get-Variable $Name -EA SilentlyContinue \r + if ($variable -ne $null) {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null -or $result -eq \"\") {\r + if ($Required) {\r + throw \"Missing parameter value $Name\"\r + } else {\r + $result = $Default\r + }\r + }\r +\r + return $result\r +}\r +\r +function Get-File-Name($backupFolder, $webSiteName) {\r + $folder = Join-Path -Path $backupFolder -ChildPath $webSiteName\r +\r + if((Test-Path $folder) -eq $false) {\r + mkdir $folder | Out-Null\r + }\r +\r + $fullPath = $null;\r +\r + if($OctopusParameters -eq $null) {\r + $fullPath = Join-Path -Path $folder -ChildPath \"site_backup.xml\"\r + } else {\r + $fileName = $OctopusParameters[\"Octopus.Release.Number\"] + \"_\" + $OctopusParameters[\"Octopus.Environment.Name\"] + \".xml\"\r + $fullPath = Join-Path -Path $folder -ChildPath $fileName\r + }\r +\r + return $fullPath\r +}\r +\r +& {\r + param(\r + [string]$webSiteName,\r + [string]$backupFolder\r + ) \r +\r + Write-Host \"Save $webSiteName bindings to bindings variable\"\r +\r + try {\r + Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue\r + Import-Module WebAdministration -ErrorAction SilentlyContinue\r +\r + $currentBindings = Get-WebBinding -Name $webSiteName\r + $bindingsBackupFile = Get-File-Name $backupFolder $webSiteName\r + $currentBindings | Export-CliXML $bindingsBackupFile\r +\r + Write-Host \"Done\"\r + } catch {\r + Write-Host $_.Exception|format-list -force\r + Write-Host \"There was a problem saving bindings\" \r + }\r +\r + } `\r + (Get-Param 'webSiteName' -Required) (Get-Param 'backupFolder' -Required)\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "webSiteName", + "Label": "Web Site Name", + "HelpText": "Name of the web site for which we should backup bindings", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "backupFolder", + "Label": "Folder where backed up binding file will be stored", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "jmalczak", + "$Meta": { + "ExportedAt": "2016-01-04T14:42:14.131+00:00", + "OctopusVersion": "3.2.13", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-bindings-enable-ssl-ccs.json.human b/step-templates/iis-bindings-enable-ssl-ccs.json.human new file mode 100644 index 000000000..0600c0466 --- /dev/null +++ b/step-templates/iis-bindings-enable-ssl-ccs.json.human @@ -0,0 +1,95 @@ +{ + "Name": "IIS - Add CCS HTTPS bindings based on HTTP bindings", + "Description": "Add IIS Centralized Certificate Store (CCS) SSL bindings to a website based on HTTP bindings (binds to same IP and hostname but different port and protocol). + +This is used to enable CCS on IIS websites created with the normal Octopus IIS website creation step.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$WebSiteName = $OctopusParameters['IisCcsWebSiteName']; +[bool]$SNI = $false; +if (-not [bool]::TryParse($OctopusParameters['IisCcsSNI'], [ref]$SNI)) { + $SNI = $true; +} +$SslFlags = 2; # Use CCS +if ($SNI) { +\t$SslFlags = 3; # Use SNI CCS +} +$PortMap = [System.Collections.Generic.Dictionary[int,int]]::new(); +foreach ($mapping in [regex]::Matches($OctopusParameters['IisCcsPortMap'],\"([0-9]+):([0-9]+)\")) { + $PortMap.Add([int]$mapping.Groups[1].Value, [int]$mapping.Groups[2].Value); +} +$httpBindings = Get-WebBinding -Name $WebSiteName -Protocol http | Foreach-Object { $_.bindingInformation }; +if (-not $httpBindings) { + Write-Error \"The site $WebSiteName does not exist, or it has not HTTP binding\" +} +foreach ($binding in (Get-WebBinding -Name $WebSiteName -Protocol http | Foreach-Object { $_.bindingInformation })) { + $parts = $binding.Split(\":\"); + $IPAddress = $parts[0]; + $HostHeader = $parts[2]; + [int]$Port = 0; + if ([string]::IsNullOrEmpty($HostHeader)) { + Write-Warning \"The binding $binding has no hostname, skipping\"; + } elseif (-not $PortMap.TryGetValue([int]$parts[1], [ref]$Port)) { + Write-Warning \"There is no port mapping for the binding $binding, skipping\"; + } else { + Write-Verbose \"Binding HTTP $binding to HTTPS $($IPAddress):$($Port):$($HostHeader)\"; + $existingBinding = Get-WebBinding -Name $WebSiteName -Protocol https -IPAddress $IPAddress -Port $Port -HostHeader $HostHeader; + if ($existingBinding) { + if ($existingBinding.sslFlags -ne $SslFlags) { + Write-Host \"Change SSL flags of binding $($IPAddress):$($Port):$($HostHeader)\"; + Set-WebBinding -Name $WebSiteName -IPAddress $IPAddress -Port $Port -HostHeader $HostHeader -PropertyName SslFlags -Value $SslFlags; + } + } else { + Write-Host \"Create HTTPS binding $($IPAddress):$($Port):$($HostHeader)\"; + New-WebBinding -Name $WebSiteName -Protocol https -IPAddress $IPAddress -Port $Port -HostHeader $HostHeader -SslFlags $SslFlags; + } + } +}" + }, + "Parameters": [ + { + "Id": "e17b143f-d26c-458b-be41-6afca9e2666f", + "Name": "IisCcsWebSiteName", + "Label": "Website Name", + "HelpText": "Name of the website, can be retrieved from a IIS website deployment step: `#{Octopus.Action[*].IISWebSite.WebSiteName}`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "de1633dd-5dcd-4d8f-8c69-d031a6b32434", + "Name": "IisCcsSNI", + "Label": "Enable SNI", + "HelpText": "Enable SNI for the binding(s), default is enabled.", + "DefaultValue": "true", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "e6faf04c-441b-4ef2-8ad6-2ce0b4dc93df", + "Name": "IisCcsPortMap", + "Label": "Port mapping", + "HelpText": "Mapping of HTTP to HTTPS ports as list of integer parts. Default is `80:443` (multiple mappings may be added, separated by spaces).", + "DefaultValue": "80:443", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2019-09-14T22:53:52.793Z", + "OctopusVersion": "2019.7.1", + "Type": "ActionTemplate" + }, + "Id": "0f21bd9a-2dea-4432-a1a1-2dee302ca418", + "Category": "iis" +} diff --git a/step-templates/iis-bindings-restore.json.human b/step-templates/iis-bindings-restore.json.human new file mode 100644 index 000000000..557598a0c --- /dev/null +++ b/step-templates/iis-bindings-restore.json.human @@ -0,0 +1,135 @@ +{ + "Id": "9d357886-1896-4240-a89f-118c7ca8ec05", + + "Name": "IIS Bindings - Restore", + "Description": "This step will restore IIS bindings from backup taken in IIS Bindings - Backup step. Only bindings which doesn't exist will be restored.", + "ActionType": "Octopus.Script", + "Version": 10, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\r +param(\r + [string]$webSiteName,\r + [string]$backupFolder,\r + [switch]$whatIf\r +) \r +\r +$ErrorActionPreference = \"Stop\" \r +\r +function Get-Param($Name, [switch]$Required, $Default) {\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + $variable = Get-Variable $Name -EA SilentlyContinue \r + if ($variable -ne $null) {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null -or $result -eq \"\") {\r + if ($Required) {\r + throw \"Missing parameter value $Name\"\r + } else {\r + $result = $Default\r + }\r + }\r +\r + return $result\r +}\r +\r +function Get-File-Name($backupFolder, $webSiteName) {\r + $folder = Join-Path -Path $backupFolder -ChildPath $webSiteName\r +\r + if((Test-Path $folder) -eq $false) {\r + mkdir $folder | Out-Null\r + }\r +\r + $fullPath = $null;\r +\r + if($OctopusParameters -eq $null) {\r + $fullPath = Join-Path -Path $folder -ChildPath \"site_backup.xml\"\r + } else {\r + $fileName = $OctopusParameters[\"Octopus.Release.Number\"] + \"_\" + $OctopusParameters[\"Octopus.Environment.Name\"] + \".xml\"\r + $fullPath = Join-Path -Path $folder -ChildPath $fileName\r + }\r +\r + return $fullPath\r +}\r +\r +& {\r + param(\r + [string]$webSiteName,\r + [string]$backupFolder\r + ) \r +\r + Write-Host \"Restore $webSiteName bindings from bindings variable\"\r +\r + try {\r + Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue\r + Import-Module WebAdministration -ErrorAction SilentlyContinue\r + \r + $bindingsBackupFile = Get-File-Name $backupFolder $webSiteName\r + $currentBindings = Import-CliXML $bindingsBackupFile \r +\r + if($currentBindings -eq $null) {\r + Write-Host \"There is no saved bindings, you have to run IIS save bindings before\"\r + } else {\r + foreach($binding in $currentBindings) {\r + $bindingArray = $binding.bindingInformation.Split(\":\")\r + $existing = Get-WebBinding -Name $webSiteName -Protocol $binding.protocol | Where-Object { $_.bindingInformation -eq $binding.bindingInformation }\r +\r + if($existing -eq $null) {\r + Write-Host \"Adding binding\" $binding.protocol $binding.bindingInformation \r + New-WebBinding -Name $webSiteName -Protocol $binding.protocol -IPAddress $bindingArray[0] -Port $bindingArray[1] -HostHeader $bindingArray[2] -SslFlags $binding.sslFlags\r + if ($binding.protocol -eq \"https\" -and $binding.certificateHash) {\r + $newBinding = Get-WebBinding -Name $webSiteName -Protocol $binding.protocol -IPAddress $bindingArray[0] -Port $bindingArray[1] -HostHeader $bindingArray[2]\r + Write-Host \"Assigning certificate\" $binding.certificateHash \"to binding\"\r + $newBinding.AddSslCertificate($binding.certificateHash, $binding.certificateStoreName)\r + }\r + }\r + }\r + }\r +\r + Write-Host \"Done\"\r + } catch {\r + Write-Host $_.Exception|format-list -force\r + Write-Host \"There was a problem restoring bindings\" \r + }\r +\r + } `\r + (Get-Param 'webSiteName' -Required) (Get-Param 'backupFolder' -Required)\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "webSiteName", + "Label": "Web Site Name", + "HelpText": "Name of the web site for which bindings will be restored from backup taken in previous IIS Bindings - Backup task. Only bindings which doesn't already exist will be restored.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "backupFolder", + "Label": "Folder where backed up binding file is stored", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "krusoleo", + "$Meta": { + "ExportedAt": "2023-01-30T13:19:56.331Z", + "OctopusVersion": "2019.6.7", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-change-app-offline-online.json.human b/step-templates/iis-change-app-offline-online.json.human new file mode 100644 index 000000000..f7de7e153 --- /dev/null +++ b/step-templates/iis-change-app-offline-online.json.human @@ -0,0 +1,102 @@ +{ + "Id": "78169a17-54da-4fb8-afe7-774a1e857112", + "Name": "IIS - Change App Offline", + "Description": "Change the an app_offline file to app_online vice versa to turn the maintenance page on and off.", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$offlineHtml = Join-Path $OctopusParameters[\"InstallationFolder\"] $OctopusParameters[\"AppOfflineFileName\"] +$onlineHtml = Join-Path $OctopusParameters[\"InstallationFolder\"] $OctopusParameters[\"AppOnlineFileName\"] + +#If neither file exists, throw a fit +if ($OctopusParameters[\"ChangeAppOffline.CheckForFile\"] -eq $True -and !(Test-Path($offlineHtml)) -and !(Test-Path($onlineHtml))) +{ +\tWrite-Error \"Missing both online and offline files!\" + return +} + + +if (\"#{ChangeMode}\" -eq \"Online\") +{ + #Offline exists and so does online - remove offline + if ((Test-Path($offlineHtml)) -and (Test-Path($onlineHtml))) + { + Remove-Item $offlineHtml + } + + #Offline exists and online doesn't - move offline to online + if ((Test-Path($offlineHtml)) -and !(Test-Path($onlineHtml))) + { + Move-Item $offlineHtml $onlineHtml + } +} + +if (\"#{ChangeMode}\" -eq \"Offline\") +{ + #Online exists and offline doesn't - move online to offline + if ((Test-Path($onlineHtml)) -and !(Test-Path($offlineHtml))) + { + Move-Item $onlineHtml $offlineHtml + } +}" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "AppOfflineFileName", + "Label": null, + "HelpText": null, + "DefaultValue": "app_offline.htm", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AppOnlineFileName", + "Label": null, + "HelpText": null, + "DefaultValue": "app_online.htm", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "InstallationFolder", + "Label": null, + "HelpText": "Where the application is installed on tentacle server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ChangeMode", + "Label": null, + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Offline|Offline +Online|Online" + } + }, + { + "Name": "ChangeAppOffline.CheckForFile", + "Label": "", + "HelpText": "Check that at least one of the files exists first", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2021-12-03T00:14:51.509+00:00", + "LastModifiedBy": "kseinyar-stcu", + "$Meta": { + "ExportedAt": "2015-12-09T16:23:35.320+00:00", + "OctopusVersion": "3.2.8", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-configure-compression.json.human b/step-templates/iis-configure-compression.json.human new file mode 100644 index 000000000..7017af4c1 --- /dev/null +++ b/step-templates/iis-configure-compression.json.human @@ -0,0 +1,82 @@ +{ + "Id": "66dc7184-8736-4ab8-be65-51cb3933eaef", + "Name": "IIS - Configure Compression", + "Description": "Configures the MIME types used in static and dynamic compression e.g. The application/json MIME type can be configured to be served using GZIP compression.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$psPath = 'MACHINE/WEBROOT/APPHOST'; + +if ($StaticMimeTypes) +{ + $filter = \"system.webServer/httpCompression/staticTypes\"; + + $existingStaticMimeTypes = (Get-WebConfigurationProperty -pspath $psPath -filter $filter -name \".\").Collection; + foreach ($staticMimeType in $StaticMimeTypes.split(\",\")) + { + if ($staticMimeType) + { + if (($existingStaticMimeTypes | ? { $_.mimeType -eq $staticMimeType }).Count -ne 0) + { + Remove-WebConfigurationProperty -pspath $psPath -filter $filter -name \".\" -AtElement @{mimeType=$staticMimeType}; + Write-Output \"Static MIME type $staticMimeType removed.\"; + } + + Add-WebConfigurationProperty -pspath $psPath -filter $filter -name \".\" -value @{mimeType=$staticMimeType;enabled='True'}; + Write-Output \"Static MIME type $staticMimeType added.\"; + } + } +} + +if ($DynamicMimeTypes) +{ + $filter = \"system.webServer/httpCompression/dynamicTypes\"; + + $existingDynamicMimeTypes = (Get-WebConfigurationProperty -pspath $psPath -filter $filter -name \".\").Collection; + foreach ($dynamicMimeType in $DynamicMimeTypes.split(\",\")) + { + if ($dynamicMimeType) + { + if (($existingDynamicMimeTypes | ? { $_.mimeType -eq $dynamicMimeType }).Count -ne 0) + { + Remove-WebConfigurationProperty -pspath $psPath -filter $filter -name \".\" -AtElement @{mimeType=$dynamicMimeType}; + Write-Output \"Dynamic MIME type $dynamicMimeType removed.\"; + } + + Add-WebConfigurationProperty -pspath $psPath -filter $filter -name \".\" -value @{mimeType=$dynamicMimeType;enabled='True'}; + Write-Output \"Dynamic MIME type $dynamicMimeType added.\"; + } + } +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "StaticMimeTypes", + "Label": "Static Compression MIME Types", + "HelpText": "The MIME types to be added for static compression separated by commas. Example: _application/json,application/xml_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DynamicMimeTypes", + "Label": "Dynamic Compression MIME Types", + "HelpText": "The MIME types to be added for dynamic compression separated by commas. Example: _application/json,application/xml_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2016-01-22T10:27:42.848+00:00", + "OctopusVersion": "3.2.19", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-list-sites.json.human b/step-templates/iis-list-sites.json.human new file mode 100644 index 000000000..2b21df833 --- /dev/null +++ b/step-templates/iis-list-sites.json.human @@ -0,0 +1,48 @@ +{ + "Id": "05e87da8-a9df-449f-bdbb-98fa44740b46", + "Name": "IIS - List Sites", + "Description": "List sites on the server.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "try {\r +\t$iisFeature = Get-WindowsFeature Web-WebServer -ErrorAction Stop\r +\tif ($iisFeature -eq $null -or $iisFeature.Installed -eq $false) {\r +\t\tWrite-Error \"It looks like IIS is not installed on this server and the deployment is likely to fail.\"\r +\t\tWrite-Error \"Tip: You can use PowerShell to ensure IIS is installed: 'Install-WindowsFeature Web-WebServer'\"\r +\t\tWrite-Error \" You are likely to want more IIS features than just the web server. Run 'Get-WindowsFeature *web*' to see all of the features you can install.\"\r +\t\texit 1\r +\t}\r +} catch {\r +\tWrite-Verbose \"Call to `Get-WindowsFeature Web-WebServer` failed.\"\r +\tWrite-Verbose \"Unable to determine if IIS is installed on this server but will optimistically continue.\"\r +}\r +\r +try {\r +\tAdd-PSSnapin WebAdministration -ErrorAction Stop\r +} catch {\r + try {\r +\t\t Import-Module WebAdministration -ErrorAction Stop\r +\t\t} catch {\r +\t\t\tWrite-Warning \"We failed to load the WebAdministration module. This usually resolved by doing one of the following:\"\r +\t\t\tWrite-Warning \"1. Install .NET Framework 3.5.1\"\r +\t\t\tWrite-Warning \"2. Upgrade to PowerShell 3.0 (or greater)\"\r +\t\t\tWrite-Warning \"3. On Windows 2008 you might need to install PowerShell SnapIn for IIS from http://www.iis.net/downloads/microsoft/powershell#additionalDownloads\"\r +\t\t\tthrow ($error | Select-Object -First 1)\r + }\r +}\r +\r +Get-WebSite", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [], + "LastModifiedOn": "2016-12-19T23:20:00.000+00:00", + "LastModifiedBy": "BrettJaner", + "$Meta": { + "ExportedAt": "2016-12-19T23:20:00.000+00:00", + "OctopusVersion": "2.4.7.85", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-redirect-to-https.json.human b/step-templates/iis-redirect-to-https.json.human new file mode 100644 index 000000000..04a05568f --- /dev/null +++ b/step-templates/iis-redirect-to-https.json.human @@ -0,0 +1,84 @@ +{ + "Id": "929f72d3-e1c3-4b97-bac9-d84434ca63c3", + "Name": "IIS Website - HTTP to HTTPS Redirect", + "Description": "Adds either a global or site specific rule which redirects from http to https", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$Name = \"HTTP to HTTPS Redirect (Octopus Deploy)\"\r +$PsPath = \"MACHINE/WEBROOT/APPHOST\"\r +$Filter = \"system.webserver/rewrite/GlobalRules\"\r +\r +Clear-WebConfiguration -pspath $PsPath -filter \"$Filter/rule[@name='$Name']\"\r +if ($Site) {\r + $Filter = \"system.webserver/rewrite/rules\"\r + Clear-WebConfiguration -location $Site -pspath $PsPath -filter \"$Filter/rule[@name='$Name']\"\r +}\r +\r +if ($Disabled -eq \"true\") {\r + exit\r +}\r +\r +#Clear-WebConfiguration -location $Site -pspath $PsPath -filter \"$Filter/rule[@name='$Name']\"\r +Add-WebConfigurationProperty -location $Site -pspath $PsPath -filter \"$Filter\" -name \".\" -value @{name=$Name; patternSyntax='ECMAScript'; stopProcessing='True'}\r +Set-WebConfigurationProperty -location $Site -pspath $PsPath -filter \"$Filter/rule[@name='$Name']/match\" -name url -value \"(.*)\"\r +Add-WebConfigurationProperty -location $Site -pspath $PsPath -filter \"$Filter/rule[@name='$Name']/conditions\" -name \".\" -value @{input=\"{HTTPS}\"; pattern='^OFF$'}\r +if ($EnableProxyRules -eq \"true\") {\r + Add-WebConfigurationProperty -location $Site -pspath $PsPath -filter \"$Filter/rule[@name='$Name']/conditions\" -name \".\" -value @{input=\"{HTTP_X_FORWARDED_PROTO}\"; pattern='^HTTP$'}\r +}\r +\r +Set-WebConfigurationProperty -location $Site -pspath $PsPath -filter \"$Filter/rule[@name='$Name']/action\" -name \"type\" -value \"Redirect\"\r +Set-WebConfigurationProperty -location $Site -pspath $PsPath -filter \"$Filter/rule[@name='$Name']/action\" -name \"url\" -value \"https://{HTTP_HOST}/{R:1}\"\r +Set-WebConfigurationProperty -location $Site -pspath $PsPath -filter \"$Filter/rule[@name='$Name']/action\" -name \"redirectType\" -value \"Permanent\" ", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "4cb5e060-d027-465f-aa17-29d806f7cb6f", + "Name": "Site", + "Label": "Name of Website", + "HelpText": "Set the rule for a specific website. +If not supplied, a global rule will be created.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2854ab2d-a000-48dd-9b20-6930c73c96fb", + "Name": "EnableProxyRules", + "Label": "Enable proxy rules", + "HelpText": "If the website is running behind a proxy, this setting most likely need to be checked.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "4f341e5c-0694-471c-ac04-3ce525fefad5", + "Name": "Disabled", + "Label": "Disable", + "HelpText": "Disable adding rules. +This will remove any rules mathing current values.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "LastModifiedBy": "perosb", + "$Meta": { + "ExportedAt": "2017-02-20T12:37:13.724Z", + "OctopusVersion": "3.7.4", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-reset.json.human b/step-templates/iis-reset.json.human new file mode 100644 index 000000000..f98708525 --- /dev/null +++ b/step-templates/iis-reset.json.human @@ -0,0 +1,90 @@ +{ + "Id": "2db9af2b-1d9a-4089-9d84-98d9118b39ad", + "Name": "IIS - Reset", + "Description": "Starts, stops, or resets IIS using iisreset", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$wait = $OctopusParameters[\"iisWait\"] -and [boolean]::Parse($OctopusParameters[\"iisWait\"]) +$action = $OctopusParameters[\"iisAction\"] +$errorAction = $OctopusParameters[\"iisErrorAction\"] +if ($Action -eq \"/RESTART\") { Write-Host \"Restarting IIS\" } +elseif ($Action -eq \"/START\") { Write-Host \"Starting IIS\" } +elseif ($Action -eq \"/STOP\") { Write-Host \"Stopping IIS\" } +else { + Write-Error \"Unknown action $action\" + exit 1 +} + +if (($errorAction -ne \"Stop\") -and ($errorAction -ne \"Continue\") -and ($errorAction -ne \"SilentlyContinue\")) { + Write-Error \"Unknown ErrorAction $errorAction\" + exit 1 +} + +if ($wait) { + Write-Host \"Running with wait\" + Start-Process -FilePath \"iisreset\" -ArgumentList $Action -ErrorAction $OctopusParameters[\"iisErrorAction\"] -Wait +} +else { + Write-Host \"Running without wait\" + Start-Process -FilePath \"iisreset\" -ArgumentList $Action -ErrorAction $OctopusParameters[\"iisErrorAction\"] +} +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "aff3b743-7ca7-452b-a4bb-0bc4b4811763", + "Name": "iisWait", + "Label": "Wait for command to finish", + "HelpText": null, + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "993ab611-208d-4a41-bd76-cd243fa5e5ce", + "Name": "iisErrorAction", + "Label": "Action on error", + "HelpText": "", + "DefaultValue": "Stop", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Stop|Stop +Continue|Continue +SilentlyContinue|SilentlyContinue" + }, + "Links": {} + }, + { + "Id": "a1826626-c86a-4ce0-892a-8a09a53f9f8b", + "Name": "iisAction", + "Label": "Action", + "HelpText": "Start, stop, or reset", + "DefaultValue": "/RESTART", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "/RESTART|Restart +/START|Start +/STOP|Stop" + }, + "Links": {} + } + ], + "LastModifiedOn": "2017-07-31T09:33:19.272Z", + "LastModifiedBy": "zappy-shu", + "$Meta": { + "ExportedAt": "2017-07-31T09:29:18.058Z", + "OctopusVersion": "3.12.4", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-set-authentication.steptemplate.json.human b/step-templates/iis-set-authentication.steptemplate.json.human new file mode 100644 index 000000000..a129540f4 --- /dev/null +++ b/step-templates/iis-set-authentication.steptemplate.json.human @@ -0,0 +1,115 @@ +{ + "Id": "d3aa2e7b-d716-4f66-8d85-068cacedbb4f", + "Name": "IIS - Enable or Disable Authentication Methods", + "Description": "Step template to set the desired IIS Authentication (Anonymous, Windows, Digest) State for IIS site(s)", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.ScriptBody": "#requires -version 3 + +function Update-IISSiteAuthentication { + param + ( + [ValidateNotNullOrEmpty()] + [Parameter(Mandatory = $true)] + [boolean]$State, + [ValidateNotNullOrEmpty()] + [Parameter(Mandatory = $true)] + [string]$SitePath, + [ValidateNotNullOrEmpty()] + [Parameter(Mandatory = $true)] + [string]$AuthMethod + ) + + # check if WebAdministration module exists on the server + $cmd = (Get-Command \"Get-Website\" -errorAction SilentlyContinue) + if ($null -eq $cmd) { + throw \"The Windows PowerShell snap-in 'WebAdministration' is not installed on this server. Details can be found at https://technet.microsoft.com/en-us/library/ee790599.aspx.\" + } + + $IISSecurityPath = \"/system.WebServer/security/authentication/$AuthMethod\" + $separator = \"`r\",\"`n\",\",\" + $IISSites = $sitepath.split($separator, [System.StringSplitOptions]::RemoveEmptyEntries).Trim(' ') + + $IISValidSites = New-Object System.Collections.ArrayList +\tforeach ($website in Get-Website) { + $IISValidSites.Add($website.name) + foreach ($app in Get-WebApplication -Site $website.name) { +\t\t\t$path = $website.name + $app.path +\t\t\t$IISValidSites.Add($path) +\t\t} +\t} + + $IISValidSiteNames = $IISValidSites -join ', ' + + foreach($Site in $IISSites) { + $IISSiteAvailable = $IISValidSites | Where-Object { $_ -eq $Site } + + if ($IISSiteAvailable) { + Set-WebConfigurationProperty -Filter $IISSecurityPath -Name Enabled -Value $State -PSPath IIS:\\\\ -Location $Site + Write-Output \"$AuthMethod for site '$Site' set successfully to '$State'.\" + } + else { + Write-Output \"The IISSitePath '$Site' cannot be found. The valid sites are $IISValidSiteNames\" + throw \"The IISSitePath '$Site' cannot be found. The valid sites are $IISValidSiteNames\" + } + } +} + +if (Test-Path Variable:OctopusParameters) { + Update-IISSiteAuthentication -State ($AnonymousAuth -eq \"True\") -SitePath $IISSitePaths -AuthMethod \"AnonymousAuthentication\" + Update-IISSiteAuthentication -State ($WindowsAuth -eq \"True\") -SitePath $IISSitePaths -AuthMethod \"WindowsAuthentication\" + Update-IISSiteAuthentication -State ($DigestAuth -eq \"True\") -SitePath $IISSitePaths -AuthMethod \"DigestAuthentication\" +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "AnonymousAuth", + "Label": "Anonymous Authentication", + "HelpText": "Enable Anonymous Authentication.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "WindowsAuth", + "Label": "Windows Authentication", + "HelpText": "Enable Windows Authentication.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DigestAuth", + "Label": "Digest Authentication", + "HelpText": "Enable Digest Authentication.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "IISSitePaths", + "Label": "IIS Site name(s)", + "HelpText": "The IIS site(s) which will have security permissions transformed. Multiple values are to be new-line separated.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedBy": "HumanPrinter", + "$Meta": { + "ExportedAt": "2017-06-28T11:48:00+00:00", + "OctopusVersion": "3.3.10", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-set-http-redirect.json.human b/step-templates/iis-set-http-redirect.json.human new file mode 100644 index 000000000..09d1bf05d --- /dev/null +++ b/step-templates/iis-set-http-redirect.json.human @@ -0,0 +1,168 @@ +{ + "Id": "684b2249-a274-4518-ba73-e254eed037ac", + "Name": "IIS - Set HTTP Redirect on Site or Application", + "Description": "Configure a redirect for an IIS Site or Web Application", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Set temporary location for PowerShell modules +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\" +$iisPath = \"IIS:\\Sites\\$iisSiteName\" + +# Convert checkbox variables into true Boolean object types +$enableRedirect = [System.Convert]::ToBoolean($enableRedirect) +$exactDestination = [System.Convert]::ToBoolean($exactDestination) + +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Get list of installed modules + $installedModules = Get-Module -ListAvailable + + # Check to see if the module is installed + if ($null -ne ($installedModules | Where-Object {$_.Name -eq $PowerShellModuleName})) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-TemporaryPowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + + # Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +# Check to see if WebAdministration module installed +if (!(Get-ModuleInstalled -PowerShellModuleName \"WebAdministration\")) +{ + # Temporarily install module + Write-Output \"Tempoarily installing PowerShell module WebAdministration.\" + + Install-TemporaryPowerShellModule -PowerShellModuleName \"WebAdministration\" -LocalModulesPath $LocalModules +} + +# Import the WebAdministartion module +Import-Module -Name \"WebAdministration\" + +# Verify the site exists +if ($null -eq (Get-WebSite -Name $iisSiteName)) +{ + # Throw error + throw \"Site $iisSiteName not found!\" +} + +# Check to see if the an application was specified +if (!([string]::IsNullOrEmpty($iisApplicationName))) +{ + # Verify the appliation exists + if ($null -eq (Get-WebApplication -Site $iisSiteName -Name $iisApplicationName)) + { + # Throw error + throw \"Web application $iisApplicationName not found on site $iisSiteName!\" + } + + # Append application name to iis path + $iisPath += \"\\$iisApplicationName\" +} + +# Set redirect on application +Set-WebConfiguration system.webserver/httpRedirect -PSPath $iisPath -Value @{enabled=\"$enableRedirect\";destination=\"$redirectUrl\";exactDestination=\"$exactDestination\";httpResponseStatus=\"$httpResponseStatus\"} +" + }, + "Parameters": [ + { + "Id": "aba6b325-6d4e-4ebf-8c68-17b345db261d", + "Name": "iisSiteName", + "Label": "IIS Site Name", + "HelpText": "Name of the site to configure", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "97526f94-11ac-4050-b9b2-0af0a83e4095", + "Name": "iisApplicationName", + "Label": "IIS Web Application Name", + "HelpText": "Name of the Web Application on the IIS Site to configure the redirect on. Leave blank if setting the redirect at the Site level.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "89af1c5d-17ee-476b-ada6-49ad5b0b3dc9", + "Name": "redirectUrl", + "Label": "Redirect URL", + "HelpText": "URL that the redirect points to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d76bd7c1-6075-4d14-b841-8c098e223bfd", + "Name": "enableRedirect", + "Label": "Enable Redirect", + "HelpText": "Boolean value for whether to enable or disable the redirect.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "4b8f8b38-d449-40c7-b2c9-050f10645ae4", + "Name": "exactDesintation", + "Label": "Exact Destination", + "HelpText": "Boolean value to redirect all requests to exact destination (instead of relative to destination)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "25ab7952-c2ff-43f8-9b96-14ad24fac282", + "Name": "httpResponseStatus", + "Label": "Status Code", + "HelpText": "Web Response code sent to the client when redirecting.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Found|Found +Permanent|Permanent +Temporary|Temporary +PermRedirect|Permanent Redirect +" + } + } + ], + "LastModifiedOn": "2019-01-23T17:34:26.109Z", + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2019-01-23T17:34:26.109Z", + "OctopusVersion": "2018.12.0", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-vdir-create.json.human b/step-templates/iis-vdir-create.json.human new file mode 100644 index 000000000..1667b965d --- /dev/null +++ b/step-templates/iis-vdir-create.json.human @@ -0,0 +1,242 @@ +{ + "Id": "2cfbcd72-cf43-43fd-8291-9bb564cc512c", + "Name": "IIS Virtual Directory - Create", + "Description": "Create an IIS virtual directory.", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.ScriptBody": "## --------------------------------------------------------------------------------------\r +## Input\r +## --------------------------------------------------------------------------------------\r +\r +$virtualPath = $OctopusParameters['VirtualPath'].TrimStart('/',' ').TrimEnd('/', ' ')\r +$physicalPath = $OctopusParameters['PhysicalPath']\r +$parentSite = $OctopusParameters['ParentSite']\r +$application = $OctopusParameters['ApplicationName']\r +$username = $OctopusParameters['Username']\r +$password = $OctopusParameters['Password']\r +$createPhysicalPath = $OctopusParameters['CreatePhysicalPath']\r +\r +## --------------------------------------------------------------------------------------\r +## Helpers\r +## --------------------------------------------------------------------------------------\r +# Helper for validating input parameters\r +function Confirm-Parameter([string]$parameterInput, [string[]]$validInput, $parameterName) {\r + Write-Host \"${parameterName}: $parameterInput\"\r + if (! $parameterInput) {\r + throw \"No value was set for $parameterName, and it cannot be empty\"\r + }\r +\r + if ($validInput) {\r + if (! $validInput -contains $parameterInput) {\r + throw \"'$input' is not a valid input for '$parameterName'\"\r + }\r + }\r +\r +}\r +\r +# Helper to run a block with a retry if things go wrong\r +$maxFailures = 5\r +$sleepBetweenFailures = Get-Random -minimum 1 -maximum 4\r +function Invoke-CommandWithRetry([ScriptBlock] $command) {\r + $attemptCount = 0\r + $operationIncomplete = $true\r +\r + while ($operationIncomplete -and $attemptCount -lt $maxFailures) {\r + $attemptCount = ($attemptCount + 1)\r +\r + if ($attemptCount -ge 2) {\r + Write-Output \"Waiting for $sleepBetweenFailures seconds before retrying...\"\r + Start-Sleep -s $sleepBetweenFailures\r + Write-Output \"Retrying...\"\r + }\r +\r + try {\r + & $command\r +\r + $operationIncomplete = $false\r + } catch [System.Exception] {\r + if ($attemptCount -lt ($maxFailures)) {\r + Write-Output (\"Attempt $attemptCount of $maxFailures failed: \" + $_.Exception.Message)\r +\r + }\r + else {\r + throw \"Failed to execute command\"\r + }\r + }\r + }\r +}\r +\r +## --------------------------------------------------------------------------------------\r +## Configuration\r +## --------------------------------------------------------------------------------------\r +Confirm-Parameter $virtualPath -parameterName \"Virtual path\"\r +Confirm-Parameter $physicalPath -parameterName \"Physical path\"\r +Confirm-Parameter $parentSite -parameterName \"Parent site\"\r +\r +if (![string]::IsNullOrEmpty($application)) {\r + $application = $application.TrimStart('/',' ').TrimEnd('/',' ')\r +}\r +\r +Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue\r +Import-Module WebAdministration -ErrorAction SilentlyContinue\r +\r +\r +## --------------------------------------------------------------------------------------\r +## Run\r +## --------------------------------------------------------------------------------------\r +\r +Write-Host \"Getting web site $parentSite\"\r +$site = Get-Website -name $parentSite\r +if (!$site) {\r + throw \"The web site '$parentSite' does not exist. Please create the site first.\"\r +}\r +\r +$virtualFullPath = $virtualPath\r +\r +if ($application) {\r + Write-Host \"Verifying existance of application $application\"\r + $app = Get-WebApplication -site $parentSite -name $application\r + if (!$app) {\r + throw \"The application '$parentSite' does not exist. Please create the application first.\"\r + } else {\r + $virtualFullPath = $application + '/' + $virtualPath\r + }\r +}\r +\r +# If the physical path down not exist and $createPhysicalPath is true,\r +# then attempt create it, otherwise throw an error.\r +if (!(Test-Path $physicalPath)) {\r + if ($createPhysicalPath) {\r + try {\r + Write-Host \"Attempting to create physical path '$physicalPath'\"\r + New-Item -Type Directory -Path $physicalPath -Force\r + } catch {\r + throw \"Couldn't create physical path!\"\r + }\r + } else {\r + throw \"Physical path does not exist!\"\r + }\r +}\r +\r +# This needs to be improved, especially given applicaltions can be nested.\r +if ($application) {\r + $existing = Get-WebVirtualDirectory -site $parentSite -Application $application -Name $virtualPath\r +} else {\r + $existing = Get-WebVirtualDirectory -site $parentSite -Name $virtualPath\r +}\r +\r +Invoke-CommandWithRetry {\r +\r + $virtualDirectoryPath = \"IIS:\\Sites\\$parentSite\\$virtualFullPath\"\r +\r + if (!$existing) {\r + Write-Host \"Creating virtual directory '$virtualPath'\"\r +\r + New-Item $virtualDirectoryPath -type VirtualDirectory -physicalPath $physicalPath\r +\r + Write-Host \"Virtual directory created\"\r + }\r + else {\r + Write-Host \"The virtual directory '$virtualPath' already exists. Checking physical path.\"\r +\r + $currentPath = (Get-ItemProperty $virtualDirectoryPath).physicalPath\r + Write-Host \"Physical path currently set to $currentPath\"\r +\r + if ([string]::Compare($currentPath, $physicalPath, $True) -ne 0) {\r + Set-ItemProperty $virtualDirectoryPath -name physicalPath -value $physicalPath\r + Write-Host \"Physical path changed to $physicalPath\"\r + }\r + }\r +\r + ## Set vdir pass-through credentails, if applicable\r + if (![string]::IsNullOrEmpty($username) -and ![string]::IsNullOrEmpty($password)) {\r + Write-Host \"Setting Pass-through credentials for username '$username'\"\r +\r + Set-ItemProperty $virtualDirectoryPath -Name userName -Value $username\r + Set-ItemProperty $virtualDirectoryPath -Name password -Value $password\r +\r + Write-Host \"Pass-through credentials set\"\r + }\r +}\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "VirtualPath", + "Label": "Virtual path", + "HelpText": "The full path to the virtual directory you wish to create. Do not include the application (if any) the directory will be created under. The path, not including the virtual directory itself must already exist. Eg. If the virtual directory is to be created under `myapp/someFolder/myVdir` enter: `someFolder/myVdir`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApplicationName", + "Type": "String", + "Label": "Application", + "HelpText": "Name of the IIS application to create the virtual directory under (not required).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "PhysicalPath", + "Label": "Physical path", + "HelpText": "Physical folder that the application will serve files from. Example: `C:\\MyApp`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "CreatePhysicalPath", + "Label": "Create Physical Path (If not exists)", + "HelpText": "Create the physical path if it does not exist.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "ParentSite", + "Label": "Parent site", + "HelpText": "The name of the IIS web site to attach the application to. For example, to put the application under the default web site, enter: + + Default Web Site", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Username", + "Label": "Username", + "HelpText": "Pass-through authentication username", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Password", + "Label": "Password", + "HelpText": "Pass-through authentication password", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedOn": "2017-03-21T23:56:11+00:00", + "LastModifiedBy": "jaymickey", + "$Meta": { + "ExportedAt": "2015-08-25T13:55:15.518+00:00", + "OctopusVersion": "3.0.9.2259", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-add-http-s-bindings.json.human b/step-templates/iis-website-add-http-s-bindings.json.human new file mode 100644 index 000000000..1ccc4a327 --- /dev/null +++ b/step-templates/iis-website-add-http-s-bindings.json.human @@ -0,0 +1,210 @@ +{ + "Id": "0ad0ad00-adad-adad-adad-000000000003", + "Name": "IIS - Add HTTP(S) Bindings", + "Description": "Adds HTTP and HTTPS bindings to a website using the specified host name, port numbers, Certificate Location, and SSL Thumbprint", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +Param( + [string] $AD_AddBinding_WebsiteName, + [string] $AD_AddBinding_HostName, + [UInt32] $AD_AddBinding_HttpPort = 80, + [UInt32] $AD_AddBinding_HttpsPort = 443, + [string] $AD_AddBinding_SSLThumbprint = $null, + [string] $AD_AddBinding_SSLCertificateLocation = \"My\", + [Int16] $AD_AddBinding_Attempts = 5, + [switch] $WhatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($null -ne $OctopusParameters) { + $result = $OctopusParameters[$Name] + } + + if ($null -eq $result) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($null -ne $variable) { + $result = $variable.Value + } + } + + if ($null -eq $result) { + if ($Required) { + throw \"Missing parameter value $Name\" + } + else { + $result = $Default + } + } + + return $result +} + +function Execute( + [Parameter(Mandatory = $true)][string] $WebsiteName, + [Parameter(Mandatory = $true)][string] $HostName, + [Parameter(Mandatory = $false)][uint32] $HttpPort = 80, + [Parameter(Mandatory = $false)][uint32] $HttpsPort = 443, + [Parameter(Mandatory = $false)][string] $SSLThumbprint = $null, + [Parameter(Mandatory = $false)][string] $SSLCertificateLocation = \"My\", + [Parameter(Mandatory = $false)][Int16] $Attempts = 5 +) { + Import-Module WebAdministration + + $attemptCount = 0 + $operationIncomplete = $true + $maxFailures = $Attempts + $sleepBetweenFailures = 1 + + $appId = '{00112233-4455-6677-8899-AABBCCDDEEFF}' + + while ($operationIncomplete -and $attemptCount -lt $maxFailures) { + $attemptCount = ($attemptCount + 1) + if ($attemptCount -ge 2) { + Write-Output \"Waiting for $sleepBetweenFailures seconds before retrying...\" + Start-Sleep -s $sleepBetweenFailures + Write-Output \"Retrying...\" + $sleepBetweenFailures = ($sleepBetweenFailures * 2) + } + try { + $protocol = \"http\" + $otherProtocol = \"https\" + + $existingBinding = Get-WebBinding -Name $WebsiteName -Port $HttpPort -HostHeader $HostName + $msg = \"Binding '{0} *:{1}:{2} sslFlags=0' on '{3}'\" -f $protocol, $HttpPort, $HostName, $WebsiteName + if ($null -eq $existingBinding) { + Write-Output \"$msg doesn't exist - ADDING...\" + if (-Not ($WhatIf)) { + New-WebBinding -Name $WebsiteName -Protocol $protocol -Port $HttpPort -HostHeader $HostName -SslFlags 0 + } + Write-Output \"$msg - ADDED\" + } + elseif ($existingBinding.protocol -contains $protocol) { + Write-Output \"$msg already exists - SKIPING\" + } + else { + Write-Error \"$msg can't be added because it already exists on $otherProtocol\" + } + Write-Output \"SSL is : $SSLThumbprint\" + if (-Not ([string]::IsNullOrWhitespace($SSLThumbprint))) { + $protocol = \"https\" + $otherProtocol = \"http\" + $existingBinding = Get-WebBinding -Name $WebsiteName -Port $HttpsPort -HostHeader $HostName + $msg = \"Binding '{0} *:{1}:{2} sslFlags=1' on '{3}'\" -f $protocol, $HttpsPort, $HostName, $WebsiteName + if ($null -eq $existingBinding) { + Write-Output \"$msg doesn't exist - ADDING...\" + if (-Not ($WhatIf)) { + New-WebBinding -Name $WebsiteName -Protocol $protocol -Port $HttpsPort -HostHeader $HostName -SslFlags 1 + netsh http add sslcert hostnameport=$($HostName):$HttpsPort certhash=$SSLThumbprint appid=$appId certstorename=$SSLCertificateLocation + } + Write-Output \"$msg - ADDED\" + } + elseif ($existingBinding.protocol -contains $protocol) { + Write-Output \"$msg already exists - SKIPING\" + } + else { + Write-Error \"$msg can't be added because it already exists on $otherProtocol\" + } + } + $operationIncomplete = $false + } + catch [System.Exception] { + if ($attemptCount -lt ($maxFailures)) { + Write-Host (\"Attempt $attemptCount of $maxFailures failed: \" + $_.Exception.Message) + } + else { + throw + } + } + } +} +& Execute ` +(Get-Param 'AD_AddBinding_WebsiteName' -Required)` +(Get-Param 'AD_AddBinding_HostName' -Required)` +(Get-Param 'AD_AddBinding_HttpPort')` +(Get-Param 'AD_AddBinding_HttpsPort')` +(Get-Param 'AD_AddBinding_SSLThumbprint')` +(Get-Param 'AD_AddBinding_SSLCertificateLocation')` +(Get-Param 'AD_AddBinding_Attempts') +" + }, + "Parameters": [ + { + "Name": "AD_AddBinding_WebsiteName", + "Label": "Website name", + "HelpText": "The website name to apply the binding to", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AD_AddBinding_HostName", + "Label": "Host name", + "HelpText": "The host name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AD_AddBinding_HttpPort", + "Label": "Http Port", + "HelpText": "Optional HTTP port number", + "DefaultValue": "80", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AD_AddBinding_HttpsPort", + "Label": "HTTPS Port", + "HelpText": "Optional HTTPS port number", + "DefaultValue": "443", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AD_AddBinding_SSLThumbprint", + "Label": "SSL Thumbprint", + "HelpText": "The SSL certificate thumbprint(no spaces)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AD_AddBinding_SSLCertificateLocation", + "Label": "Certificate Store Location", + "HelpText": "Optional Certificate Store location, Defaults to \"My\"", + "DefaultValue": "My", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AD_AddBinding_Attempts", + "Label": "Nr of attempts", + "HelpText": "Optional number of attempts before failing", + "DefaultValue": "5", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "hasherdk", + "$Meta": { + "ExportedAt": "2020-07-17T05:49:27.280Z", + "OctopusVersion": "2019.8.6", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-add-release-number-to-response-header.json.human b/step-templates/iis-website-add-release-number-to-response-header.json.human new file mode 100644 index 000000000..081b6e64e --- /dev/null +++ b/step-templates/iis-website-add-release-number-to-response-header.json.human @@ -0,0 +1,90 @@ +{ + "Id": "7b51887e-1ad2-4133-b3a3-37688bb49b01", + "Name": "IIS Website - Add Release Number to Response Header", + "Description": "Adds the Octopus Deploy Release number to the IIS response header. When you browse your site you can look at the response header to verify the build number that is running.", + "ActionType": "Octopus.Script", + "Version": 10, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Write-Host \"Adding release number to response header\" + +function Get-IISServerManager +{ + [CmdletBinding()] + [OutputType([System.Object])] + param () + + $iisInstallPath = (Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\INetStp' -Name InstallPath).InstallPath + if (-not $iisInstallPath) + { + throw ('IIS installation path not found') + } + $assyPath = Join-Path -Path $iisInstallPath -ChildPath 'Microsoft.Web.Administration.dll' -Resolve -ErrorAction:SilentlyContinue + if (-not $assyPath) + { + throw 'IIS version of Microsoft.Web.Administration.dll not found' + } + $assy = [System.Reflection.Assembly]::LoadFrom($assyPath) + return [System.Activator]::CreateInstance($assy.FullName, 'Microsoft.Web.Administration.ServerManager').Unwrap() +} + +$iis = Get-IISServerManager +$config = $iis.GetWebConfiguration($OctopusParameters['headerWebsiteName']) +$httpProtocolSection = $config.GetSection(\"system.webServer/httpProtocol\") +$customHeadersCollection = $httpProtocolSection.GetCollection(\"customHeaders\") + +$update = $true + +foreach($path in $customHeadersCollection.GetCollection()) { + if ($path.GetAttributeValue(\"name\") -eq $OctopusParameters['headerFieldName']) { + write-host \"Release number is already in the response header, skipping\" + $update = $false + break + } +} + +if ($update) +{ + $fieldName = $OctopusParameters['headerFieldName'] + $releaseNumber = $OctopusParameters['Octopus.Release.Number'] + + Write-Host \"Adding release number $releaseNumber to custom header $fieldName\" + + $addElement = $customHeadersCollection.CreateElement(\"add\") + $addElement[\"name\"] = $fieldName + $addElement[\"value\"] = $releaseNumber + $customHeadersCollection.Add($addElement) + + $iis.CommitChanges() | Write-Host +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "headerFieldName", + "Label": "Field name", + "HelpText": "The name of the custom header field with the release information.", + "DefaultValue": "Release", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "headerWebsiteName", + "Label": "Website name", + "HelpText": "The name of the website in IIS.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2019-03-19T16:25:56.946Z", + "LastModifiedBy": "geeeyetee", + "$Meta": { + "ExportedAt": "2019-03-19T16:25:56.946Z", + "OctopusVersion": "2019.1.9", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-add-windows-auth-providers.json.human b/step-templates/iis-website-add-windows-auth-providers.json.human new file mode 100644 index 000000000..1e866e7dc --- /dev/null +++ b/step-templates/iis-website-add-windows-auth-providers.json.human @@ -0,0 +1,128 @@ +{ + "Id": "f3cf7831-d47c-4b5f-8f76-6bc649d59dd9", + "Name": "Explicitly Add IIS WindowsAuthentication Providers", + "Description": "Clears the WindowsAuthentication Providers, and explicitly adds the ones provided.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "## --------------------------------------------------------------------------------------\r +## Configuration\r +## --------------------------------------------------------------------------------------\r +\r +$isEnabled = $OctopusParameters[\"add-windows-authentication-providers.is-enabled\"]\r +if (!$isEnabled -or ![boolean]::Parse($isEnabled))\r +{\r + exit 0\r +}\r +\r +try {\r + Add-PSSnapin WebAdministration\r +} catch {\r + try {\r + Import-Module WebAdministration\r + } catch {\r +\t\tWrite-Warning \"We failed to load the WebAdministration module. This usually resolved by doing one of the following:\"\r +\t\tWrite-Warning \"1. Install .NET Framework 3.5.1\"\r +\t\tWrite-Warning \"2. Upgrade to PowerShell 3.0 (or greater)\"\r + throw ($error | Select-Object -First 1)\r + }\r +}\r +\r +$webSiteName = $OctopusParameters[\"add-windows-authentication-providers.website-name\"]\r +$providersString = $OctopusParameters[\"add-windows-authentication-providers.providers\"]\r +$providers = ($providersString.Split(\"`r`n,\") | % {$_.Trim() } | ? {$_})\r +\r +## --------------------------------------------------------------------------------------\r +## Helpers\r +## --------------------------------------------------------------------------------------\r +$maxFailures = 5\r +$sleepBetweenFailures = Get-Random -minimum 1 -maximum 4\r +function Execute-WithRetry([ScriptBlock] $command) {\r + $attemptCount = 0\r + $operationIncomplete = $true\r +\r + while ($operationIncomplete -and $attemptCount -lt $maxFailures) {\r + $attemptCount = ($attemptCount + 1)\r +\r + if ($attemptCount -ge 2) {\r + Write-Output \"Waiting for $sleepBetweenFailures seconds before retrying...\"\r + Start-Sleep -s $sleepBetweenFailures\r + Write-Output \"Retrying...\"\r + }\r +\r + try {\r + & $command\r +\r + $operationIncomplete = $false\r + } catch [System.Exception] {\r + if ($attemptCount -lt ($maxFailures)) {\r + Write-Output (\"Attempt $attemptCount of $maxFailures failed: \" + $_.Exception.Message)\r + }\r + else {\r + throw \"Failed to execute command\"\r + }\r + }\r + }\r +}\r +\r +## --------------------------------------------------------------------------------------\r +## Run\r +## --------------------------------------------------------------------------------------\r +Execute-WithRetry { \r + Write-Host \"Clearing Windows Authentication Providers for $webSiteName\"\r + Remove-WebConfigurationProperty -PSPath IIS:\\ -Location \"$webSiteName\" -filter system.webServer/security/authentication/windowsAuthentication/providers -name \".\"\r +}\r +\r +$providersPrintedString = $providers -join \", \"\r +Write-Host \"Providers to add: $providersPrintedString\"\r +foreach ($provider in $providers) {\r + Write-Host \"Windows Authentication Provider $provider\"\r + Execute-WithRetry { \r + Add-WebConfiguration -Filter system.webServer/security/authentication/windowsAuthentication/providers -PSPath IIS:\\ -Location \"$webSiteName\" -Value \"$provider\"\r + }\r +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "add-windows-authentication-providers.is-enabled", + "Label": "Add Windows Authentication Providers", + "HelpText": "If enabled, This step will clear the Windows Authentication Providers, and then add the ones listed in the + Windows Authentication Providers +field.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "add-windows-authentication-providers.website-name", + "Label": "Web Site name", + "HelpText": "The display name of the IIS web site to add Windows Authentication providers to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "add-windows-authentication-providers.providers", + "Label": "Windows Authentication Providers", + "HelpText": "A comma- or newline-separated list of Windows Authentication Providers to add.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedBy": "KShanafelt", + "$Meta": { + "ExportedAt": "2016-05-27T01:18:58.041+00:00", + "OctopusVersion": "3.3.8", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-appfabric-application-start.json.human b/step-templates/iis-website-appfabric-application-start.json.human new file mode 100644 index 000000000..a2bd55495 --- /dev/null +++ b/step-templates/iis-website-appfabric-application-start.json.human @@ -0,0 +1,59 @@ +{ + "Id": "3f80830a-8920-44d9-bf2f-af6b84262631", + "Name": "IIS Website AppFabric Application - Start", + "Description": "Starts an AppFabric application in IIS.", + "ActionType": "Octopus.Script", + "Version": 11, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Import-Module WebAdministration + +$webSiteName = $OctopusParameters['WebSiteName'] +$applicationName = $OctopusParameters['ApplicationName'] + +if (!$webSiteName) +{ + Write-Error \"No Website name was specified. Please specify the name of the Website that contains the AppFabric application.\" + exit -2 +} + +if (!$applicationName) +{ + Write-Error \"No Application name was specified. Please specify the name of the AppFabric Application contained in the Website.\" + exit -2 +} + +Write-Output \"Starting IIS AppFabric application $applicationName in website $webSiteName\" +Start-AsApplication $webSiteName $applicationName +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "WebsiteName", + "Label": "Website name", + "HelpText": "The name of the site in IIS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApplicationName", + "Label": "Application Name", + "HelpText": "The name of the AppFabric application inside the Website in IIS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2014-09-20T21:03:29.922+00:00", + "LastModifiedBy": "caioproiete", + "$Meta": { + "ExportedAt": "2014-09-20T21:16:03.171+00:00", + "OctopusVersion": "2.5.8.447", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-appfabric-application-stop.json.human b/step-templates/iis-website-appfabric-application-stop.json.human new file mode 100644 index 000000000..e9f4a45a3 --- /dev/null +++ b/step-templates/iis-website-appfabric-application-stop.json.human @@ -0,0 +1,59 @@ +{ + "Id": "bf1f9d1d-3200-4ae6-bab1-12e42aa829e6", + "Name": "IIS Website AppFabric Application - Stop", + "Description": "Stops an AppFabric application in IIS.", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Import-Module WebAdministration + +$webSiteName = $OctopusParameters['WebSiteName'] +$applicationName = $OctopusParameters['ApplicationName'] + +if (!$webSiteName) +{ + Write-Error \"No Website name was specified. Please specify the name of the Website that contains the AppFabric application.\" + exit -2 +} + +if (!$applicationName) +{ + Write-Error \"No Application name was specified. Please specify the name of the AppFabric Application contained in the Website.\" + exit -2 +} + +Write-Output \"Stopping IIS AppFabric application $applicationName in website $webSiteName\" +Stop-AsApplication $webSiteName $applicationName +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "WebsiteName", + "Label": "Website name", + "HelpText": "The name of the site in IIS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApplicationName", + "Label": "Application Name", + "HelpText": "The name of the AppFabric application inside the Website in IIS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2014-09-20T21:03:33.663+00:00", + "LastModifiedBy": "caioproiete", + "$Meta": { + "ExportedAt": "2014-09-20T21:19:58.914+00:00", + "OctopusVersion": "2.5.8.447", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-create.json.human b/step-templates/iis-website-create.json.human new file mode 100644 index 000000000..c8c99d5ae --- /dev/null +++ b/step-templates/iis-website-create.json.human @@ -0,0 +1,345 @@ +{ + "Id": "74c6ab38-f56c-4637-918c-1b46b4e24049", + "Name": "IIS Website - Create", + "Description": "Creates a new website in IIS", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.ScriptBody": "## -------------------------------------------------------------------------------------- +## Input +## -------------------------------------------------------------------------------------- + +$webSiteName = $OctopusParameters['WebSiteName'] +$applicationPoolName = $OctopusParameters[\"ApplicationPoolName\"] +$bindingProtocol = $OctopusParameters[\"BindingProtocol\"] +$bindingPort = $OctopusParameters[\"BindingPort\"] +$bindingIpAddress = $OctopusParameters[\"BindingIpAddress\"] +$bindingHost = $OctopusParameters[\"BindingHost\"] +$bindingSslThumbprint = $OctopusParameters[\"BindingSslThumbprint\"] +$webRoot = $OctopusParameters[\"WebRoot\"] +$iisAuthentication = $OctopusParameters[\"IisAuthentication\"] +$webSiteStart = $OctopusParameters[\"WebsiteStart\"] + + +$anonymousAuthentication = \"Anonymous\" +$basicAuthentication = \"Basic\" +$windowsAuthentication = \"Windows\" +## -------------------------------------------------------------------------------------- +## Helpers +## -------------------------------------------------------------------------------------- +# Helper for validating input parameters +function Validate-Parameter($foo, [string[]]$validInput, $parameterName) { + Write-Host \"${parameterName}: ${foo}\" + if (! $foo) { + throw \"$parameterName cannot be empty, please specify a value\" + } + + if ($validInput) { + @($foo) | % { + if ($validInput -notcontains $_) { + throw \"'$_' is not a valid input for '$parameterName'\" + } + } + } +} + +# Helper to run a block with a retry if things go wrong +$maxFailures = 5 +$sleepBetweenFailures = Get-Random -minimum 1 -maximum 4 +function Execute-WithRetry([ScriptBlock] $command) { +\t$attemptCount = 0 +\t$operationIncomplete = $true + +\twhile ($operationIncomplete -and $attemptCount -lt $maxFailures) { +\t\t$attemptCount = ($attemptCount + 1) + +\t\tif ($attemptCount -ge 2) { +\t\t\tWrite-Output \"Waiting for $sleepBetweenFailures seconds before retrying...\" +\t\t\tStart-Sleep -s $sleepBetweenFailures +\t\t\tWrite-Output \"Retrying...\" +\t\t} + +\t\ttry { +\t\t\t& $command + +\t\t\t$operationIncomplete = $false +\t\t} catch [System.Exception] { +\t\t\tif ($attemptCount -lt ($maxFailures)) { +\t\t\t\tWrite-Output (\"Attempt $attemptCount of $maxFailures failed: \" + $_.Exception.Message) +\t\t\t +\t\t\t} +\t\t\telse { +\t\t\t throw \"Failed to execute command\" +\t\t\t} +\t\t} +\t} +} + +## -------------------------------------------------------------------------------------- +## Validate Input +## -------------------------------------------------------------------------------------- + +Write-Output \"Validating paramters...\" +Validate-Parameter $webSiteName -parameterName \"Web Site Name\" +Validate-Parameter $applicationPoolName -parameterName \"Application Pool Name\" +Validate-Parameter $bindingProtocol -validInput @(\"HTTP\", \"HTTPS\") -parameterName \"Protocol\" +Validate-Parameter $bindingPort -parameterName \"Port\" +if($bindingProtocol.ToLower() -eq \"https\") { + Validate-Parameter $bindingSslThumbprint -parameterName \"SSL Thumbprint\" +} + +$enabledIisAuthenticationOptions = $iisAuthentication -split '\\s*[,;]\\s*' + +Validate-Parameter $enabledIisAuthenticationOptions -validInput @($anonymousAuthentication, $basicAuthentication, $windowsAuthentication) -parameterName \"IIS Authentication\" + +$enableAnonymous = $enabledIisAuthenticationOptions -contains $anonymousAuthentication +$enableBasic = $enabledIisAuthenticationOptions -contains $basicAuthentication +$enableWindows = $enabledIisAuthenticationOptions -contains $windowsAuthentication + +## -------------------------------------------------------------------------------------- +## Configuration +## -------------------------------------------------------------------------------------- +if (! $webRoot) { +\t$webRoot = (Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\InetStp' -name PathWWWRoot).PathWWWRoot +} +$webRoot = (resolve-path $webRoot).ProviderPath +Validate-Parameter $webRoot -parameterName \"Relative Home Directory\" + +$bindingInformation = \"${bindingIpAddress}:${bindingPort}:${bindingHost}\" + +Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue +Import-Module WebAdministration -ErrorAction SilentlyContinue + +$wsBindings = new-object System.Collections.ArrayList +$wsBindings.Add(@{ protocol=$bindingProtocol;bindingInformation=$bindingInformation }) | Out-Null +if (! [string]::IsNullOrEmpty($bindingSslThumbprint)) { + $wsBindings.Add(@{ thumbprint=$bindingSslThumbprint }) | Out-Null + + $sslCertificateThumbprint = $bindingSslThumbprint.Trim() + Write-Output \"Finding SSL certificate with thumbprint $sslCertificateThumbprint\" + + $certificate = Get-ChildItem Cert:\\LocalMachine -Recurse | Where-Object { $_.Thumbprint -eq $sslCertificateThumbprint -and $_.HasPrivateKey -eq $true } | Select-Object -first 1 + if (! $certificate) + { + throw \"Could not find certificate under Cert:\\LocalMachine with thumbprint $sslCertificateThumbprint. Make sure that the certificate is installed to the Local Machine context and that the private key is available.\" + } + + Write-Output (\"Found certificate: \" + $certificate.Subject) + + if ((! $bindingIpAddress) -or ($bindingIpAddress -eq '*')) { + $bindingIpAddress = \"0.0.0.0\" + } + $port = $bindingPort + + $sslBindingsPath = (\"IIS:\\SslBindings\\\" + $bindingIpAddress + \"!\" + $port) + +\tExecute-WithRetry { +\t\t$sslBinding = get-item $sslBindingsPath -ErrorAction SilentlyContinue +\t\tif (! $sslBinding) { +\t\t\tNew-Item $sslBindingsPath -Value $certificate | Out-Null +\t\t} else { +\t\t\tSet-Item $sslBindingsPath -Value $certificate | Out-Null +\t\t}\t\t +\t} +} + +## -------------------------------------------------------------------------------------- +## Run +## -------------------------------------------------------------------------------------- + +pushd IIS:\\ + +$appPoolPath = (\"IIS:\\AppPools\\\" + $applicationPoolName) + +Execute-WithRetry { + Write-Output \"Finding application pool $applicationPoolName\" +\t$pool = Get-Item $appPoolPath -ErrorAction SilentlyContinue +\tif (!$pool) { +\t\tthrow \"Application pool $applicationPoolName does not exist\" +\t} +} + +$sitePath = (\"IIS:\\Sites\\\" + $webSiteName) + +Write-Output $sitePath + +$site = Get-Item $sitePath -ErrorAction SilentlyContinue +if (!$site) { +\tWrite-Output \"Creating web site $webSiteName\" + Execute-WithRetry { +\t\t$id = (dir iis:\\sites | foreach {$_.id} | sort -Descending | select -first 1) + 1 +\t\tnew-item $sitePath -bindings ($wsBindings[0]) -id $id -physicalPath $webRoot -confirm:$false + } +} else { +\twrite-host \"Web site $webSiteName already exists\" +} + +$cmd = { +\tWrite-Output \"Assigning website to application pool: $applicationPoolName\" +\tSet-ItemProperty $sitePath -name applicationPool -value $applicationPoolName +} +Execute-WithRetry -Command $cmd + +Execute-WithRetry { +\tWrite-Output \"Setting home directory: $webRoot\" +\tSet-ItemProperty $sitePath -name physicalPath -value \"$webRoot\" +} + +try { +\tExecute-WithRetry { +\t\tWrite-Output \"Anonymous authentication enabled: $enableAnonymous\" +\t\tSet-WebConfigurationProperty -filter /system.webServer/security/authentication/anonymousAuthentication -name enabled -value \"$enableAnonymous\" -location $WebSiteName -PSPath \"IIS:\\\" +\t} + +\tExecute-WithRetry { +\t\tWrite-Output \"Basic authentication enabled: $enableBasic\" +\t\tSet-WebConfigurationProperty -filter /system.webServer/security/authentication/basicAuthentication -name enabled -value \"$enableBasic\" -location $WebSiteName -PSPath \"IIS:\\\" +\t} + +\tExecute-WithRetry { +\t\tWrite-Output \"Windows authentication enabled: $enableWindows\" +\t\tSet-WebConfigurationProperty -filter /system.webServer/security/authentication/windowsAuthentication -name enabled -value \"$enableWindows\" -location $WebSiteName -PSPath \"IIS:\\\" +\t} +} catch [System.Exception] { +\tWrite-Output \"Authentication options could not be set. This can happen when there is a problem with your application's web.config. For example, you might be using a section that requires an extension that is not installed on this web server (such as URL Rewriting). It can also happen when you have selected an authentication option and the appropriate IIS module is not installed (for example, for Windows authentication, you need to enable the Windows Authentication module in IIS/Windows first)\" +\tthrow +} + +# It can take a while for the App Pool to come to life +Start-Sleep -s 1 + +Execute-WithRetry { +\t$state = Get-WebAppPoolState $applicationPoolName +\tif ($state.Value -eq \"Stopped\") { +\t\tWrite-Output \"Application pool is stopped. Attempting to start...\" +\t\tStart-WebAppPool $applicationPoolName +\t} +} + +if($webSiteStart -eq $true) { + Execute-WithRetry { + \t$state = Get-WebsiteState $webSiteName + \tif ($state.Value -eq \"Stopped\") { + \t\tWrite-Output \"Web site is stopped. Attempting to start...\" + \t\tStart-Website $webSiteName + \t} + } +} else { +\twrite-host \"Not starting Web site $webSiteName\" +} + +popd + +Write-Output \"IIS configuration complete\"", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "WebsiteName", + "Label": "Website name", + "HelpText": "The display name of the IIS website to create. + +Example: Default Web Site", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "WebRoot", + "Label": "Relative home directory", + "HelpText": "The directory which will be used as the home directory of the IIS website. This should be bound to the installation directory of a previous step. + +Example: C:\\inetpub\\wwwroot", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApplicationPoolName", + "Label": "Application pool name", + "HelpText": "Name of the application pool in IIS to use for the new website", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BindingProtocol", + "Label": "Protocol", + "HelpText": "The protocol to use for the new website", + "DefaultValue": "http", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BindingPort", + "Label": "Port", + "HelpText": "The port to use for the new website", + "DefaultValue": "80", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BindingIpAddress", + "Label": "IP address", + "HelpText": "The IP address to use for the new website", + "DefaultValue": "*", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BindingHost", + "Label": "Host Header", + "HelpText": "The host name to use for the new website + +Example: company.example.com", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BindingSslThumbprint", + "Label": "Thumbprint", + "HelpText": "The thumbprint of the SSL certificate to use for the new website when using the HTTPS protocol + +Example: 7c003ac253aa41e89976f139c11edd7b", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "IisAuthentication", + "Label": "IIS Authentication", + "HelpText": "The authentication mode to use for the new website (can be Anonymous, Basic or Windows), specify multiple modes by entering the modes required separated by a ',' or ';'", + "DefaultValue": "Anonymous", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "WebsiteStart", + "Label": "Start the Website", + "HelpText": "Uncheck if you don't want the website started after it is created.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2016-02-04T01:00:46.771+00:00", + "OctopusVersion": "3.2.19", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-delete.json.human b/step-templates/iis-website-delete.json.human new file mode 100644 index 000000000..f4388ced4 --- /dev/null +++ b/step-templates/iis-website-delete.json.human @@ -0,0 +1,112 @@ +{ + "Id": "a032159b-0742-4982-95f4-59877a31fba3", + "Name": "IIS Website - Delete", + "Description": "Deletes a website in IIS.", + "ActionType": "Octopus.Script", + "Version": 10, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$webSiteName = $OctopusParameters['WebSiteName'] +if (! $webSiteName) { + throw \"Web Site Name cannot be empty, please specify the web site to delete\" +} + +# Helper to run a block with a retry if things go wrong +$maxFailures = 5 +$sleepBetweenFailures = Get-Random -minimum 1 -maximum 4 +function Execute-WithRetry([ScriptBlock] $command) { +\t$attemptCount = 0 +\t$operationIncomplete = $true + +\twhile ($operationIncomplete -and $attemptCount -lt $maxFailures) { +\t\t$attemptCount = ($attemptCount + 1) + +\t\tif ($attemptCount -ge 2) { +\t\t\tWrite-Output \"Waiting for $sleepBetweenFailures seconds before retrying...\" +\t\t\tStart-Sleep -s $sleepBetweenFailures +\t\t\tWrite-Output \"Retrying...\" +\t\t} + +\t\ttry { +\t\t\t& $command + +\t\t\t$operationIncomplete = $false +\t\t} catch [System.Exception] { +\t\t\tif ($attemptCount -lt ($maxFailures)) { +\t\t\t\tWrite-Output (\"Attempt $attemptCount of $maxFailures failed: \" + $_.Exception.Message) +\t\t\t +\t\t\t} +\t\t\telse { +\t\t\t throw \"Failed to execute command\" +\t\t\t} +\t\t} +\t} +} + +Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue +Import-Module WebAdministration -ErrorAction SilentlyContinue + +pushd IIS:\\ + +try { + Write-Output \"Deleting web site $webSiteName\" + $sitePath = (\"IIS:\\Sites\\\" + $webSiteName) + + Write-Output $sitePath + + $site = Get-Item $sitePath -ErrorAction SilentlyContinue + if (! $site) { + Write-Output \"$webSiteName does not exist\" + } + else { + + Execute-WithRetry { + $state = Get-WebSiteState $webSiteName + if($state.Value -eq \"Started\") { + Write-Output \"Web site is running. Attempting to stop...\" + Stop-WebSite $webSiteName + } + } + + Write-Output \"Attempting to delete $webSiteName...\" + Execute-WithRetry { + Write-Output \"Removing SSL Bindings...\" + #Skipping default binding (Hostname $null) as it will break all sites which depend on this binding (non-SNI enabled sites will be grouped on the default binding! Remove-WebSite can handle this properly.) + Get-Item 'IIS:\\SslBindings\\' | Get-ChildItem | select $_.Sites | Where-Object { ($_.Sites -contains $webSiteName) -and ($_.Hostname -ne $null) } | Remove-Item + Write-Output \"Removing Web Bindings...\" + Get-WebBinding -Name $webSiteName | Remove-WebBinding + Write-Output \"Removing web site...\" + Remove-WebSite $webSiteName + } + } +} catch [System.Exception] { + throw (\"Failed to execute command\" + $_.Exception.Message) +} + +popd + +Write-Output \"IIS Configuration complete\"", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "WebsiteName", + "Label": "Website name", + "HelpText": "The name of the website in IIS to delete. + +Example: Default Web Site", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2021-04-22T14:41:30.252Z", + "OctopusVersion": "2020.4.6", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-restart.json.human b/step-templates/iis-website-restart.json.human new file mode 100644 index 000000000..7aaa8debb --- /dev/null +++ b/step-templates/iis-website-restart.json.human @@ -0,0 +1,44 @@ +{ + "Id": "6a17bd83-ef96-4c22-b212-91a89ca92fe6", + "Name": "IIS Website - Restart", + "Description": "Restarts a website in IIS.", + "ActionType": "Octopus.Script", + "Version": 12, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Load IIS module: +Import-Module WebAdministration + +# Set a name of the site we want to restart +$webSiteName = $OctopusParameters['webSiteName'] + +# Get web site object +$webSite = Get-Item \"IIS:\\Sites\\$webSiteName\" + +Write-Output \"Stopping IIS web site $webSiteName\" +$webSite.Stop() +Write-Output \"Starting IIS web site $webSiteName\" +$webSite.Start() +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "WebsiteName", + "Label": "Website name", + "HelpText": "The name of the site in IIS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-16T06:21:11.211+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-set-ip-security.json.human b/step-templates/iis-website-set-ip-security.json.human new file mode 100644 index 000000000..290d6b2f3 --- /dev/null +++ b/step-templates/iis-website-set-ip-security.json.human @@ -0,0 +1,144 @@ +{ + "Id": "2aab8e8b-6a55-462c-91ca-3c2a395e82a5", + "Name": "IIS Website - Set IP Security", + "Description": "Takes a list of ip/mask and allow them in ipsecurity", + "ActionType": "Octopus.Script", + "Version": 10, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$existingRules = Get-WebConfiguration /system.webServer/security/ipSecurity/* -location $Site -pspath IIS://;\r +[Object[]]$newRules = @();\r +\r +$ips = $IpAddresses -split '\ +';\r +$ips = $ips.Trim('\\r');\r +foreach ($u in $ips) {\r + $a = $u.Split(\"/\");\r + $newRules += [Object]@{ ipAddress = $a[0]; subnetMask = $a[1]; allowed = $true };\r +}\r +\r +if ($EnableProxyMode -eq \"true\") {\r + Write-Output \"Enabling proxy mode\"\r + set-webconfigurationproperty -Filter /system.webServer/security/ipSecurity -location $Site -Name \"enableProxyMode\" -Value \"true\"\r +}\r +\r +if ($SetDeny -eq \"true\") {\r + Write-Output \"Setting Deny rule\"\r + set-webconfigurationproperty -Filter /system.webServer/security/ipSecurity -location $Site -Name \"allowUnlisted\" -Value \"false\"\r +}\r +\r +function addRules([string]$website, [Object[]]$newRules, [Object[]]$oldRules) {\r +\r + foreach ($rule in $newRules) {\r + if (ruleExists $rule $oldRules) {\r + Write-Host \"Rule $($rule.ipAddress)/$($rule.subnetMask) already exists\";\r +\r + continue;\r + }\r +\r + $value = @{ipAddress = $($rule.ipAddress); allowed = \"true\" }\r + if ([string]::IsNullOrEmpty($rule.subnetMask)) {\r + Write-Output \"Adding ip $($rule.ipAddress) to allow\"\r + }\r + else {\r + Write-Output \"Adding ip $($rule.ipAddress)/$($rule.subnetMask) to allow\"\r + $value.subnetMask = $rule.subnetMask;\r + }\r +\r + add-webconfiguration /system.webServer/security/ipSecurity -location $website -value $value -pspath IIS://\r + } \r +}\r +\r +function clearRules([string]$website, [Object[]]$newRules, [Object[]]$oldRules) {\r +\r + foreach ($rule in $oldRules) {\r + if (ruleExists $rule $newRules) {\r + continue;\r + }\r +\r + Write-Host \"Rule $($rule.ipAddress)/$($rule.subnetMask) is not exists, remove it\";\r +\r + Clear-WebConfiguration -Filter $rule.ItemXPath -location $rule.Location\r + } \r +}\r +\r +function ruleExists([Object]$rule, [Object[]]$rules) {\r + foreach ($r in $rules) {\r + if ($r.ipAddress -eq $rule.ipAddress -and $r.allowed -eq $rule.allowed) {\r + if ($r.subnetMask -eq $rule.subnetMask) {\r + return $true;\r + }\r +\r + if (([string]::IsNullOrEmpty($r.subnetMask) -or $r.subnetMask -eq \"255.255.255.255\") -and ([string]::IsNullOrEmpty($rule.subnetMask) -or $rule.subnetMask -eq \"255.255.255.255\")) {\r + return $true;\r + }\r + }\r + }\r +\r + return $false;\r +}\r +\r +addRules $Site $newRules $existingRules;\r +clearRules $Site $newRules $existingRules;\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "357e87c7-908c-4cdb-af63-8818cef5bd23", + "Name": "Site", + "Label": "Name of Website", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "9db7ed01-c348-48ee-868b-ebcd0d87cce1", + "Name": "IpAddresses", + "Label": "List of ip addresses", + "HelpText": "A newline separated list of IP addresses and/or IP address and subnet mask pairs in the format /.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "2854ab2d-a000-48dd-9b20-6930c73c96fb", + "Name": "EnableProxyMode", + "Label": "Enable proxy mode", + "HelpText": "If the website is running behind a proxy, this setting most likely need to be checked.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "7d8cc90e-281e-4718-a0e9-b1daa522d5c9", + "Name": "SetDeny", + "Label": "Set deny", + "HelpText": "If checked, IIS will be changed to \"Deny\" all IP addresses not added to the list.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "LastModifiedBy": "olsh", + "$Meta": { + "ExportedAt": "2021-03-01T11:24:20.680Z", + "OctopusVersion": "3.7.4", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-set-log-format.json.human b/step-templates/iis-website-set-log-format.json.human new file mode 100644 index 000000000..472bc6146 --- /dev/null +++ b/step-templates/iis-website-set-log-format.json.human @@ -0,0 +1,412 @@ +{ + "Id": "e46aad55-5484-48a7-a90d-970d40129893", + "Name": "IIS Website - Set log format", + "Description": "Sets fields included in IIS logging. Uses named checkboxes where the names are identical to the naming scheme in IIS Manager.", + "ActionType": "Octopus.Script", + "Version": 14, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "Import-Module \"WebAdministration\" -ErrorAction Stop + +function SetIisLogPath { + param($logPath, $IISsitename) + write-host \"#Updating IIS Log path\" + + if (!(Test-Path \"IIS:\\Sites\\$($IISsitename)\")) { + write-host \"$IISsitename does not exist in IIS\" + } else { + Set-ItemProperty IIS:\\Sites\\$($IISsitename) -name logFile.directory -value $logPath + write-host \"IIS LogPath updated to $logPath\" + } +} + +function AdvancedLogging-GenerateAppCmdScriptToConfigureAndRun +{ + param([string] $site) + + #Clear existing log definition if it exists. We use site name to make it apparent where it belongs. + clear-WebConfiguration -PSPath IIS:\\ -Filter \"system.webServer/advancedLogging/server/logDefinitions/logDefinition[@baseFileName='$site']\" + + #Get current powershell execution folder + $currentLocation = Get-Location + + #Create an empty bat which will be populated with appcmd instructions + $stream = [System.IO.StreamWriter] \"$currentLocation\\$site.bat\" + + $stream.WriteLine(\"%systemroot%\\system32\\inetsrv\\appcmd.exe clear config \"\"$site\"\" -section:system.webServer/advancedLogging/server /commit:apphost\") + + #Create site specific log definition + $stream.WriteLine(\"%systemroot%\\system32\\inetsrv\\appcmd.exe set config \"\"$site\"\" -section:system.webServer/advancedLogging/server /+`\"logDefinitions.[baseFileName='$site',enabled='True',logRollOption='Schedule',schedule='Daily',publishLogEvent='False']`\" /commit:apphost\") + + #Get all available fields for logging + $availableFields = Get-WebConfiguration \"system.webServer/advancedLogging/server/fields\" + + $targetFields = ((GetIisLogFields).iisHeader) + Write-Host \"Target fields: \" (($targetFields) -join ',') + #Add appcmd instruction to add all the selected fields above to be logged as part of the logging + #The below section can be extended to filter out any unwanted fields + foreach ($field in $targetFields) { + \t$f = (($availableFields.Collection) |Where-Object {$_.logHeaderName -eq \"$field\"}) + \tWrite-Host \"Appending \" $f.iisHeader $f.id + $stream.WriteLine(\"C:\\windows\\system32\\inetsrv\\appcmd.exe set config \"\"$site\"\" -section:system.webServer/advancedLogging/server /+`\"logDefinitions.[baseFileName='$site'].selectedFields.[id='$($f.id)',logHeaderName='$($f.logHeaderName)']`\" /commit:apphost\") + } + + $stream.close() + + # execute the batch file create to configure the site specific Advanced Logging + Start-Process -FilePath $currentLocation\\$site.bat + Start-Sleep -Seconds 10 +} + +function GetIisLogFields { + $IisLogFields = @() + if ($OctopusParameters['Date'] -eq \"True\") \t\t { $IisLogFields += New-Object PSObject -Property @{id = \"Date\"; iisHeader = \"date\" } } + if ($OctopusParameters['Time'] -eq \"True\") \t\t { $IisLogFields += New-Object PSObject -Property @{id = \"Time\"; iisHeader = \"time\" } } + if ($OctopusParameters['ClientIP'] -eq \"True\") \t{ $IisLogFields += New-Object PSObject -Property @{id = \"ClientIP\"; iisHeader = \"c-ip\"} } + if ($OctopusParameters['UserName'] -eq \"True\") \t{ $IisLogFields += New-Object PSObject -Property @{id = \"UserName\"; iisHeader = \"cs-username\" } } + if ($OctopusParameters['SiteName'] -eq \"True\") \t { $IisLogFields += New-Object PSObject -Property @{id = \"SiteName\"; iisHeader = \"s-sitename\" } } + if ($OctopusParameters['ComputerName'] -eq \"True\") { $IisLogFields += New-Object PSObject -Property @{id = \"ComputerName\"; iisHeader = \"s-computername\" } } + if ($OctopusParameters['ServerIP'] -eq \"True\") \t { $IisLogFields += New-Object PSObject -Property @{id = \"ServerIP\"; iisHeader = \"s-ip\" } } + if ($OctopusParameters['ServerPort'] -eq \"True\") \t { $IisLogFields += New-Object PSObject -Property @{id = \"ServerPort\"; iisHeader = \"s-port\" } } + if ($OctopusParameters['Method'] -eq \"True\") \t\t{ $IisLogFields += New-Object PSObject -Property @{id = \"Method\"; iisHeader = \"cs-method\" } } + if ($OctopusParameters['UriStem'] -eq \"True\") \t \t{ $IisLogFields += New-Object PSObject -Property @{id = \"UriStem\"; iisHeader = \"cs-uri-stem\" } } + if ($OctopusParameters['UriQuery'] -eq \"True\") \t { $IisLogFields += New-Object PSObject -Property @{id = \"UriQuery\"; iisHeader = \"cs-uri-query\" } } + if ($OctopusParameters['HttpStatus'] -eq \"True\") \t { $IisLogFields += New-Object PSObject -Property @{id = \"HttpStatus\"; iisHeader = \"sc-status\" } } + if ($OctopusParameters['HttpSubStatus'] -eq \"True\") \t{ $IisLogFields += New-Object PSObject -Property @{id = \"HttpSubStatus\"; iisHeader = \"sc-substatus\" } } + if ($OctopusParameters['Win32Status'] -eq \"True\") \t { $IisLogFields += New-Object PSObject -Property @{id = \"Win32Status\"; iisHeader = \"sc-win32-status\" } } + if ($OctopusParameters['BytesSent'] -eq \"True\") \t { $IisLogFields += New-Object PSObject -Property @{id = \"BytesSent\"; iisHeader = \"sc-bytes\" } } + if ($OctopusParameters['BytesRecv'] -eq \"True\") \t { $IisLogFields += New-Object PSObject -Property @{id = \"BytesRecv\"; iisHeader = \"cs-bytes\" } } + if ($OctopusParameters['TimeTaken'] -eq \"True\") \t{ $IisLogFields += New-Object PSObject -Property @{id = \"TimeTaken\"; iisHeader = \"TimeTakenMS\" } } + if ($OctopusParameters['ProtocolVersion'] -eq \"True\") \t{ $IisLogFields += New-Object PSObject -Property @{id = \"ProtocolVersion\"; iisHeader = \"cs-version\" } } + if ($OctopusParameters['Host'] -eq \"True\") \t\t { $IisLogFields += New-Object PSObject -Property @{id = \"Host\"; iisHeader = \"cs(Host)\" } } + if ($OctopusParameters['UserAgent'] -eq \"True\") \t { $IisLogFields += New-Object PSObject -Property @{id = \"UserAgent\"; iisHeader = \"cs(User-Agent)\" } } + if ($OctopusParameters['Cookie'] -eq \"True\") \t\t{ $IisLogFields += New-Object PSObject -Property @{id = \"Cookie\"; iisHeader = \"cs(Cookie)\" } } + if ($OctopusParameters['Referer'] -eq \"True\") \t\t { $IisLogFields += New-Object PSObject -Property @{id = \"Referer\"; iisHeader = \"cs(Referer)\" } } + if ($OctopusParameters['OriginalIP'] -eq \"True\") \t { $IisLogFields += New-Object PSObject -Property @{id = \"OriginalIP\"; iisHeader = \"x-forwarded-for\" } } + return $IisLogFields +} + +function SetForIISAboveV7 { + param($SiteName) + [System.Collections.ArrayList]$logFields = ((GetIisLogFields).id) + $filter = \"/system.applicationHost/sites/site[@Name=\"\"$SiteName\"\"]/logFile\" + write-host \"Filter: $filter\" + + #Clear all existing custom fields... + clear-WebConfiguration -PSPath IIS:\\ -Filter \"$filter/customFields\" + + if ($logFields.Contains(\"OriginalIP\")) { + add-WebConfiguration -PSPath IIS:\\ -Filter \"$filter/customFields\" -Value @{logFieldName='OriginalIP';sourceType='RequestHeader';sourceName='X-FORWARDED-FOR'} + } + + Write-Host (($logFields) -join ',') + # This is part of extended logging and cannot be set using the syntax below. + $logFields.Remove(\"OriginalIP\") + Set-WebConfigurationProperty -Filter $filter -Value (($logFields) -join ',') -Name \"LogExtFileFlags\" +} + +function SetForIISV7 { + param($site, $logDirectory) + Write-Host 'Disables http logging module' + Set-WebConfigurationProperty -Filter system.webServer/httpLogging -PSPath machine/webroot/apphost -Name dontlog -Value true + Write-Host 'Adding X-Forwarded-For as OriginalIP to advanced logging' + if (Get-WebConfigurationProperty \"system.webServer/advancedLogging/server/fields\" -Name Collection |Where-Object {$_.id -eq \"OriginalID\"}) { +\twrite-host \"OriginalID field already exists. Will not modify existing definition.\" + } else { + Add-WebConfiguration \"system.webServer/advancedLogging/server/fields\" -value @{id=\"OriginalID\";sourceName=\"X-Forwarded-For\";sourceType=\"RequestHeader\";logHeaderName=\"X-Forwarded-For\";category=\"Default\";loggingDataType=\"TypeLPCSTR\"} + } + # Disables the default advanced logging config + Set-WebConfigurationProperty -Filter \"system.webServer/advancedLogging/server/logDefinitions/logDefinition[@baseFileName='%COMPUTERNAME%-Server']\" -name enabled -value false + # Enable Advanced Logging + Set-WebConfigurationProperty -Filter system.webServer/advancedLogging/server -PSPath machine/webroot/apphost -Name enabled -Value true + + # Set log directory at server level + Set-WebConfigurationProperty -Filter system.applicationHost/advancedLogging/serverLogs -PSPath machine/webroot/apphost -Name directory -Value $logDirectory + + # Set log directory at site default level + Set-WebConfigurationProperty -Filter system.applicationHost/sites/siteDefaults/advancedLogging -PSPath machine/webroot/apphost -Name directory -Value $logDirectory\t + + AdvancedLogging-GenerateAppCmdScriptToConfigureAndRun $site\t +} + +Write-Host \"Value of UriQuery parameter \" $OctopusParameters['UriQuery'] +$logPath = $OctopusParameters['IISLogPath'] +$IISsitename = $OctopusParameters['webSiteName'] +$iisMajorVersion = (get-itemproperty HKLM:\\SOFTWARE\\Microsoft\\InetStp\\ |select MajorVersion).MajorVersion +if ($iisMajorVersion -gt 7) { + SetForIISAboveV7 $OctopusParameters['SiteName'] + SetIisLogPath $OctopusParameters['iisLogDirectory'] $OctopusParameters['SiteName'] +} elseif ($iisMajorVersion -lt 7) { + Write-Host 'Cannot handle IIS versions below 7. Found IIS version ' $iisMajorVersion + exit 1 +} else { + SetForIISV7 $OctopusParameters['SiteName'] $OctopusParameters['iisLogDirectory'] +} + +", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "1a1520b9-58b1-44ae-b9df-991b34f6b62b", + "Name": "SiteName", + "Label": "SiteName", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "66d6d9d2-3cc0-47f3-bcd1-6b1d9f0846dc", + "Name": "iisLogDirectory", + "Label": "Where should the logs be placed?", + "HelpText": "", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "61656b11-a130-494d-aac4-a352db00a405", + "Name": "Date", + "Label": "Date (date)", + "HelpText": "", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "0c9f888f-5187-4218-b69b-e3b02f5f84d0", + "Name": "Time", + "Label": "Time (time)", + "HelpText": "", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "816117df-bedb-4c2b-8b17-046be5f04954", + "Name": "ClientIP", + "Label": "ClientIP IP Address (c-ip)", + "HelpText": "", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "8b56b533-52b2-4c90-9c8c-bff00e05104c", + "Name": "UserName", + "Label": "User Name (cs-username)", + "HelpText": "", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "97c865b4-cd9f-4e7c-9163-eb3bafdf7a31", + "Name": "ServiceName", + "Label": "Service Name (s-sitename)", + "HelpText": "", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "d84fd363-736d-4f2f-94df-48833c240c32", + "Name": "ComputerName", + "Label": "ServerName (s-computername)", + "HelpText": null, + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "52514137-54a9-4f7c-b701-3f16b0702ff7", + "Name": "ServerIP", + "Label": "Server IP Address (s-ip)", + "HelpText": "", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "66c3b111-8c40-4100-bae2-df4cee69c7a4", + "Name": "Method", + "Label": "Method (cs-method)", + "HelpText": "", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "c049ab95-a33b-401e-be55-1e5a4f216eab", + "Name": "UriStem", + "Label": "UriStem (cs-uri-stem)", + "HelpText": null, + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "ed535131-6fcd-440c-bf41-9356cc1a6b2f", + "Name": "UriQuery", + "Label": "Uri query (cs-uri-query)", + "HelpText": null, + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "2d2d5072-8d94-4098-99a9-646918128f1a", + "Name": "HttpStatus", + "Label": "Protocol Status (sc-status)", + "HelpText": "", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "26839cc3-d338-4637-ba34-09134378dbc9", + "Name": "Win32Status", + "Label": "Win32 Status (sc-win32-status)", + "HelpText": null, + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "b3ddf483-63c6-4f5d-a3db-7e7fc802d8f5", + "Name": "HttpSubStatus", + "Label": "Protocol Substatus (sc-substatus)", + "HelpText": "", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "cc63cdc5-6ce0-4476-960e-aa59eb064095", + "Name": "BytesSent", + "Label": "Bytes sent (sc-bytes)", + "HelpText": "", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "cd121c63-7f76-4259-92aa-d013c7fedb97", + "Name": "BytesRecv", + "Label": "Bytes received (cs-bytes)", + "HelpText": null, + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "da122f26-639e-49fa-b49c-25c281d24bc6", + "Name": "TimeTaken", + "Label": "Time taken (time-taken)", + "HelpText": "", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "1343443a-d3d9-4d84-a16b-76ed7d07d668", + "Name": "ProtocolVersion", + "Label": "Protocol version (cs-version)", + "HelpText": null, + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "7ba34ff4-b677-4d4d-8302-a42b6a774295", + "Name": "Host", + "Label": "Host (cs-host)", + "HelpText": "", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "96379a19-d9c8-4808-9e2c-29a7e13f89a9", + "Name": "UserAgent", + "Label": "User agent (cs(UserAgent))", + "HelpText": null, + "DefaultValue": "true", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "c8d37be1-16e7-46de-92e0-c037c5227aa0", + "Name": "Cookie", + "Label": "Cookie (cs(Cookie))", + "HelpText": "", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "4b4877b4-80f0-46fd-b272-26781706d717", + "Name": "Referer", + "Label": "Referer (cs(Referer))", + "HelpText": null, + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "986223cd-eb82-4744-a27c-3dfba3d8c87a", + "Name": "ServerPort", + "Label": "Server Port (s-port)", + "HelpText": "", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "779b89aa-8631-443f-9108-a44acd92f5ef", + "Name": "OriginalIP", + "Label": "Original IP", + "HelpText": "From X-FORWARDED-FOR in request.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "arnodenuijl", + "$Meta": { + "ExportedAt": "2017-12-22T14:56:00.000Z", + "OctopusVersion": "3.4.1", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-start.json.human b/step-templates/iis-website-start.json.human new file mode 100644 index 000000000..1bace0cca --- /dev/null +++ b/step-templates/iis-website-start.json.human @@ -0,0 +1,49 @@ +{ + "Id": "2b2cf57e-24fa-4094-9513-89647fe5f807", + "Name": "IIS Website - Start", + "Description": "Starts a website in IIS.", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Load IIS module: +Import-Module WebAdministration + +# Set a name of the site we want to start +$webSiteName = $OctopusParameters['webSiteName'] + +# Get web site object +try { +$webSite = Get-Item \"IIS:\\Sites\\$webSiteName\" +} +Catch [System.IO.FileNotFoundException]{ +\t# Some OS bug out if Default Web Site is deleted. +# So we need to call this 2 times. +$webSite = Get-Item \"IIS:\\Sites\\$webSiteName\" +} + +Write-Output \"Starting IIS web site $webSiteName\" +$webSite.Start() +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "WebsiteName", + "Label": "Website name", + "HelpText": "The name of the site in IIS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2020-11-10T07:29:37.424Z", + "OctopusVersion": "2019.13.7", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-stop.json.human b/step-templates/iis-website-stop.json.human new file mode 100644 index 000000000..5489bd14f --- /dev/null +++ b/step-templates/iis-website-stop.json.human @@ -0,0 +1,88 @@ +{ + "Id": "9eb40453-ac5d-4cb0-8666-046ff6305a3a", + "Name": "IIS Website - Stop", + "Description": "Stops a website in IIS.", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Load IIS module:\r +Import-Module WebAdministration\r +\r +# Get WebSite Name\r +$webSiteName = $OctopusParameters['webSiteName']\r +# Get the number of retries\r +$retries = $OctopusParameters['webSiteCheckRetries']\r +# Get the number of attempts\r +$delay = $OctopusParameters['webSiteCheckDelay']\r +\r +# Check if exists\r +if(Test-Path IIS:\\Sites\\$webSiteName) {\r +\r + # Stop Website if not already stopped\r + if ((Get-WebSiteState $webSiteName).Value -ne \"Stopped\") {\r + Write-Output \"Stopping IIS Website $webSiteName\"\r + Stop-WebSite $webSiteName\r + \r + $state = (Get-WebSiteState $webSiteName).Value\r + $counter = 1\r + \r + # Wait for the Website to the \"Stopped\" before proceeding\r + do{ \r + $state = (Get-WebSiteState $webSiteName).Value\r + Write-Output \"$counter/$retries Waiting for IIS Website $webSiteName to shut down completely. Current status: $state\"\r + $counter++\r + Start-Sleep -Milliseconds $delay\r + }\r + while($state -ne \"Stopped\" -and $counter -le $retries) \r + \r + # Throw an error if the Website is not stopped\r + if($counter -gt $retries) { \r + throw \"Could not shut down IIS Website $webSiteName. `nTry to increase the number of retries ($retries) or delay between attempts ($delay milliseconds).\" }\r + }\r +}\r +else {\r + Write-Output \"IIS Website $webSiteName doesn't exist\"\r +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [{ + "Name": "WebsiteName", + "Label": "Website name", + "HelpText": "The name of the site in IIS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "WebSiteCheckDelay", + "Label": "Status check interval", + "HelpText": "The delay, in milliseconds, between each attempt to query the website to see if its status is \"Stopped\"", + "DefaultValue": "500", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "WebSiteCheckRetries", + "Label": "Status check retries", + "HelpText": "The number of retries before an error is thrown.", + "DefaultValue": "20", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2017-02-09T23:04:46.440Z", + "OctopusVersion": "3.3.17", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-update-iis-log-path.json.human b/step-templates/iis-website-update-iis-log-path.json.human new file mode 100644 index 000000000..f19f5e9b4 --- /dev/null +++ b/step-templates/iis-website-update-iis-log-path.json.human @@ -0,0 +1,56 @@ +{ + "Id": "c9c5d076-6936-4781-be3b-3a41912f7b67", + "Name": "IIS Website - Update IIS Log Path", + "Description": "Updates the IIS Log Path if needed.", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.ScriptBody": "write-host \"#Updating IIS Log path\" + +Import-Module \"WebAdministration\" -ErrorAction Stop + +$logPath = $OctopusParameters['LogPath'] +$IISsitename = $OctopusParameters['webSiteName'] + +if (!(Test-Path \"IIS:\\Sites\\$($IISsitename)\")) { + write-host \"$IISsitename does not exist in IIS\" +} else { + $currentLogPath = (Get-ItemProperty IIS:\\Sites\\$($IISsitename)).logFile.directory + write-host \"IIS LogPath currently set to $currentLogPath\" + if ([string]::Compare($currentLogPath, $logPath, $True) -ne 0) { + Set-ItemProperty IIS:\\Sites\\$($IISsitename) -name logFile.directory -value $logPath + write-host \"IIS LogPath updated to $logPath\" + } +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "LogPath", + "Label": "Log path", + "HelpText": "The path where you want to store your logs", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "WebsiteName", + "Label": "Website name", + "HelpText": "The name of the site in IIS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-05-05T09:23:23.465+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/iis-website-update-property.json.human b/step-templates/iis-website-update-property.json.human new file mode 100644 index 000000000..ecd7506e3 --- /dev/null +++ b/step-templates/iis-website-update-property.json.human @@ -0,0 +1,128 @@ +{ + "Id": "34118a0e-f872-435a-8522-d3c7f8515cb8", + "Name": "IIS WebSite - Update Property", + "Description": "Updates property for specified WebSite", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +param( + [string]$webSiteName, + [string]$propertyName, + [string]$propertyValue, + [switch]$whatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if ($result -eq $null -or $result -eq \"\") { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +& { + param( + [string]$webSiteName, + [string]$propertyName, + [string]$propertyValue + ) + + Write-Host \"Setting $webSiteName property $propertyName to $propertyValue\" + + try { + Add-PSSnapin WebAdministration -ErrorAction SilentlyContinue + Import-Module WebAdministration -ErrorAction SilentlyContinue + + $oldValue = Get-ItemProperty \"IIS:\\Sites\\$webSiteName\" -Name $propertyName + $oldValueString = \"\" + + + if ($oldValue.GetType() -eq [Microsoft.IIs.PowerShell.Framework.ConfigurationAttribute]) + { + $oldValueString = ($oldValue | Select-Object -ExpandProperty \"Value\") + } + elseif ($oldValue.GetType() -eq [System.String]) + { + $oldValueString = $oldValue + } + elseif ($oldValue.GetType() -eq [System.Management.Automation.PSCustomObject]) + { + $oldValueString = ($oldValue | Select-Object -ExpandProperty $propertyName) + } + + Write-Host \"Old value $oldValueString\" + Set-ItemProperty \"IIS:\\Sites\\$webSiteName\" -Name $propertyName -Value $propertyValue + Write-Host \"New value $propertyValue\" + Write-Host \"Done\" + } catch { + Write-Host $_.Exception|format-list -force + Write-Host \"There was a problem setting property\" + } + + } ` + (Get-Param 'webSiteName' -Required) (Get-Param 'propertyName' -Required) (Get-Param 'propertyValue' -Required) +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "webSiteName", + "Label": "Web site name", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "propertyName", + "Label": "Name of the property to set", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "propertyValue", + "Label": "Value of the property to set", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2016-05-30T11:42:50.429+00:00", + "LastModifiedBy": "ylashin", + "$Meta": { + "ExportedAt": "2016-05-30T11:42:50.429+00:00", + "OctopusVersion": "3.3.15", + "Type": "ActionTemplate" + }, + "Category": "iis" +} diff --git a/step-templates/import-cert-from-azure-keyvault.json.human b/step-templates/import-cert-from-azure-keyvault.json.human new file mode 100644 index 000000000..af43bb566 --- /dev/null +++ b/step-templates/import-cert-from-azure-keyvault.json.human @@ -0,0 +1,239 @@ +{ + "Id": "e06e7e2a-5510-4b6d-bd46-22d3bc01291d", + "Name": "Import Certificate from Azure Key Vault", + "Description": "Imports a certificate from Azure Key Vault to the tentacle", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Import-Module AzureRM.Profile +Import-Module AzureRM.KeyVault + +Function Validate-Parameter($parameterValue, [string[]]$validInput, $parameterName) { + Write-Host \"${parameterName}: ${parameterValue}\" + if (! $parameterValue) { + throw \"$parameterName cannot be empty, please specify a value\" + } +} + +Function Install-AzureKeyVaultCertificate { + Param( + [string]$keyVaultName, + [string]$certificateName, + [string]$certificateVersion, + [string]$certificateStoreName, + [string]$certificateStoreLocation, + [string]$certificateFriendlyName + ) + + Write-Output \"Retrieving '$certificateName' from '$keyVaultName' ...\" + $getSecretParams = @{ + \tVaultName = $keyVaultName + Name = $certificateName + } + +\tif($certificateVersion -notmatch \"latest\") { + $getSecretParams[\"Version\"] = $certificateVersion + } + +\t$cert = Get-AzureKeyVaultSecret @getSecretParams + $b64 = [System.Convert]::FromBase64String($cert.SecretValueText) + $pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($b64, \"\", \"MachineKeySet,PersistKeySet\") + Write-Output \"Certificate information:\" + Write-Output ($pfx | fl | Out-String) + + $certPath = \"Cert:\\$certificateStoreLocation\\$certificateStoreName\\$($pfx.Thumbprint)\" + if (Test-Path $certPath) { + \"A certificate with thumbprint '$($pfx.Thumbprint)' appears to already exist in the certificate store. Skipping...\" + } + else { + Write-Output \"Opening certificate store '$certificateStoreName' in '$certificateStoreLocation' ...\" + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store($certificateStoreName, $certificateStoreLocation) + $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + +\t\tif($certificateFriendlyName) { + Write-Output \"Setting certificate friendly name to '$certificateFriendlyName'...\" + $pfx.FriendlyName = $certificateFriendlyName +\t\t} + + Write-Output \"Adding certificate...\" + $store.Add($pfx) + $store.Close() + Write-Output \"Certificate added.\" + + Write-Output \"Verifying - searching certificate store for thumbprint '$($pfx.Thumbprint)'...\" + if (Test-Path $certPath) { + Write-Output \"Certificate is successfully imported!\" + } + else { + Write-Error \"ERROR: Certificate with thumbprint '$($pfx.Thumbprint)' was not found in certificate store '$certificateStoreName' in '$certificateStoreLocation'\" + } + } +} + +$azureSubscriptionId = $OctopusParameters['Azure.GetKeyVaultCertificate.SubscriptionId'] +$azureTenantId = $OctopusParameters['Azure.GetKeyVaultCertificate.TenantId'] +$azureClientId = $OctopusParameters['Azure.GetKeyVaultCertificate.ClientId'] +$azurePassword = $OctopusParameters['Azure.GetKeyVaultCertificate.Password'] +$keyVaultName = $OctopusParameters['Azure.GetKeyVaultCertificate.KeyVaultName'] +$certificateName = $OctopusParameters['Azure.GetKeyVaultCertificate.CertificateName'] +$certificateVersion = $OctopusParameters['Azure.GetKeyVaultCertificate.CertificateVersion'] +$certificateStoreName = $OctopusParameters['Azure.GetKeyVaultCertificate.CertificateStoreName'] +$certificateStoreLocation = $OctopusParameters['Azure.GetKeyVaultCertificate.CertificateStoreLocation'] +$certificateFriendlyName = $OctopusParameters['Azure.GetKeyVaultCertificate.CertificateFriendlyName'] + +# Validate that all parameters have values +Write-Output \"Validating parameters...\" +Validate-Parameter $azureSubscriptionId -parameterName \"azureSubscriptionId\" +Validate-Parameter $azureTenantId -parameterName \"azureTenantId\" +Validate-Parameter $azureClientId -parameterName \"azureClientId\" +Validate-Parameter $azurePassword -parameterName \"azurePassword\" +Validate-Parameter $keyVaultName -parameterName \"keyVaultName\" +Validate-Parameter $certificateName -parameterName \"certificateName\" +Validate-Parameter $certificateVersion -parameterName \"certificateVersion\" +Validate-Parameter $certificateStoreName -parameterName \"certificateStoreName\" +Validate-Parameter $certificateStoreLocation -parameterName \"certificateStoreLocation\" + +$azureCreds = New-Object System.Management.Automation.PSCredential($azureClientId, (ConvertTo-SecureString -String $azurePassword -AsPlainText -Force)) +Login-AzureRmAccount -ServicePrincipal -SubscriptionId $azureSubscriptionId -TenantId $azureTenantId -Credential $azureCreds + +$params = @{ + keyVaultName = $keyVaultName + certificateName = $certificateName + certificateVersion = $certificateVersion + certificateStoreName = $certificateStoreName + certificateStoreLocation = $certificateStoreLocation + certificateFriendlyName = $certificateFriendlyName +} + +Install-AzureKeyVaultCertificate @params" + }, + "Parameters": [ + { + "Id": "70c9f9dd-22b6-4285-8d8a-f64278de0dc1", + "Name": "Azure.GetKeyVaultCertificate.SubscriptionId", + "Label": "Azure Service Principal SubscriptionId", + "HelpText": "Azure SubscriptionId for the Service Principal account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "9a421884-0f63-417e-b2a9-b1039a1e8bf8", + "Name": "Azure.GetKeyVaultCertificate.TenantId", + "Label": "Azure Active Directory Tenant Id", + "HelpText": "The Azure Active Directory Tenant Id associated with the Service Principal account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "bfb4a0a1-dab2-4c8f-bcb8-51033c35f633", + "Name": "Azure.GetKeyVaultCertificate.ClientId", + "Label": "Azure Service Principal Client Id", + "HelpText": "The Client Id associated with the Service Principal account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "49857bcc-f3a1-4984-a2b1-ddeeca52114a", + "Name": "Azure.GetKeyVaultCertificate.Password", + "Label": "Azure Service Principal Password", + "HelpText": "The password or \"key\" for the Service Principal account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "220d17f6-070c-4a3d-b742-205d56b27f47", + "Name": "Azure.GetKeyVaultCertificate.KeyVaultName", + "Label": "Key Vault Name", + "HelpText": "The name of the Azure Key Vault", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "930e6703-3df4-40bb-b3ae-6d367bf5cc5d", + "Name": "Azure.GetKeyVaultCertificate.CertificateName", + "Label": "Certificate Name", + "HelpText": "The name of the certificate to retrieve from the Key Vault", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b3616901-a27a-4960-984c-59b2388b243e", + "Name": "Azure.GetKeyVaultCertificate.CertificateVersion", + "Label": "Certificate Version", + "HelpText": "_[Optional]_ Enter the specific version of the certificate. Defaults to `latest`.", + "DefaultValue": "latest", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "840f8939-4d87-42c7-9d6e-232d4617b90f", + "Name": "Azure.GetKeyVaultCertificate.CertificateStoreName", + "Label": "Certificate Store Name", + "HelpText": "Certificate store name. E.g. `My`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "My|My +CertificateAuthority|CertificateAuthority +Root|Root +TrustedPeople|TrustedPeople +TrustedPublisher|TrustedPublisher" + }, + "Links": {} + }, + { + "Id": "15916c8a-709b-4f14-af36-63ee5d3265e9", + "Name": "Azure.GetKeyVaultCertificate.CertificateStoreLocation", + "Label": "Certificate Store Location", + "HelpText": "Certificate store location. E.g. \"LocalMachine\"", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "LocalMachine|LocalMachine +CurrentUser|CurrentUser" + }, + "Links": {} + }, + { + "Id": "3915f38e-947f-4313-b207-4e88b5f63969", + "Name": "Azure.GetKeyVaultCertificate.CertificateFriendlyName", + "Label": "Certificate Friendly Name", + "HelpText": "_[Optional]_ A friendly name to give the certificate when importing. E.g. `Client Auth Cert for FooService`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "nshenoy", + "$Meta": { + "ExportedAt": "2018-04-17T20:24:57.757Z", + "OctopusVersion": "2018.3.13", + "Type": "ActionTemplate" + }, + "Category": "azure" +} diff --git a/step-templates/import-databricks-workbooks.json.human b/step-templates/import-databricks-workbooks.json.human new file mode 100644 index 000000000..f4954eab4 --- /dev/null +++ b/step-templates/import-databricks-workbooks.json.human @@ -0,0 +1,204 @@ +{ + "Id": "0bbe289c-3ea9-47f8-970c-caa946878f49", + "Name": "Import Databricks Workbooks", + "Description": "Import Databricks workbooks (current supported files `.ipynb` and `.scala`) to a databricks instance", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "7885715a-c3e8-492a-ba61-23c34d2e9447", + "Name": "DeployDataBricksWorkBookPackage", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "DeployDataBricksWorkBookPackage" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\" #region fucntions +function Set-DatabricksWorkBook +{ + [CmdletBinding()] + param ( + [Parameter()] + [String] + $AccessToken, + [Parameter()] + [String] + $DataBricksInstanceUri, + [Parameter()] + [String] + $WorkbooksUploadPath, + [Parameter()] + [String] + $DatabrickImportFolder + ) + + $headers = @{ + 'Authorization' = (\"Bearer {0}\" -f $AccessToken ) + } + $APIVersion = '/api/2.0' + $APICommand = '/workspace/import' + $Uri = \"https://$DataBricksInstanceUri$APIVersion$APICommand\" + + Get-ChildItem -Path $WorkbooksUploadPath -Recurse -File | ForEach-Object{ $currentWorkBook = $_ + Write-Host (\"Importing Workbook:{0}\" -f $currentWorkBook.FullName) + \t\t$workbookContent = [Convert]::ToBase64String((Get-Content -path $currentWorkBook.FullName -Encoding byte)) + + if($DatabrickImportFolder.EndsWith(\"/\")) + \t{ + \t\t$workbookPath = \"{0}{1}\" -f $DatabrickImportFolder , $currentWorkBook.BaseName + \t} + \telse + \t{ + \t$workbookPath = \"{0}/{1}\" -f $DatabrickImportFolder , $currentWorkBook.BaseName + \t} + + switch ($currentWorkBook.Extension.ToLower()) + { + '.ipynb' { + $workbookLanguage = \"PYTHON\" + $workbookFormat = \"JUPYTER\" + break + } + '.scala' { + $workbookLanguage = \"SCALA\" + $workbookFormat = \"SOURCE\" + break + } + Default + { + $workbookLanguage = \"SQL\" + $workbookFormat = \"SOURCE\" + } + } + $requestBody = ConvertTo-Json -InputObject @{ + content = $workbookContent + path = $workbookPath + language = $workbookLanguage + format = $workbookFormat + overwrite = $true + } + + $apiResponse = Invoke-RestMethod -Method Post -Uri $Uri -Headers $headers -Body $requestBody + return $apiResponse + } +} + + +function Set-DatabricksWorkspaceFolder +{ + [CmdletBinding()] + param ( + [Parameter()] + [String] + $AccessToken, + [Parameter()] + [String] + $DataBricksInstanceUri, + [Parameter()] + [String] + $DatabrickFolder + ) + + $headers = @{ + 'Authorization' = (\"Bearer {0}\" -f $AccessToken ) + } + $APIVersion = '/api/2.0' + $APIListCommand = '/workspace/list' + $APIMkdirsCommand = '/workspace/mkdirs' + $ListUri = \"https://$DataBricksInstanceUri$APIVersion$APIListCommand\" + $MkdirsUri = \"https://$DataBricksInstanceUri$APIVersion$APIMkdirsCommand\" + + $pathRoute = $DatabrickFolder.Substring(1) -split '/' + $basePath = \"/\" + foreach($path in $pathRoute) + { + $requestBody = @{ + path = $basePath + } + $apiResponse = Invoke-RestMethod -Uri $ListUri -Headers $headers -Body $requestBody -ContentType application/json + $workSpaceFolder = $apiResponse.objects | Where-Object {$_.object_type -eq \"DIRECTORY\" -and $_.path -eq ( \"{0}{1}\" -f $basePath , $path) } + if($null -eq $workSpaceFolder) + { + $requestBody = ConvertTo-Json -InputObject @{ + path = ( \"{0}{1}\" -f $basePath , $path) + } + Invoke-RestMethod -Method Post -Uri $MkdirsUri -Headers $headers -Body $requestBody + } + $basePath = \"{0}/{1}/\" -f $basePath , $path + if($basePath.StartsWith(\"//\")) + { + $basePath = $basePath.Substring(1) + } + } +} + +#endregion fucntions + + +$DatabrickWorkBookImportFolder = $OctopusParameters[\"Octopus.Action.Package[DeployDataBricksWorkBookPackage].ExtractedPath\"] + +Write-Host \"Checking WorkSpace Folders\" +Set-DatabricksWorkspaceFolder -AccessToken $DataBricksAccessToken -DataBricksInstanceUri $DataBricksInstanceUri -DatabrickFolder $DatabricksImportFolder +Write-Host \"Importing Databricks Workbooks\" +Set-DatabricksWorkBook -AccessToken $DataBricksAccessToken -DataBricksInstanceUri $DataBricksInstanceUri -WorkbooksUploadPath $DatabrickWorkBookImportFolder -DatabrickImportFolder $DatabricksImportFolder +" + }, + "Parameters": [ + { + "Id": "b8cd5d73-29d9-4ba2-bdfa-86cc1316a16f", + "Name": "DeployDataBricksWorkBookPackage", + "Label": "DataBricks WorkBook Package", + "HelpText": "The Databricks workbook package", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "f81b9ccb-beea-4d8f-a049-ee9ea6da643e", + "Name": "DataBricksInstanceUri", + "Label": "Databricks Instance Uri", + "HelpText": "The Databricks Instance URL", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6c1aed25-f9a6-4c1f-b8e9-d50cc8669f2d", + "Name": "DataBricksAccessToken", + "Label": "Databricks Access Token", + "HelpText": "The access token to authenticate against the Databricks instance", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c95af088-180b-419e-8c44-ce3fb4d96f57", + "Name": "DatabricksImportFolder", + "Label": "Databricks Workbook Import Folder", + "HelpText": "Databricks Workbook import folder location", + "DefaultValue": "/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-10-05T09:38:27.445Z", + "OctopusVersion": "2021.2.7428", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "Zogamorph", + "Category": "databricks" +} diff --git a/step-templates/jasper-clear-cache.json.human b/step-templates/jasper-clear-cache.json.human new file mode 100644 index 000000000..48f8dcdd3 --- /dev/null +++ b/step-templates/jasper-clear-cache.json.human @@ -0,0 +1,81 @@ +{ + "Id": "a9fd43af-257e-49c9-9434-618568e1df52", + "Name": "Clear Jasper Web-Cache", + "Description": "Clears the Jasper web cache", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Name: ClearCache.ps1\r +# Author: Matt Smith\r +# Created Date: 28 July 2014\r +# Modified Date: 13 October 2014\r +# Version: 1.3\r +\r +$servers = $OctopusParameters['fqdn'] -split \";\"\r +\r +foreach ($server in $servers)\r +{\r + Write-Host 'Clearing cache in '$server\r + $url = 'http://' + $server + '/' + $OctopusParameters['environment'] + '_web/report/meta'\r +\r + Function ClearCache($type)\r + { \r + return Invoke-WebRequest -Uri $url/$type -Method GET -Headers @{\"Authorization\" = \"Basic \"+[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($OctopusParameters['username']+\":\"+$OctopusParameters['password'] ))}\r + }\r + \r + # Clear cache\r + $reportresult = ClearCache -type 'reportcache?CLEAR=Clear+Cache'\r + $templateresult = ClearCache -type 'templatecache?CLEAR=Clear+Cache'\r + $imageresult = ClearCache -type 'imagescache?CLEAR=Clear+Cache'\r +\r +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "fqdn", + "Label": "Server Name", + "HelpText": "Enter the server name of your Jasper web server", + "DefaultValue": "server", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "environment", + "Label": "Environment", + "HelpText": "Enter the environment", + "DefaultValue": "dev", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "username", + "Label": "Username", + "HelpText": "Enter the username to authenticate.", + "DefaultValue": "username", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "password", + "Label": "Password", + "HelpText": "Enter the password", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-09-01T16:24:16.807+00:00", + "LastModifiedBy": "matt40k", + "$Meta": { + "ExportedAt": "2015-07-09T12:17:59.666+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "jasper" +} diff --git a/step-templates/jira-transition-issues.json.human b/step-templates/jira-transition-issues.json.human new file mode 100644 index 000000000..dc1e9335b --- /dev/null +++ b/step-templates/jira-transition-issues.json.human @@ -0,0 +1,233 @@ +{ + "Id": "f0730d85-6ada-44d9-bd95-63b5c236e716", + "Name": "JIRA - Transition Issues", + "Description": "Transitions JIRA issues as the code they are associated with gets deployed.", + "ActionType": "Octopus.Script", + "Version": 9, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\" +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 + +$Uri = $OctopusParameters[\"Jira.Transition.Url\"] +$Jql = $OctopusParameters[\"Jira.Transition.Query\"] +$Transition = $OctopusParameters[\"Jira.Transition.Name\"] +$User = $OctopusParameters[\"Jira.Transition.Username\"] +$Password = $OctopusParameters[\"Jira.Transition.Password\"] + +if ([string]::IsNullOrWhitespace($Uri)) { + throw \"Missing parameter value for 'Jira.Transition.Url'\" +} +if ([string]::IsNullOrWhitespace($Jql)) { + throw \"Missing parameter value for 'Jira.Transition.Query'\" +} +if ([string]::IsNullOrWhitespace($Transition)) { + throw \"Missing parameter value for 'Jira.Transition.Name'\" +} +if ([string]::IsNullOrWhitespace($User)) { + throw \"Missing parameter value for 'Jira.Transition.Username'\" +} +if ([string]::IsNullOrWhitespace($Password)) { + throw \"Missing parameter value for 'Jira.Transition.Password'\" +} + +function Create-Uri { + Param ( + $BaseUri, + $ChildUri + ) + + if ([string]::IsNullOrWhitespace($BaseUri)) { + throw \"BaseUri is null or empty!\" + } + if ([string]::IsNullOrWhitespace($ChildUri)) { + throw \"ChildUri is null or empty!\" + } + $CombinedUri = \"$($BaseUri.TrimEnd(\"/\"))/$($ChildUri.TrimStart(\"/\"))\" + return New-Object -TypeName System.Uri $CombinedUri +} + +function Jira-QueryApi { + Param ( + [Uri]$Query, + [string]$Username, + [string]$Password + ); + + Write-Output \"Querying JIRA API $($Query.AbsoluteUri)\" + + # Prepare the Basic Authorization header - PSCredential doesn't seem to work + $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $Username, $Password))) + $headers = @{Authorization = (\"Basic {0}\" -f $base64AuthInfo) } + + # Execute the query + Invoke-RestMethod -Uri $Query -Headers $headers +} + +function Jira-ExecuteApi { + Param ( + [Uri]$Query, + [string]$Body, + [string]$Username, + [string]$Password + ); + + Write-Output \"Posting JIRA API $($Query.AbsoluteUri)\" + + $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $Username, $Password))) + $headers = @{Authorization = (\"Basic {0}\" -f $base64AuthInfo) } + + Invoke-RestMethod -Uri $Query -Headers $headers -UseBasicParsing -Body $Body -Method Post -ContentType \"application/json\" +} + +function Jira-GetTransitions { + Param ( + [Uri]$TransitionsUri, + [string]$Username, + [string]$Password + ); + + $transitions = Jira-QueryApi -Query $TransitionsUri -Username $Username -Password $Password + $transitions.transitions +} + +function Jira-PostTransition { + Param ( + [Uri]$TransitionsUri, + [string]$Username, + [string]$Password, + [string]$Body + ); + + Jira-ExecuteApi -Query $TransitionsUri -Body $body -Username $Username -Password $Password +} + +function Jira-TransitionTicket { + Param ( + [Uri]$IssueUri, + [string]$Username, + [string]$Password, + [string]$Transition + ); + + $query = $IssueUri.AbsoluteUri + \"/transitions\" + $uri = [System.Uri] $query + + $transitions = Jira-GetTransitions -TransitionsUri $uri -Username $Username -Password $Password + $match = $transitions | Where-Object name -eq $Transition | Select-Object -First 1 + $comment = \"Status automatically updated via Octopus Deploy with release {0} of {1} to {2}\" -f $OctopusParameters['Octopus.Action.Package.PackageVersion'], $OctopusParameters['Octopus.Project.Name'], $OctopusParameters['Octopus.Environment.Name'] + + If ($null -ne $match) { + $transitionId = $match.id + $body = \"{ \"\"update\"\": { \"\"comment\"\": [ { \"\"add\"\" : { \"\"body\"\" : \"\"$comment\"\" } } ] }, \"\"transition\"\": { \"\"id\"\": \"\"$transitionId\"\" } }\" + + Jira-PostTransition -TransitionsUri $uri -Body $body -Username $Username -Password $Password + } +} + +function Jira-TransitionTickets { + Param ( + [string]$BaseUri, + [string]$Username, + [string]$Password, + [string]$Jql, + [string]$Transition + ); + + $childUri = (\"/rest/api/2/search?jql=\" + $Jql) + $queryUri = Create-Uri -BaseUri $BaseUri -ChildUri $childUri + + $json = Jira-QueryApi -Query $queryUri -Username $Username -Password $Password + + If ($json.total -eq 0) { + Write-Output \"No issues were found that matched your query : $Jql\" + } + Else { + ForEach ($issue in $json.issues) { + Jira-TransitionTicket -IssueUri $issue.self -Transition $Transition -Username $Username -Password $Password + } + } +} + +Write-Output \"JIRA - Create Transition\" +Write-Output \" JIRA URL : $Uri\" +Write-Output \" JIRA JQL : $Jql\" +Write-Output \" Transition : $Transition\" +Write-Output \" Username : $User\" + +# Some sample values: +# $uri = \"http://tempuri.org\" +# $Jql = \"fixVersion = 11.3.1 AND status = Completed\" +# $Ttransition = \"Deploy\" +# $User = \"admin\" +# $Pass = \"admin\" + +try { + Jira-TransitionTickets -BaseUri $Uri -Jql $Jql -Transition $Transition -Username $User -Password $Password +} +catch { + Write-Error \"An error occurred while attempting to transition the JIRA issues: $($_.Exception)\" +}" + }, + "Parameters": [ + { + "Id": "9da7c3ad-e06f-4727-8519-3860fbee6420", + "Name": "Jira.Transition.Url", + "Label": "JIRA URL", + "HelpText": "The base URL of the JIRA Server (e.g. http://tempuri.org/jira)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b37a4dde-f7f4-4e30-b992-3ebf6a6d6963", + "Name": "Jira.Transition.Username", + "Label": "Username", + "HelpText": "The username of the account that will be used to run the transition. The account should have sufficient permissions in JIRA to run the transition.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1f28d6cd-3d73-4ac2-b5ab-750b9efaabff", + "Name": "Jira.Transition.Password", + "Label": "Password", + "HelpText": "The password of the account that will be used to run the transaction.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "38aaca97-9fdc-4eef-8c90-8dfd2cf2844e", + "Name": "Jira.Transition.Name", + "Label": "Transition", + "HelpText": "The name of the transition that should be applied to the JIRA tickets. If an issue does not have the named transition, it will be ignored.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e16422bb-fd58-401e-bc25-a3817bab65e3", + "Name": "Jira.Transition.Query", + "Label": "JQL", + "HelpText": "The JIRA query that should be used to select issues that will be transitioned (e.g. status = Completed AND fixVersion = 1.2.3)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2022-01-26T15:11:13.454Z", + "OctopusVersion": "2021.3.12055", + "Type": "ActionTemplate" + }, + "Category": "jira" +} diff --git a/step-templates/jira-update-version.json.human b/step-templates/jira-update-version.json.human new file mode 100644 index 000000000..1f293c681 --- /dev/null +++ b/step-templates/jira-update-version.json.human @@ -0,0 +1,314 @@ +{ + "Id": "c691729f-685a-4339-bba9-716633b221ae", + "Name": "JIRA - Update Version Number", + "Description": "Update version to Jira tickets based on release number being deployed", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "#require version 3.0 + +param ( + [System.Uri]$Uri, + [string]$Jql, + [string]$Version, + [string]$User, + [string]$Password, + [string]$ProjectKey +) + +$ErrorActionPreference = \"Stop\" +$AllProtocols = [System.Net.SecurityProtocolType]'Tls,Tls11,Tls12' +[Net.ServicePointManager]::SecurityProtocol = $AllProtocols + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if ($result -eq $null) { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +function Jira-QueryApi +{ + Param ( + [Uri]$Query, + [string]$Username, + [string]$Password + ); + + Write-Host \"Querying JIRA API $($Query.AbsoluteUri)\" + + # Prepare the Basic Authorization header - PSCredential doesn't seem to work + $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $Username,$Password))) + $headers = @{Authorization=(\"Basic {0}\" -f $base64AuthInfo)} + + # Execute the query + Invoke-RestMethod -Uri $Query -Headers $headers +} + +function Jira-ExecuteApi +{ + Param ( + [Uri]$Query, + [string]$Body, + [string]$Username, + [string]$Password + ); + + Write-Host \"Updating ticket : $($Query.AbsoluteUri)\" + + $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $Username,$Password))) + $headers = @{Authorization=(\"Basic {0}\" -f $base64AuthInfo)} + Invoke-RestMethod -Uri $Query -Headers $headers -UseBasicParsing -Body $Body -Method Put -ContentType \"application/json\" +} + +function Jira-CreateVersion +{ + Param ( + [Uri]$Query, + [string]$Body, + [string]$Username, + [string]$Password + ); + + Write-Host \"Creating a version $($Query.AbsoluteUri)\" + + $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $Username,$Password))) + $headers = @{Authorization=(\"Basic {0}\" -f $base64AuthInfo)} + Invoke-RestMethod -Uri $Query -Headers $headers -UseBasicParsing -Body $Body -Method Post -ContentType \"application/json\" +} + +function Jira-GetVersions +{ + Param ( + [Uri]$VersionsUri, + [string]$Username, + [string]$Password + ); + + $versions = Jira-QueryApi -Query $VersionsUri -Username $Username -Password $Password + $versions +} + +function Jira-PostUpdate +{ + Param ( + [Uri]$IssueUri, + [string]$Username, + [string]$Password, + [string]$Body + ); + + Jira-ExecuteApi -Query $IssueUri -Body $body -Username $Username -Password $Password +} + +function Jira-UpdateTicket +{ + Param ( + \t[Uri]$BaseUri, + [Uri]$IssueUri, + [string]$Username, + [string]$Password, + [string]$Version, + [string]$ProjectKey, + [System.Uri]$GetVersionsAPIURL, + [System.Uri]$CreateVersionAPIURL + ); + + $query = $IssueUri.AbsoluteUri + $uri = [System.Uri] $query +\t + $versionuri = $GetVersionsAPIURL + $createversionuri = $CreateVersionAPIURL + + $versions = Jira-GetVersions -VersionsUri $versionuri -Username $Username -Password $Password + + $match = $versions | Where name -eq $Version | Select -First 1 + + If ($match -ne $null) + { +\t\t$body = \"{ \"\"update\"\" : { \"\"fixVersions\"\" : [ {\"\"add\"\" : {\"\"name\"\" : \"\"$Version\"\"} } ] } }\" + Jira-PostUpdate -IssueUri $uri -Body $body -Username $Username -Password $Password + } + else + { + \t$body = \"{ \"\"name\"\": \"\"$Version\"\",\t\"\"project\"\": \"\"$ProjectKey\"\"}\" + \tJira-CreateVersion -Query $createversionuri -Body $body -Username $Username -Password $Password + + $body = \"{ \"\"update\"\" : { \"\"fixVersions\"\" : [ {\"\"add\"\" : {\"\"name\"\" : \"\"$Version\"\"} } ] } }\" + Jira-PostUpdate -IssueUri $uri -Body $body -Username $Username -Password $Password + } +} + +function Jira-UpdateTickets +{ + Param ( + [Uri]$BaseUri, + [string]$Username, + [string]$Password, + [string]$Jql, + [string]$Version, + [string]$ProjectKey, + [System.Uri]$GetVersionsAPIURL, + [System.Uri]$CreateVersionAPIURL + ); + + $api = New-Object -TypeName System.Uri -ArgumentList $BaseUri, (\"/rest/api/2/search?jql=\" + $Jql) + $json = Jira-QueryApi -Query $api -Username $Username -Password $Password + + If ($json.total -eq 0) + { + Write-Output \"No issues were found that matched your query : $Jql\" + } + Else + { + ForEach ($issue in $json.issues) + { + Jira-UpdateTicket -BaseUri $BaseUri -IssueUri $issue.self -Version $Version -Username $Username -Password $Password -ProjectKey $ProjectKey -GetVersionsAPIURL $GetVersionsAPIURL -CreateVersionAPIURL $CreateVersionAPIURL + } + } +} + +& { + param( + [System.Uri]$Uri, + [string]$Jql, + [string]$Version, + [string]$User, + [string]$Password, + [string]$ProjectKey, + [System.Uri]$GetVersionsAPIURL, + [System.Uri]$CreateVersionAPIURL + ) + + Write-Host \"JIRA - Update Version Number\" + Write-Host \" Updating Fix Versions to : $Version\" + + try { + Jira-UpdateTickets -BaseUri $Uri -Jql $Jql -Version $Version -Username $User -Password $Password -ProjectKey $ProjectKey -GetVersionsAPIURL $GetVersionsAPIURL -CreateVersionAPIURL $CreateVersionAPIURL + } catch { + Write-Host -ForegroundColor Red \"An error occurred while attempting to update Fix Versions in JIRA issues\" + Write-Host -ForegroundColor Red $_.Exception | Format-List -Force + } +} ` +(Get-Param \"Jira.Version.Url\" -Required) ` +(Get-Param \"Jira.Version.Query\" -Required) ` +(Get-Param \"Jira.Version.Name\" -Required) ` +(Get-Param \"Jira.Version.Username\" -Required) ` +(Get-Param \"Jira.Version.Password\" -Required) ` +(Get-Param \"Jira.Version.ProjectKey\" -Required) ` +(Get-Param \"Jira.Version.GetVersionsAPIURL\" -Required) ` +(Get-Param \"Jira.Version.CreateVersionAPIURL\" -Required) +" + }, + "Parameters": [ + { + "Id": "4f231879-328a-4ea0-986c-aa6a2abf8e40", + "Name": "Jira.Version.Url", + "Label": "JIRA URL", + "HelpText": "The base URL of the JIRA Server (e.g. https://company.atlassian.net)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b16ac5da-e984-4f46-8cbd-793401be3a9b", + "Name": "Jira.Version.Username", + "Label": "Username", + "HelpText": "The username of the account that will be used to update Jira ticket. The account should have sufficient permissions in JIRA to update ticket.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "056b87ba-930c-4607-81e4-557503daa237", + "Name": "Jira.Version.Password", + "Label": "Password", + "HelpText": "The password of the account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "458dcb98-1843-41c7-bf22-c451a18aa693", + "Name": "Jira.Version.Name", + "Label": "Version", + "HelpText": "The version that needs to updated to Fix Versions field in Jira ticket. If the version is not found, a new version will be created under respective project", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d7f0fddd-9518-4070-9c78-166b2dc67197", + "Name": "Jira.Version.Query", + "Label": "JQL", + "HelpText": "The JIRA query that should be used to select issues that will be updated with Fix Versions (e.g. status = Completed AND Status = \"Ready for Test Deploy\")", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "30334b2e-db2f-4a3e-878b-511686a58c9f", + "Name": "Jira.Version.ProjectKey", + "Label": "Project Key", + "HelpText": "The project where version to be created", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d858e2de-4dcc-4b1f-bd56-f0cb4d11b347", + "Name": "Jira.Version.GetVersionsAPIURL", + "Label": "Get Versions API URL", + "HelpText": "API URL to get all the versions from a particular project.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9747a96d-788e-45c7-816c-ee1bf91a6665", + "Name": "Jira.Version.CreateVersionAPIURL", + "Label": "Create Version API URL", + "HelpText": "API URL to create version on a particular project.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2019-03-26T19:05:07.140Z", + "OctopusVersion": "2018.10.2", + "Type": "ActionTemplate" + } +} diff --git a/step-templates/json-merge.json.human b/step-templates/json-merge.json.human new file mode 100644 index 000000000..06fc819b8 --- /dev/null +++ b/step-templates/json-merge.json.human @@ -0,0 +1,146 @@ +{ + "Id": "6bb1fb50-de38-4380-a5ef-7f21ac3a3e6f", + "Name": "JSON - Merge Files", + "Description": "Merge JSON files", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "function Get-RequiredParam($Name) {\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + $variable = Get-Variable $Name -EA SilentlyContinue \r + if ($variable -ne $null) {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null) {\r +\t\tthrow \"Missing parameter value $Name\"\r + }\r +\r + return $result\r +}\r +\r +function Merge-Objects($file1, $file2) {\r + $propertyNames = $($file2 | Get-Member -MemberType *Property).Name\r + foreach ($propertyName in $propertyNames) {\r +\t\t# Check if property already exists\r + if ($file1.PSObject.Properties.Match($propertyName).Count) {\r + if ($file1.$propertyName.GetType().Name -eq 'PSCustomObject') {\r +\t\t\t\t# Recursively merge subproperties\r + $file1.$propertyName = Merge-Objects $file1.$propertyName $file2.$propertyName\r + } else {\r +\t\t\t\t# Overwrite Property\r + $file1.$propertyName = $file2.$propertyName\r + }\r + } else {\r +\t\t\t# Add property\r + $file1 | Add-Member -MemberType NoteProperty -Name $propertyName -Value $file2.$propertyName\r + }\r + }\r + return $file1\r +}\r +\r +function Merge-Json($sourcePath, $transformPath, $failIfTransformMissing, $outputPath) {\r +\tif(!(Test-Path $sourcePath)) {\r +\t\tWrite-Host \"Source file $sourcePath does not exist!\"\r +\t\tExit 1\r +\t}\r +\t\r +\t$sourceObject = (Get-Content -Path $sourcePath -Encoding UTF8) -join \"`n\" | ConvertFrom-Json\r +\t$mergedObject = $sourceObject\r +\t\r +\tif (!(Test-Path $transformPath)) {\r +\t\tWrite-Host \"Transform file $transformPath does not exist!\"\r +\t\tif ([System.Convert]::ToBoolean($failIfTransformMissing)) {\r +\t\t\tExit 1\r +\t\t}\r +\t\tWrite-Host 'Source file will be written to output without changes'\r +\t} else {\r +\t\tWrite-Host 'Applying transformations'\r +\t\t$transformObject = (Get-Content -Path $transformPath -Encoding UTF8) -join \"`n\" | ConvertFrom-Json\r +\t\t$mergedObject = Merge-Objects $sourceObject $transformObject\r +\t}\r +\t\r +\tWrite-Host \"Writing merged JSON to $outputPath...\"\r +\t$mergedJson = $mergedObject | ConvertTo-Json -Depth 200\r +\t[System.IO.File]::WriteAllLines($outputPath, $mergedJson)\r +}\r +\r +$ErrorActionPreference = 'Stop'\r +\r +if($OctopusParameters -eq $null) {\r + Write-Host 'OctopusParameters is null...exiting with 1'\r + Exit 1 \r +}\r +\r +$sourceFilePath = Get-RequiredParam 'jmf_SourceFile'\r +$transformFilePath = Get-RequiredParam 'jmf_TransformFile'\r +$failIfTransformFileMissing = Get-RequiredParam 'jmf_FailIfTransformFileMissing'\r +$outputFilePath = Get-RequiredParam 'jmf_OutputFile'\r +\r +Merge-Json $sourceFilePath $transformFilePath $failIfTransformFileMissing $outputFilePath" + }, + "Parameters": [ + { + "Id": "ff6ebb11-b388-460b-835e-5cf98ae394ed", + "Name": "jmf_SourceFile", + "Label": "Source file", + "HelpText": "Path to the source file", + "DefaultValue": "Required", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f4695bf4-817a-4029-92b8-3e0c9f7da324", + "Name": "jmf_TransformFile", + "Label": "Transform file", + "HelpText": "Path to the file with the changes", + "DefaultValue": "Required", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "8b5e7f58-a167-46c9-9260-6c49f4945943", + "Name": "jmf_FailIfTransformFileMissing", + "Label": "Fail if transform file missing", + "HelpText": "Should the script fail if the transformation file is missing?", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "16e43e45-5bc7-4e13-a0d1-4c699cadd259", + "Name": "jmf_OutputFile", + "Label": "Output file", + "HelpText": "Path to the output file", + "DefaultValue": "Required", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "jonasjanusauskas", + "$Meta": { + "ExportedAt": "2019-01-31T13:01:36.386Z", + "OctopusVersion": "2018.2.3", + "Type": "ActionTemplate" + }, + "Category": "json" +} diff --git a/step-templates/json-validate-format-and-schema.json.human b/step-templates/json-validate-format-and-schema.json.human new file mode 100644 index 000000000..02819d2d2 --- /dev/null +++ b/step-templates/json-validate-format-and-schema.json.human @@ -0,0 +1,118 @@ +{ + "Id": "7f4f6d86-50aa-43b1-be7b-f0ab27e4a0ac", + "Name": "JSON - Validate object", + "Description": "Validates the consistency of an input JSON object against provided JSON schema", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Python", + "Octopus.Action.Script.ScriptBody": "import json +from jsonschema import validate + +def validateJSON(jsonData): + try: + #jsonData must be STRING type + json.loads(jsonData) + except Exception as e: + #print(str(e)) + #print(e.message) + return str(e) + return None + +def validateSchema(jsonData, jsonSchema): + try: + validate(instance=json.loads(jsonData), schema=json.loads(jsonSchema)) + except Exception as e: + #print(e.message) + #print(str(e)) + return e.message + return None + +vSchema = get_octopusvariable(\"vSchema\") +vJsonData = get_octopusvariable(\"vJsonData\") + +vError = validateJSON(vJsonData) +if vError == None: + vRslt = 'Correct!' + print('JSON Structure is valid !','\ +') + + if vSchema: + vError = validateSchema(vJsonData, vSchema) + if vError == None: + vRslt = 'Correct!' + print('JSON Schema is valid !','\ +') + else: + vRslt = 'Wrong !' + print('JSON Schema error:', vError, file=sys.stderr) +else: + vRslt = 'Wrong!' + print('JSON structure error:', vError, file=sys.stderr) + +print ('Result:', vRslt)" + }, + "Parameters": [ + { + "Id": "1905df72-1da7-446b-8857-4df1795fb19a", + "Name": "vJsonData", + "Label": "JSON input object", + "HelpText": "JSON object to be validates + +Ejem: + +``` +{ + \"name\" : \"John\", + \"age\" : 45 +} +```", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "3089fe56-964f-4d5f-b940-31ca81e358cb", + "Name": "vSchema", + "Label": "Schema", + "HelpText": "JSON object called schema, who defines the layout of the JSON input object + +``` +{ + \"type\":\"object\", + \"properties\":{ + \"name\":{ + \"type\":\"string\", + \"minimum\":1 + }, + \"age\":{ + \"type\":\"number\", + \"minimum\":1 + } + }, + \"additionalProperties\":false, + \"required\":[ + \"name\", + \"age\" + ] +} +```", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2022-06-02T19:54:24.484Z", + "OctopusVersion": "2022.1.2121", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "cramoscr", + "Category": "json" +} diff --git a/step-templates/jwt-generate-json-web-token.json.human b/step-templates/jwt-generate-json-web-token.json.human new file mode 100644 index 000000000..1aa44fe3b --- /dev/null +++ b/step-templates/jwt-generate-json-web-token.json.human @@ -0,0 +1,684 @@ +{ + "Id": "1ca0401c-dfca-420e-81ca-1f4b7cf02d2d", + "Name": "JWT - Generate JSON Web Token", + "Description": "Generates a [Json Web Token (JWT)](https://en.wikipedia.org/wiki/JSON_Web_Token) for use with applications that require a JWT token. The resulting JWT will be stored as a [sensitive output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) called **JWT**. + +A private key needs to be provided that will sign the combined JWT header and payload. + +Currently, the following three signing algorithms are supported: + +1. `RS256` - RSASSA-PKCS1-v1_5 using SHA-256 +2. `RS384` - RSASSA-PKCS1-v1_5 using SHA-384 +3. `RS512` - RSASSA-PKCS1-v1_5 using SHA-512 + +The default is `RS256`. + +**Notes:** +- Tested on Windows and Linux (PowerShell Core) +- Tested with Octopus **2020.1**", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +# Variables +$GENERATE_JWT_PRIVATE_KEY = $OctopusParameters[\"Generate.JWT.PrivateKey\"] +$GENERATE_JWT_ALGORITHM = $OctopusParameters[\"Generate.JWT.Signing.Algorithm\"] +$GENERATE_JWT_EXPIRES_MINS = $OctopusParameters[\"Generate.JWT.ExpiresAfterMinutes\"] + +# Optional +$GENERATE_JWT_ISSUER = $OctopusParameters[\"Generate.JWT.Issuer\"] +$GENERATE_JWT_SUBJECT = $OctopusParameters[\"Generate.JWT.Subject\"] +$GENERATE_JWT_GROUPS = $OctopusParameters[\"Generate.JWT.Groups\"] +$GENERATE_JWT_AUDIENCE = $OctopusParameters[\"Generate.JWT.Audience\"] +$GENERATE_JWT_TTL = $OctopusParameters[\"Generate.JWT.TTL\"] +$GENERATE_JWT_MAX_TTL = $OctopusParameters[\"Generate.JWT.TTL.Max\"] +$GENERATE_JWT_PRIVATE_CLAIM_NAME = $OctopusParameters[\"Generate.JWT.PrivateClaim.Name\"] +$GENERATE_JWT_PRIVATE_CLAIM_VALUE = $OctopusParameters[\"Generate.JWT.PrivateClaim.Value\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($GENERATE_JWT_PRIVATE_KEY)) { + throw \"Required parameter Generate.JWT.PrivateKey not specified.\" +} +if ([string]::IsNullOrWhiteSpace($GENERATE_JWT_ALGORITHM)) { + throw \"Required parameter Generate.JWT.Signing.Algorithm not specified.\" +} +if ([string]::IsNullOrWhiteSpace($GENERATE_JWT_EXPIRES_MINS)) { + throw \"Required parameter Generate.JWT.ExpiresAfterMinutes not specified.\" +} +if ([string]::IsNullOrWhiteSpace($GENERATE_JWT_AUDIENCE)) { + throw \"Required parameter Generate.JWT.Audience not specified.\" +} + +# Optional fields that require validation +if ([string]::IsNullOrWhiteSpace($GENERATE_JWT_PRIVATE_CLAIM_NAME) -eq $False) { + if ([string]::IsNullOrWhiteSpace($GENERATE_JWT_PRIVATE_CLAIM_VALUE)) { + throw \"A private claim name has been specified with no value found in Generate.JWT.PrivateClaim.Value.\" + } +} + +# Helper functions +############################################################################### + +function ConvertTo-JwtBase64 { + param ( + $Value + ) + if ($Value -is [string]) { + $ConvertedValue = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Value)) -replace '\\+', '-' -replace '/', '_' -replace '=' + } + elseif ($Value -is [byte[]]) { + $ConvertedValue = [Convert]::ToBase64String($Value) -replace '\\+', '-' -replace '/', '_' -replace '=' + } + + return $ConvertedValue +} + +############################################################################### + +# Signing functions +############################################################################### + +$RsaPrivateKey_Header = \"-----BEGIN RSA PRIVATE KEY-----\" +$RsaPrivateKey_Footer = \"-----END RSA PRIVATE KEY-----\" +$Pkcs8_PrivateKey_Header = \"-----BEGIN PRIVATE KEY-----\" +$Pkcs8_PrivateKey_Footer = \"-----END PRIVATE KEY-----\" + +function ExtractPemData { + param ( + [string]$Pem, + [string]$Header, + [string]$Footer + ) + + $Start = $Pem.IndexOf($Header) + $Header.Length + $End = $Pem.IndexOf($Footer, $Start) - $Start + $EncodedPem = ($Pem.Substring($Start, $End).Trim()) -Replace \" \", \"`n\" + + $PemData = [Convert]::FromBase64String($EncodedPem) + return [byte[]]$PemData +} + +function DecodeIntSize { + param ( + [System.IO.BinaryReader]$BinaryReader + ) + + [byte]$byteValue = $BinaryReader.ReadByte() + + # If anything other than 0x02, an ASN.1 integer follows. + if ($byteValue -ne 0x02) { + return 0; + } + + $byteValue = $BinaryReader.ReadByte() + # 0x81 == Data size in next byte. + if ($byteValue -eq 0x81) { + $size = $BinaryReader.ReadByte() + } + # 0x82 == Data size in next 2 bytes. + else { + if ($byteValue -eq 0x82) { + [byte]$high = $BinaryReader.ReadByte() + [byte]$low = $BinaryReader.ReadByte() + $byteValues = [byte[]]@($low, $high, 0x00, 0x00) + $size = [System.BitConverter]::ToInt32($byteValues, 0) + } + else { + # Otherwise, data size has already been read above. + $size = $byteValue + } + } + # Remove high-order zeros in data + $byteValue = $BinaryReader.ReadByte() + while ($byteValue -eq 0x00) { + $byteValue = $BinaryReader.ReadByte() + $size -= 1 + } + + $BinaryReader.BaseStream.Seek(-1, [System.IO.SeekOrigin]::Current) | Out-Null + return $size +} + +function PadByteArray { + param ( + [byte[]]$Bytes, + [int]$Size + ) + + if ($Bytes.Length -eq $Size) { + return $Bytes + } + if ($Bytes.Length -gt $Size) { + throw \"Specified size '$Size' to pad is too small for byte array of size '$($Bytes.Length)'.\" + } + + [byte[]]$PaddedBytes = New-Object Byte[] $Size + [System.Array]::Copy($Bytes, 0, $PaddedBytes, $Size - $bytes.Length, $bytes.Length) | Out-Null + return $PaddedBytes +} + +function Compare-ByteArrays { + param ( + [byte[]]$First, + [byte[]]$Second + ) + if ($First.Length -ne $Second.Length) { + return $False + } + [int]$i = 0 + foreach ($byte in $First) { + if ($byte -ne $Second[$i]) { + return $False + } + $i = $i + 1 + } + return $True +} + +function CreateRSAFromPkcs8 { + param ( + [byte[]]$KeyBytes + ) + Write-Verbose \"Reading RSA Pkcs8 private key bytes\" + + # The encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = \"1.2.840.113549.1.1.1\" + # this byte[] includes the sequence byte and terminal encoded null + [byte[]]$SeqOID = 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 + [byte[]]$Seq = New-Object byte[] 15 + + # Have to wrap $KeyBytes in another array :| + $MemoryStream = New-Object System.IO.MemoryStream(, $KeyBytes) + $reader = New-Object System.IO.BinaryReader($MemoryStream) + $StreamLength = [int]$MemoryStream.Length + + try { + [UInt16]$Bytes = $reader.ReadUInt16() + + if ($Bytes -eq 0x8130) { + $reader.ReadByte() | Out-Null + } + elseif ($Bytes -eq 0x8230) { + $reader.ReadInt16() | Out-Null + } + else { + return $null + } + + [byte]$byteValue = $reader.ReadByte() + + if ($byteValue -ne 0x02) { + return $null + } + + $Bytes = $reader.ReadUInt16() + + if ($Bytes -ne 0x0001) { + return $null + } + + # Read the Sequence OID + $Seq = $reader.ReadBytes(15) + $SequenceMatches = Compare-ByteArrays -First $Seq -Second $SeqOID + if ($SequenceMatches -eq $False) { + Write-Verbose \"Sequence OID doesnt match\" + return $null + } + + $byteValue = $reader.ReadByte() + # Next byte should be a Octet string + if ($byteValue -ne 0x04) { + return $null + } + # Read next byte / 2 bytes. + # Should be either: 0x81 or 0x82; otherwise it's the byte count. + $byteValue = $reader.ReadByte() + if ($byteValue -eq 0x81) { + $reader.ReadByte() | Out-Null + } + else { + if ($byteValue -eq 0x82) { + $reader.ReadUInt16() | Out-Null + } + } + + # Remaining sequence *should* be the RSA Pkcs1 private Key bytes + [byte[]]$RsaKeyBytes = $reader.ReadBytes([int]($StreamLength - $MemoryStream.Position)) + Write-Verbose \"Attempting to create RSA object from remaining Pkcs1 bytes\" + $rsa = CreateRSAFromPkcs1 -KeyBytes $RsaKeyBytes + return $rsa + } + catch { + Write-Warning \"CreateRSAFromPkcs8: Exception occurred - $($_.Exception.Message)\" + return $null + } + finally { + if ($null -ne $reader) { $reader.Close() } + if ($null -ne $MemoryStream) { $MemoryStream.Close() } + } +} + +function CreateRSAFromPkcs1 { + param ( + [byte[]]$KeyBytes + ) + Write-Verbose \"Reading RSA Pkcs1 private key bytes\" + # Have to wrap $KeyBytes in another array :| + $MemoryStream = New-Object System.IO.MemoryStream(, $KeyBytes) + $reader = New-Object System.IO.BinaryReader($MemoryStream) + try { + + [UInt16]$Bytes = $reader.ReadUInt16() + + if ($Bytes -eq 0x8130) { + $reader.ReadByte() | Out-Null + } + elseif ($Bytes -eq 0x8230) { + $reader.ReadInt16() | Out-Null + } + else { + return $null + } + + $Bytes = $reader.ReadUInt16() + if ($Bytes -ne 0x0102) { + return $null + } + + [byte]$byteValue = $reader.ReadByte() + if ($byteValue -ne 0x00) { + return $null + } + + # Private key parameters are integer sequences. + # For a summary of the RSA Parameters fields, + # See https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.rsaparameters#summary-of-fields + + $Modulus_Size = DecodeIntSize -BinaryReader $reader + $Modulus = $reader.ReadBytes($Modulus_Size) + + $E_Size = DecodeIntSize -BinaryReader $reader + $E = $reader.ReadBytes($E_Size) + + $D_Size = DecodeIntSize -BinaryReader $reader + $D = $reader.ReadBytes($D_Size) + + $P_Size = DecodeIntSize -BinaryReader $reader + $P = $reader.ReadBytes($P_Size) + + $Q_Size = DecodeIntSize -BinaryReader $reader + $Q = $reader.ReadBytes($Q_Size) + + $DP_Size = DecodeIntSize -BinaryReader $reader + $DP = $reader.ReadBytes($DP_Size) + + $DQ_Size = DecodeIntSize -BinaryReader $reader + $DQ = $reader.ReadBytes($DQ_Size) + + $IQ_Size = DecodeIntSize -BinaryReader $reader + $IQ = $reader.ReadBytes($IQ_Size) + + $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider + $rsaParameters = New-Object System.Security.Cryptography.RSAParameters + $rsaParameters.Modulus = $Modulus + $rsaParameters.Exponent = $E + $rsaParameters.P = $P + $rsaParameters.Q = $Q + # Some RSAParameter values dont play well with byte buffers having leading zeroes removed. + $rsaParameters.D = PadByteArray -Bytes $D -Size $Modulus.Length + $rsaParameters.DP = PadByteArray -Bytes $DP -Size $P.Length + $rsaParameters.DQ = PadByteArray -Bytes $DQ -Size $Q.Length + $rsaParameters.InverseQ = PadByteArray -Bytes $IQ -Size $Q.Length + $rsa.ImportParameters($rsaParameters) + + Write-Verbose \"Completed RSA object creation\" + return $rsa + } + catch { + Write-Warning \"CreateRSA-FromPkcs1: Exception occurred - $($_.Exception.Message)\" + return $null + } + finally { + if ($null -ne $reader) { $reader.Close() } + if ($null -ne $MemoryStream) { $MemoryStream.Close() } + } +} + +function CreateSigningKey { + param ( + [string]$Key + ) + try { + $Key = $Key.Trim() + switch -Wildcard($Key) { + \"$Pkcs8_PrivateKey_Header*\" { + $KeyBytes = ExtractPemData -PEM $Key -Header $Pkcs8_PrivateKey_Header -Footer $Pkcs8_PrivateKey_Footer + $SigningKey = CreateRSAFromPkcs8 -KeyBytes $KeyBytes + return $SigningKey + } + \"$RsaPrivateKey_Header*\" { + $KeyBytes = ExtractPemData -PEM $Key -Header $RsaPrivateKey_Header -Footer $RsaPrivateKey_Footer + $SigningKey = CreateRSAFromPkcs1 -KeyBytes $KeyBytes + return $SigningKey + } + default { + Write-Verbose \"The PEM header could not be found. Accepted headers: 'BEGIN PRIVATE KEY', 'BEGIN RSA PRIVATE KEY'\" + return $null + } + } + } + catch { + Write-Warning \"Couldn't create signing key: $($_.Exception.Message)\" + return $null + } +} + +############################################################################### + +# Local variables +$audiences = @() +if (![string]::IsNullOrWhiteSpace($GENERATE_JWT_AUDIENCE)) { + @(($GENERATE_JWT_AUDIENCE -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + $audiences += $_ + } + } +} + +$groups = @() +if (![string]::IsNullOrWhiteSpace($GENERATE_JWT_GROUPS)) { + @(($GENERATE_JWT_GROUPS -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + $groups += $_ + } + } +} + +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] +$OutputVariableName = \"JWT\" + +Write-Verbose \"Generate.JWT.Signing.Algorithm: $GENERATE_JWT_ALGORITHM\" +Write-Verbose \"Generate.JWT.ExpiresAfterMinutes: $GENERATE_JWT_EXPIRES_MINS\" +Write-Verbose \"Generate.JWT.Issuer: $GENERATE_JWT_ISSUER\" +Write-Verbose \"Generate.JWT.Subject: $GENERATE_JWT_SUBJECT\" +Write-Verbose \"Generate.JWT.Audience(s): $($audiences -Join \",\")\" +Write-Verbose \"Generate.JWT.Group(s): $($groups -Join \",\")\" +Write-Verbose \"Generate.JWT.TTL: $GENERATE_JWT_TTL\" +Write-Verbose \"Generate.JWT.TTL.Max: $GENERATE_JWT_MAX_TTL\" +Write-Verbose \"Generate.JWT.PrivateClaim.Name: $GENERATE_JWT_PRIVATE_CLAIM_NAME\" +Write-Verbose \"Generate.JWT.PrivateClaim.Value: $GENERATE_JWT_PRIVATE_CLAIM_VALUE\" +Write-Verbose \"Step Name: $StepName\" + +try { + + # Created + Expires + $Created = (Get-Date).ToUniversalTime() + $Expires = $Created.AddMinutes([int]$GENERATE_JWT_EXPIRES_MINS) + + $createDate = [Math]::Floor([decimal](Get-Date($Created) -UFormat \"%s\")) + $expiryDate = [Math]::Floor([decimal](Get-Date($Expires) -UFormat \"%s\")) + + $JwtHeader = @{ + alg = $GENERATE_JWT_ALGORITHM; + typ = \"JWT\"; + } | ConvertTo-Json -Compress + + $JwtPayload = [Ordered]@{ + iat = [long]$createDate; + exp = [long]$expiryDate; + } + + # Check for optional issuer: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 + if ([string]::IsNullOrWhiteSpace($GENERATE_JWT_ISSUER) -eq $False) { + $JwtPayload | Add-Member -NotePropertyName iss -NotePropertyValue $GENERATE_JWT_ISSUER + } + + # Check for optional subject: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2 + if ([string]::IsNullOrWhiteSpace($GENERATE_JWT_SUBJECT) -eq $False) { + $JwtPayload | Add-Member -NotePropertyName sub -NotePropertyValue $GENERATE_JWT_SUBJECT + } + + # Check for optional audience: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3 + if ($audiences.Length -gt 0) { + $JwtPayload | Add-Member -NotePropertyName aud -NotePropertyValue $audiences + } + # Check for optional \"groups\" field + if ($groups.Length -gt 0) { + $JwtPayload | Add-Member -NotePropertyName groups -NotePropertyValue $groups + } + + # Check for optional ttl field + if ([string]::IsNullOrWhiteSpace($GENERATE_JWT_TTL) -eq $False) { + $JwtPayload | Add-Member -NotePropertyName ttl -NotePropertyValue $GENERATE_JWT_TTL + } + # Check for optional max_ttl field + if ([string]::IsNullOrWhiteSpace($GENERATE_JWT_MAX_TTL) -eq $False) { + $JwtPayload | Add-Member -NotePropertyName max_ttl -NotePropertyValue $GENERATE_JWT_MAX_TTL + } + + # Check for an optional private claim name and value: https://datatracker.ietf.org/doc/html/rfc7519#section-4.3 + if ([string]::IsNullOrWhiteSpace($GENERATE_JWT_PRIVATE_CLAIM_NAME) -eq $False) { + $JwtPayload | Add-Member -NotePropertyName $GENERATE_JWT_PRIVATE_CLAIM_NAME -NotePropertyValue $GENERATE_JWT_PRIVATE_CLAIM_VALUE + } + + $JwtPayload = $JwtPayload | ConvertTo-Json -Compress + + $base64Header = ConvertTo-JwtBase64 -Value $JwtHeader + $base64Payload = ConvertTo-JwtBase64 -Value $JwtPayload + + $Jwt = $base64Header + '.' + $base64Payload + + $JwtBytes = [System.Text.Encoding]::UTF8.GetBytes($Jwt) + $JwtSignature = $null + + switch ($GENERATE_JWT_ALGORITHM) { + \"RS256\" { + try { + + $rsa = CreateSigningKey -Key $GENERATE_JWT_PRIVATE_KEY + if ($null -eq $rsa) { + throw \"Couldn't create RSA object\" + } + $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1) + $JwtSignature = ConvertTo-JwtBase64 -Value $Signature + } + catch { throw \"Signing with SHA256 and Pkcs1 padding failed using private key: $($_.Exception.Message)\" } + finally { if ($null -ne $rsa) { $rsa.Dispose() } } + + } + \"RS384\" { + try { + $rsa = CreateSigningKey -Key $GENERATE_JWT_PRIVATE_KEY + if ($null -eq $rsa) { + throw \"Couldn't create RSA object\" + } + $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA384, [Security.Cryptography.RSASignaturePadding]::Pkcs1) + $JwtSignature = ConvertTo-JwtBase64 -Value $Signature + } + catch { throw \"Signing with SHA384 and Pkcs1 padding failed using private key: $($_.Exception.Message)\" } + finally { if ($null -ne $rsa) { $rsa.Dispose() } } + } + \"RS512\" { + try { + $rsa = CreateSigningKey -Key $GENERATE_JWT_PRIVATE_KEY + if ($null -eq $rsa) { + throw \"Couldn't create RSA object\" + } + $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA512, [Security.Cryptography.RSASignaturePadding]::Pkcs1) + $JwtSignature = ConvertTo-JwtBase64 -Value $Signature + } + catch { throw \"Signing with SHA512 and Pkcs1 padding failed using private key: $($_.Exception.Message)\" } + finally { if ($null -ne $rsa) { $rsa.Dispose() } } + } + default { + throw \"The algorithm is not one of the supported: 'RS256', 'RS384', 'RS512'\" + } + } + if ([string]::IsNullOrWhiteSpace($JwtSignature) -eq $True) { + throw \"JWT signature empty.\" + } + + $Jwt = \"$Jwt.$JwtSignature\" + Set-OctopusVariable -Name $OutputVariableName -Value $Jwt -Sensitive + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$OutputVariableName}\" +} +catch { + $ExceptionMessage = $_.Exception.Message + $Message = \"An error occurred generating a JWT: $ExceptionMessage\" + Write-Error $Message -Category InvalidResult +}" + }, + "Parameters": [ + { + "Id": "71bacc82-21d7-4d08-a944-15dacd0a3405", + "Name": "Generate.JWT.PrivateKey", + "Label": "Private key used to sign the JWT", + "HelpText": "Provide the private key in PEM format to be used to sign the JWT. + +Accepted headers: + +- `----BEGIN RSA PRIVATE KEY-----` +- `-----BEGIN PRIVATE KEY----` + +**Note:** It's recommended to use a sensitive variable to provide this value. If you instead enter the private key directly, the step template will attempt to detect this by replacing spaces with new lines ``n`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "84759ecc-a3f6-4822-a678-833269a8bf0d", + "Name": "Generate.JWT.Signing.Algorithm", + "Label": "JWT signing algorithm", + "HelpText": "The JWA [algorithm](https://datatracker.ietf.org/doc/html/rfc7518#section-3.1) to use when signing the JWT. The default is `RS256` (RSASSA-PKCS1-v1_5 using SHA-256). + +This value is also used for the `alg` [algorithm header](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1).", + "DefaultValue": "RS256", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "RS256|RS256: RSASSA-PKCS1-v1_5 using SHA-256 +RS384|RS384: RSASSA-PKCS1-v1_5 using SHA-384 +RS512|RS512: RSASSA-PKCS1-v1_5 using SHA-512" + } + }, + { + "Id": "76d289c5-f6f9-4cfb-b876-13515745f03f", + "Name": "Generate.JWT.ExpiresAfterMinutes", + "Label": "JWT expiry time in minutes", + "HelpText": "The number of minutes after the JWT is created that it expires. This value is used for the `exp` [expiration time claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4). The default is `20` minutes.", + "DefaultValue": "20", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "20d57320-15e4-4777-916b-8563192a326d", + "Name": "Generate.JWT.Issuer", + "Label": "JWT Issuer (optional)", + "HelpText": "*Optional* - The `iss` [isuer claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1) identifies the principal that issued the JWT.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e7026958-60cb-4652-9fe0-2ef2965de951", + "Name": "Generate.JWT.Subject", + "Label": "JWT Subject (optional)", + "HelpText": "*Optional* subject (`sub`) of the JWT.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "12a68971-8e0b-4b90-9a27-4c81413d0fb7", + "Name": "Generate.JWT.Audience", + "Label": "Audience claims (optional)", + "HelpText": "*Optional* - The `aud` [audience claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3) identifies the recipients that the JWT is intended for. This is typically the base address of the resource being accessed, such as `https://example-domain.com` + +*Note:* +- For multiple audiences, enter each entry on a new line. +- Any entries that are an empty string will be ignored.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "1bd2294c-64a3-4dad-bb9f-1a4cce5eaff5", + "Name": "Generate.JWT.Groups", + "Label": "Group claims (optional)", + "HelpText": "*Optional* - A `groups` [private claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.3) that identifies groups associated with this JWT. + +*Note:* +- For multiple groups, enter each entry on a new line. +- Any entries that are an empty string will be ignored.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "cdc3fe41-4669-48c8-8e65-1265faa79361", + "Name": "Generate.JWT.TTL", + "Label": "TTL claim (optional)", + "HelpText": "*Optional* - A `ttl` [private claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.3) that identifies the time-to-live for the JWT. + +This value can be useful when authenticating with a Secrets Manager and the `ttl` value is mapped through to a resulting token used for authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "51b2ec44-1a8d-43ce-8e06-4d0262eed45e", + "Name": "Generate.JWT.TTL.Max", + "Label": "Max TTL claim (optional)", + "HelpText": "*Optional* - A `max_ttl` [private claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.3) that identifies the time-to-live for the JWT. + +This value can be useful when authenticating with a Secrets Manager and the `max_ttl` value is mapped through to a resulting token used for authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "028ec077-a40f-4aa3-8099-2e873ab710a5", + "Name": "Generate.JWT.PrivateClaim.Name", + "Label": "Custom private claim name (optional)", + "HelpText": "*Optional* - The name of a custom [private claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.3) that can be included in this JWT. + +**Note:** + +- Don't include the value for the private claim. Set the value in the `Generate.JWT.PrivateClaim.Value` parameter instead. +- If this value is null or an empty string, the private claim won't be added to the JWT payload.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "84a50912-284b-4c8a-bfee-87cd7cbc527e", + "Name": "Generate.JWT.PrivateClaim.Value", + "Label": "Custom private claim value (optional)", + "HelpText": "*Optional* - The value for the custom [private claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.3) as specified in the parameter `Generate.JWT.PrivateClaim.Name` to be included in this JWT.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedAt": "2021-08-09T11:51:09.772Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2021-08-09T11:51:09.772Z", + "OctopusVersion": "2020.1.7316", + "Type": "ActionTemplate" + }, + "Category": "jwt" + } diff --git a/step-templates/k8s-create-serviceaccount-and-target.json.human b/step-templates/k8s-create-serviceaccount-and-target.json.human new file mode 100644 index 000000000..be9a7fabf --- /dev/null +++ b/step-templates/k8s-create-serviceaccount-and-target.json.human @@ -0,0 +1,205 @@ +{ + "Id": "05d2d9a8-3862-49ff-97d6-5f08935f6fa3", + "Name": "Kubernetes - Create Service Account and Target", + "Description": "Create a service account with a role granting full access to everything in the namespace, and create a Kubernetes target with the new account in Octopus", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 14, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "if ([string]::IsNullOrWhitespace($CreateK8sTargetNamespace)) { +\tWrite-Error \"The namespace variable must be defined\" + exit 1 +} + +if ([string]::IsNullOrWhitespace($CreateK8sTargetRole)) { +\tWrite-Error \"The role variable must be defined\" + exit 1 +} + +$target = if ([string]::IsNullOrEmpty($CreateK8sTargetName)) {\"$($CreateK8sTargetNamespace)-k8s\"} else {$CreateK8sTargetName} +$serviceaccount = \"$($CreateK8sTargetNamespace)-deployer\" +$rolename = \"$($CreateK8sTargetNamespace)-deployer-role\" +$binding = \"$($CreateK8sTargetNamespace)-deployer-binding\" + +$count = (kubectl get namespaces -o json | +\tConvertFrom-JSON | + Select-Object -ExpandProperty items | + ? {$_.metadata.name -eq $CreateK8sTargetNamespace}).Count + +if ($count -eq 0) { + Set-Content -Path namespace.yaml -Value @\" + apiVersion: v1 + kind: Namespace + metadata: + name: $CreateK8sTargetNamespace +\"@ + + if (![string]::IsNullOrWhitespace($CreateK8sTargetNamespaceAnnotations)) { + \tAdd-Content -Path namespace.yaml -Value @\" + annotations: +\"@ +\t$annotations = ($CreateK8sTargetNamespaceAnnotations -split '\\r?\ +').Trim() + foreach ($annotation in $annotations) { + Add-Content -Path namespace.yaml -Value @\" + $annotation +\"@ + } + } + + kubectl apply -f namespace.yaml +} + +Set-Content -Path serviceaccount.yaml -Value @\" +apiVersion: v1 +kind: ServiceAccount +metadata: + name: $serviceaccount + namespace: $CreateK8sTargetNamespace +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + namespace: $CreateK8sTargetNamespace + name: $rolename +rules: +- apiGroups: [\"*\"] + resources: [\"*\"] + verbs: [\"*\"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: $binding + namespace: $CreateK8sTargetNamespace +subjects: +- kind: ServiceAccount + name: $serviceaccount + apiGroup: \"\" +roleRef: + kind: Role + name: $rolename + apiGroup: \"\" +\"@ + +kubectl apply -f serviceaccount.yaml + +Set-Content -Path secret.yaml -Value @\" +apiVersion: v1 +kind: Secret +type: kubernetes.io/service-account-token +metadata: + name: $serviceaccount + namespace: $CreateK8sTargetNamespace + annotations: + kubernetes.io/service-account.name: \"$serviceaccount\" +\"@ + +kubectl apply -f secret.yaml + +$data = kubectl get secret $serviceaccount -o jsonpath=\"{.data.token}\" --namespace=$CreateK8sTargetNamespace + +$token = [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($data)) +$url = (kubectl config view -o json | ConvertFrom-Json).clusters[0].cluster.server + +New-OctopusTokenAccount -Name $target -token $token -updateIfExisting + +if ([string]::IsNullOrEmpty(\"#{Octopus.Action.Kubernetes.CertificateAuthority}\") -or \"#{Octopus.Action.Kubernetes.AksAdminLogin}\" -ieq \"True\") { +\tNew-OctopusKubernetesTarget ` +\t\t-name $target ` +\t\t-clusterUrl $url ` +\t\t-octopusRoles $CreateK8sTargetRole ` +\t\t-octopusAccountIdOrName $target ` +\t\t-namespace $CreateK8sTargetNamespace ` +\t\t-updateIfExisting ` +\t\t-skipTlsVerification True ` +\t\t-octopusDefaultWorkerPoolIdOrName \"#{Octopus.WorkerPool.Id}\" ` + -healthCheckContainerImageFeedIdOrName \"$CreateK8sTargetContainerImageFeed\" ` + \t-healthCheckContainerImage \"$CreateK8sTargetContainerImage\" +} else { +\tNew-OctopusKubernetesTarget ` +\t\t-name $target ` +\t\t-clusterUrl $url ` +\t\t-octopusRoles $CreateK8sTargetRole ` +\t\t-octopusAccountIdOrName $target ` +\t\t-namespace $CreateK8sTargetNamespace ` +\t\t-updateIfExisting ` + -octopusServerCertificateIdOrName \"#{Octopus.Action.Kubernetes.CertificateAuthority}\" ` +\t\t-octopusDefaultWorkerPoolIdOrName \"#{Octopus.WorkerPool.Id}\" ` + -healthCheckContainerImageFeedIdOrName \"$CreateK8sTargetContainerImageFeed\" ` + \t-healthCheckContainerImage \"$CreateK8sTargetContainerImage\" +}" + }, + "Parameters": [ + { + "Id": "66047c5e-8827-4bce-85ef-98466e656d0e", + "Name": "CreateK8sTargetName", + "Label": "Target name", + "HelpText": "The optional name of the target. Defaults to \"-k8s\" if no value is defined.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1505ab59-1b6b-497e-9aa5-888f583c2cb2", + "Name": "CreateK8sTargetRole", + "Label": "Target Role", + "HelpText": "The role to assign to the new Kubernetes target", + "DefaultValue": "k8s", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e0dc3fc2-b422-4b27-8353-efee0233cf7f", + "Name": "CreateK8sTargetNamespace", + "Label": "Target Namespace", + "HelpText": "The namespace that the service account is granted access to, as well as the default namespace on the target.", + "DefaultValue": "#{Octopus.Environment.Name | ToLower}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "34a94173-611f-4cbf-b30a-66fb9456d1cf", + "Name": "CreateK8sTargetNamespaceAnnotations", + "Label": "Namespace annotations", + "HelpText": "An optional list of annotations to apply to the namespace", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "f14880f9-5a57-4469-85d7-5e68e090ed43", + "Name": "CreateK8sTargetContainerImageFeed", + "Label": "Container Image Feed", + "HelpText": "The optional name of the Docker feed where the container image is sourced from.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8d8d4317-af86-4a2a-9c04-eef1d8624e4a", + "Name": "CreateK8sTargetContainerImage", + "Label": "Container image", + "HelpText": "The optional name of the health check container image.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2023-03-07T11:26:30.753Z", + "OctopusVersion": "2023.1.9608", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "isaaccalligeros", + "Category": "k8s" +} diff --git a/step-templates/k8s-deploy-kustomize.json.human b/step-templates/k8s-deploy-kustomize.json.human new file mode 100644 index 000000000..b7c4dbee8 --- /dev/null +++ b/step-templates/k8s-deploy-kustomize.json.human @@ -0,0 +1,75 @@ +{ + "Id": "4b0be8ea-e191-4e27-9294-6cc56daaa488", + "Name": "Kubernetes - kustomize template deployment (bash)", + "Description": "Load package with kubernetes configuration and deploy to cluster with [kustomize](https://kustomize.io/) evaluation. + +kubectl must be installed on the worker executing the step.", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "df0b9b7f-458e-4ea2-a8e6-1636d0aa7042", + "Name": "K8sKustomize.DeploymentConfiguration", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "K8sKustomize.DeploymentConfiguration", + "Purpose": "" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "# kubectl is required +if ! [ -x \"$(command -v kubectl)\" ]; then +\tfail_step 'kubectl command not found' +fi + +REFERENCED_PACKAGE_NAME=\"K8sKustomize.DeploymentConfiguration\" +KUSTOMIZE_OVERLAY_PATH=$(get_octopusvariable \"K8sKustomize.OverlayPath\") + +echo \"Referenced package name:$REFERENCED_PACKAGE_NAME\" +echo \"Overlay path: $KUSTOMIZE_OVERLAY_PATH\" + +PACKAGE_LOCATION=$(get_octopusvariable \"Octopus.Action.Package[\"$REFERENCED_PACKAGE_NAME\"].ExtractedPath\") +echo \"Extracted package location: $PACKAGE_LOCATION\" + +cd $PACKAGE_LOCATION +kubectl apply -k $KUSTOMIZE_OVERLAY_PATH" + }, + "Parameters": [ + { + "Id": "f0001e7b-0af8-47f5-8f2d-49e81d1a18e5", + "Name": "K8sKustomize.DeploymentConfiguration", + "Label": "Deployment configuration package", + "HelpText": "Package which contains kubernetes manifest files which are using Kustomize.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "56502ffe-f96d-48a0-9b5d-d9647c86f67c", + "Name": "K8sKustomize.OverlayPath", + "Label": "Kustomize path to deploy", + "HelpText": "Path to kustomize Overlay which should be used for deployment. For example: overlay/environment/dev", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.KubernetesRunScript", + "$Meta": { + "ExportedAt": "2022-03-08T14:51:57.629Z", + "OctopusVersion": "2021.3.12258", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "tunkl", + "Category": "k8s" +} diff --git a/step-templates/k8s-eksctl-create-cluster-bash.json.human b/step-templates/k8s-eksctl-create-cluster-bash.json.human new file mode 100644 index 000000000..485786bea --- /dev/null +++ b/step-templates/k8s-eksctl-create-cluster-bash.json.human @@ -0,0 +1,154 @@ +{ + "Id": "0388c8c3-9f27-4132-90c6-2c11b727d9fd", + "Name": "eksctl - Create Cluster (bash)", + "Description": "Uses [eksctl](https://eksctl.io/) to create an EKS cluster and register it as a [kubernetes target](https://octopus.com/docs/infrastructure/deployment-targets/kubernetes-target) in Octopus. + +eksctl must be installed on the worker executing the step.", + "ActionType": "Octopus.AwsRunScript", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "OctopusUseBundledTooling": "False", + "Octopus.Action.AwsAccount.Variable": "eksctl.account", + "Octopus.Action.Script.ScriptBody": "clusterName='#{eksctl.cluster.name}' +region='#{eksctl.region}' +kubeConfig='eks-sandbox.yaml' +eksctlYaml='eksctl.yaml' + + +# eksctl is required +if ! [ -x \"$(command -v eksctl)\" ]; then +\tfail_step 'eksctl command not found' +fi + +# yq is required +if ! [ -x \"$(command -v yq)\" ]; then +\tfail_step 'yq command not found' +fi + +cat >\"./$eksctlYaml\" <&1 + +if [ $? -ne 0 ] +then +\techo \"Cluster does not exist. Creating cluster...\" + eksctl create cluster -f $eksctlYaml --kubeconfig $kubeConfig +else +\techo \"Cluster exists. Reading cluster details...\" + eksctl utils write-kubeconfig --cluster $clusterName --kubeconfig $kubeConfig +fi + +if [ ! -f $kubeConfig ] +then +\techo \"$kubeConfig does not exist, so the Kubernetes target can not be created!\" +\texit 1 +fi + +accountId=$(get_octopusvariable '#{eksctl.account}') +workerPool=$(get_octopusvariable 'eksctl.octopus.target.defaultWorkerPool') +clusterUrl=$(yq r $kubeConfig 'clusters[0].cluster.server') + +# Write service message to create the k8s target +echo \"##octopus[create-kubernetestarget \\ + name=\\\"$(encode_servicemessagevalue '#{eksctl.octopus.target.name}')\\\" \\ + octopusRoles=\\\"$(encode_servicemessagevalue '#{eksctl.octopus.target.roles}')\\\" \\ + clusterName=\\\"$(encode_servicemessagevalue \"$clusterName\")\\\" \\ + clusterUrl=\\\"$(encode_servicemessagevalue \"$clusterUrl\")\\\" \\ + octopusAccountIdOrName=\\\"$(encode_servicemessagevalue \"$accountId\")\\\" \\ + namespace=\\\"$(encode_servicemessagevalue 'default')\\\" \\ + octopusDefaultWorkerPoolIdOrName=\\\"$(encode_servicemessagevalue \"$workerPool\")\\\" \\ + updateIfExisting=\\\"$(encode_servicemessagevalue 'True')\\\" \\ + skipTlsVerification=\\\"$(encode_servicemessagevalue 'True')\\\"]\"", + "Octopus.Action.Aws.Region": "#{eksctl.region}" + }, + "Parameters": [ + { + "Id": "df94426d-1270-4cf6-8b6d-1d7b6ab9579d", + "Name": "eksctl.cluster.name", + "Label": "Cluster Name", + "HelpText": "The name of the EKS cluster", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "48ae65bd-df84-42e1-ab80-5c8eba31d948", + "Name": "eksctl.account", + "Label": "AWS Account", + "HelpText": "The AWS account used to create the cluster", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "2769000f-795b-486d-844f-2d5d0bb74fc2", + "Name": "eksctl.region", + "Label": "AWS Region", + "HelpText": "The [AWS region code](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints) (this can be overridden in the eksctl config)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "80603dbb-08e2-42e1-91b3-7693bab55eb8", + "Name": "eksctl.yaml", + "Label": "eksctl config", + "HelpText": "The eksctl configuration YAML. See [ekstl docs](https://eksctl.io/usage/creating-and-managing-clusters/).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "4259b03f-33a5-4b18-af55-f605de2125fe", + "Name": "eksctl.octopus.target.name", + "Label": "Octopus Target Name", + "HelpText": "The name of the octopus kubernetes cluster target to create ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1da097f4-f09d-437e-b5ab-f1f08faa1d31", + "Name": "eksctl.octopus.target.roles", + "Label": "Octopus Target Roles", + "HelpText": "Comma-separated list of the roles to assign to the created target in octopus", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "483e7e32-60ec-4c9e-bc0c-15ae5a483046", + "Name": "eksctl.octopus.target.defaultWorkerPool", + "Label": "Default Worker Pool", + "HelpText": "The name or ID of the default worker pool for the created kubernetes target. +Leave blank to use the [default pool](https://octopus.com/docs/infrastructure/workers/worker-pools#default-worker-pool).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-05-02T15:04:09.845Z", + "OctopusVersion": "2020.1.15", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "MJRichardson", + "Category": "k8s" +} diff --git a/step-templates/k8s-hpa-external-metrics.json.human b/step-templates/k8s-hpa-external-metrics.json.human new file mode 100644 index 000000000..c43a348df --- /dev/null +++ b/step-templates/k8s-hpa-external-metrics.json.human @@ -0,0 +1,148 @@ +{ + "Id": "cc91e00c-e950-4a2a-81d0-d5e8fc09f6a4", + "Name": "Kubernetes - Deploy Horizontal Pod Autoscaler (with external metrics)", + "Description": "Apply a Horizontal Pod Autoscaler monitoring external metrics to a Kubernetes cluster.", + "ActionType": "Octopus.KubernetesDeployRawYaml", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.KubernetesContainers.CustomResourceYaml": "# https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscaler-v1-autoscaling +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: #{HorizontalPodAutoscalerName} +spec: + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling + scaleTargetRef: + apiVersion: #{HorizontalPodAutoscalerTargetApiVersion} + kind: #{HorizontalPodAutoscalerTargetKind} + name: #{HorizontalPodAutoscalerTargetName} + minReplicas: #{HorizontalPodAutoscalerMinReplicas} + maxReplicas: #{HorizontalPodAutoscalerMaxReplicas} + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metricspec-v2beta2-autoscaling + metrics: + - type: External + external: + metric: + name: #{HorizontalPodAutoscalerMetricName} + #{if HorizontalPodAutoscalerMetricSelector}selector: {#{HorizontalPodAutoscalerMetricSelector}}#{/if} + target: + type: #{HorizontalPodAutoscalerMetricType} + #{if HorizontalPodAutoscalerMetricType == \"AverageValue\"}#{if HorizontalPodAutoscalerAverageValue}averageValue: #{HorizontalPodAutoscalerAverageValue}#{/if}#{/if} + #{if HorizontalPodAutoscalerMetricType == \"Value\"}#{if HorizontalPodAutoscalerValue}value: #{HorizontalPodAutoscalerValue}#{/if}#{/if}" + }, + "Parameters": [ + { + "Id": "e2788bc8-8157-412f-bd4e-b1a1a52cfb26", + "Name": "HorizontalPodAutoscalerName", + "Label": "Name", + "HelpText": null, + "DefaultValue": "my-hpa", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f14eefc4-0f46-461b-a736-bcd175d85934", + "Name": "HorizontalPodAutoscalerTargetKind", + "Label": "Target Kind", + "HelpText": "See the [Kubernetes documentation](https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds) for a list of kinds. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v1-autoscaling)", + "DefaultValue": "Deployment", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "821a1df1-1aa0-4113-b3d3-0e3abb967f61", + "Name": "HorizontalPodAutoscalerTargetName", + "Label": "Target Name", + "HelpText": "The name of the target. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v1-autoscaling)", + "DefaultValue": "my-deployment", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "23470b8c-d28c-43a5-8127-3f87f9024850", + "Name": "HorizontalPodAutoscalerMinReplicas", + "Label": "Min Replicas", + "HelpText": "The minimum number of replicas. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling)", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f9954197-1f82-4fd0-82b4-49589927cdd4", + "Name": "HorizontalPodAutoscalerMaxReplicas", + "Label": "Max Replicas", + "HelpText": "The maximum number of replicas. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling)", + "DefaultValue": "3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "459e202a-37a9-4c48-948c-4110a8778710", + "Name": "HorizontalPodAutoscalerMetricName", + "Label": "Metric Name", + "HelpText": "The name of the metroc. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#resourcemetricsource-v2beta2-autoscaling)", + "DefaultValue": "requests-per-second", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "280fde89-b463-4082-9c22-950456d020b7", + "Name": "HorizontalPodAutoscalerMetricSelector", + "Label": "Metric Selector", + "HelpText": "An option selector to limit the metric by label. This will be in the form of a label string like `queue=worker_tasks`. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metricidentifier-v2beta2-autoscaling)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "52523154-c413-4349-8ea6-401435cf551c", + "Name": "HorizontalPodAutoscalerMetricType", + "Label": "Metric Type", + "HelpText": "The type of measurement made on the target metric. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling)", + "DefaultValue": "AverageValue", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Value|Value +AverageValue|Average Value" + } + }, + { + "Id": "e913c0e5-6436-44e5-8de4-3739dd0b4e32", + "Name": "HorizontalPodAutoscalerAverageValue", + "Label": "Average Value", + "HelpText": "The average value. Used when metric type is Average Value. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bdb556b2-fb8e-4d64-9cae-07fced57e304", + "Name": "HorizontalPodAutoscalerValue", + "Label": "Value", + "HelpText": "The value. Used when the metric type is Value. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-07-09T03:38:24.896Z", + "OctopusVersion": "2020.3.0-rc0001", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "Category": "k8s" +} diff --git a/step-templates/k8s-hpa-object-metrics.json.human b/step-templates/k8s-hpa-object-metrics.json.human new file mode 100644 index 000000000..00201fe34 --- /dev/null +++ b/step-templates/k8s-hpa-object-metrics.json.human @@ -0,0 +1,194 @@ +{ + "Id": "1a55a26d-53f7-44ae-8aa9-dd52abbfebe3", + "Name": "Kubernetes - Deploy Horizontal Pod Autoscaler (with object metrics)", + "Description": "Apply a Horizontal Pod Autoscaler monitoring object metrics to a Kubernetes cluster.", + "ActionType": "Octopus.KubernetesDeployRawYaml", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.KubernetesContainers.CustomResourceYaml": "# https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscaler-v1-autoscaling +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: #{HorizontalPodAutoscalerName} +spec: + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling + scaleTargetRef: + apiVersion: #{HorizontalPodAutoscalerTargetApiVersion} + kind: #{HorizontalPodAutoscalerTargetKind} + name: #{HorizontalPodAutoscalerTargetName} + minReplicas: #{HorizontalPodAutoscalerMinReplicas} + maxReplicas: #{HorizontalPodAutoscalerMaxReplicas} + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metricspec-v2beta2-autoscaling + metrics: + - type: Object + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmetricsource-v2beta2-autoscaling + object: + metric: + name: #{HorizontalPodAutoscalerMetricName} + #{if HorizontalPodAutoscalerMetricSelector}selector: {#{HorizontalPodAutoscalerMetricSelector}}#{/if} + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v2beta2-autoscaling + describedObject: + apiVersion: #{HorizontalPodAutoscalerObjectApiVersion} + kind: #{HorizontalPodAutoscalerObjectApiKind} + name: #{HorizontalPodAutoscalerObjectName} + target: + type: #{HorizontalPodAutoscalerMetricType} + #{if HorizontalPodAutoscalerMetricType == \"AverageValue\"}#{if HorizontalPodAutoscalerAverageValue}averageValue: #{HorizontalPodAutoscalerAverageValue}#{/if}#{/if} + #{if HorizontalPodAutoscalerMetricType == \"Value\"}#{if HorizontalPodAutoscalerValue}value: #{HorizontalPodAutoscalerValue}#{/if}#{/if}" + }, + "Parameters": [ + { + "Id": "e2788bc8-8157-412f-bd4e-b1a1a52cfb26", + "Name": "HorizontalPodAutoscalerName", + "Label": "Name", + "HelpText": null, + "DefaultValue": "my-hpa", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f14eefc4-0f46-461b-a736-bcd175d85934", + "Name": "HorizontalPodAutoscalerTargetKind", + "Label": "Target Kind", + "HelpText": "See the [Kubernetes documentation](https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds) for a list of kinds. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v1-autoscaling)", + "DefaultValue": "Deployment", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "821a1df1-1aa0-4113-b3d3-0e3abb967f61", + "Name": "HorizontalPodAutoscalerTargetName", + "Label": "Target Name", + "HelpText": "The name of the target. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v1-autoscaling)", + "DefaultValue": "my-deployment", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "23470b8c-d28c-43a5-8127-3f87f9024850", + "Name": "HorizontalPodAutoscalerMinReplicas", + "Label": "Min Replicas", + "HelpText": "The minimum number of replicas. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling)", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f9954197-1f82-4fd0-82b4-49589927cdd4", + "Name": "HorizontalPodAutoscalerMaxReplicas", + "Label": "Max Replicas", + "HelpText": "The maximum number of replicas. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling)", + "DefaultValue": "3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "459e202a-37a9-4c48-948c-4110a8778710", + "Name": "HorizontalPodAutoscalerMetricName", + "Label": "Metric Name", + "HelpText": "The name of the metroc. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#resourcemetricsource-v2beta2-autoscaling)", + "DefaultValue": "requests-per-second", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "84d90192-0273-40ca-9410-8ebdb34a190e", + "Name": "HorizontalPodAutoscalerTargetApiVersion", + "Label": "Target Api Version", + "HelpText": "Target API version. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v1-autoscaling)", + "DefaultValue": "apps/v1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "749a45a2-3a2d-4b9d-abfa-3e793a10dccc", + "Name": "HorizontalPodAutoscalerObjectApiVersion", + "Label": "Object API Version", + "HelpText": null, + "DefaultValue": "networking.k8s.io/v1beta1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8a2dfaa7-d4c5-41e9-956e-ac0d91214824", + "Name": "HorizontalPodAutoscalerObjectApiKind", + "Label": "Object API Kind", + "HelpText": null, + "DefaultValue": "Ingress", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "641ceb41-4fdc-4d98-9b6a-96142499ed87", + "Name": "HorizontalPodAutoscalerObjectName", + "Label": "Object Name", + "HelpText": null, + "DefaultValue": "main-route", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "280fde89-b463-4082-9c22-950456d020b7", + "Name": "HorizontalPodAutoscalerMetricSelector", + "Label": "Metric Selector", + "HelpText": "An option selector to limit the metric by label. This will be in the form of a label string like `matchLabels: {verb: GET}`. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metricidentifier-v2beta2-autoscaling)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "93dd4745-a19e-4d31-9357-7dc0ca74d8ef", + "Name": "HorizontalPodAutoscalerMetricType", + "Label": "", + "HelpText": "The type of measurement made on the target metric. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling)", + "DefaultValue": "AverageValue", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Value|Value +AverageValue|Average Value" + } + }, + { + "Id": "e913c0e5-6436-44e5-8de4-3739dd0b4e32", + "Name": "HorizontalPodAutoscalerValue", + "Label": "Value", + "HelpText": "The value. Used when the metric type is Value. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0b7b9e29-576b-4fa1-b0c0-667d1d2c53ee", + "Name": "HorizontalPodAutoscalerAverageValue", + "Label": "Average Value", + "HelpText": "The average value. Used when the metric type is Average Value. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-07-09T03:41:50.692Z", + "OctopusVersion": "2020.3.0-rc0001", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "Category": "k8s" +} diff --git a/step-templates/k8s-hpa-pod-metrics.json.human b/step-templates/k8s-hpa-pod-metrics.json.human new file mode 100644 index 000000000..11375cd5f --- /dev/null +++ b/step-templates/k8s-hpa-pod-metrics.json.human @@ -0,0 +1,136 @@ +{ + "Id": "f373e6e4-6bbd-4f9d-a966-98de706df42e", + "Name": "Kubernetes - Deploy Horizontal Pod Autoscaler (with pod metrics)", + "Description": "Apply a Horizontal Pod Autoscaler monitoring pod metrics to a Kubernetes cluster.", + "ActionType": "Octopus.KubernetesDeployRawYaml", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.KubernetesContainers.CustomResourceYaml": "# https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscaler-v1-autoscaling +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: #{HorizontalPodAutoscalerName} +spec: + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling + scaleTargetRef: + apiVersion: #{HorizontalPodAutoscalerTargetApiVersion} + kind: #{HorizontalPodAutoscalerTargetKind} + name: #{HorizontalPodAutoscalerTargetName} + minReplicas: #{HorizontalPodAutoscalerMinReplicas} + maxReplicas: #{HorizontalPodAutoscalerMaxReplicas} + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metricspec-v2beta2-autoscaling + metrics: + - type: Pods + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#podsmetricsource-v2beta2-autoscaling + pods: + metric: + name: #{HorizontalPodAutoscalerMetricName} + #{if HorizontalPodAutoscalerMetricSelector}selector: {#{HorizontalPodAutoscalerMetricSelector}}#{/if} + target: + type: AverageValue + averageValue: #{HorizontalPodAutoscalerResourceAverageValue}" + }, + "Parameters": [ + { + "Id": "e2788bc8-8157-412f-bd4e-b1a1a52cfb26", + "Name": "HorizontalPodAutoscalerName", + "Label": "Name", + "HelpText": null, + "DefaultValue": "my-hpa", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f14eefc4-0f46-461b-a736-bcd175d85934", + "Name": "HorizontalPodAutoscalerTargetKind", + "Label": "Target Kind", + "HelpText": "See the [Kubernetes documentation](https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds) for a list of kinds. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v1-autoscaling)", + "DefaultValue": "Deployment", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "821a1df1-1aa0-4113-b3d3-0e3abb967f61", + "Name": "HorizontalPodAutoscalerTargetName", + "Label": "Target Name", + "HelpText": "The name of the target. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v1-autoscaling)", + "DefaultValue": "my-deployment", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "23470b8c-d28c-43a5-8127-3f87f9024850", + "Name": "HorizontalPodAutoscalerMinReplicas", + "Label": "Min Replicas", + "HelpText": "The minimum number of replicas. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling)", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f9954197-1f82-4fd0-82b4-49589927cdd4", + "Name": "HorizontalPodAutoscalerMaxReplicas", + "Label": "Max Replicas", + "HelpText": "The maximum number of replicas. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling)", + "DefaultValue": "3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "459e202a-37a9-4c48-948c-4110a8778710", + "Name": "HorizontalPodAutoscalerMetricName", + "Label": "Metric Name", + "HelpText": "The name of the metric to measure. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#resourcemetricsource-v2beta2-autoscaling)", + "DefaultValue": "packets-per-second", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e913c0e5-6436-44e5-8de4-3739dd0b4e32", + "Name": "HorizontalPodAutoscalerResourceAverageValue", + "Label": "Average Value", + "HelpText": "The average value. Used when Metric Type is set to Average Value. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "84d90192-0273-40ca-9410-8ebdb34a190e", + "Name": "HorizontalPodAutoscalerTargetApiVersion", + "Label": "Target Api Version", + "HelpText": "Target API version. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v1-autoscaling)", + "DefaultValue": "apps/v1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e6b190c9-4465-4085-8a0e-f2dba72abf7d", + "Name": "HorizontalPodAutoscalerMetricSelector", + "Label": "Metric Selector", + "HelpText": "An option selector to limit the metric by label. This will be in the form of a label selector like `matchLabels: {verb: GET}`. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metricidentifier-v2beta2-autoscaling)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-07-09T02:52:33.066Z", + "OctopusVersion": "2020.3.0-rc0001", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "Category": "k8s" +} diff --git a/step-templates/k8s-hpa-resource-metrics.json.human b/step-templates/k8s-hpa-resource-metrics.json.human new file mode 100644 index 000000000..4fe624655 --- /dev/null +++ b/step-templates/k8s-hpa-resource-metrics.json.human @@ -0,0 +1,160 @@ +{ + "Id": "95c2a2a3-ca45-4721-989c-9454a63a5b01", + "Name": "Kubernetes - Deploy Horizontal Pod Autoscaler (with resource metrics)", + "Description": "Apply a Horizontal Pod Autoscaler monitoring resource metrics to a Kubernetes cluster.", + "ActionType": "Octopus.KubernetesDeployRawYaml", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.KubernetesContainers.CustomResourceYaml": "# https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscaler-v1-autoscaling +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: #{HorizontalPodAutoscalerName} +spec: + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling + scaleTargetRef: + apiVersion: #{HorizontalPodAutoscalerTargetApiVersion} + kind: #{HorizontalPodAutoscalerTargetKind} + name: #{HorizontalPodAutoscalerTargetName} + minReplicas: #{HorizontalPodAutoscalerMinReplicas} + maxReplicas: #{HorizontalPodAutoscalerMaxReplicas} + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metricspec-v2beta2-autoscaling + metrics: \t + - type: Resource + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#resourcemetricsource-v2beta2-autoscaling + resource: + name: #{HorizontalPodAutoscalerResourceName} + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling + target: + type: #{HorizontalPodAutoscalerMetricType} + #{if HorizontalPodAutoscalerMetricType == \"AverageUtilization\"}#{if HorizontalPodAutoscalerResourceAverageUtilization}averageUtilization: #{HorizontalPodAutoscalerResourceAverageUtilization}#{/if}#{/if} + #{if HorizontalPodAutoscalerMetricType == \"AverageValue\"}#{if HorizontalPodAutoscalerResourceAverageValue}averageValue: #{HorizontalPodAutoscalerResourceAverageValue}#{/if}#{/if} + #{if HorizontalPodAutoscalerMetricType == \"Value\"}#{if HorizontalPodAutoscalerResourceValue}value: #{HorizontalPodAutoscalerResourceValue}#{/if}#{/if}" + }, + "Parameters": [ + { + "Id": "e2788bc8-8157-412f-bd4e-b1a1a52cfb26", + "Name": "HorizontalPodAutoscalerName", + "Label": "Name", + "HelpText": null, + "DefaultValue": "my-hpa", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "84d90192-0273-40ca-9410-8ebdb34a190e", + "Name": "HorizontalPodAutoscalerTargetApiVersion", + "Label": "Target Api Version", + "HelpText": "Target API version. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v1-autoscaling)", + "DefaultValue": "apps/v1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f14eefc4-0f46-461b-a736-bcd175d85934", + "Name": "HorizontalPodAutoscalerTargetKind", + "Label": "Target Kind", + "HelpText": "See the [Kubernetes documentation](https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds) for a list of kinds. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v1-autoscaling)", + "DefaultValue": "Deployment", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "821a1df1-1aa0-4113-b3d3-0e3abb967f61", + "Name": "HorizontalPodAutoscalerTargetName", + "Label": "Target Name", + "HelpText": "The name of the target. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#crossversionobjectreference-v1-autoscaling)", + "DefaultValue": "my-deployment", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "23470b8c-d28c-43a5-8127-3f87f9024850", + "Name": "HorizontalPodAutoscalerMinReplicas", + "Label": "Min Replicas", + "HelpText": "The minimum number of replicas. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling)", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f9954197-1f82-4fd0-82b4-49589927cdd4", + "Name": "HorizontalPodAutoscalerMaxReplicas", + "Label": "Max Replicas", + "HelpText": "The maximum number of replicas. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#horizontalpodautoscalerspec-v1-autoscaling)", + "DefaultValue": "3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "459e202a-37a9-4c48-948c-4110a8778710", + "Name": "HorizontalPodAutoscalerResourceName", + "Label": "Resource Name", + "HelpText": "The name of the resource. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#resourcemetricsource-v2beta2-autoscaling)", + "DefaultValue": "cpu", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "974aca8a-d49b-437c-a289-19fb5a38e2d1", + "Name": "HorizontalPodAutoscalerMetricType", + "Label": "Metric Type", + "HelpText": "The type of measurement made on the target metric. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling)", + "DefaultValue": "Utilization", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Utilization|Utilization +Value|Value +AverageValue|Average Value" + } + }, + { + "Id": "2ae3c78c-95e4-457b-9236-22dc62dc8323", + "Name": "HorizontalPodAutoscalerResourceAverageUtilization", + "Label": "Average Utilization", + "HelpText": "The utilization. Used when Metric Type is set to Utilization. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e913c0e5-6436-44e5-8de4-3739dd0b4e32", + "Name": "HorizontalPodAutoscalerResourceAverageValue", + "Label": "Average Value", + "HelpText": "The average value. Used when Metric Type is set to Average Value. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fdab7800-8d01-4a44-926e-038d4ee7080d", + "Name": "HorizontalPodAutoscalerResourceValue", + "Label": "Value", + "HelpText": "The value. Used when Metric Type is set to Value. [Documentation link](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#metrictarget-v2beta2-autoscaling)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-07-09T00:30:39.078Z", + "OctopusVersion": "2020.3.0-rc0001", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "Category": "k8s" +} diff --git a/step-templates/k8s-inspect-resource-pwsh.json.human b/step-templates/k8s-inspect-resource-pwsh.json.human new file mode 100644 index 000000000..1bdfab40d --- /dev/null +++ b/step-templates/k8s-inspect-resource-pwsh.json.human @@ -0,0 +1,244 @@ +{ + "Id": "53fe7e02-d003-4860-bf15-1122a128d7c0", + "Name": "Kubernetes - Inspect Resources", + "Category": "k8s", + "LastModifiedBy": "mcasperson", + "Description": "Inspect K8S resources with common actions like get, describe and logs. Optionally create artifacts containing the output.", + "ActionType": "Octopus.KubernetesRunScript", + "Version": 6, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "<# + This script provides a general purpose method for querying Kubernetes resources. It supports common operations + like get, describe, logs and output formats like yaml and json. Output can be captured as artifacts. +#> + +<# +.Description +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605 +#> +Function Execute-Command ($commandPath, $commandArguments) +{ + Write-Host \"Executing: $commandPath $($commandArguments -join \" \")\" + + Try { + $pinfo = New-Object System.Diagnostics.ProcessStartInfo + $pinfo.FileName = $commandPath + $pinfo.RedirectStandardError = $true + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + $pinfo.Arguments = $commandArguments + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $pinfo + $p.Start() | Out-Null + [pscustomobject]@{ + stdout = $p.StandardOutput.ReadToEnd() + stderr = $p.StandardError.ReadToEnd() + ExitCode = $p.ExitCode + } + $p.WaitForExit() + } + Catch { + exit + } +} + +<# +.Description +Find any resource names that match a wildcard input if one was specified +#> +function Get-Resources() +{ + $names = $OctopusParameters[\"K8SInspectNames\"] -Split \"`n\" | % {$_.Trim()} + + if ($OctopusParameters[\"K8SInspectNames\"] -match '\\*' ) + { + return Execute-Command kubectl (@(\"-o\", \"json\", \"get\", $OctopusParameters[\"K8SInspectResource\"])) | + # Select the stdout property from the execution + Select-Object -ExpandProperty stdout | + # Convert the output from JSON + ConvertFrom-JSON | + # Get the items object from the kubectl response + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} | + # Extract the name + % {$_.metadata.name} | + # Find any matching resources + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0} + } + else + { + return $names + } +} + +<# +.Description +Get the kubectl arguments for a given action +#> +function Get-KubectlVerb() +{ + switch($OctopusParameters[\"K8SInspectKubectlVerb\"]) + { + \"get json\" {return ,@(\"-o\", \"json\", \"get\")} + \"get yaml\" {return ,@(\"-o\", \"yaml\", \"get\")} + \"describe\" {return ,@(\"describe\")} + \"logs\" {return ,@(\"logs\")} + \"logs tail\" {return ,@(\"logs\", \"--tail\", \"100\")} + \"previous logs\" {return ,@(\"logs\", \"--previous\")} + \"previous logs tail\" {return ,@(\"logs\", \"--previous\", \"--tail\", \"100\")} + default {return ,@(\"get\")} + } +} + +<# +.Description +Get an appropiate file extension based on the selected action +#> +function Get-ArtifactExtension() +{ + switch($OctopusParameters[\"K8SInspectKubectlVerb\"]) + { + \"get json\" {\"json\"} + \"get yaml\" {\"yaml\"} + default {\"txt\"} + } +} + +if ($OctopusParameters[\"K8SInspectKubectlVerb\"] -like \"*logs*\") +{ + if ( -not @($OctopusParameters[\"K8SInspectResource\"]) -like \"pod*\") + { + Write-Error \"Logs can only be returned for pods, not $($OctopusParameters[\"K8SInspectResource\"])\" + } + else + { + Execute-Command kubectl (@(\"-o\", \"json\", \"get\", \"pods\") + (Get-Resources)) | + # Select the stdout property from the execution + Select-Object -ExpandProperty stdout | + # Convert the output from JSON + ConvertFrom-JSON | + # Get the items object from the kubectl response + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} | + # Get the pod logs for each container + % { + $podDetails = $_ + @{ + logs=$podDetails.spec.containers | % {$logs=\"\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \"-c\", $_.name))) -ExpandProperty stdout)} {$logs}; + name=$podDetails.metadata.name + } + } | + # Write the output + % {Write-Host $_.logs; $_} | + # Optionally capture the artifact + % { + if ($OctopusParameters[\"K8SInspectCreateArtifact\"] -ieq \"true\") + { + Set-Content -Path \"$($_.name).$(Get-ArtifactExtension)\" -Value $_.logs + New-OctopusArtifact \"$($_.name).$(Get-ArtifactExtension)\" + } + } + } +} +else +{ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\"K8SInspectResource\"]) + (Get-Resources)) | + % {Select-Object -InputObject $_ -ExpandProperty stdout} | + % {Write-Host $_; $_} | + % { + if ($OctopusParameters[\"K8SInspectCreateArtifact\"] -ieq \"true\") + { + Set-Content -Path \"output.$(Get-ArtifactExtension)\" -Value $_ + New-OctopusArtifact \"output.$(Get-ArtifactExtension)\" + } + } +} +", + "Octopus.Action.KubernetesContainers.Namespace": "#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}" + }, + "Parameters": [ + { + "Id": "8a1ebc8c-ddf3-42bb-be49-9b36ee417f5d", + "Name": "K8SInspectResource", + "Label": "Resource", + "HelpText": "The type of Kubernetes resource to inspect. The list provided is not comprehensive, and any resource can be used if the field is bound.", + "DefaultValue": "pod", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "pod|Pod +service|Service +ingress|Ingress +deployment|Deployment +statefulset|StatefulSet +daemonset|DaemonSet +replicaset|ReplicaSet +configmap|ConfigMap +secret|Secret +node|Node +virtualservice|VirtualService +gateway|Gateway +persistentvolume|PersistentVolume +serviceaccount|Service Account +rolebinding|Role Binding +clusterrolebinding|Cluster Role Binding +role|Role +clusterrole|Cluster Role" + } + }, + { + "Id": "735e2fa4-4f9a-4183-aafe-653f3f6fb103", + "Name": "K8SInspectKubectlVerb", + "Label": "Kubectl Verb", + "HelpText": "The action used to inspect the Kubernetes resource.", + "DefaultValue": "get", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "get|Get +get json|Get JSON +get yaml|Get YAML +logs|Pod Logs +logs tail|Pod Logs Tail +previous logs|Previous Pod Logs +previous logs tail|Previous Pod Logs Tail +describe|Describe" + } + }, + { + "Id": "9c9dcd65-07eb-4e7d-a61a-370d35d1cf76", + "Name": "K8SInspectNames", + "Label": "Resource Names", + "HelpText": "An optional line break separated list of resources to inspect. If left blank, all resources are inspected. An asterisk can be used as a wildcard.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "4805afba-9ff9-45a6-b2c2-764c2d0e5240", + "Name": "K8SInspectCreateArtifact", + "Label": "Create Artifact", + "HelpText": "Check this to create an artifact capturing the output of the kubectl command.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "da70800b-0ec2-4bef-918c-08ef88c9f411", + "Name": "K8SInspectNamespace", + "Label": "Namespace", + "HelpText": "The Kubernetes namespace to inspect. Leave blank to use the default namespace of the Kubernetes target.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-04-22T01:41:35.012Z", + "OctopusVersion": "2020.2.4-ci0070", + "Type": "ActionTemplate" + } +} diff --git a/step-templates/keeper-secretsmanager-retrieve-secrets.json.human b/step-templates/keeper-secretsmanager-retrieve-secrets.json.human new file mode 100644 index 000000000..988e99386 --- /dev/null +++ b/step-templates/keeper-secretsmanager-retrieve-secrets.json.human @@ -0,0 +1,285 @@ +{ + "Id": "95a35cf6-ce95-4b81-b8de-0892cffca4c4", + "Name": "Keeper Secrets Manager - Retrieve Secrets", + "Description": "This step retrieves one or more secrets from a Keeper Vault and creates [sensitive output variables](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) for each value retrieved. These values can be used in other steps in your deployment or runbook process. + +You can retrieve secrets using Keeper Notation URIs, and you can choose a custom output variable name for each secret. + +--- + +**Required:** +- A [Keeper Secrets Manager](https://docs.keeper.io/secrets-manager/) application with permissions to retrieve secrets from the Keeper Vault. +- The `SecretManagement.Keeper.Extension` PowerShell module installed on the target or worker. If the module can't be found, the step will fail. *The `SecretManagement.Keeper` module(s) can be installed from the [PowerShell gallery](https://www.powershellgallery.com/packages/SecretManagement.Keeper)* + +Notes: + +- Tested on Octopus `2022.4`. +- Tested with both Windows PowerShell and PowerShell Core on Linux. + +", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +# Variables +$KsmModuleName = \"SecretManagement.Keeper.Extension\" +$KsmParentModuleName = \"SecretManagement.Keeper\" +$KsmConfig = $OctopusParameters[\"Keeper.SecretsManager.RetrieveSecrets.Config\"] +$VaultSecrets = $OctopusParameters[\"Keeper.SecretsManager.RetrieveSecrets.VaultSecrets\"] +$KsmModuleSpecificVersion = $OctopusParameters[\"Keeper.SecretsManager.RetrieveSecrets.KsmModule.SpecificVersion\"] +$KsmModuleCustomInstallLocation = $OctopusParameters[\"Keeper.SecretsManager.RetrieveSecrets.KsmModule.CustomInstallLocation\"] +$PrintVariableNames = $OctopusParameters[\"Keeper.SecretsManager.RetrieveSecrets.PrintVariableNames\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($VaultSecrets)) { + throw \"Required parameter Keeper.SecretsManager.RetrieveSecrets.VaultSecrets not specified\" +} + +if ([string]::IsNullOrWhiteSpace($KsmModuleSpecificVersion) -eq $False) { + $requiredVersion = [Version]$KdmModuleSpecificVersion +} + +# Cross-platform bits +$WindowsPowerShell = $True +if ($PSEdition -eq \"Core\") { + $WindowsPowerShell = $False +} + +### Helper functions +function Get-Module-CrossPlatform { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [string] $Name + ) + + $module = Get-Module -Name $Name -ListAvailable + if($WindowsPowerShell -eq $True -and $null -eq $module) { + $module = Get-InstalledModule -Name $Name + } + + return $module +} + +function Load-Module { + Param( + [Parameter(Mandatory = $true)][string] $name + ) + + $retVal = $true + if (!(Get-Module -Name $name)) { + $isAvailable = Get-Module -ListAvailable | where { $_.Name -eq $name } + if ($isAvailable) { + try { + Import-Module $name -ErrorAction SilentlyContinue + } catch { + $retVal = $false + } + } else { + $retVal = $false + } + } + return $retVal +} + +$PowerShellModuleName = $KsmModuleName + +# Check for Custom install location specified for KsmModule +if ([string]::IsNullOrWhiteSpace($KsmModuleCustomInstallLocation) -eq $false) { + if ((Test-Path $KsmModuleCustomInstallLocation -IsValid) -eq $false) { + throw \"The path $KsmModuleCustomInstallLocation is not valid, please use a relative or absolute path.\" + } + + $KsmModulesFolder = [System.IO.Path]::GetFullPath($KsmModuleCustomInstallLocation) + $LocalModules = (New-Item \"$KsmModulesFolder\" -ItemType Directory -Force).FullName + $env:PSModulePath = $LocalModules + [System.IO.Path]::PathSeparator + $env:PSModulePath + + # Check to see if there + if ((Test-Path -Path \"$LocalModules/$KsmModuleName\") -eq $true) + { + # Use specific location + $PowerShellModuleName = \"$LocalModules/$PowerShellModuleName\" + } +} + +# Import module +if([string]::IsNullOrWhiteSpace($KsmModuleSpecificVersion)) { + Write-Host \"Importing module $PowerShellModuleName ...\" + if ((Load-Module -Name $PowerShellModuleName) -eq $false) { + Write-Host \"Extension module not found $PowerShellModuleName - trying to find sub-module in parent $KsmParentModuleName\" + if (Get-Module -ListAvailable -Name $KsmParentModuleName) { + $KsmParentModuleDir = Split-Path -Path (Get-Module -ListAvailable -Name $KsmParentModuleName).Path + $KsmModuleFolder = [System.IO.Path]::GetFullPath($KsmParentModuleDir) + $LocalModules = (New-Item \"$KsmModuleFolder\" -ItemType Directory -Force).FullName + $env:PSModulePath = $LocalModules + [System.IO.Path]::PathSeparator + $env:PSModulePath + + if ((Test-Path -Path \"$LocalModules/$KsmModuleName\") -eq $true) + { + $PowerShellModuleName = \"$LocalModules/$PowerShellModuleName\" + try { + Import-Module -Name $PowerShellModuleName -ErrorAction SilentlyContinue + Write-Host \"Imported sub-module $PowerShellModuleName ...\" + } catch { + Write-Host \"Failed to import sub-module $PowerShellModuleName ...\" + } + } + } else { + Write-Host \"Module does not exist\" + } + } +} +else { + Write-Host \"Importing module $PowerShellModuleName ($KsmModuleSpecificVersion)...\" + Import-Module -Name $PowerShellModuleName -RequiredVersion $requiredVersion +} + +# Check if SecretManagement.Keeper.Extension Module is installed. +$ksmVaultModule = Get-Module-CrossPlatform -Name $KsmModuleName +if ($null -eq $ksmVaultModule) { + throw \"Cannot find the '$KsmModuleName' module on the machine. If you think it is installed, try restarting the Tentacle service for it to be detected.\" +} + +$Secrets = @() +$VariablesCreated = 0 +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +# Extract lines and split into notations and variables +$index = 0 +$usedNames = @() +@(($VaultSecrets -Split \"`n\").Trim()) | ForEach-Object { + if (![string]::IsNullOrWhiteSpace($_)) { + Write-Verbose \"Working on: '$_'\" + + # Split 'Notation | VariableName' and generate new var name if needed + $notation = $_ + $variableName = \"\" + $n = $_.LastIndexOf(\"|\") + if ($n -ge 0) { + if ($n -lt $notation.Length-1) { + $variableName = $notation.SubString($n+1).Trim() + } + $notation = $notation.SubString(0, $n).Trim() + } + if ([string]::IsNullOrWhiteSpace($variableName)) { + do { + $index++ + $variableName = \"KsmSecret\" + $index + } while ($usedNames.Contains($variableName)) + } + # Duplicate var - either overlapping KsmSecretN or another user variable + if($usedNames.Contains($variableName)) { + throw \"Duplicate variable name: '$variableName'\" + } + $usedNames += $variableName + + if([string]::IsNullOrWhiteSpace($notation)) { + throw \"Unable to establish notation URI from: '$($_)'\" + } + $secret = [PsCustomObject]@{ + Notation = $notation + VariableName = $variableName + } + $Secrets += $secret + } +} + +Write-Verbose \"Print variables: $PrintVariableNames\" +Write-Verbose \"Secrets to retrieve: $($Secrets.Count)\" +Write-Verbose \"KSM Version specified: $KsmModuleSpecificVersion\" +Write-Verbose \"KSM Custom Install Dir: $KsmModuleCustomInstallLocation\" + +# Retrieve Secrets +foreach($secret in $secrets) { + $notation = $secret.Notation + $variableName = $secret.VariableName + + $ksmSecretValue = Get-Notation -Notation $notation -Config $KsmConfig + + Set-OctopusVariable -Name $variableName -Value $ksmSecretValue -Sensitive + + if($PrintVariableNames -eq $True) { + Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$variableName}\" + } + $VariablesCreated += 1 +} + +Write-Host \"Created $variablesCreated output variables\" +" + }, + "Parameters": [ + { + "Id": "7daedc7d-7623-47b8-98ba-747290f04372", + "Name": "Keeper.SecretsManager.RetrieveSecrets.Config", + "Label": "Keeper Secrets Manager Configuration", + "HelpText": "Keeper Secrets Manager [configuration](https://docs.keeper.io/secrets-manager/secrets-manager/about/secrets-manager-configuration) for [KSM Application](https://docs.keeper.io/secrets-manager/secrets-manager/quick-start-guide) with permissions to retrieve secrets from the Keeper Vault. To generate KSM Configuration in Web Vault: Secrets Manager - KSM Application Name - Devices - Edit - Add Device, and switch to Method: Configuration File, preferably in Base64 format", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "10df4954-8683-434c-b708-cd25b3b395ff", + "Name": "Keeper.SecretsManager.RetrieveSecrets.VaultSecrets", + "Label": "Vault Secrets to retrieve", + "HelpText": "Use [Secrets Manager Notation URIs](https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation) to specify the Secrets to be returned from Keeper Vault, in the format `SecretsManagerNotation URI | OutputVariableName` where: + +- `OutputVariableName` is the _optional_ Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) name to store the secret's value in. *If this value isn't specified, an output name will be generated dynamically*. + +**Note:** Multiple fields can be retrieved by entering each one on a new line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "15bd51e5-72be-4600-85ba-7b4cf1a8e157", + "Name": "Keeper.SecretsManager.RetrieveSecrets.PrintVariableNames", + "Label": "Print output variable names", + "HelpText": "Write out the Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) names to the task log. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "6888b6b2-8916-4f88-9c7a-d811654e5a2b", + "Name": "Keeper.SecretsManager.RetrieveSecrets.KsmModule.SpecificVersion", + "Label": "SecretManagement.Keeper.Extension PowerShell Module version (optional)", + "HelpText": "If you wish to use a specific version of the `SecretManagement.Keeper.Extension` PowerShell module (rather than the default), enter the version number here. e.g. `16.5.0`. + +**Note:** The version specified must exist on the machine. Version 16.5.0 is the lowest supported version +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0a2b750b-0ad3-48fc-9d68-e133746226a9", + "Name": "Keeper.SecretsManager.RetrieveSecrets.KsmModule.CustomInstallLocation", + "Label": "SecretManagement.Keeper.Extension PowerShell Install Location (optional)", + "HelpText": "If you wish to provide a custom path to the `SecretManagement.Keeper.Extension` PowerShell module (rather than the default), enter the value here. + +**Note:** The Module must exist at the specified location on the machine. This step template will not download the Module. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "idimov-keeper", + "LastModifiedAt": "2024-06-12T00:54:34.7240000Z", + "$Meta": { + "ExportedAt": "2024-06-12T00:54:34.7240000Z", + "OctopusVersion": "2022.4.8319", + "Type": "ActionTemplate" + }, + "Category": "keeper-secretsmanager" +} diff --git a/step-templates/launchdarkly-toggle-feature-flag.json.human b/step-templates/launchdarkly-toggle-feature-flag.json.human new file mode 100644 index 000000000..33e808beb --- /dev/null +++ b/step-templates/launchdarkly-toggle-feature-flag.json.human @@ -0,0 +1,76 @@ +{ + "Id": "bf51e357-8b88-4cfb-8568-550fe0a2d68a", + "Name": "LaunchDarkly - Toggle a feature flag", + "Description": "Toggle a LaunchDarkly feature flag on or off ", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$headers = @{ Authorization = \"#{launchdarkly-access-token}\"; \"content-type\" = \"application/json\" } +$body = @( @{ op = \"replace\"; path = \"/environments/#{launchdarkly-environment-key}/on\"; value = #{if launchdarkly-flag-value}$true#{/if}#{unless launchdarkly-flag-value}$false#{/unless} } ) +$bodyAsJson = ConvertTo-Json -InputObject $body -Compress + +Invoke-RestMethod 'https://app.launchdarkly.com/api/v2/flags/#{launchdarkly-project-key}/#{launchdarkly-flag-key}' -Method Patch -Body $bodyAsJson -Headers $headers" + }, + "Parameters": [ + { + "Id": "6504527d-cdb9-43d5-be88-45f942a4ac8d", + "Name": "launchdarkly-access-token", + "Label": "Access Token", + "HelpText": "Your LaunchDarkly access token", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2cc43e76-7d49-48ad-922a-9b2ddfaa2098", + "Name": "launchdarkly-project-key", + "Label": "Project key", + "HelpText": "The key of the project containing this feature flag", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "01c4084a-368f-4bc0-9eb9-8b7764905a90", + "Name": "launchdarkly-environment-key", + "Label": "Environment key", + "HelpText": "The key of the environment to change this feature flag value in", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cfa20800-262b-47d8-b9d0-a50283d08f37", + "Name": "launchdarkly-flag-key", + "Label": "Feature flag key", + "HelpText": "The key of the feature flag you want to toggle", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b91bcc79-3672-4679-b82b-2bd616f455f8", + "Name": "launchdarkly-flag-value", + "Label": "Feature flag enabled", + "HelpText": "Enable targeting for this feature flag?", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "AndyMDoyle", + "$Meta": { + "ExportedAt": "2018-07-09T09:48:16.600Z", + "OctopusVersion": "2018.5.7", + "Type": "ActionTemplate" + }, + "Category": "launchdarkly" +} diff --git a/step-templates/letsencrypt-azure-dns.json.human b/step-templates/letsencrypt-azure-dns.json.human new file mode 100644 index 000000000..72b566508 --- /dev/null +++ b/step-templates/letsencrypt-azure-dns.json.human @@ -0,0 +1,441 @@ +{ + "Id": "79e0dd12-6222-4f8a-a8dc-bcbe579ed729", + "Name": "Lets Encrypt - Azure DNS", + "Description": "Request (or renew) a X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/). + +#### Features + +- ACME v2 protocol support which allows generating wildcard certificates (*.example.com) +- [Azure DNS](https://azure.microsoft.com/en-us/services/dns/) Challenge for TLD, CNAME and Wildcard domains. +- Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates). +- Verified to work on both Windows (PowerShell 5+) and Linux (PowerShell 6+) deployment Targets or Workers.", + "ActionType": "Octopus.Script", + "Version": 11, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "############################################################################### +# TLS 1.2 +############################################################################### +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +############################################################################### +# Required Modules folder +############################################################################### +Write-Host \"Checking for required powershell modules folder\" +$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\" +if ($PSEdition -eq \"Core\") { + if ($PSVersionTable.Platform -eq \"Unix\") { + $ModulesFolder = \"$HOME/.local/share/powershell/Modules\" + } + else { + $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\" + } +} +$PSModuleFolderExists = (Test-Path $ModulesFolder) +if ($PSModuleFolderExists -eq $False) { +\tWrite-Host \"Creating directory: $ModulesFolder\" +\tNew-Item $ModulesFolder -ItemType Directory -Force + $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath +} + +############################################################################### +# Required Modules +############################################################################### +Write-Host \"Checking for required modules.\" +$required_posh_acme_version = 3.12.0 +$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version } + +if (-not ($module_check)) { + Write-Host \"Ensuring NuGet provider is bootstrapped.\" + Get-PackageProvider NuGet -ForceBootstrap | Out-Null + Write-Host \"Installing Posh-ACME.\" + Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force +} + +Import-Module Posh-ACME + +############################################################################### +# Constants +############################################################################### +$LE_AzureDNS_CertificateDomain = $OctopusParameters[\"LE_AzureDNS_CertificateDomain\"] +$LE_AzureDNS_CertificateName = \"Lets Encrypt - $($LE_AzureDNS_CertificateDomain)\" + +# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt +$LE_AzureDNS_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\") +$LE_AzureDNS_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\") + +############################################################################### +# Helpers +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $response = $reader.ReadToEnd() + + return $response | ConvertFrom-Json + } + } + else { + return $RequestError.ErrorDetails.Message + } +} + +############################################################################### +# Functions +############################################################################### +function Get-LetsEncryptCertificate { + Write-Debug \"Entering: Get-LetsEncryptCertificate\" + + if ($OctopusParameters[\"LE_AzureDNS_Use_Staging\"] -eq $True) { + Write-Host \"Using Lets Encrypt Server: Staging\" + Set-PAServer LE_STAGE; + } + else { + Write-Host \"Using Lets Encrypt Server: Production\" + Set-PAServer LE_PROD; + } + + # Clobber account if it exists. + $le_account = Get-PAAccount + if ($le_account) { + Remove-PAAccount $le_account.Id -Force + } + + $azure_password = ConvertTo-SecureString -String $OctopusParameters[\"LE_AzureDNS_AzureAccount.Password\"] -AsPlainText -Force + $azure_credential = New-Object System.Management.Automation.PSCredential($OctopusParameters[\"LE_AzureDNS_AzureAccount.Client\"], $azure_password) + $azure_params = @{ + AZSubscriptionId = $OctopusParameters[\"LE_AzureDNS_AzureAccount.SubscriptionNumber\"]; + AZTenantId = $OctopusParameters[\"LE_AzureDNS_AzureAccount.TenantId\"]; + AZAppCred = $azure_credential + } + + try { + + $DnsPlugins = @(\"Azure\") + $DomainList = @($LE_AzureDNS_CertificateDomain) + + # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com. + if ($LE_AzureDNS_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_AzureDNS_CreateWildcardSAN\"] -eq $True) { + $LE_AzureDNS_Certificate_SAN = $LE_AzureDNS_CertificateDomain.Replace(\"*.\",\"\") + $DomainList += $LE_AzureDNS_Certificate_SAN + # Include additional DnsPlugin of same type to suppress warning. + $DnsPlugins += \"Azure\" + } + + $Cert_Params = @{ + Domain = $DomainList + AcceptTOS = $True; + Contact = $OctopusParameters[\"LE_AzureDNS_ContactEmailAddress\"]; + DnsPlugin = $DnsPlugins; + PluginArgs = $azure_params; + PfxPass = $OctopusParameters[\"LE_AzureDNS_PfxPassword\"]; + Force = $True; + } + + return New-PACertificate @Cert_Params + } + catch { + Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Get-OctopusCertificates { + Write-Debug \"Entering: Get-OctopusCertificates\" + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_AzureDNS_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_AzureDNS_CertificateDomain)\" + + try { + # Get a list of certificates that match our domain search criteria. + $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items + + # We don't want to confuse Production and Staging Lets Encrypt Certificates. + $possible_issuers = $LE_AzureDNS_Issuers + if ($OctopusParameters[\"LE_AzureDNS_Use_Staging\"] -eq $True) { + $possible_issuers = $LE_AzureDNS_Fake_Issuers + } + + return $certificates_search | Where-Object { + $_.SubjectCommonName -eq $LE_AzureDNS_CertificateDomain -and + $possible_issuers -contains $_.IssuerCommonName -and + $null -eq $_.ReplacedBy -and + $null -eq $_.Archived + } + } + catch { + Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Publish-OctopusCertificate { + param ( + [string] $JsonBody + ) + + Write-Debug \"Entering: Publish-OctopusCertificate\" + + if (-not ($JsonBody)) { + Write-Host \"Existing Certificate is required.\" + exit 1 + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_AzureDNS_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Published $($LE_AzureDNS_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\" + } + catch { + Write-Host \"Failed to publish $($LE_AzureDNS_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Update-OctopusCertificate { + param ( + [string]$Certificate_Id, + [string]$JsonBody + ) + + Write-Debug \"Entering: Update-OctopusCertificate\" + + if (-not ($Certificate_Id -and $JsonBody)) { + Write-Host \"Existing Certificate Id and a replace Certificate are required.\" + exit 1 + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_AzureDNS_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Replaced $($LE_AzureDNS_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\" + } + catch { + Write-Error \"Failed to replace $($LE_AzureDNS_CertificateDomain) certificate. Error: $($_.Exception.Message)\" + exit 1 + } +} + +function Get-NewCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-NewCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit 1 + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + Name = \"$LE_AzureDNS_CertificateName\"; + Notes = \"\"; + CertificateData = @{ + HasValue = $true; + NewValue = $certificate_base64; + }; + Password = @{ + HasValue = $true; + NewValue = $OctopusParameters[\"LE_AzureDNS_PfxPassword\"]; + }; + } + + return $certificate_body | ConvertTo-Json +} + +function Get-ReplaceCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit 1 + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + CertificateData = $certificate_base64; + Password = $OctopusParameters[\"LE_AzureDNS_PfxPassword\"]; + } + + return $certificate_body | ConvertTo-Json +} + +############################################################################### +# DO THE THING | MAIN | +############################################################################### +Write-Debug \"Do the Thing\" + +Write-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\" +$certificates = Get-OctopusCertificates + +# Check for PFX & PEM +if ($certificates) { + + # Handle weird behavior between Powershell 5 and Powershell 6+ + $certificate_count = 1 + if ($certificates.Count -ge 1) { + $certificate_count = $certificates.Count + } + + Write-Host \"Found $certificate_count for $($LE_AzureDNS_CertificateDomain).\" + Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_AzureDNS_ReplaceIfExpiresInDays\"]) days.\" + + # Check Expiry Dates + $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_AzureDNS_ReplaceIfExpiresInDays\"]) } + + if ($expiring_certificates) { + Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_AzureDNS_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_AzureDNS_CertificateDomain) from Lets Encrypt\" + $le_certificate = Get-LetsEncryptCertificate + + # PFX + $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1 + $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate + Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json + } + else { + Write-Host \"Nothing to do here...\" + } + + exit 0 +} + +# No existing Certificates - Lets get some new ones. +Write-Host \"No existing certificates found for $($LE_AzureDNS_CertificateDomain).\" +Write-Host \"Request New Certificate for $($LE_AzureDNS_CertificateDomain) from Lets Encrypt\" + +# New Certificate.. +$le_certificate = Get-LetsEncryptCertificate + +Write-Host \"Publishing: LetsEncrypt - $($LE_AzureDNS_CertificateDomain) (PFX)\" +$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate +Publish-OctopusCertificate -JsonBody $certificate_as_json + +Write-Host \"GREAT SUCCESS\" +", + "Octopus.Action.SubstituteInFiles.Enabled": "True" + }, + "Parameters": [{ + "Id": "f1739a56-2603-42e0-b629-801dd71b0b0c", + "Name": "LE_AzureDNS_CertificateDomain", + "Label": "Certificate Domain", + "HelpText": "Domain (TLD, CNAME or Wildcard) to create a certificate for. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c1f44b38-8025-4b12-b010-df48c02b3da4", + "Name": "LE_AzureDNS_PfxPassword", + "Label": "PFX Password", + "HelpText": "Password to use when converting to / from PFX. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c258ee0e-e298-4fa1-9c66-5ac0749409c5", + "Name": "LE_AzureDNS_ReplaceIfExpiresInDays", + "Label": "Replace expiring certificate before N days", + "HelpText": "Replace the certificate if it expiries within N days", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2935998d-d030-4af6-ad42-39e8b85e2dce", + "Name": "LE_AzureDNS_AzureAccount", + "Label": "Azure account", + "HelpText": "An Azure Account that has API access to make DNS changes. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "85af482d-e577-40b8-94e5-626e545adab5", + "Name": "LE_AzureDNS_Octopus_APIKey", + "Label": "Octopus Deploy API key", + "HelpText": "A Octopus Deploy API key with access to change Certificates in the Certificate Store. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "00d513ed-3d75-48d2-954a-0a0165d85530", + "Name": "LE_AzureDNS_Use_Staging", + "Label": "Use Lets Encrypt Staging", + "HelpText": "Should the Certificate be generated using the Lets Encrypt Staging infrastructure?", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "f1fd315d-9fc5-4b15-b53c-9381ecc5cb88", + "Name": "LE_AzureDNS_ContactEmailAddress", + "Label": "Contact Email Address", + "HelpText": "Email Address", + "DefaultValue": "#{Octopus.Deployment.CreatedBy.EmailAddress}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2dc7d9bc-9eee-4ee0-a33c-ef9371ed69f1", + "Name": "LE_AzureDNS_CreateWildcardSAN", + "Label": "Create Wildcard SAN", + "HelpText": "Should the certificate have a Subject Alternative Name (SAN) excluding the wildcard? + +e.g. a certificate domain of `*.internal.example-domain.com` could also have a SAN of `internal.example-domain.com`", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedAt": "2022-02-07T09:38:11.788Z", + "$Meta": { + "ExportedAt": "2024-06-24T06:57:36.821Z", + "OctopusVersion": "2024.3.4152", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "benjimac93", + "Category": "lets-encrypt" +} diff --git a/step-templates/letsencrypt-cloudflare.json.human b/step-templates/letsencrypt-cloudflare.json.human new file mode 100644 index 000000000..44d44e102 --- /dev/null +++ b/step-templates/letsencrypt-cloudflare.json.human @@ -0,0 +1,456 @@ +{ + "Id": "cfb61368-7a85-480a-9770-b6d268f77a13", + "Name": "Lets Encrypt - Cloudflare", + "Description": "Request (or renew) a X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/). + +#### Features + +- ACME v2 protocol support which allows generating wildcard certificates (*.example.com) +- [Cloudflare DNS](https://www.cloudflare.com/en-au/dns/) Challenge for TLD, CNAME and Wildcard domains. +- Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates). +- Verified to work on both Windows (PowerShell 5+) and Linux (PowerShell 6+) deployment Targets or Workers.", + "ActionType": "Octopus.Script", + "Version": 10, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "############################################################################### +# TLS 1.2 +############################################################################### +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +############################################################################### +# Required Modules folder +############################################################################### +Write-Host \"Checking for required powershell modules folder\" +$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\" +if ($PSEdition -eq \"Core\") { + if ($PSVersionTable.Platform -eq \"Unix\") { + $ModulesFolder = \"$HOME/.local/share/powershell/Modules\" + } + else { + $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\" + } +} +$PSModuleFolderExists = (Test-Path $ModulesFolder) +if ($PSModuleFolderExists -eq $False) { +\tWrite-Host \"Creating directory: $ModulesFolder\" +\tNew-Item $ModulesFolder -ItemType Directory -Force + $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath +} + +############################################################################### +# Required Modules +############################################################################### +Write-Host \"Checking for required modules.\" +$required_posh_acme_version = 3.12.0 +$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version } + +if (-not ($module_check)) { + Write-Host \"Ensuring NuGet provider is bootstrapped.\" + Get-PackageProvider NuGet -ForceBootstrap | Out-Null + Write-Host \"Installing Posh-ACME.\" + Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force +} + +Import-Module Posh-ACME + +############################################################################### +# Constants +############################################################################### +$LE_Cloudflare_CertificateDomain = $OctopusParameters[\"LE_Cloudflare_CertificateDomain\"] +$LE_Cloudflare_CertificateName = \"Lets Encrypt - $($LE_Cloudflare_CertificateDomain)\" + +# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt +$LE_Cloudflare_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\") +$LE_Cloudflare_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\") + +############################################################################### +# Helpers +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $response = $reader.ReadToEnd() + + return $response | ConvertFrom-Json + } + } + else { + return $RequestError.ErrorDetails.Message + } +} + +############################################################################### +# Functions +############################################################################### +function Get-LetsEncryptCertificate { + Write-Debug \"Entering: Get-LetsEncryptCertificate\" + + if ($OctopusParameters[\"LE_Cloudflare_Use_Staging\"] -eq $True) { + Write-Host \"Using Lets Encrypt Server: Staging\" + Set-PAServer LE_STAGE; + } + else { + Write-Host \"Using Lets Encrypt Server: Production\" + Set-PAServer LE_PROD; + } + + # Clobber account if it exists. + $le_account = Get-PAAccount + if ($le_account) { + Remove-PAAccount $le_account.Id -Force + } + + # Cloudflare API tokens require some special wrangling. + $cloudflare_token = ConvertTo-SecureString -String $OctopusParameters[\"LE_Cloudflare_PrimaryToken\"] -AsPlainText -Force + $cloudflare_args = @{ + CFToken = $cloudflare_token + } + + if ($OctopusParameters[\"LE_Cloudflare_SecondaryToken\"]) { + Write-Debug \"LE_Cloudflare_SecondaryToken has a value. Passing it to the Cloudflare DNS plugin as a Read All Token.\" + $cloudflare_token_secondary = ConvertTo-SecureString -String $OctopusParameters[\"LE_Cloudflare_SecondaryToken\"] -AsPlainText -Force + $cloudflare_args.CFTokenReadAll = $cloudflare_token_secondary + } + + try { + + $DnsPlugins = @(\"Cloudflare\") + $DomainList = @($LE_Cloudflare_CertificateDomain) + + # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com. + if ($LE_Cloudflare_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_Cloudflare_CreateWildcardSAN\"] -eq $True) { + $LE_Cloudflare_Certificate_SAN = $LE_Cloudflare_CertificateDomain.Replace(\"*.\",\"\") + $DomainList += $LE_Cloudflare_Certificate_SAN + # Include additional DnsPlugin of same type to suppress warning. + $DnsPluginList += \"Cloudflare\" + } + + $Cert_Params = @{ + Domain = $DomainList + AcceptTOS = $True; + Contact = $OctopusParameters[\"LE_Cloudflare_ContactEmailAddress\"]; + DnsPlugin = $DnsPlugins; + PluginArgs = $cloudflare_args; + PfxPass = $OctopusParameters[\"LE_Cloudflare_PfxPassword\"]; + Force = $True; + } + + return New-PACertificate @Cert_Params + } + catch { + Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Get-OctopusCertificates { + Write-Debug \"Entering: Get-OctopusCertificates\" + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Cloudflare_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_Cloudflare_CertificateDomain)\" + + try { + # Get a list of certificates that match our domain search criteria. + $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items + + # We don't want to confuse Production and Staging Lets Encrypt Certificates. + $possible_issuers = $LE_Cloudflare_Issuers + if ($OctopusParameters[\"LE_Cloudflare_Use_Staging\"] -eq $True) { + $possible_issuers = $LE_Cloudflare_Fake_Issuers + } + + return $certificates_search | Where-Object { + $_.SubjectCommonName -eq $LE_Cloudflare_CertificateDomain -and + $possible_issuers -contains $_.IssuerCommonName -and + $null -eq $_.ReplacedBy -and + $null -eq $_.Archived + } + } + catch { + Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Publish-OctopusCertificate { + param ( + [string] $JsonBody + ) + + Write-Debug \"Entering: Publish-OctopusCertificate\" + + if (-not ($JsonBody)) { + Write-Host \"Existing Certificate is required.\" + exit 1 + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Cloudflare_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Published $($LE_Cloudflare_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\" + } + catch { + Write-Host \"Failed to publish $($LE_Cloudflare_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Update-OctopusCertificate { + param ( + [string]$Certificate_Id, + [string]$JsonBody + ) + + Write-Debug \"Entering: Update-OctopusCertificate\" + + if (-not ($Certificate_Id -and $JsonBody)) { + Write-Host \"Existing Certificate Id and a replace Certificate are required.\" + exit 1 + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Cloudflare_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Replaced $($LE_Cloudflare_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\" + } + catch { + Write-Error \"Failed to replace $($LE_Cloudflare_CertificateDomain) certificate. Error: $($_.Exception.Message)\" + exit 1 + } +} + +function Get-NewCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-NewCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit 1 + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + Name = \"$LE_Cloudflare_CertificateName\"; + Notes = \"\"; + CertificateData = @{ + HasValue = $true; + NewValue = $certificate_base64; + }; + Password = @{ + HasValue = $true; + NewValue = $OctopusParameters[\"LE_Cloudflare_PfxPassword\"]; + }; + } + + return $certificate_body | ConvertTo-Json +} + +function Get-ReplaceCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit 1 + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + CertificateData = $certificate_base64; + Password = $OctopusParameters[\"LE_Cloudflare_PfxPassword\"]; + } + + return $certificate_body | ConvertTo-Json +} + +############################################################################### +# DO THE THING | MAIN | +############################################################################### +Write-Debug \"Do the Thing\" + +Write-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\" +$certificates = Get-OctopusCertificates + +# Check for PFX & PEM +if ($certificates) { + + # Handle weird behavior between Powershell 5 and Powershell 6+ + $certificate_count = 1 + if ($certificates.Count -ge 1) { + $certificate_count = $certificates.Count + } + + Write-Host \"Found $certificate_count for $($LE_Cloudflare_CertificateDomain).\" + Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_Cloudflare_ReplaceIfExpiresInDays\"]) days.\" + + # Check Expiry Dates + $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_Cloudflare_ReplaceIfExpiresInDays\"]) } + + if ($expiring_certificates) { + Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_Cloudflare_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_Cloudflare_CertificateDomain) from Lets Encrypt\" + $le_certificate = Get-LetsEncryptCertificate + + # PFX + $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1 + $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate + Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json + } + else { + Write-Host \"Nothing to do here...\" + } + + exit 0 +} + +# No existing Certificates - Lets get some new ones. +Write-Host \"No existing certificates found for $($LE_Cloudflare_CertificateDomain).\" +Write-Host \"Request New Certificate for $($LE_Cloudflare_CertificateDomain) from Lets Encrypt\" + +# New Certificate.. +$le_certificate = Get-LetsEncryptCertificate + +Write-Host \"Publishing: LetsEncrypt - $($LE_Cloudflare_CertificateDomain) (PFX)\" +$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate +Publish-OctopusCertificate -JsonBody $certificate_as_json + +Write-Host \"GREAT SUCCESS\" +", + "Octopus.Action.SubstituteInFiles.Enabled": "True" + }, + "Parameters": [{ + "Id": "f1739a56-2603-42e0-b629-801dd71b0b0c", + "Name": "LE_Cloudflare_CertificateDomain", + "Label": "Certificate Domain", + "HelpText": "Domain (TLD, CNAME or Wildcard) to create a certificate for. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c1f44b38-8025-4b12-b010-df48c02b3da4", + "Name": "LE_Cloudflare_PfxPassword", + "Label": "PFX Password", + "HelpText": "Password to use when converting to / from PFX. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c258ee0e-e298-4fa1-9c66-5ac0749409c5", + "Name": "LE_Cloudflare_ReplaceIfExpiresInDays", + "Label": "Replace expiring certificate before N days", + "HelpText": "Replace the certificate if it expiries within N days", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fc4f6877-2412-4af6-b87a-652da484cb5e", + "Name": "LE_Cloudflare_PrimaryToken", + "Label": "Primary Cloudflare API Token (Edit Permissions)", + "HelpText": "[Cloudflare API Token](https://support.cloudflare.com/hc/en-us/articles/200167836-Managing-API-Tokens-and-Keys) with the appropriate [Zone Edit](https://github.com/rmbolger/Posh-ACME/blob/master/Posh-ACME/DnsPlugins/Cloudflare-Readme.md) permissions.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e01cc404-2631-4f88-9a1a-9e860bc98e58", + "Name": "LE_Cloudflare_SecondaryToken", + "Label": "Secondary Cloudflare API Token", + "HelpText": "[Cloudflare API Token](https://support.cloudflare.com/hc/en-us/articles/200167836-Managing-API-Tokens-and-Keys) with the appropriate [Zone Read](https://github.com/rmbolger/Posh-ACME/blob/master/Posh-ACME/DnsPlugins/Cloudflare-Readme.md) permissions.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "85af482d-e577-40b8-94e5-626e545adab5", + "Name": "LE_Cloudflare_Octopus_APIKey", + "Label": "Octopus Deploy API key", + "HelpText": "A Octopus Deploy API key with access to change Certificates in the Certificate Store. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "00d513ed-3d75-48d2-954a-0a0165d85530", + "Name": "LE_Cloudflare_Use_Staging", + "Label": "Use Lets Encrypt Staging", + "HelpText": "Should the Certificate be generated using the Lets Encrypt Staging infrastructure?", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "f1fd315d-9fc5-4b15-b53c-9381ecc5cb88", + "Name": "LE_Cloudflare_ContactEmailAddress", + "Label": "Contact Email Address", + "HelpText": "Email Address", + "DefaultValue": "#{Octopus.Deployment.CreatedBy.EmailAddress}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2dc7d9bc-9eee-4ee0-a33c-ef9371ed69f1", + "Name": "LE_Cloudflare_CreateWildcardSAN", + "Label": "Create Wildcard SAN", + "HelpText": "Should the certificate have a Subject Alternative Name (SAN) excluding the wildcard? + +e.g. a certificate domain of `*.internal.example-domain.com` could also have a SAN of `internal.example-domain.com`", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedAt": "2022-02-07T09:38:11.788Z", + "$Meta": { + "ExportedAt": "2024-06-24T06:57:36.821Z", + "OctopusVersion": "2024.3.4152", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "benjimac93", + "Category": "lets-encrypt" +} diff --git a/step-templates/letsencrypt-dnsimple.json.human b/step-templates/letsencrypt-dnsimple.json.human new file mode 100644 index 000000000..27e775858 --- /dev/null +++ b/step-templates/letsencrypt-dnsimple.json.human @@ -0,0 +1,471 @@ +{ + "Id": "21583723-1283-46aa-bb7b-121d365837cb", + "Name": "Lets Encrypt - DNSimple", + "Description": "Request (or renew) a X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/). + +#### Features + +- ACME v2 protocol support which allows generating wildcard certificates (*.example.com) +- [DNSimple](https://dnsimple.com/) Challenge for TLD, CNAME and Wildcard domains. +- Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates). +- Verified to work on both Windows (PowerShell 5+) and Linux (PowerShell 6+) deployment Targets or Workers.", + "ActionType": "Octopus.Script", + "Version": 7, + "CommunityActionTemplateId": null, + "Packages": [ + + ], + "Properties":{ + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "############################################################################### +# TLS 1.2 +############################################################################### +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +############################################################################### +# Required Modules folder +############################################################################### +Write-Host \"Checking for required powershell modules folder\" +$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\" +if ($PSEdition -eq \"Core\") { + if ($PSVersionTable.Platform -eq \"Unix\") { + $ModulesFolder = \"$HOME/.local/share/powershell/Modules\" + } + else { + $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\" + } +} +$PSModuleFolderExists = (Test-Path $ModulesFolder) +if ($PSModuleFolderExists -eq $False) { +\tWrite-Host \"Creating directory: $ModulesFolder\" +\tNew-Item $ModulesFolder -ItemType Directory -Force + $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath +} + +############################################################################### +# Required Modules +############################################################################### +Write-Host \"Checking for required modules.\" +$required_posh_acme_version = 3.12.0 +$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version } + +if (-not ($module_check)) { + Write-Host \"Ensuring NuGet provider is bootstrapped.\" + Get-PackageProvider NuGet -ForceBootstrap | Out-Null + Write-Host \"Installing Posh-ACME.\" + Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force +} + +Import-Module Posh-ACME + +############################################################################### +# DebugOutput +############################################################################### +if ($OctopusParameters[\"LE_DNSimple_Debug_Output\"] -eq $True) { +\tWrite-Host \"Setting DebugPreference to Continue\" + $DebugPreference = 'Continue' +} + +############################################################################### +# Constants +############################################################################### +$LE_DNSimple_CertificateDomain = $OctopusParameters[\"LE_DNSimple_CertificateDomain\"] +$LE_DNSimple_CertificateName = \"Lets Encrypt - $($LE_DNSimple_CertificateDomain)\" + +# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt +$LE_DNSimple_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\") +$LE_DNSimple_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\") + + +############################################################################### +# Helpers +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $response = $reader.ReadToEnd() + + return $response | ConvertFrom-Json + } + } + else { + return $RequestError.ErrorDetails.Message + } +} + +############################################################################### +# Functions +############################################################################### +function Get-LetsEncryptCertificate { + Write-Debug \"Entering: Get-LetsEncryptCertificate\" + + if ($OctopusParameters[\"LE_DNSimple_Use_Staging\"] -eq $True) { + Write-Host \"Using Lets Encrypt Server: Staging\" + Set-PAServer LE_STAGE; + } + else { + Write-Host \"Using Lets Encrypt Server: Production\" + Set-PAServer LE_PROD; + } + + # Clobber account if it exists. + $le_account = Get-PAAccount + if ($le_account) { + Remove-PAAccount $le_account.Id -Force + } + +\t$dnsimple_args = @{} + # DNSimple requires a token. If it's windows, Secure-String is supported. + if ($IsWindows -and 'Desktop' -eq $PSEdition) { + $token = ConvertTo-SecureString -String $OctopusParameters[\"LE_DNSimple_Token\"] -AsPlainText -Force + \t$dnsimple_args = @{ + \tDSToken = $token + \t} + } + else { + \t$token = $OctopusParameters[\"LE_DNSimple_Token\"] + \t$dnsimple_args = @{ + \tDSTokenInsecure = $token + \t} + } + + try { + + $DnsPlugins = @(\"DNSimple\") + $DomainList = @($LE_DNSimple_CertificateDomain) + + # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com. + if ($LE_DNSimple_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_DNSimple_CreateWildcardSAN\"] -eq $True) { + $LE_DNSimple_Certificate_SAN = $LE_DNSimple_CertificateDomain.Replace(\"*.\",\"\") + $DomainList += $LE_DNSimple_Certificate_SAN + # Include additional DnsPlugin of same type to surpress warning. + $DnsPlugins += \"DNSimple\" + } + + $Cert_Params = @{ + Domain = $DomainList + AcceptTOS = $True; + Contact = $OctopusParameters[\"LE_DNSimple_ContactEmailAddress\"]; + DnsPlugin = $DnsPlugins; + PluginArgs = $dnsimple_args; + PfxPass = $OctopusParameters[\"LE_DNSimple_PfxPassword\"]; + Force = $True; + } + + return New-PACertificate @Cert_Params + } + catch { + Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Get-OctopusCertificates { + Write-Debug \"Entering: Get-OctopusCertificates\" + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_DNSimple_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_DNSimple_CertificateDomain)\" + + try { + # Get a list of certificates that match our domain search criteria. + $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items + + # We don't want to confuse Production and Staging Lets Encrypt Certificates. + $possible_issuers = $LE_DNSimple_Issuers + if ($OctopusParameters[\"LE_DNSimple_Use_Staging\"] -eq $True) { + $possible_issuers = $LE_DNSimple_Fake_Issuers + } + + return $certificates_search | Where-Object { + $_.SubjectCommonName -eq $LE_DNSimple_CertificateDomain -and + $possible_issuers -contains $_.IssuerCommonName -and + $null -eq $_.ReplacedBy -and + $null -eq $_.Archived + } + } + catch { + Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Publish-OctopusCertificate { + param ( + [string] $JsonBody + ) + + Write-Debug \"Entering: Publish-OctopusCertificate\" + + if (-not ($JsonBody)) { + Write-Host \"Existing Certificate is required.\" + exit 1 + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_DNSimple_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Published $($LE_DNSimple_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\" + } + catch { + Write-Host \"Failed to publish $($LE_DNSimple_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Update-OctopusCertificate { + param ( + [string]$Certificate_Id, + [string]$JsonBody + ) + + Write-Debug \"Entering: Update-OctopusCertificate\" + + if (-not ($Certificate_Id -and $JsonBody)) { + Write-Host \"Existing Certificate Id and a replace Certificate are required.\" + exit 1 + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_DNSimple_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Replaced $($LE_DNSimple_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\" + } + catch { + Write-Error \"Failed to replace $($LE_DNSimple_CertificateDomain) certificate. Error: $($_.Exception.Message)\" + exit 1 + } +} + +function Get-NewCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-NewCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit 1 + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + Name = \"$LE_DNSimple_CertificateName\"; + Notes = \"\"; + CertificateData = @{ + HasValue = $true; + NewValue = $certificate_base64; + }; + Password = @{ + HasValue = $true; + NewValue = $OctopusParameters[\"LE_DNSimple_PfxPassword\"]; + }; + } + + return $certificate_body | ConvertTo-Json +} + +function Get-ReplaceCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit 1 + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + CertificateData = $certificate_base64; + Password = $OctopusParameters[\"LE_DNSimple_PfxPassword\"]; + } + + return $certificate_body | ConvertTo-Json +} + +############################################################################### +# DO THE THING | MAIN | +############################################################################### +Write-Debug \"Do the Thing\" + +Write-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\" +$certificates = Get-OctopusCertificates + +# Check for PFX & PEM +if ($certificates) { + + # Handle weird behavior between Powershell 5 and Powershell 6+ + $certificate_count = 1 + if ($certificates.Count -ge 1) { + $certificate_count = $certificates.Count + } + + Write-Host \"Found $certificate_count for $($LE_DNSimple_CertificateDomain).\" + Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_DNSimple_ReplaceIfExpiresInDays\"]) days.\" + + # Check Expiry Dates + $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_DNSimple_ReplaceIfExpiresInDays\"]) } + + if ($expiring_certificates) { + Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_DNSimple_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_DNSimple_CertificateDomain) from Lets Encrypt\" + $le_certificate = Get-LetsEncryptCertificate + + # PFX + $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1 + $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate + Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json + } + else { + Write-Host \"Nothing to do here...\" + } + + exit 0 +} + +# No existing Certificates - Lets get some new ones. +Write-Host \"No existing certificates found for $($LE_DNSimple_CertificateDomain).\" +Write-Host \"Request New Certificate for $($LE_DNSimple_CertificateDomain) from Lets Encrypt\" + +# New Certificate.. +$le_certificate = Get-LetsEncryptCertificate + +Write-Host \"Publishing: LetsEncrypt - $($LE_DNSimple_CertificateDomain) (PFX)\" +$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate +Publish-OctopusCertificate -JsonBody $certificate_as_json + +Write-Host \"GREAT SUCCESS\" +", + "Octopus.Action.SubstituteInFiles.Enabled": "True" + }, + "Parameters": [ + { + "Id": "d0984e44-0783-4ddc-8a57-8008997edb2a", + "Name": "LE_DNSimple_CertificateDomain", + "Label": "Certificate Domain", + "HelpText": "Domain (TLD, CNAME or Wildcard) to create a certificate for. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c68389d4-17e4-491b-a6ce-ee6b09ae1579", + "Name": "LE_DNSimple_PfxPassword", + "Label": "PFX Password", + "HelpText": "Password to use when converting to / from PFX. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "0bce1a67-4981-474d-8b93-873fa3b28712", + "Name": "LE_DNSimple_ReplaceIfExpiresInDays", + "Label": "Replace expiring certificate before N days", + "HelpText": "Replace the certificate if it expiries within N days", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f1f25997-9733-4882-a546-ef6c76b7c7f1", + "Name": "LE_DNSimple_Token", + "Label": "DNSimple API Token", + "HelpText": "DNSimple API Token created from your [DNSimple account](https://github.com/rmbolger/Posh-ACME/blob/master/Posh-ACME/DnsPlugins/DNSimple-Readme.md#setup)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "1addd2e6-782d-4a1f-bd12-480a9dd964cd", + "Name": "LE_DNSimple_Octopus_APIKey", + "Label": "Octopus Deploy API key", + "HelpText": "A Octopus Deploy API key with access to change Certificates in the Certificate Store. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ad07684b-93ea-4d65-b2a8-395e3bbfdaf8", + "Name": "LE_DNSimple_Use_Staging", + "Label": "Use Lets Encrypt Staging", + "HelpText": "Should the Certificate be generated using the Lets Encrypt Staging infrastructure?", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "9a358e2e-07df-42d1-9f0a-04cbc800dacf", + "Name": "LE_DNSimple_ContactEmailAddress", + "Label": "Contact Email Address", + "HelpText": "Email Address", + "DefaultValue": "#{Octopus.Deployment.CreatedBy.EmailAddress}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3a9e5773-9fbb-4dd4-a9c2-0f66a36b74a2", + "Name": "LE_DNSimple_Debug_Output", + "Label": "Debug Output", + "HelpText": "Tick this to provide debug information in the output", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "2dc7d9bc-9eee-4ee0-a33c-ef9371ed69f1", + "Name": "LE_DNSimple_CreateWildcardSAN", + "Label": "Create Wildcard SAN", + "HelpText": "Should the certificate have a Subject Alternative Name (SAN) excluding the wildcard? + +e.g. a certificate domain of `*.internal.example-domain.com` could also have a SAN of `internal.example-domain.com`", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedAt": "2022-02-07T09:38:11.788Z", + "$Meta": { + "ExportedAt": "2024-06-24T06:57:36.821Z", + "OctopusVersion": "2024.3.4152", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "benjimac93", + "Category": "lets-encrypt" +} diff --git a/step-templates/letsencrypt-google-cloud.json.human b/step-templates/letsencrypt-google-cloud.json.human new file mode 100644 index 000000000..e57d4d60b --- /dev/null +++ b/step-templates/letsencrypt-google-cloud.json.human @@ -0,0 +1,441 @@ +{ + "Id": "0ea316b3-562e-4782-8611-ce82f5224eff", + "Name": "Lets Encrypt - Google Cloud DNS", + "Description": "Request (or renew) a X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/). + +#### Features + +- ACME v2 protocol support which allows generating wildcard certificates (*.example.com) +- [Google Cloud DNS](https://cloud.google.com/dns) Challenge for TLD, CNAME and Wildcard domains. +- Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates). +- Verified to work on both Windows (PowerShell 5+) and Linux (PowerShell 6+) deployment Targets or Workers.", + "ActionType": "Octopus.Script", + "Version": 11, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "############################################################################### +# TLS 1.2 +############################################################################### +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +############################################################################### +# Required Modules folder +############################################################################### +Write-Host \"Checking for required powershell modules folder\" +$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\" +if ($PSEdition -eq \"Core\") { + if ($PSVersionTable.Platform -eq \"Unix\") { + $ModulesFolder = \"$HOME/.local/share/powershell/Modules\" + } + else { + $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\" + } +} +$PSModuleFolderExists = (Test-Path $ModulesFolder) +if ($PSModuleFolderExists -eq $False) { +\tWrite-Host \"Creating directory: $ModulesFolder\" +\tNew-Item $ModulesFolder -ItemType Directory -Force + $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath +} + +############################################################################### +# Required Modules +############################################################################### +Write-Host \"Checking for required modules.\" +$required_posh_acme_version = 3.12.0 +$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version } + +if (-not ($module_check)) { + Write-Host \"Ensuring NuGet provider is bootstrapped.\" + Get-PackageProvider NuGet -ForceBootstrap | Out-Null + Write-Host \"Installing Posh-ACME.\" + Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force +} + +Import-Module Posh-ACME + +############################################################################### +# Constants +############################################################################### +$LE_GCloudDNS_CertificateDomain = $OctopusParameters[\"LE_GCloudDNS_CertificateDomain\"] +$LE_GCloudDNS_CertificateName = \"Lets Encrypt - $($LE_GCloudDNS_CertificateDomain)\" + +# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt +$LE_GCloudDNS_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\") +$LE_GCloudDNS_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\") + +############################################################################### +# Helpers +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $response = $reader.ReadToEnd() + + return $response | ConvertFrom-Json + } + } + else { + return $RequestError.ErrorDetails.Message + } +} + +############################################################################### +# Functions +############################################################################### +function Get-LetsEncryptCertificate { + Write-Debug \"Entering: Get-LetsEncryptCertificate\" + + if ($OctopusParameters[\"LE_GCloudDNS_Use_Staging\"] -eq $True) { + Write-Host \"Using Lets Encrypt Server: Staging\" + Set-PAServer LE_STAGE; + } + else { + Write-Host \"Using Lets Encrypt Server: Production\" + Set-PAServer LE_PROD; + } + + # Clobber account if it exists. + $le_account = Get-PAAccount + if ($le_account) { + Remove-PAAccount $le_account.Id -Force + } + + # Google Cloud requires JSON data to be passed in. + $gcloud_json = \"$(New-Guid).json\" + Set-Content -Path $gcloud_json -Value $OctopusParameters[\"LE_GCloudDNS_JSON\"] + + $gcloud_args = @{ + GCKeyFile = $gcloud_json + } + + try { + $DnsPlugins = @(\"GCloud\") + $DomainList = @($LE_GCloudDNS_CertificateDomain) + + # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com. + if ($LE_GCloudDNS_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_GCloudDNS_CreateWildcardSAN\"] -eq $True) { + $LE_GCloudDNS_Certificate_SAN = $LE_GCloudDNS_CertificateDomain.Replace(\"*.\",\"\") + $DomainList += $LE_GCloudDNS_Certificate_SAN + # Include additional DnsPlugin of same type to surpress warning. + $DnsPlugins += \"GCloud\" + } + + $Cert_Params = @{ + Domain = $DomainList + AcceptTOS = $True; + Contact = $OctopusParameters[\"LE_GCloudDNS_ContactEmailAddress\"]; + DnsPlugin = $DnsPlugins; + PluginArgs = $gcloud_args; + PfxPass = $OctopusParameters[\"LE_GCloudDNS_PfxPassword\"]; + Force = $True; + } + + return New-PACertificate @Cert_Params + } + catch { + Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Get-OctopusCertificates { + Write-Debug \"Entering: Get-OctopusCertificates\" + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_GCloudDNS_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_GCloudDNS_CertificateDomain)\" + + try { + # Get a list of certificates that match our domain search criteria. + $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items + + # We don't want to confuse Production and Staging Lets Encrypt Certificates. + $possible_issuers = $LE_GCloudDNS_Issuers + if ($OctopusParameters[\"LE_GCloudDNS_Use_Staging\"] -eq $True) { + $possible_issuers = $LE_GCloudDNS_Fake_Issuers + } + + return $certificates_search | Where-Object { + $_.SubjectCommonName -eq $LE_GCloudDNS_CertificateDomain -and + $possible_issuers -contains $_.IssuerCommonName -and + $null -eq $_.ReplacedBy -and + $null -eq $_.Archived + } + } + catch { + Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Publish-OctopusCertificate { + param ( + [string] $JsonBody + ) + + Write-Debug \"Entering: Publish-OctopusCertificate\" + + if (-not ($JsonBody)) { + Write-Host \"Existing Certificate is required.\" + exit 1 + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_GCloudDNS_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Published $($LE_GCloudDNS_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\" + } + catch { + Write-Host \"Failed to publish $($LE_GCloudDNS_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Update-OctopusCertificate { + param ( + [string]$Certificate_Id, + [string]$JsonBody + ) + + Write-Debug \"Entering: Update-OctopusCertificate\" + + if (-not ($Certificate_Id -and $JsonBody)) { + Write-Host \"Existing Certificate Id and a replace Certificate are required.\" + exit 1 + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_GCloudDNS_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Replaced $($LE_GCloudDNS_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\" + } + catch { + Write-Error \"Failed to replace $($LE_GCloudDNS_CertificateDomain) certificate. Error: $($_.Exception.Message)\" + exit 1 + } +} + +function Get-NewCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-NewCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit 1 + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + Name = \"$LE_GCloudDNS_CertificateName\"; + Notes = \"\"; + CertificateData = @{ + HasValue = $true; + NewValue = $certificate_base64; + }; + Password = @{ + HasValue = $true; + NewValue = $OctopusParameters[\"LE_GCloudDNS_PfxPassword\"]; + }; + } + + return $certificate_body | ConvertTo-Json +} + +function Get-ReplaceCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit 1 + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + CertificateData = $certificate_base64; + Password = $OctopusParameters[\"LE_GCloudDNS_PfxPassword\"]; + } + + return $certificate_body | ConvertTo-Json +} + +############################################################################### +# DO THE THING | MAIN | +############################################################################### +Write-Debug \"Do the Thing\" + +Write-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\" +$certificates = Get-OctopusCertificates + +# Check for PFX & PEM +if ($certificates) { + + # Handle weird behavior between Powershell 5 and Powershell 6+ + $certificate_count = 1 + if ($certificates.Count -ge 1) { + $certificate_count = $certificates.Count + } + + Write-Host \"Found $certificate_count for $($LE_GCloudDNS_CertificateDomain).\" + Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_GCloudDNS_ReplaceIfExpiresInDays\"]) days.\" + + # Check Expiry Dates + $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_GCloudDNS_ReplaceIfExpiresInDays\"]) } + + if ($expiring_certificates) { + Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_GCloudDNS_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_GCloudDNS_CertificateDomain) from Lets Encrypt\" + $le_certificate = Get-LetsEncryptCertificate + + # PFX + $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1 + $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate + Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json + } + else { + Write-Host \"Nothing to do here...\" + } + + exit 0 +} + +# No existing Certificates - Lets get some new ones. +Write-Host \"No existing certificates found for $($LE_GCloudDNS_CertificateDomain).\" +Write-Host \"Request New Certificate for $($LE_GCloudDNS_CertificateDomain) from Lets Encrypt\" + +# New Certificate.. +$le_certificate = Get-LetsEncryptCertificate + +Write-Host \"Publishing: LetsEncrypt - $($LE_GCloudDNS_CertificateDomain) (PFX)\" +$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate +Publish-OctopusCertificate -JsonBody $certificate_as_json + +Write-Host \"GREAT SUCCESS\" +", + "Octopus.Action.SubstituteInFiles.Enabled": "True" + }, + "Parameters": [{ + "Id": "f1739a56-2603-42e0-b629-801dd71b0b0c", + "Name": "LE_GCloudDNS_CertificateDomain", + "Label": "Certificate Domain", + "HelpText": "Domain (TLD, CNAME or Wildcard) to create a certificate for. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c1f44b38-8025-4b12-b010-df48c02b3da4", + "Name": "LE_GCloudDNS_PfxPassword", + "Label": "PFX Password", + "HelpText": "Password to use when converting to / from PFX. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c258ee0e-e298-4fa1-9c66-5ac0749409c5", + "Name": "LE_GCloudDNS_ReplaceIfExpiresInDays", + "Label": "Replace expiring certificate before N days", + "HelpText": "Replace the certificate if it expiries within N days", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d346e12a-4f9f-4317-8399-469c4b71b0a5", + "Name": "LE_GCloudDNS_JSON", + "Label": "Google Cloud IAM Json", + "HelpText": "Google Cloud IAM JSON with the [appropriate permissions](https://github.com/rmbolger/Posh-ACME/blob/master/Posh-ACME/DnsPlugins/GCloud-Readme.md).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "85af482d-e577-40b8-94e5-626e545adab5", + "Name": "LE_GCloudDNS_Octopus_APIKey", + "Label": "Octopus Deploy API key", + "HelpText": "A Octopus Deploy API key with access to change Certificates in the Certificate Store. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "00d513ed-3d75-48d2-954a-0a0165d85530", + "Name": "LE_GCloudDNS_Use_Staging", + "Label": "Use Lets Encrypt Staging", + "HelpText": "Should the Certificate be generated using the Lets Encrypt Staging infrastructure?", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "f1fd315d-9fc5-4b15-b53c-9381ecc5cb88", + "Name": "LE_GCloudDNS_ContactEmailAddress", + "Label": "Contact Email Address", + "HelpText": "Email Address", + "DefaultValue": "#{Octopus.Deployment.CreatedBy.EmailAddress}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2dc7d9bc-9eee-4ee0-a33c-ef9371ed69f1", + "Name": "LE_GCloudDNS_CreateWildcardSAN", + "Label": "Create Wildcard SAN", + "HelpText": "Should the certificate have a Subject Alternative Name (SAN) excluding the wildcard? + +e.g. a certificate domain of `*.internal.example-domain.com` could also have a SAN of `internal.example-domain.com`", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedAt": "2022-02-07T09:38:11.788Z", + "$Meta": { + "ExportedAt": "2024-06-24T06:57:36.821Z", + "OctopusVersion": "2024.3.4152", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "benjimac93", + "Category": "lets-encrypt" +} diff --git a/step-templates/letsencrypt-route-53.json.human b/step-templates/letsencrypt-route-53.json.human new file mode 100644 index 000000000..af7a012c8 --- /dev/null +++ b/step-templates/letsencrypt-route-53.json.human @@ -0,0 +1,439 @@ +{ + "Id": "55b654f4-3e01-424f-b299-f99beddf0501", + "Name": "Lets Encrypt - Route53", + "Description": "Request (or renew) a X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/). + +#### Features + +- ACME v2 protocol support which allows generating wildcard certificates (*.example.com) +- [AWS Route53](https://aws.amazon.com/route53/) Challenge for TLD, CNAME and Wildcard domains. +- Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates). +- Verified to work on both Windows (PowerShell 5+) and Linux (PowerShell 6+) deployment Targets or Workers.", + "ActionType": "Octopus.Script", + "Version": 12, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "############################################################################### +# TLS 1.2 +############################################################################### +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +############################################################################### +# Required Modules folder +############################################################################### +Write-Host \"Checking for required powershell modules folder\" +$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\" +if ($PSEdition -eq \"Core\") { + if ($PSVersionTable.Platform -eq \"Unix\") { + $ModulesFolder = \"$HOME/.local/share/powershell/Modules\" + } + else { + $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\" + } +} +$PSModuleFolderExists = (Test-Path $ModulesFolder) +if ($PSModuleFolderExists -eq $False) { +\tWrite-Host \"Creating directory: $ModulesFolder\" +\tNew-Item $ModulesFolder -ItemType Directory -Force + $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath +} + +############################################################################### +# Required Modules +############################################################################### +Write-Host \"Checking for required modules.\" +$required_posh_acme_version = 3.12.0 +$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version } + +if (-not ($module_check)) { + Write-Host \"Ensuring NuGet provider is bootstrapped.\" + Get-PackageProvider NuGet -ForceBootstrap | Out-Null + Write-Host \"Installing Posh-ACME.\" + Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force +} + +Import-Module Posh-ACME + +############################################################################### +# Constants +############################################################################### +$LE_Route53_CertificateDomain = $OctopusParameters[\"LE_Route53_CertificateDomain\"] +$LE_Route53_CertificateName = \"Lets Encrypt - $($LE_Route53_CertificateDomain)\" + +# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt +$LE_Route53_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\") +$LE_Route53_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\") + +############################################################################### +# Helpers +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $response = $reader.ReadToEnd() + + return $response | ConvertFrom-Json + } + } + else { + return $RequestError.ErrorDetails.Message + } +} + +############################################################################### +# Functions +############################################################################### +function Get-LetsEncryptCertificate { + Write-Debug \"Entering: Get-LetsEncryptCertificate\" + + if ($OctopusParameters[\"LE_Route53_Use_Staging\"] -eq $True) { + Write-Host \"Using Lets Encrypt Server: Staging\" + Set-PAServer LE_STAGE; + } + else { + Write-Host \"Using Lets Encrypt Server: Production\" + Set-PAServer LE_PROD; + } + + # Clobber account if it exists. + $le_account = Get-PAAccount + if ($le_account) { + Remove-PAAccount $le_account.Id -Force + } + + $aws_secret_key = ConvertTo-SecureString -String $OctopusParameters[\"LE_Route53_AWSAccount.SecretKey\"] -AsPlainText -Force + $route53_params = @{ + R53AccessKey = $OctopusParameters[\"LE_Route53_AWSAccount.AccessKey\"]; + R53SecretKey = $aws_secret_key + } + + try { + $DnsPlugins = @(\"Route53\") + $DomainList = @($LE_Route53_CertificateDomain) + + # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com. + if ($LE_Route53_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_Route53_CreateWildcardSAN\"] -eq $True) { + $LE_Route53_Certificate_SAN = $LE_Route53_CertificateDomain.Replace(\"*.\",\"\") + $DomainList += $LE_Route53_Certificate_SAN + # Include additional DnsPlugin of same type to surpress warning. + $DnsPlugins += \"Route53\" + } + + $Cert_Params = @{ + Domain = $DomainList + AcceptTOS = $True; + Contact = $OctopusParameters[\"LE_Route53_ContactEmailAddress\"]; + DnsPlugin = $DnsPlugins; + PluginArgs = $route53_params; + PfxPass = $OctopusParameters[\"LE_Route53_PfxPassword\"]; + Force = $True; + } + + return New-PACertificate @Cert_Params + } + catch { + Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Get-OctopusCertificates { + Write-Debug \"Entering: Get-OctopusCertificates\" + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Route53_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_Route53_CertificateDomain)\" + + try { + # Get a list of certificates that match our domain search criteria. + $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items + + # We don't want to confuse Production and Staging Lets Encrypt Certificates. + $possible_issuers = $LE_Route53_Issuers + if ($OctopusParameters[\"LE_Route53_Use_Staging\"] -eq $True) { + $possible_issuers = $LE_Route53_Fake_Issuers + } + + return $certificates_search | Where-Object { + $_.SubjectCommonName -eq $LE_Route53_CertificateDomain -and + $possible_issuers -contains $_.IssuerCommonName -and + $null -eq $_.ReplacedBy -and + $null -eq $_.Archived + } + } + catch { + Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Publish-OctopusCertificate { + param ( + [string] $JsonBody + ) + + Write-Debug \"Entering: Publish-OctopusCertificate\" + + if (-not ($JsonBody)) { + Write-Host \"Existing Certificate Id and a replace Certificate are required.\" + exit 1 + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Route53_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Published $($LE_Route53_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\" + } + catch { + Write-Host \"Failed to publish $($LE_Route53_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Update-OctopusCertificate { + param ( + [string]$Certificate_Id, + [string]$JsonBody + ) + + Write-Debug \"Entering: Update-OctopusCertificate\" + + if (-not ($Certificate_Id -and $JsonBody)) { + Write-Host \"Existing Certificate Id and a replace Certificate are required.\" + exit 1 + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Route53_Octopus_APIKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Replaced $($LE_Route53_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\" + } + catch { + Write-Error \"Failed to replace $($LE_Route53_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + exit 1 + } +} + +function Get-NewCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-NewCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit 1 + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + Name = \"$LE_Route53_CertificateName\"; + Notes = \"\"; + CertificateData = @{ + HasValue = $true; + NewValue = $certificate_base64; + }; + Password = @{ + HasValue = $true; + NewValue = $OctopusParameters[\"LE_Route53_PfxPassword\"]; + }; + } + + return $certificate_body | ConvertTo-Json +} + +function Get-ReplaceCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit 1 + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + CertificateData = $certificate_base64; + Password = $OctopusParameters[\"LE_Route53_PfxPassword\"]; + } + + return $certificate_body | ConvertTo-Json +} + +############################################################################### +# DO THE THING | MAIN | +############################################################################### +Write-Debug \"Do the Thing\" + +Write-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\" +$certificates = Get-OctopusCertificates + +# Check for PFX & PEM +if ($certificates) { + + # Handle weird behavior between Powershell 5 and Powershell 6+ + $certificate_count = 1 + if ($certificates.Count -ge 1) { + $certificate_count = $certificates.Count + } + + Write-Host \"Found $certificate_count for $($LE_Route53_CertificateDomain).\" + Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_Route53_ReplaceIfExpiresInDays\"]) days.\" + + # Check Expiry Dates + $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_Route53_ReplaceIfExpiresInDays\"]) } + + if ($expiring_certificates) { + Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_Route53_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_Route53_CertificateDomain) from Lets Encrypt\" + $le_certificate = Get-LetsEncryptCertificate + + # PFX + $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1 + $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate + Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json + } + else { + Write-Host \"Nothing to do here...\" + } + + exit 0 +} + +# No existing Certificates - Lets get some new ones. +Write-Host \"No existing certificates found for $($LE_Route53_CertificateDomain).\" +Write-Host \"Request New Certificate for $($LE_Route53_CertificateDomain) from Lets Encrypt\" + +# New Certificate.. +$le_certificate = Get-LetsEncryptCertificate + +Write-Host \"Publishing: LetsEncrypt - $($LE_Route53_CertificateDomain) (PFX)\" +$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate +Publish-OctopusCertificate -JsonBody $certificate_as_json + +Write-Host \"GREAT SUCCESS\" +", + "Octopus.Action.SubstituteInFiles.Enabled": "True" + }, + "Parameters": [{ + "Id": "f1739a56-2603-42e0-b629-801dd71b0b0c", + "Name": "LE_Route53_CertificateDomain", + "Label": "Certificate Domain", + "HelpText": "Domain (TLD, CNAME or Wildcard) to create a certificate for. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c1f44b38-8025-4b12-b010-df48c02b3da4", + "Name": "LE_Route53_PfxPassword", + "Label": "PFX Password", + "HelpText": "Password to use when converting to / from PFX. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c258ee0e-e298-4fa1-9c66-5ac0749409c5", + "Name": "LE_Route53_ReplaceIfExpiresInDays", + "Label": "Replace expiring certificate before N days", + "HelpText": "Replace the certificate if it expiries within N days", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2935998d-d030-4af6-ad42-39e8b85e2dce", + "Name": "LE_Route53_AWSAccount", + "Label": "AWS account", + "HelpText": "An AWS Account that has API access to make Route53 changes. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "85af482d-e577-40b8-94e5-626e545adab5", + "Name": "LE_Route53_Octopus_APIKey", + "Label": "Octopus Deploy API key", + "HelpText": "A Octopus Deploy API key with access to change Certificates in the Certificate Store. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "00d513ed-3d75-48d2-954a-0a0165d85530", + "Name": "LE_Route53_Use_Staging", + "Label": "Use Lets Encrypt Staging", + "HelpText": "Should the Certificate be generated using the Lets Encrypt Staging infrastructure?", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "f1fd315d-9fc5-4b15-b53c-9381ecc5cb88", + "Name": "LE_Route53_ContactEmailAddress", + "Label": "Contact Email Address", + "HelpText": "Email Address", + "DefaultValue": "#{Octopus.Deployment.CreatedBy.EmailAddress}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2dc7d9bc-9eee-4ee0-a33c-ef9371ed69f1", + "Name": "LE_Route53_CreateWildcardSAN", + "Label": "Create Wildcard SAN", + "HelpText": "Should the certificate have a Subject Alternative Name (SAN) excluding the wildcard? + +e.g. a certificate domain of `*.internal.example-domain.com` could also have a SAN of `internal.example-domain.com`", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedAt": "2022-02-07T09:38:11.788Z", + "$Meta": { + "ExportedAt": "2024-06-24T06:57:36.821Z", + "OctopusVersion": "2024.3.4152", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "benjimac93", + "Category": "lets-encrypt" +} diff --git a/step-templates/letsencrypt-selfhosted-http.json.human b/step-templates/letsencrypt-selfhosted-http.json.human new file mode 100644 index 000000000..bd4707889 --- /dev/null +++ b/step-templates/letsencrypt-selfhosted-http.json.human @@ -0,0 +1,519 @@ +{ + "Id": "e3614dd6-3a78-4220-97f0-b0e44415e58c", + "Name": "Lets Encrypt - Self-Hosted HTTP Challenge", + "Description": "Request (or renew) an X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/) using the Self-hosted HTTP Challenge Listener provided by the [Posh-ACME](https://github.com/rmbolger/Posh-ACME/) PowerShell Module. + +--- +#### Please Note + +It's generally a better idea to use one of the Posh-ACME [DNS providers](https://github.com/rmbolger/Posh-ACME/wiki/List-of-Supported-DNS-Providers) for Let's Encrypt. + +There are a number of Octopus Step templates in the [Community Library](https://library.octopus.com/listing/letsencrypt) that support DNS providers. + +--- + +#### Features + +- ACME v2 protocol support which allows generating wildcard certificates (*.example.com). +- [Self-hosted HTTP Challenge](https://github.com/rmbolger/Posh-ACME/wiki/How-To-Self-Host-HTTP-Challenges) Challenge for TLD, CNAME, and Wildcard domains. +- _Optionally_ Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates). +- _Optionally_ import SSL Certificate into the local machine store. +- _Optionally_ Export PFX (PKCS#12) SSL Certificate to a supplied file path. +- Verified to work on Windows and Linux deployment targets + +#### Pre-requisites + +- There are specific requirements when [running on Windows](https://github.com/rmbolger/Posh-ACME/wiki/How-To-Self-Host-HTTP-Challenges#windows-only-prerequisites). +- HTTP Challenge Listener must be available on Port 80. +- When updating the Octopus Certificate Store, access to the Octopus Server from where the script template runs e.g. deployment target or worker is required.", + "ActionType": "Octopus.Script", + "Version": 10, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# TLS 1.2 +Write-Host \"Enabling TLS 1.2 for script execution\" +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 + +############################################################################### +# Required Modules folder +############################################################################### +Write-Host \"Checking for required powershell modules folder\" +$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\" +if ($PSEdition -eq \"Core\") { + if ($PSVersionTable.Platform -eq \"Unix\") { + $ModulesFolder = \"$HOME/.local/share/powershell/Modules\" + } + else { + $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\" + } +} +$PSModuleFolderExists = (Test-Path $ModulesFolder) +if ($PSModuleFolderExists -eq $False) { +\tWrite-Host \"Creating directory: $ModulesFolder\" +\tNew-Item $ModulesFolder -ItemType Directory -Force + $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath +} + +############################################################################### +# Required Modules +############################################################################### +Write-Host \"Checking for required modules.\" +$required_posh_acme_version = 3.12.0 +$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version } + +if (-not ($module_check)) { + Write-Host \"Ensuring NuGet provider is bootstrapped.\" + Get-PackageProvider NuGet -ForceBootstrap | Out-Null + Write-Host \"Installing Posh-ACME.\" + Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force +} + +Write-Host \"Importing Posh-ACME\" +Import-Module Posh-ACME + +# Variables +$LE_SelfHosted_CertificateDomain = $OctopusParameters[\"LE_SelfHosted_CertificateDomain\"] +$LE_SelfHosted_Contact = $OctopusParameters[\"LE_SelfHosted_ContactEmailAddress\"] +$LE_SelfHosted_PfxPass = $OctopusParameters[\"LE_SelfHosted_PfxPass\"] +$LE_SelfHosted_Use_Staging = $OctopusParameters[\"LE_SelfHosted_Use_Staging\"] +$LE_SelfHosted_HttpListenerTimeout = $OctopusParameters[\"LE_SelfHosted_HttpListenerTimeout\"] +$LE_Self_Hosted_UpdateOctopusCertificateStore = $OctopusParameters[\"LE_Self_Hosted_UpdateOctopusCertificateStore\"] +$LE_SelfHosted_Octopus_APIKey = $OctopusParameters[\"LE_SelfHosted_Octopus_APIKey\"] +$LE_SelfHosted_ReplaceIfExpiresInDays = $OctopusParameters[\"LE_SelfHosted_ReplaceIfExpiresInDays\"] +$LE_SelfHosted_Install = $OctopusParameters[\"LE_SelfHosted_Install\"] +$LE_SelfHosted_ExportFilePath = $OctopusParameters[\"LE_SelfHosted_ExportFilePath\"] +$LE_SelfHosted_Export = -not [System.String]::IsNullOrWhiteSpace($LE_SelfHosted_ExportFilePath) +$LE_SelfHosted_TempFileLocation=[System.IO.Path]::GetTempFileName() + +# Consts +$LE_SelfHosted_Certificate_Name = \"Lets Encrypt - $LE_SelfHosted_CertificateDomain\" + +# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt +$LE_SelfHosted_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\") +$LE_SelfHosted_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\") + +# Helper(s) +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $response = $reader.ReadToEnd() + return $response | ConvertFrom-Json + } + } + else { + return $RequestError.ErrorDetails.Message + } +} + +function Clean-TempFiles { +\tif(Test-Path -Path $LE_SelfHosted_TempFileLocation) { +\t\tWrite-Debug \"Removing temporary file...\" +\t\tRemove-Item $LE_SelfHosted_TempFileLocation -Force +\t} +} + +function Exit-Failure { + \tClean-TempFiles +\tExit 1 +} + +function Exit-Success { + \tClean-TempFiles +\tExit 0 +} + +# Functions +function Get-LetsEncryptCertificate { + Write-Debug \"Entering: Get-LetsEncryptCertificate\" + + if ($LE_SelfHosted_Use_Staging -eq $True) { + Write-Host \"Using Lets Encrypt Server: Staging\" + Set-PAServer LE_STAGE; + } + else { + Write-Host \"Using Lets Encrypt Server: Production\" + Set-PAServer LE_PROD; + } + + $le_account = Get-PAAccount + if ($le_account) { + Write-Host \"Removing existing PA-Account...\" + Remove-PAAccount $le_account.Id -Force + } + + Write-Host \"Assigning new PA-Account...\" + $le_account = New-PAAccount -Contact $LE_SelfHosted_Contact -AcceptTOS -Force + + Write-Host \"Requesting new order for $LE_SelfHosted_CertificateDomain...\" + $order = New-PAOrder -Domain $LE_SelfHosted_CertificateDomain -PfxPass $LE_SelfHosted_PfxPass -Force + + try { + \tWrite-Host \"Invoking Self-Hosted HttpChallengeListener with timeout of $LE_SelfHosted_HttpListenerTimeout seconds...\" + \tInvoke-HttpChallengeListener -Verbose -ListenerTimeout $LE_SelfHosted_HttpListenerTimeout + \t + Write-Host \"Getting validated certificate...\" + $pArgs = @{ManualNonInteractive=$True} + $cert = New-PACertificate $LE_SelfHosted_CertificateDomain -PluginArgs $pArgs + + if ($LE_SelfHosted_Install -eq $True) { + \tif (-not $IsWindows -and 'Desktop' -ne $PSEdition) { + Write-Host \"Installing certificate currently only works on Windows\" + \t} + else { + Write-Host \"Installing certificate to local store...\" + $cert | Install-PACertificate + } + \t} + + # Linux showed weird $null issues using the .PfxFullChain path + if(Test-Path -Path $LE_SelfHosted_TempFileLocation) { + \tWrite-Debug \"Creating temp copy of certificate to: $LE_SelfHosted_TempFileLocation\" + \t$bytes = [System.IO.File]::ReadAllBytes($cert.PfxFullChain) + New-Item -Path $LE_SelfHosted_TempFileLocation -ItemType \"file\" -Force + [System.IO.File]::WriteAllBytes($LE_SelfHosted_TempFileLocation, $bytes) + } + + if($LE_SelfHosted_Export -eq $True) { + \tWrite-Host \"Exporting certificate to: $LE_SelfHosted_ExportFilePath\" + \t$bytes = [System.IO.File]::ReadAllBytes($LE_SelfHosted_TempFileLocation) + New-Item -Path $LE_SelfHosted_ExportFilePath -ItemType \"file\" -Force + [System.IO.File]::WriteAllBytes($LE_SelfHosted_ExportFilePath, $bytes) + \t} + + return $cert + } + catch { + Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + Exit-Failure + } +} + +function Get-OctopusCertificates { + Write-Debug \"Entering: Get-OctopusCertificates\" + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $LE_SelfHosted_Octopus_APIKey } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$LE_SelfHosted_CertificateDomain\" + + try { + # Get a list of certificates that match our domain search criteria. + $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items + + # We don't want to confuse Production and Staging Lets Encrypt Certificates. + $possible_issuers = $LE_SelfHosted_Issuers + if ($LE_SelfHosted_Use_Staging -eq $True) { + $possible_issuers = $LE_SelfHosted_Fake_Issuers + } + + return $certificates_search | Where-Object { + $_.SubjectCommonName -eq $LE_SelfHosted_CertificateDomain -and + $possible_issuers -contains $_.IssuerCommonName -and + $null -eq $_.ReplacedBy -and + $null -eq $_.Archived + } + } + catch { + Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + Exit-Failure + } +} + +function Publish-OctopusCertificate { + param ( + [string] $JsonBody + ) + + Write-Debug \"Entering: Publish-OctopusCertificate\" + + if (-not ($JsonBody)) { + Write-Host \"Existing Certificate Id and a replace Certificate are required.\" + Exit-Failure + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $LE_SelfHosted_Octopus_APIKey } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\" +\tWrite-Verbose \"Preparing to publish to: $octopus_certificates_uri\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Published $LE_SelfHosted_CertificateDomain certificate to the Octopus Deploy Certificate Store.\" + } + catch { + Write-Host \"Failed to publish $LE_SelfHosted_CertificateDomain certificate. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + Exit-Failure + } +} + +function Update-OctopusCertificate { + param ( + [string]$Certificate_Id, + [string]$JsonBody + ) + + Write-Debug \"Entering: Update-OctopusCertificate\" + + if (-not ($Certificate_Id -and $JsonBody)) { + Write-Host \"Existing Certificate Id and a replace Certificate are required.\" + Exit-Failure + } + + $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"] + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $LE_SelfHosted_Octopus_APIKey } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\" + + try { + Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing + Write-Host \"Replaced $LE_SelfHosted_CertificateDomain certificate in the Octopus Deploy Certificate Store.\" + } + catch { + Write-Error \"Failed to replace $LE_SelfHosted_CertificateDomain certificate. Error: $($_.Exception.Message). See Debug output for details.\" + Write-Debug (Get-WebRequestErrorBody -RequestError $_) + Exit-Failure + } +} + +function Get-NewCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-NewCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit-Failure + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($LE_SelfHosted_TempFileLocation) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + Name = \"$LE_SelfHosted_CertificateDomain\"; + Notes = \"\"; + CertificateData = @{ + HasValue = $true; + NewValue = $certificate_base64; + }; + Password = @{ + HasValue = $true; + NewValue = $LE_SelfHosted_PfxPass; + }; + } + + return $certificate_body | ConvertTo-Json +} + +function Get-ReplaceCertificatePFXAsJson { + param ( + $Certificate + ) + + Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\" + + if (-not ($Certificate)) { + Write-Host \"Certificate is required.\" + Exit-Failure + } + + [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($LE_SelfHosted_TempFileLocation) + $certificate_base64 = [convert]::ToBase64String($certificate_buffer) + + $certificate_body = @{ + CertificateData = $certificate_base64; + Password = $LE_SelfHosted_PfxPass; + } + + return $certificate_body | ConvertTo-Json +} + +# Main Execution starts here + +Write-Debug \"Running MAIN function...\" + +if ($LE_Self_Hosted_UpdateOctopusCertificateStore -eq $True) { + Write-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store...\" + $certificates = Get-OctopusCertificates + + # Check for PFX & PEM + if ($certificates) { + + # Handle behavior between Powershell 5 and Powershell 6+ + $certificate_count = 1 + if ($certificates.Count -ge 1) { + $certificate_count = $certificates.Count + } + + Write-Host \"Found $certificate_count for $LE_SelfHosted_CertificateDomain.\" + Write-Host \"Checking to see if any expire within $LE_SelfHosted_ReplaceIfExpiresInDays days.\" + + # Check Expiry Dates + $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($LE_SelfHosted_ReplaceIfExpiresInDays) } + + if ($expiring_certificates) { + Write-Host \"Found certificates that expire with $LE_SelfHosted_ReplaceIfExpiresInDays days. Requesting new certificates for $LE_SelfHosted_CertificateDomain from Lets Encrypt\" + $le_certificate = Get-LetsEncryptCertificate + + # PFX + $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1 + $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate + Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json + } + else { + Write-Host \"Nothing to do here...\" + } + +\tWrite-Host \"Completed running...\" + Exit-Success + } +} + +Write-Host \"Requesting New Certificate for $LE_SelfHosted_CertificateDomain from Lets Encrypt\" + +$le_certificate = Get-LetsEncryptCertificate + +if($LE_Self_Hosted_UpdateOctopusCertificateStore -eq $True) { + Write-Host \"Publishing new LetsEncrypt - $LE_SelfHosted_CertificateDomain (PFX) to Octopus Certificate Store\" + $certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate + Publish-OctopusCertificate -JsonBody $certificate_as_json +} +else { + Write-Host \"Certificate generated...\" + $le_certificate | fl +} + +Write-Host \"Completed running...\" +Exit-Success" + }, + "Parameters": [ + { + "Id": "3d7e44e3-8a29-4458-b3ba-c09817566492", + "Name": "LE_SelfHosted_CertificateDomain", + "Label": "Certificate Domain", + "HelpText": "Domain (TLD, CNAME, or Wildcard) to create a certificate for.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7f882a93-511d-4c8c-a946-832d69739773", + "Name": "LE_SelfHosted_ContactEmailAddress", + "Label": "Contact Email Address", + "HelpText": "The Email address used when requesting the SSL certificate. _Default: `#{Octopus.Deployment.CreatedBy.EmailAddress}`_.", + "DefaultValue": "#{Octopus.Deployment.CreatedBy.EmailAddress}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c91935f5-64cb-48c3-940b-981fcbfb942a", + "Name": "LE_SelfHosted_PfxPass", + "Label": "PFX Password", + "HelpText": "The password to use when converting to/from PFX.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "dd75b10a-17ca-4859-ae8d-adf0de74dbce", + "Name": "LE_SelfHosted_Use_Staging", + "Label": "Use Lets Encrypt Staging", + "HelpText": "Should the Certificate be generated using the Lets Encrypt Staging infrastructure? _Default: `False`_.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "9a5d53f9-c5e5-4c83-9769-43b987ba04b0", + "Name": "LE_SelfHosted_HttpListenerTimeout", + "Label": "Http Listener Timeout", + "HelpText": "Self-Hosted Http Listener Timeout in Seconds. _Default: 120 seconds_.", + "DefaultValue": "120", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7d8c49b8-684e-49d3-95aa-353c6e087843", + "Name": "LE_Self_Hosted_UpdateOctopusCertificateStore", + "Label": "Update Octopus Certificate Store?", + "HelpText": "Should any generated certificate be updated in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates) _Default: `True`_.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "83e2d3cf-66a6-47b4-bae8-e207a6918048", + "Name": "LE_SelfHosted_Octopus_APIKey", + "Label": "Octopus Deploy API key", + "HelpText": "An Octopus Deploy API key with access to change Certificates in the Certificate Store. + +**Note:** Required if `LE_Self_Hosted_UpdateOctopusCertificateStore` is set to `True`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "95ff51cc-2390-4b5d-89b0-bd62e83bb4f8", + "Name": "LE_SelfHosted_ReplaceIfExpiresInDays", + "Label": "Replace expiring certificate before N days", + "HelpText": "Replace the certificate if it expiries within N days. _Default: 30 days_. + +**Note:** Required if `LE_Self_Hosted_UpdateOctopusCertificateStore` is set to `True`.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d429561f-cf00-41f6-9956-e994f623696a", + "Name": "LE_SelfHosted_Install", + "Label": "Install Certificate?", + "HelpText": "Installs the certificate in the local store. _Default: `False`_.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "f8abcf14-c9a1-4e15-87dc-95004f2216e6", + "Name": "LE_SelfHosted_ExportFilePath", + "Label": "PFX Export Filepath", + "HelpText": "Exports the full certificate chain as PKCS#12 archive (.PFX used by Windows and IIS) e.g. C:\\Temp\\octopus.com.pfx", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedAt": "2022-02-07T09:38:11.788Z", + "$Meta": { + "ExportedAt": "2024-06-24T06:57:36.821Z", + "OctopusVersion": "2024.3.4152", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "benjimac93", + "Category": "lets-encrypt" +} diff --git a/step-templates/linux-service-start-stop-restart.json.human b/step-templates/linux-service-start-stop-restart.json.human new file mode 100644 index 000000000..64c173ae9 --- /dev/null +++ b/step-templates/linux-service-start-stop-restart.json.human @@ -0,0 +1,158 @@ +{ + "Id": "cc2aa1d1-975b-4ac4-a145-094bbd92a2c9", + "Name": "Linux Service - Start, Stop, Restart", + "Description": null, + "ActionType": "Octopus.Script", + "Version": 1, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "serviceName=$(get_octopusvariable \"templateServiceName\") +action=$(get_octopusvariable \"templateAction\") +sleepInSeconds=$(get_octopusvariable \"templateSleepInSeconds\") + +function get_service_running () { +\tlocal linuxService=$1 + local state=$(systemctl is-active \"$linuxService\") + + + if [[ $state == \"active\" ]] + then + state=true + else + state=false + fi + + \t# Return the result + echo \"$state\" +} + +function service_found () { +\tlocal serviceName=$1 +\tlocal result=\"\" + +\tif [[ ! -z $(systemctl status $serviceName | grep \"$serviceName\") ]] +\tthen +\t\tresult=true +\telse +\t\tresult=false +\tfi + +\techo \"$result\" +} + +# Check for service +if [[ $(service_found \"$serviceName\") == true ]] +then +\t# Perform action +\tcase $action in +\t\tstart) +\t\t\tif [[ $(get_service_running \"$serviceName\") == false ]] +\t\t\tthen +\t\t\t\techo \"Starting service $serviceName...\" +\t\t\t\tsystemctl start $serviceName + +\t\t\t\tsleep $sleepInSeconds + +\t\t\t\tif [[ $(get_service_running \"$serviceName\") == true ]] +\t\t\t\tthen +\t\t\t\t\techo \"$serviceName started successfully.\" +\t\t\t\telse +\t\t\t\t\tfail_step \"$serviceName did not start within the specified wait time.\" +\t\t\t\tfi +\t\t\telse +\t\t\t\tfail_step \"Service $serviceName is already running!\" +\t\t\tfi +\t\t\t;; +\t\tstop) +\t\t\tif [[ $(get_service_running \"$serviceName\") == true ]] +\t\t\tthen +\t\t\t\techo \"Stopping $serviceName...\" +\t\t\t\tsystemctl stop $serviceName + +\t\t\t\tsleep $sleepInSeconds + +\t\t\t\tif [[ $(get_service_running \"$serviceName\") == false ]] +\t\t\t\tthen +\t\t\t\t\techo \"Stopped $serviceName successfully.\" +\t\t\t\telse +\t\t\t\t\tfail_step \"$serviceName failed to stop within the specified wait time.\" +\t\t\t\tfi +\t\t\telse +\t\t\t\tfail_step \"Service $serviceName is not running!\" +\t\t\tfi +\t\t\t;; + +\t\trestart) +\t\t\tif [[ $(get_service_running \"$serviceName\") == true ]] +\t\t\tthen +\t\t\t\techo \"Restarting $serviceName...\" +\t\t\t\tsystemctl restart $serviceName + +\t\t\t\tsleep $sleepInSeconds + +\t\t\t\tif [[ $(get_service_running \"$serviceName\") == true ]] +\t\t\t\tthen +\t\t\t\t\techo \"Restarted $serviceName successfully.\" +\t\t\t\telse +\t\t\t\t\tfail_step \"$serviceName did not restart within the specified wait time\" +\t\t\t\tfi +\t\t\telse +\t\t\t\tfail_step \"$serviceName is stopped!\" +\t\t\tfi +\t\t\t;; + + +\t\t*) +\t\t\tfail_step \"Invalid action. Valid actions are start|stop|restart.\" +\t\t\t;; +\tesac +else +\tfail_step \"Service $serviceName not found!\" +fi" + }, + "Parameters": [ + { + "Id": "e8b2d399-ecad-4f25-bff5-f198b36a9de3", + "Name": "templateServiceName", + "Label": "Service Name", + "HelpText": "Name of the service", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3a1857db-c40e-4a18-8205-994fd30a9f9b", + "Name": "templateAction", + "Label": "Action", + "HelpText": "Action to take on the service", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "start|Start +stop|Stop +restart|Restart" + } + }, + { + "Id": "e42aff9e-776d-4046-9a6a-7979bf6acf82", + "Name": "templateSleepInSeconds", + "Label": "Sleep in seconds", + "HelpText": "Number of seconds to wait after starting, stopping, or restarting to make sure the action worked successfully.", + "DefaultValue": "5", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2020-03-04T18:52:00.622Z", + "OctopusVersion": "2019.13.7", + "Type": "ActionTemplate" + }, + "Category": "Linux" + } diff --git a/step-templates/liquibase-apply-changeset.json.human b/step-templates/liquibase-apply-changeset.json.human new file mode 100644 index 000000000..b8a79cac9 --- /dev/null +++ b/step-templates/liquibase-apply-changeset.json.human @@ -0,0 +1,668 @@ +{ + "Id": "6a276a58-d082-425f-a77a-ff7b3979ce2e", + "Name": "Liquibase - Apply changeset", + "Description": "Deploy database updates using Liquibase. You can include Liquibase in the package itself or choose Download to download it during runtime. Use the `Report only?` option to output the SQL it will run and upload it as an Artifact. NOTE: `Report only?` does not work with NoSQL databases such as MongoDB + +**This template is now deprecated, please use `Liquibase - Run command`**", + "ActionType": "Octopus.Script", + "Version": 13, + "Author": "twerthi", + "Packages": [ + { + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "liquibaseChangeset" + }, + "Id": "15eeeac8-d80d-46ba-bc52-413fddae36f3", + "Name": "liquibaseChangeset" + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Configure template + +# Set TLS +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Disable progress bar for PowerShell +$ProgressPreference = 'SilentlyContinue' + +# Downloads and extracts liquibase to the work folder +Function Get-Liquibase +{ + # Define parameters + param ($Version) + +\t$repositoryName = \"liquibase/liquibase\" + + # Check to see if version wasn't specified + if ([string]::IsNullOrEmpty($Version)) + { + # Get the latest version download url + $downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName | Where-Object {$_.EndsWith(\".zip\")}) + } + else + { + \t$downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName -Version $Version | Where-Object {$_.EndsWith(\".zip\")}) + } + + # Check for download folder + if ((Test-Path -Path \"$PSSCriptRoot\\liquibase\") -eq $false) + { + # Create the folder + New-Item -ItemType Directory -Path \"$PSSCriptRoot\\liquibase\" + } + + # Download the zip file + Write-Output \"Downloading Liquibase from $downloadUrl ...\" + $liquibaseZipFile = \"$PSScriptroot\\liquibase\\$($downloadUrl.Substring($downloadUrl.LastIndexOf(\"/\")))\" + Invoke-WebRequest -Uri $downloadUrl -OutFile $liquibaseZipFile -UseBasicParsing + + + # Extract package + Write-Output \"Extracting Liqbuibase ...\" + Expand-Archive -Path $liquibaseZipFile -DestinationPath \"$PSSCriptRoot\\liquibase\" +} + +# Downloads and extracts Java to the work folder, then adds the location of java.exe to the $env:PATH variabble so it can be called +Function Get-Java +{ + # Check to see if a folder needs to be created + if((Test-Path -Path \"$PSScriptRoot\\jdk\") -eq $false) + { + # Create new folder + New-Item -ItemType Directory -Path \"$PSSCriptRoot\\jdk\" + } + + # Download java + Write-Output \"Downloading Java ... \" + Invoke-WebRequest -Uri \"https://download.java.net/java/GA/jdk14.0.2/205943a0976c4ed48cb16f1043c5c647/12/GPL/openjdk-14.0.2_windows-x64_bin.zip\" -OutFile \"$PSScriptroot\\jdk\\openjdk-14.0.2_windows-x64_bin.zip\" -UseBasicParsing + + # Extract + Write-Output \"Extracting Java ... \" + Expand-Archive -Path \"$PSScriptroot\\jdk\\openjdk-14.0.2_windows-x64_bin.zip\" -DestinationPath \"$PSSCriptRoot\\jdk\" + + # Get Java executable + $javaExecutable = Get-ChildItem -Path \"$PSScriptRoot\\jdk\" -Recurse | Where-Object {$_.Name -eq \"java.exe\"} + + # Add path to current session + $env:PATH += \";$($javaExecutable.Directory)\" +} + +# Gets download url of latest release with an asset +Function Get-LatestVersionDownloadUrl +{ + # Define parameters + param( + \t$Repository, + $Version + ) + + # Define local variables + $releases = \"https://api.github.com/repos/$Repository/releases\" + + # Get latest version + Write-Host \"Determining latest release ...\" + + $tags = (Invoke-WebRequest $releases -UseBasicParsing | ConvertFrom-Json) + + if ($null -ne $Version) + { + \t$tags = ($tags | Where-Object {$_.name.EndsWith($Version)}) + } + + # Find the latest version with a downloadable asset + foreach ($tag in $tags) + { + if ($tag.assets.Count -gt 0) + { + return $tag.assets.browser_download_url + } + } + + # Return the version + return $null +} + +# Finds the specified changelog file +Function Get-ChangeLog +{ + # Define parameters + param ($FileName) + + # Find file + $fileReference = (Get-ChildItem -Path $OctopusParameters[\"Octopus.Action.Package[liquibaseChangeSet].ExtractedPath\"] -Recurse | Where-Object {$_.Name -eq $FileName}) + + # Check to see if something weas returned + if ($null -eq $fileReference) + { + # Not found + Write-Error \"$FileName was not found in $PSScriptRoot or subfolders.\" + } + + # Return the reference + return $fileReference +} + +# Downloads the appropriate JDBC driver +Function Get-DatabaseJar +{ + # Define parameters + param ($DatabaseType) + + # Declare local variables + $driverPath = \"\" + + # Check to see if a folder needs to be created + if((Test-Path -Path \"$PSScriptRoot\\DatabaseDriver\") -eq $false) + { + # Create new folder + New-Item -ItemType Directory -Path \"$PSSCriptRoot\\DatabaseDriver\" | Out-Null + } + + # Download the driver for the selected type + switch ($DatabaseType) + { + \"MariaDB\" + { + # Download MariaDB driver + Write-Host \"Downloading MariaDB driver ...\" + $driverPath = \"$PSScriptroot\\DatabaseDriver\\mariadb-java-client-2.6.2.jar\" + Invoke-WebRequest -Uri \"https://downloads.mariadb.com/Connectors/java/connector-java-2.6.2/mariadb-java-client-2.6.2.jar\" -OutFile $driverPath -UseBasicParsing + + break + } + \"MongoDB\" + { + \t# Set repo name + $repositoryName = \"liquibase/liquibase-mongodb\" + + # Download MongoDB driver + Write-Host \"Downloading Maven MongoDB driver ...\" + $driverPath = \"$PSScriptroot\\DatabaseDriver\\mongo-java-driver-3.12.7.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/org/mongodb/mongo-java-driver/3.12.7/mongo-java-driver-3.12.7.jar\" -Outfile $driverPath -UseBasicParsing + + if ([string]::IsNullOrEmpty($liquibaseVersion)) + { + \t# Get the latest version for the extension + \t$downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName | Where-Object {$_.EndsWith(\".jar\")}) + \t} + else + { + \t# Download version matching extension + $downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName -Version $liquibaseVersion | Where-Object {$_.EndsWith(\".jar\")}) + } + +\t\t\tWrite-Host \"Downloading MongoDB Liquibase extension from $downloadUrl ...\" + $extensionPath = \"$PSScriptroot\\$($downloadUrl.Substring($downloadUrl.LastIndexOf(\"/\")))\" + + Invoke-WebRequest -Uri $downloadUrl -Outfile $extensionPath -UseBasicParsing + + # Make driver path null + $driverPath = \"$driverPath;$extensionPath\" + + break + } + \"MySQL\" + { + # Download MariaDB driver + Write-Host \"Downloading MySQL driver ...\" + Invoke-WebRequest -Uri \"https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-8.0.21.zip\" -OutFile \"$PSScriptroot\\DatabaseDriver\\mysql-connector-java-8.0.21.zip\" -UseBasicParsing + + # Extract package + Write-Host \"Extracting MySQL driver ...\" + Expand-Archive -Path \"$PSScriptroot\\DatabaseDriver\\mysql-connector-java-8.0.21.zip\" -DestinationPath \"$PSSCriptRoot\\DatabaseDriver\" + + # Find driver + $driverPath = (Get-ChildItem -Path \"$PSScriptRoot\\DatabaseDriver\" -Recurse | Where-Object {$_.Name -eq \"mysql-connector-java-8.0.21.jar\"}).FullName + + break + } + \"Oracle\" + { + # Download Oracle driver + Write-Host \"Downloading Oracle driver ...\" + $driverPath = \"$PSScriptroot\\DatabaseDriver\\ojdbc10.jar\" + Invoke-WebRequest -Uri \"https://download.oracle.com/otn-pub/otn_software/jdbc/211/ojdbc11.jar\" -OutFile $driverPath -UseBasicParsing + + break + } + \"SqlServer\" + { + # Download Microsoft driver + Write-Host \"Downloading Sql Server driver ...\" + Invoke-WebRequest -Uri \"https://go.microsoft.com/fwlink/?linkid=2137600\" -OutFile \"$PSScriptroot\\DatabaseDriver\\sqljdbc_8.4.0.0_enu.zip\" -UseBasicParsing + + # Extract package + Write-Host \"Extracting SqlServer driver ...\" + Expand-Archive -Path \"$PSScriptroot\\DatabaseDriver\\sqljdbc_8.4.0.0_enu.zip\" -DestinationPath \"$PSSCriptRoot\\DatabaseDriver\" + + # Find driver + $driverPath = (Get-ChildItem -Path \"$PSSCriptRoot\\DatabaseDriver\" -Recurse | Where-Object {$_.Name -eq \"mssql-jdbc-8.4.1.jre14.jar\"}).FullName + +\t\t\tbreak + } + \"PostgreSQL\" + { + # Download PostgreSQL driver + Write-Host \"Downloading PostgreSQL driver ...\" + $driverPath = \"$PSScriptroot\\DatabaseDriver\\postgresql-42.2.12.jar\" + Invoke-WebRequest -Uri \"https://jdbc.postgresql.org/download/postgresql-42.2.12.jar\" -OutFile $driverPath -UseBasicParsing + + break + } + default + { + # Display error + Write-Error \"Unknown database type: $DatabaseType.\" + } + } + + # Return the driver location + return $driverPath +} + +# Returns the driver name for the liquibase call +Function Get-DatabaseDriverName +{ + # Define parameters + param ($DatabaseType) + + # Declare local variables + $driverName = \"\" + + # Download the driver for the selected type + switch ($DatabaseType) + { + \"MariaDB\" + { + $driverName = \"org.mariadb.jdbc.Driver\" + break + } + \"MongoDB\" + { + \t$driverName = $null + break + } + \"MySQL\" + { + $driverName = \"com.mysql.cj.jdbc.Driver\" + break + } + \"Oracle\" + { + $driverName = \"oracle.jdbc.OracleDriver\" + break + } + \"SqlServer\" + { + $driverName = \"com.microsoft.sqlserver.jdbc.SQLServerDriver\" + break + } + \"PostgreSQL\" + { + $driverName = \"org.postgresql.Driver\" + break + } + default + { + # Display error + Write-Error \"Unkonwn database type: $DatabaseType.\" + } + } + + # Return the driver location + return $driverName +} + +# Returns the connection string formatted for the database type +Function Get-ConnectionUrl +{ + # Define parameters + param ($DatabaseType, + \t$ServerPort, + $ServerName, + $DatabaseName, + $QueryStringParameters) + + # Define local variables + $connectioUrl = \"\" + + # Download the driver for the selected type + switch ($DatabaseType) + { + \"MariaDB\" + { + $connectionUrl = \"jdbc:mariadb://{0}:{1}/{2}\" + break + } + \"MongoDB\" + { + \t$connectionUrl = \"mongodb://{0}:{1}/{2}\" + break + } + \"MySQL\" + { + $connectionUrl = \"jdbc:mysql://{0}:{1}/{2}\" + break + } + \"Oracle\" + { + $connectionUrl = \"jdbc:oracle:thin:@{0}:{1}:{2}\" + break + } + \"SqlServer\" + { + $connectionUrl = \"jdbc:sqlserver://{0}:{1};database={2};\" + break + } + \"PostgreSQL\" + { + $connectionUrl = \"jdbc:postgresql://{0}:{1}/{2}\" + break + } + default + { + # Display error + Write-Error \"Unkonwn database type: $DatabaseType.\" + } + } + + if (![string]::IsNullOrEmpty($QueryStringParameters)) + { + if ($QueryStringParameters.StartsWith(\"?\") -eq $false) + { + # Add the question mark + $connectionUrl += \"?\" + } + $connectionUrl += \"$QueryStringParameters\" + } + + # Return the url + return ($connectionUrl -f $ServerName, $ServerPort, $DatabaseName) +} + +# Create array for arguments +$liquibaseArguments = @() + +# Check for license key +if (![string]::IsNullOrEmpty($liquibaseProLicenseKey)) +{ +\t# Add key to arguments + $liquibaseArguments += \"--liquibaseProLicenseKey=$liquibaseProLicenseKey\" +} + +# Find Change log +$changeLogFile = (Get-ChangeLog -FileName $liquibaseChangeLogFileName) +$liquibaseArguments += \"--changeLogFile=$($changeLogFile.Name)\" + +# Check to see if it needs to be downloaed to machine +if ($liquibaseDownload -eq $true) +{ + # Download and extract liquibase + Get-Liquibase -Version $liquibaseVersion -DownloadFolder $workingFolder + + # Download and extract java and add it to PATH environment variable + Get-Java + + # Get the driver + $driverPath = Get-DatabaseJar -DatabaseType $liquibaseDatabaseType + +\tif (![string]::IsNullOrEmpty($driverPath)) + { + \t# Add to arguments + \t$liquibaseArguments += \"--classpath=$driverPath\" + } +} +else +{ + if (![string]::IsNullOrEmpty($liquibaseClassPath)) + { + \t$liquibaseArguments += \"--classpath=$liquibaseClassPath\" + } +} + +# Check to see if liquibase path has been defined +if ([string]::IsNullOrEmpty($liquibaseExecutablePath)) +{ + # Assign root + $liquibaseExecutablePath = $PSSCriptRoot +} + +# Get the executable location +$liquibaseExecutable = Get-ChildItem -Path $liquibaseExecutablePath -Recurse | Where-Object {$_.Name -eq \"liquibase.bat\"} + +# Add path to current session +$env:PATH += \";$($liquibaseExecutable.Directory)\" + +# Check to make sure it was found +if ([string]::IsNullOrEmpty($liquibaseExecutable)) +{ + # Could not find the executable + Write-Error \"Unable to find liquibase.bat in $PSScriptRoot or subfolders.\" +} + +# Add argument for driver +#$databaseDriver = Get-DatabaseDriverName -DatabaseType $liquibaseDatabaseType +#if (![string]::IsNullOrEmpty($databaseDriver)) +#{ +#\t$liquibaseArguments += \"--driver=$databaseDriver\" +#} + +# Add connection Url +$connectionUrl = Get-ConnectionUrl -DatabaseType $liquibaseDatabaseType -ServerPort $liquibaseServerPort -ServerName $liquibaseServerName -DatabaseName $liquibaseDatabaseName -QueryStringParameters $liquibaseQueryStringParameters +$liquibaseArguments += \"--url=$connectionUrl\" + +# Add Username and password +$liquibaseArguments += \"--username=$liquibaseUsername\" +$liquibaseArguments += \"--password=`\"$liquibasePassword`\"\" + +# Set the location to where the file is +Set-Location -Path $changeLogFile.Directory + +# Check to see if it should run or just report +if ($liquibaseReport -eq $true) +{ + # Set error action preference - updateSQL writes to stderr so this is to prevent errors from showing up + $ErrorActionPreference = \"SilentlyContinue\" + + # Add just report + $liquibaseArguments += \"updateSQL\" + + # Execute liquibase + $liquibaseProcess = Start-Process -FilePath $liquibaseExecutable.FullName -ArgumentList $liquibaseArguments -RedirectStandardError \"$PSScriptRoot\\stderr.txt\" -RedirectStandardOutput \"$PSScriptRoot\\ChangeSet.sql\" -Passthru -Wait +\t + # Display standard error + foreach ($line in (Get-Content -Path \"$PSScriptRoot\\stderr.txt\")) + { + \t# Display + Write-Host \"$line\" + } + + # Check exit code + if ($liquibaseProcess.ExitCode -eq 0) + { + \t# Attach artifact + New-OctopusArtifact -Path \"$PSScriptRoot\\ChangeSet.sql\" -Name \"ChangeSet.sql\" + } +} +else +{ + $liquibaseArguments += \"update\" + & $liquibaseExecutable.FullName $liquibaseArguments +} + +" + }, + "Parameters": [ + { + "Id": "38e92902-4a82-41f3-a86a-3e467da0c454", + "Name": "liquibaseProLicenseKey", + "Label": "Pro license key", + "HelpText": "Enter your Liquibase Pro license key. [Request a free 30-day trial.](https://www.liquibase.com/trial) Leave blank to use the Community Edition.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4477651b-2b6d-4352-a1bd-9b9e46c31c75", + "Name": "liquibaseDatabaseType", + "Label": "Database type", + "HelpText": "Select the database type to deploy to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "MariaDB|MariaDB +MongoDB|MongoDB +MySQL|MySQL +Oracle|Oracle +PostgreSQL|PostgreSQL +SqlServer|SqlServer" + } + }, + { + "Id": "4c738f97-c803-4a14-895e-ad6bacf09270", + "Name": "liquibaseChangeLogFileName", + "Label": "Change Log file name", + "HelpText": "Name of the changelog file in the package.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "25ee8ea4-f38b-42e6-a503-026d72cc83a2", + "Name": "liquibaseServerName", + "Label": "Server name", + "HelpText": "Name or IP address of the database server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "592c5987-394a-41c2-bd82-1e234e470296", + "Name": "liquibaseServerPort", + "Label": "Server port", + "HelpText": "The port the database server listens on.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8e43ac61-4623-4102-bb59-58a2571bfa42", + "Name": "liquibaseDatabaseName", + "Label": "Database name", + "HelpText": "Name of the database (or service name in case of Oracle) to deploy to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "93f218a0-8fb3-42ab-bcb9-500d9f68922b", + "Name": "liquibaseUsername", + "Label": "Username", + "HelpText": "Username of a user that has permission to make changes to the database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a2d1fee4-7161-4f8c-8d81-c00780acb79e", + "Name": "liquibasePassword", + "Label": "Password", + "HelpText": "Password for the user with permissions to make changes to the database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "42741eb0-3768-4b99-8ad2-668b92db0c77", + "Name": "liquibaseQueryStringParameters", + "Label": "Connection query string parameters", + "HelpText": "Add additional parameters to the connection string URL. Example: ?useUnicode=true", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1d907292-d8c6-4afc-bc26-34125b1f66ec", + "Name": "liquibaseClassPath", + "Label": "Database driver path", + "HelpText": "Filepath to the location of the .jar driver for the database type.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "49c3a944-b727-4487-b2b1-493527177f5d", + "Name": "liquibaseExecutablePath", + "Label": "Executable file path", + "HelpText": "File path to the Liquibase executable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6e2c9b48-9e67-4bed-aafa-3dc5257f9f62", + "Name": "liquibaseReport", + "Label": "Report only?", + "HelpText": "This option will generate the SQL statements, save them to a file, then attach them to the step as an artifact. This option only generates the statement, it will not run them.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "35500312-a25b-4a05-ac1e-68a7f51afd1e", + "Name": "liquibaseDownload", + "Label": "Download Liquibase?", + "HelpText": "Use this option to download the software necessary to deploy a Liquibase changeset. This will download Liquibase, java, and the appropriate driver for the selected database type. Using this option overrides the `Database driver path` and `Executable file path` inputs.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "3602d6ce-94af-4d59-acc9-ae8a84bafd01", + "Name": "liquibaseVersion", + "Label": "Liquibase version", + "HelpText": "Used with `Download Liquibase` to specify the version of Liquibase to use.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "235f6187-0fbf-409a-820f-61d81cea3722", + "Name": "liquibaseChangeset", + "Label": "Changeset package", + "HelpText": "Select the package with the changeset.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + } + ], + "$Meta": { + "ExportedAt": "2021-02-25T23:33:12.871Z", + "OctopusVersion": "2020.6.0-rc0003", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "Category": "liquibase" + } diff --git a/step-templates/liquibase-run-command.json.human b/step-templates/liquibase-run-command.json.human new file mode 100644 index 000000000..ab8a31cc1 --- /dev/null +++ b/step-templates/liquibase-run-command.json.human @@ -0,0 +1,1463 @@ +{ + "Id": "36df3e84-8501-4f2a-85cc-bd9eb22030d1", + "Name": "Liquibase - Run command", + "Description": "Run Liqbuibase commands against a database. You can include Liquibase in the package itself or choose Download to download it during runtime. + +Note: +- AWS EC2 IAM Authentication requires the AWS CLI to be installed. +- Windows Authentication has been tested with + - Microsoft SQL Server + - PostgreSQL + +Once the Liquibase commands have executed, the output is stored in an Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) called `LiquibaseCommandOutput` for use in subsequent Octopus deployment or runbook steps.", + "ActionType": "Octopus.Script", + "Version": 26, + "Author": "twerthi", + "Packages": [ + { + "Name": "liquibaseChangeset", + "Id": "15eeeac8-d80d-46ba-bc52-413fddae36f3", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "liquibaseChangeset" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "# Configure template + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +# Fix ANSI Color on PWSH Core issues when displaying objects +if ($PSEdition -eq \"Core\") { + $PSStyle.OutputRendering = \"PlainText\" +} + +# Set TLS +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Downloads and extracts liquibase to the work folder +Function Get-Liquibase { + # Define parameters + param ($Version) + + $repositoryName = \"liquibase/liquibase\" + + # Check to see if version wasn't specified + if ([string]::IsNullOrEmpty($Version)) { + # Get the latest version download url + $downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName | Where-Object { $_.EndsWith(\".zip\") }) + } + else { + $downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName -Version $Version | Where-Object { $_.EndsWith(\".zip\") }) + } + + # Extract the downloaded file + Expand-DownloadedFile -DownloadUrls $downloadUrl | Out-Null + + # Parse downloaded version + if ($downloadUrl -is [array]) { + $downloadedFileName = [System.IO.Path]::GetFileName($downloadUrl[0]) + } + else { + $downloadedFileName = [System.IO.Path]::GetFileName($downloadUrl) + } + + # Return the downloaded version + return $downloadedFileName.SubString($downloadedFileName.IndexOf(\"-\") + 1).Replace(\".zip\", \"\") +} + +# Downloads the files +Function Expand-DownloadedFile { + # Define parameters + param ( + $DownloadUrls + ) + + # Loop through results + foreach ($url in $DownloadUrls) { + # Download the zip file + $folderName = [System.IO.Path]::GetFileName(\"$PSScriptroot/$($url.Substring($url.LastIndexOf(\"/\")))\").Replace(\".zip\", \"\") + $zipFile = \"$PSScriptroot/$folderName/$($url.Substring($url.LastIndexOf(\"/\")))\" + Write-Host \"Downloading $zipFile from $url ...\" + + if ((Test-Path -Path \"$PSScriptroot/$folderName\") -eq $false) { + # Create folder + New-Item -Path \"$PSScriptroot/$folderName/\" -ItemType Directory + } + + # Download the zip file + Invoke-WebRequest -Uri $url -OutFile $zipFile -UseBasicParsing | Out-Null + + # Extract package + Write-Host \"Extracting $zipFile ...\" + Expand-Archive -Path $zipFile -DestinationPath \"$PSSCriptRoot/$folderName\" | Out-Null + } +} + + +# Downloads and extracts Java to the work folder, then adds the location of java.exe to the $env:PATH variabble so it can be called +Function Get-Java { + # Check to see if a folder needs to be created + if ((Test-Path -Path \"$PSScriptRoot/jdk\") -eq $false) { + # Create new folder + New-Item -ItemType Directory -Path \"$PSSCriptRoot/jdk\" + } + + # Download java + Write-Output \"Downloading Java ... \" + + # Determine OS + if ($IsWindows) { + Invoke-WebRequest -Uri \"https://download.java.net/java/GA/jdk14.0.2/205943a0976c4ed48cb16f1043c5c647/12/GPL/openjdk-14.0.2_windows-x64_bin.zip\" -OutFile \"$PSScriptroot/jdk/openjdk-14.0.2_windows-x64_bin.zip\" -UseBasicParsing + + # Extract + Write-Output \"Extracting Java ... \" + Expand-Archive -Path \"$PSScriptroot\\jdk\\openjdk-14.0.2_windows-x64_bin.zip\" -DestinationPath \"$PSSCriptRoot/jdk\" + + # Get Java executable + $javaExecutable = Get-ChildItem -Path \"$PSScriptRoot\\jdk\" -Recurse | Where-Object { $_.Name -eq \"java.exe\" } + } + + if ($IsLinux) { + Invoke-WebRequest -Uri \"https://download.java.net/openjdk/jdk14/ri/openjdk-14+36_linux-x64_bin.tar.gz\" -OutFile \"$PSScriptroot/jdk/openjdk-14+36_linux-x64_bin.tar.gz\" -UseBasicParsing + + # Extract + Write-Output \"Extracting Java ... \" + tar -xvzf \"$PSScriptroot/jdk/openjdk-14+36_linux-x64_bin.tar.gz\" --directory \"$PSScriptRoot/jdk\" + + # Get Java executable + $javaExecutable = Get-ChildItem -Path \"$PSScriptRoot/jdk\" -Recurse | Where-Object { $_.Name -eq \"java\" } + } + + # Add path to current session as first entry to bypass other versions of Java that may be installed. + $env:PATH = \"$($javaExecutable.Directory)$([IO.Path]::PathSeparator)\" + $env:PATH + +} + +Function Get-DriverAssets { + # Define parameters + param ( + $DownloadInfo + ) + + # Declare working variables + $assetFilePath = \"\" + + # Check to see if there are multiple assets to download + if ($DownloadInfo -is [array]) { + # Declare local variables + $assetFiles = @() + + # Loop through array + foreach ($url in $DownloadInfo) { + # Download the asset + Write-Host \"Downloading asset from $url...\" + $assetPath = \"$PSScriptroot/$($url.Substring($url.LastIndexOf(\"/\")))\" + + # Skip test assets + if ($assetPath.EndsWith(\"tests.jar\")) { + Write-Host \"Asset is for testing, skipping ...\" + continue + } + + Invoke-WebRequest -Uri $url -Outfile $assetPath -UseBasicParsing + $assetFiles += $assetPath + } + + # Assign paths + $assetFilePath = $assetFiles -join \"$([IO.Path]::PathSeparator)\" + } + else { + # Download asset + Write-Host \"Downloading asset from $DownloadInfo ...\" + $assetFilePath = \"$PSScriptroot/$($DownloadInfo.Substring($DownloadInfo.LastIndexOf(\"/\")))\" + Invoke-WebRequest -Uri $DownloadInfo -Outfile $assetFilePath -UseBasicParsing + } + + # Return path + return $assetFilePath +} + +# Gets download url of latest release with an asset +Function Get-LatestVersionDownloadUrl { + # Define parameters + param( + $Repository, + $Version + ) + + # Define local variables + $releases = \"https://api.github.com/repos/$Repository/releases\" + + # Get latest version + Write-Host \"Determining latest release of $Repository ...\" + + $tags = (Invoke-WebRequest $releases -UseBasicParsing | ConvertFrom-Json) + + if ($null -ne $Version) { + # Get specific version + $tags = ($tags | Where-Object { $_.name.EndsWith($Version) }) + + # Check to see if nothing was returned + if ($null -eq $tags) { + # Not found + Write-Host \"No release found matching version $Version, getting highest version using Major.Minor syntax...\" + + # Get the tags + $tags = (Invoke-WebRequest $releases -UseBasicParsing | ConvertFrom-Json) + + # Parse the version number into a version object + $parsedVersion = [System.Version]::Parse($Version) + $partialVersion = \"$($parsedVersion.Major).$($parsedVersion.Minor)\" + + # Filter tags to ones matching only Major.Minor of version specified + $tags = ($tags | Where-Object { $_.name.Contains(\"$partialVersion.\") -and $_.draft -eq $false }) + + # Grab the latest + if ($null -eq $tags) + { + \t# decrement minor version + $minorVersion = [int]$parsedVersion.Minor + $minorVersion -- + + # return the urls + return (Get-LatestVersionDownloadUrl -Repository $Repository -Version \"$($parsedVersion.Major).$($minorVersion)\") + } + } + } + + # Find the latest version with a downloadable asset + foreach ($tag in $tags) { + if ($tag.assets.Count -gt 0) { + return $tag.assets.browser_download_url + } + } + + # Return the version + return $null +} + +# Finds the specified changelog file +Function Get-ChangeLog { + # Define parameters + param ($FileName) + + # Find file + $fileReference = (Get-ChildItem -Path $OctopusParameters[\"Octopus.Action.Package[liquibaseChangeSet].ExtractedPath\"] -Recurse | Where-Object { $_.Name -eq $FileName }) + + # Check to see if something weas returned + if ($null -eq $fileReference) { + # Not found + Write-Error \"$FileName was not found in $PSScriptRoot or subfolders.\" + } + + # Return the reference + return $fileReference +} + +# Downloads the appropriate JDBC driver +Function Get-DatabaseJar { + # Define parameters + param ($DatabaseType) + + # Declare local variables + $driverPath = \"\" + + # Check to see if a folder needs to be created + if ((Test-Path -Path \"$PSScriptRoot/DatabaseDriver\") -eq $false) { + # Create new folder + New-Item -ItemType Directory -Path \"$PSSCriptRoot/DatabaseDriver\" | Out-Null + } + + # Download the driver for the selected type + switch ($DatabaseType) { + \"Cassandra\" { + +\t\t\t# Get the release download + Write-Host \"Downloading Cassandra JDBC driver bundle ...\" +\t\t\t$downloadUrl = Get-LatestVersionDownloadUrl -Repository \"ing-bank/cassandra-jdbc-wrapper\" + + # Find driver + $driverPath = (Get-DriverAssets -DownloadInfo $downloadUrl) + + # Set repo name + $repositoryName = \"liquibase/liquibase-cassandra\" + + if ([string]::IsNullOrEmpty($liquibaseVersion)) { + # Get the latest version for the extension + $downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName | Where-Object { $_.EndsWith(\".jar\") }) + \t} + else { + # Download version matching extension + $downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName -Version $liquibaseVersion | Where-Object { $_.EndsWith(\".jar\") }) + } + + $extensionPath = Get-DriverAssets -DownloadInfo $downloadUrl + + # Make driver path null + $driverPath = \"$driverPath$([IO.Path]::PathSeparator)$extensionPath\" + + break + } + \"CosmosDB\" + { +\t\t\t# Download the (long) list of dependencies + $driverPaths = @() + +\t\t\t# Set repo name + $repositoryName = \"liquibase/liquibase-cosmosdb\" + +\t\t\tif ([string]::IsNullOrEmpty($liquibaseVersion)) + { + \t# Get the latest version for the extension + \t$downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName | Where-Object {$_.EndsWith(\".jar\")}) + \t} + else + { + \t# Download version matching extension + $downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName -Version $liquibaseVersion | Where-Object {$_.EndsWith(\".jar\")}) + } + + $extensionPath = Get-DriverAssets -DownloadInfo $downloadUrl + +\t\t\t# Add to driver path + $driverPaths += $extensionPath + + Write-Host \"Downloading azure-cosmos driver ...\" + $driverVersion = \"4.28.0\" + $filePath = \"$PSScriptroot/DatabaseDriver/azure-cosmos-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/com/azure/azure-cosmos/$driverVersion/azure-cosmos-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading azure-core driver ...\" + $driverVersion = \"1.27.0\" + $filePath = \"$PSScriptroot/DatabaseDriver/azure-core-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/com/azure/azure-core/$driverVersion/azure-core-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + $files = Get-ChildItem -Path \"$PSScriptRoot/DatabaseDriver\" + + Write-Host \"There are these $files\" + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading jackson core driver ...\" + $driverVersion = \"2.13.2\" + $filePath = \"$PSScriptroot/DatabaseDriver/jackson-core-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/$driverVersion/jackson-core-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading slf4j core driver ...\" + $driverVersion = \"1.7.36\" + $filePath = \"$PSScriptroot/DatabaseDriver/slf4j-api-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/org/slf4j/slf4j-api/$driverVersion/slf4j-api-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty buffer driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-buffer-$driverVersion.final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-buffer/$driverVersion.Final/netty-buffer-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath +\t\t\t + Write-Host \"Downloading reactor-core driver ...\" + $driverVersion = \"3.4.16\" + $filePath = \"$PSScriptroot/DatabaseDriver/reactor-core-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/projectreactor/reactor-core/$driverVersion/reactor-core-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading reactor-netty-core driver ...\" + $driverVersion = \"1.0.17\" + $filePath = \"$PSScriptroot/DatabaseDriver/reactor-core-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/projectreactor/netty/reactor-netty-core/$driverVersion/reactor-netty-core-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading reactor-netty-core driver ...\" + $driverVersion = \"1.0.17\" + $filePath = \"$PSScriptroot/DatabaseDriver/reactor-netty-http-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/projectreactor/netty/reactor-netty-http/$driverVersion/reactor-netty-http-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-resolver driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-resolver-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-resolver/$driverVersion.Final/netty-resolver-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-transport driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-transport-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-transport/$driverVersion.Final/netty-transport-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading reactivestreams driver ...\" + $driverVersion = \"1.0.3\" + $filePath = \"$PSScriptroot/DatabaseDriver/reactive-streams-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/org/reactivestreams/reactive-streams/$driverVersion/reactive-streams-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading jackson-databind driver ...\" + $driverVersion = \"2.13.2.1\" + $filePath = \"$PSScriptroot/DatabaseDriver/jackson-databind-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/$driverVersion/jackson-databind-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading jackson-annotations driver ...\" + $driverVersion = \"2.13.2\" + $filePath = \"$PSScriptroot/DatabaseDriver/jackson-annotations-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/$driverVersion/jackson-annotations-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading jackson-module-afterburner driver ...\" + $driverVersion = \"2.13.2\" + $filePath = \"$PSScriptroot/DatabaseDriver/jackson-module-afterburner-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/com/fasterxml/jackson/module/jackson-module-afterburner/$driverVersion/jackson-module-afterburner-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading reactor-netty driver ...\" + $driverVersion = \"1.0.17\" + $filePath = \"$PSScriptroot/DatabaseDriver/reactor-netty-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/projectreactor/netty/reactor-netty/$driverVersion/reactor-netty-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-transport-native-unix-common driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-transport-native-unix-common-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-transport-native-unix-common/$driverVersion.Final/netty-transport-native-unix-common-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-transport-native-epoll driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-transport-native-epoll-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-transport-native-epoll/$driverVersion.Final/netty-transport-native-epoll-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-tcnative-boringssl-static driver ...\" + $driverVersion = \"2.0.51\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-tcnative-boringssl-static-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-tcnative-boringssl-static/$driverVersion.Final/netty-tcnative-boringssl-static-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-resolver-dns driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-resolver-dns-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-resolver-dns/$driverVersion.Final/netty-resolver-dns-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-handler-proxy driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-handler-proxy-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-handler-proxy/$driverVersion.Final/netty-handler-proxy-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-handler driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-handler-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-handler/$driverVersion.Final/netty-handler-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-common driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-common-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-common/$driverVersion.Final/netty-common-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-codec-socks driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-codec-socks-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-codec-socks/$driverVersion.Final/netty-codec-socks-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-codec-http driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-codec-http-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-codec-http/$driverVersion.Final/netty-codec-http-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-codec-http2 driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-codec-http2-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-codec-http2/$driverVersion.Final/netty-codec-http2-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-codec-dns driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-codec-dns-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-codec-dns/$driverVersion.Final/netty-codec-dns-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-codec driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-codec-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-codec/$driverVersion.Final/netty-codec-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading micrometer-core driver ...\" + $driverVersion = \"1.8.4\" + $filePath = \"$PSScriptroot/DatabaseDriver/micrometer-core-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/micrometer/micrometer-core/$driverVersion/micrometer-core-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading metrics-core driver ...\" + $driverVersion = \"4.2.9\" + $filePath = \"$PSScriptroot/DatabaseDriver/metrics-core-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/dropwizard/metrics/metrics-core/$driverVersion/metrics-core-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading LatencyUtils driver ...\" + $driverVersion = \"2.0.3\" + $filePath = \"$PSScriptroot/DatabaseDriver/LatencyUtils-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/org/latencyutils/LatencyUtils/$driverVersion/LatencyUtils-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading jackson-datatype-jsr310 driver ...\" + $driverVersion = \"2.12.5\" + $filePath = \"$PSScriptroot/DatabaseDriver/jackson-datatype-jsr310-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/$driverVersion/jackson-datatype-jsr310-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-tcnative-classes driver ...\" + $driverVersion = \"2.0.51\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-tcnative-classes-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-tcnative-classes/$driverVersion.Final/netty-tcnative-classes-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-transport-classes-kqueue driver ...\" + $driverVersion = \"4.1.73\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-transport-classes-kqueue-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-transport-classes-kqueue/$driverVersion.Final/netty-transport-classes-kqueue-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading slf4j-simple driver ...\" + $driverVersion = \"1.7.36\" + $filePath = \"$PSScriptroot/DatabaseDriver/slf4j-simple-$driverVersion.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/org/slf4j/slf4j-simple/$driverVersion/slf4j-simple-$driverVersion.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-transport-classes-epoll driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-transport-classes-epoll-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-transport-classes-epoll/$driverVersion.Final/netty-transport-classes-epoll-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + + Write-Host \"Downloading netty-transport-native-kqueue driver ...\" + $driverVersion = \"4.1.75\" + $filePath = \"$PSScriptroot/DatabaseDriver/netty-transport-native-kqueue-$driverVersion.Final.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/io/netty/netty-transport-native-kqueue/$driverVersion.Final/netty-transport-native-kqueue-$driverVersion.Final.jar\" -Outfile $filePath -UseBasicParsing | Out-Null + + + # Add to driver path + $driverPaths += $filePath + +\t\t\t# Return driver list separated by system specific PathSeparator + $driverPath = ($driverPaths -join [IO.Path]::PathSeparator) + + $files = Get-ChildItem -Path \"$PSScriptRoot/DatabaseDriver\" + + #Write-Host \"There are these $files\" + + \t\tbreak + } + \"DB2\" { + # Use built-in driver + $driverPath = $null + break + } + \"MariaDB\" { + # Download MariaDB driver + Write-Host \"Downloading MariaDB driver ...\" + $driverPath = \"$PSScriptroot/DatabaseDriver/mariadb-java-client-2.6.2.jar\" + Invoke-WebRequest -Uri \"https://downloads.mariadb.com/Connectors/java/connector-java-2.6.2/mariadb-java-client-2.6.2.jar\" -OutFile $driverPath -UseBasicParsing + + break + } + \"MongoDB\" { + # Download MongoDB driver + Write-Host \"Downloading Maven MongoDB driver ...\" + $driverPath = \"$PSScriptroot/DatabaseDriver/mongo-java-driver-3.12.7.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/org/mongodb/mongo-java-driver/3.12.7/mongo-java-driver-3.12.7.jar\" -Outfile $driverPath -UseBasicParsing + + # Check to see if they are using a licenced version + if (![string]::IsNullOrWhitespace($liquibaseProLicenseKey)) { + # Set the paid version url + $mongoVersions = Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/org/liquibase/ext/liquibase-commercial-mongodb\" -UseBasicParsing + + # Loop through links, look for ones that evaluate to version + $versions = @() + foreach ($link in $mongoVersions.Links) { + Write-Verbose \"Evaluating: $link\" + if (![string]::IsNullOrWhitespace($link.title)) { + # Get the inner text + $versionNumber = $link.title.Replace(\"/\", \"\") + + # Check to see if $versionNumber can be parsed as a versionNumber + $versionOut = $null + if ([System.Version]::TryParse($versionNumber, [ref]$versionOut)) { + $versions += $versionOut + } + } + } + + # Get the highest version number + $info = ($versions | Measure-Object -Maximum) + + $downloadUrl = \"https://repo1.maven.org/maven2/org/liquibase/ext/liquibase-commercial-mongodb/$($info.Maximum)/liquibase-commercial-mongodb-$($info.Maximum).jar\" + } + else { + # Set repo name + $repositoryName = \"liquibase/liquibase-mongodb\" + + # Download latest OSS version + $downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName | Where-Object { $_.EndsWith(\".jar\") }) + } + + $extensionPath = Get-DriverAssets -DownloadInfo $downloadUrl + + # Make driver path null + $driverPath = \"$driverPath$([IO.Path]::PathSeparator)$extensionPath\" + + break + } + \"MySQL\" { + # Download MariaDB driver + Write-Host \"Downloading MySQL driver ...\" + Invoke-WebRequest -Uri \"https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-8.0.28.zip\" -OutFile \"$PSScriptroot/DatabaseDriver/mysql-connector-java-8.0.28.zip\" -UseBasicParsing -UserAgent \"curl/7.8.3.1\" + + # Extract package + Write-Host \"Extracting MySQL driver ...\" + Expand-Archive -Path \"$PSScriptroot/DatabaseDriver/mysql-connector-java-8.0.28.zip\" -DestinationPath \"$PSSCriptRoot/DatabaseDriver\" + + # Find driver + $driverPath = (Get-ChildItem -Path \"$PSScriptRoot/DatabaseDriver\" -Recurse | Where-Object { $_.Name -eq \"mysql-connector-java-8.0.28.jar\" }).FullName + + break + } + \"Oracle\" { + # Download Oracle driver + Write-Host \"Downloading Oracle driver ...\" + $driverPath = \"$PSScriptroot/DatabaseDriver/ojdbc10.jar\" + Invoke-WebRequest -Uri \"https://download.oracle.com/otn-pub/otn_software/jdbc/211/ojdbc11.jar\" -OutFile $driverPath -UseBasicParsing + + break + } + \"SqlServer\" { + # Download Microsoft driver + Write-Host \"Downloading Sql Server driver ...\" + Invoke-WebRequest -Uri \"https://go.microsoft.com/fwlink/?linkid=2186163\" -OutFile \"$PSScriptroot/DatabaseDriver/sqljdbc_10.2.0.0_enu.zip\" -UseBasicParsing + + # Extract package + Write-Host \"Extracting SqlServer driver ...\" + Expand-Archive -Path \"$PSScriptroot/DatabaseDriver/sqljdbc_10.2.0.0_enu.zip\" -DestinationPath \"$PSSCriptRoot/DatabaseDriver\" + + # Find driver + $driverPath = (Get-ChildItem -Path \"$PSSCriptRoot/DatabaseDriver\" -Recurse | Where-Object { $_.Name -eq \"mssql-jdbc-10.2.0.jre11.jar\" }).FullName + + # Determine architecture + if ([System.Environment]::Is64BitOperatingSystem) { + # Locate auth dll + $authDll = Get-ChildItem -Path \"$PSScriptRoot/DatabaseDriver\" -Recurse | Where-Object { $_.Name -eq \"mssql-jdbc_auth-10.2.0.x64.dll\" } + } + else { + $authDll = Get-ChildItem -Path \"$PSScriptRoot/DatabaseDriver\" -Recurse | Where-Object { $_.Name -eq \"mssql-jdbc_auth-10.2.0.x86.dll\" } + } + + # Add the dll to the path so it can find it. + $env:PATH += \"$([IO.Path]::PathSeparator)$($authDll.Directory)\" + + break + } + \"PostgreSQL\" { + # Download PostgreSQL driver + Write-Host \"Downloading PostgreSQL driver ...\" + $driverPath = \"$PSScriptroot/DatabaseDriver/postgresql-42.2.12.jar\" + Invoke-WebRequest -Uri \"https://jdbc.postgresql.org/download/postgresql-42.2.12.jar\" -OutFile $driverPath -UseBasicParsing + + # Download the WAFFLE jna driver for Windows Authentication + $repositoryName = \"waffle/waffle\" + + # Latest version of Waffle (2.3.0) doesn't seem to work, can't find sspi method, specify version 1.9.0 + $downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName -Version \"1.9.0\" | Where-Object { $_.EndsWith(\".zip\") }) + Expand-DownloadedFile -DownloadUrls $downloadUrl | Out-Null + + # Get all waffle jars + $waffleFolder = (Get-ChildItem -Path \"$PSScriptroot\" -Recurse | Where-Object { $_.PSIsContainer -and $_.Name -like \"Waffle*\" }) + $waffleJars = (Get-ChildItem -Path $waffleFolder.FullName -Recurse | Where-Object { $_.Extension -eq \".jar\" }) + + foreach ($jar in $waffleJars) { + $driverPath += \"$([IO.Path]::PathSeparator)$($jar.FullName)\" + } + + break + } + \"Snowflake\" { + # Set repo name + $repositoryName = \"liquibase/liquibase-snowflake\" + + # Download Snowflake driver + Write-Host \"Downloading Snowflake driver ...\" + $driverPath = \"$PSScriptroot/DatabaseDriver/snowflake-jdbc-3.9.2.jar\" + Invoke-WebRequest -Uri \"https://repo1.maven.org/maven2/net/snowflake/snowflake-jdbc/3.9.2/snowflake-jdbc-3.9.2.jar\" -OutFile $driverPath -UseBasicParsing + + if ([string]::IsNullOrEmpty($liquibaseVersion)) { + # Get the latest version for the extension + $downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName | Where-Object { $_.EndsWith(\".jar\") }) + \t} + else { + # Download version matching extension + $downloadUrl = (Get-LatestVersionDownloadUrl -Repository $repositoryName -Version $liquibaseVersion | Where-Object { $_.EndsWith(\".jar\") }) + } + + $extensionPath = Get-DriverAssets -DownloadInfo $downloadUrl + + # Make driver path null + $driverPath = \"$driverPath$([IO.Path]::PathSeparator)$extensionPath\" + + + break + } + default { + # Display error + Write-Error \"Unknown database type: $DatabaseType.\" + } + } + + # Return the driver location + return $driverPath +} + +# Returns the connection string formatted for the database type +Function Get-ConnectionUrl { + # Define parameters + param ($DatabaseType, + $ServerPort, + $ServerName, + $DatabaseName, + $QueryStringParameters) + + # Define local variables + $connectionUrl = \"\" + + # Download the driver for the selected type + switch ($DatabaseType) { + \"Cassandra\" { + #$connectionUrl = \"jdbc:cassandra://{0}:{1};DefaultKeyspace={2}\" + $connectionUrl = \"jdbc:cassandra://{0}:{1}/{2}\" + break + } + \"CosmosDB\" + { + $connectionUrl = \"cosmosdb://{0}:$($liquibasePassword)@{0}:{1}/{2}\" + break + } + \"DB2\" { + $connectionUrl = \"jdbc:db2://{0}:{1}/{2}\" + break + } + \"MariaDB\" { + $connectionUrl = \"jdbc:mariadb://{0}:{1}/{2}\" + + # Check for Windows Authentication type + if ($liquibaseAuthenticationMethod -eq \"windowsauthentication\") { + # Add querysting parameter + $connectionUrl += \"?integratedSecurity=true\" + } + + break + } + \"MongoDB\" { + $connectionUrl = \"mongodb://{0}:{1}/{2}\" + break + } + \"MySQL\" { + + $connectionUrl = \"jdbc:mysql://{0}:{1}/{2}\" + + # Check for Windows Authentication type + if ($liquibaseAuthenticationMethod -eq \"windowsauthentication\") { + # Add querysting parameter + $connectionUrl += \"?integratedSecurity=true\" + } + + break + } + \"Oracle\" { + $connectionUrl = \"jdbc:oracle:thin:@{0}:{1}/{2}\" + break + } + \"SqlServer\" { + $connectionUrl = \"jdbc:sqlserver://{0}:{1};database={2};\" + + switch ($liquibaseAuthenticationMethod) { + \"azuremanagedidentity\" { + # Add querystring parameter + $connectionUrl += \"Authentication=ActiveDirectoryMSI;\" + break + } + \"windowsauthentication\" { + # Add querysting parameter + $connectionUrl += \"integratedSecurity=true;\" + \t + break + } + } + + break + } + \"PostgreSQL\" { + $connectionUrl = \"jdbc:postgresql://{0}:{1}/{2}\" + + # Check for Windows Authentication type + if ($liquibaseAuthenticationMethod -eq \"windowsauthentication\") { + # Add querysting parameter + $connectionUrl += \"?gsslib=sspi\" + } + + break + } + \"Snowflake\" { + $connectionUrl = \"jdbc:snowflake://{0}.snowflakecomputing.com?db={2}\" + break + } + default { + # Display error + Write-Error \"Unkonwn database type: $DatabaseType.\" + } + } + + if (![string]::IsNullOrWhitespace($QueryStringParameters)) { \t + if ($connectionUrl.Contains(\"?\")) { + \t# Replace the ? with & in connection string parameters + $QueryStringParameters = $QueryStringParameters.Replace(\"?\", \"&\") + } + + # Appen connecion string + $connectionUrl += \"$QueryStringParameters\" + } + + # Return the url + return ($connectionUrl -f $ServerName, $ServerPort, $DatabaseName) +} + +# Create array for arguments +$liquibaseArguments = @() + +# Check to see if it's running on Windows +if ($IsWindows) { + # Disable the progress bar so downloading files via Invoke-WebRequest are faster + $ProgressPreference = 'SilentlyContinue' +} + +# Check for license key +if (![string]::IsNullOrWhitespace($liquibaseProLicenseKey)) { + # Add key to arguments + $liquibaseArguments += \"--liquibaseProLicenseKey=$liquibaseProLicenseKey\" +} + +# Find Change log +$changeLogFile = (Get-ChangeLog -FileName $liquibaseChangeLogFileName) +$liquibaseArguments += \"--changeLogFile=$($changeLogFile.Name)\" + +# Set the location to where the file is +Set-Location -Path $changeLogFile.Directory + +# Check to see if it needs to be downloaed to machine +if ($liquibaseDownload -eq $true) { + # Download and extract liquibase - get the version for extensions that are version specific + $liquibaseVersion = Get-Liquibase -Version $liquibaseVersion -DownloadFolder $workingFolder + + # Download and extract java and add it to PATH environment variable + Get-Java + + # Get the driver + $driverPath = Get-DatabaseJar -DatabaseType $liquibaseDatabaseType + + # Check to see if it's null + if ($null -ne $driverPath) { + # Create folder to hold jar files to override + New-Item -Path \"$PWD/liquibase_libs/\" -ItemType Directory + + # Copy contents into liquibase_libs folder + $driverPaths = $driverPath.Split([IO.Path]::PathSeparator) + + foreach ($driver in $driverPaths) { + # Copy the items + $files = Get-ChildItem -Path $driver + + foreach ($file in $files) { + Write-Host \"Copying $($file.FullName) to $PWD/liquibase_libs/$($file.Name)\" + Copy-Item -Path $file.FullName -Destination \"$PWD/liquibase_libs/$($file.Name)\" + } + } + } +} +else { + if (![string]::IsNullOrEmpty($liquibaseClassPath)) { + $liquibaseArguments += \"--classpath=$liquibaseClassPath\" + } +} + +# Check to see if liquibase path has been defined +if ([string]::IsNullOrWhitespace($liquibaseExecutablePath)) { + +\tif ($env:IsContainer) + { + \t$liquibaseExecutablePath = \"/\"\t + } + else + { + \t# Assign root + \t$liquibaseExecutablePath = $PSSCriptRoot + } +} + +# Get the executable location +if ($IsWindows) { + $liquibaseExecutable = Get-ChildItem -Path $liquibaseExecutablePath -Recurse | Where-Object { $_.Name -eq \"liquibase.bat\" } +} + +if ($IsLinux) { + $liquibaseExecutable = Get-ChildItem -Path $liquibaseExecutablePath -Recurse | Where-Object { $_.Name -eq \"liquibase\" } +} + +# Add path to current session +$env:PATH += \"$([IO.Path]::PathSeparator)$($liquibaseExecutable.Directory)\" + +# Check to make sure it was found +if ([string]::IsNullOrEmpty($liquibaseExecutable)) { + # Could not find the executable + Write-Error \"Unable to find liquibase.bat in $PSScriptRoot or subfolders.\" +} + +# Get connection Url +$connectionUrl = Get-ConnectionUrl -DatabaseType $liquibaseDatabaseType -ServerPort $liquibaseServerPort -ServerName $liquibaseServerName -DatabaseName $liquibaseDatabaseName -QueryStringParameters $liquibaseQueryStringParameters + +# Add username +$liquibaseArguments += \"--username=$liquibaseUsername\" + +# Determine authentication method +switch ($liquibaseAuthenticationMethod) { + \"azuremanagedidentity\" { + # SQL Server driver doesn't assign password + if ($liquibaseDatabaseType -ne \"SqlServer\") { + # Get login token + Write-Host \"Generating Azure Managed Identity token ...\" + $token = Invoke-RestMethod -Method GET -Uri \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://ossrdbms-aad.database.windows.net\" -Headers @{\"MetaData\" = \"true\" } -UseBasicParsing + + $liquibasePassword = $token.access_token + $liquibaseArguments += \"--password=`\"$liquibasePassword`\"\" + } + } + \"awsiam\" { + # Region is part of the RDS endpoint, extract + $region = ($liquibaseServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $liquibasePassword = (aws rds generate-db-auth-token --hostname $liquibaseServerName --region $region --port $liquibaseServerPort --username $liquibaseUsername) + $liquibaseArguments += \"--password=`\"$liquibasePassword`\"\" + + break + } + \"gcpserviceaccount\" { + # Define header + $header = @{ \"Metadata-Flavor\" = \"Google\" } + + # Retrieve service accounts + $serviceAccounts = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\" -Headers $header -UseBasicParsing + + # Results returned in plain text format, get into array and remove empty entries + $serviceAccounts = $serviceAccounts.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + + # Retreive the specific service account assigned to the VM + $serviceAccount = $serviceAccounts | Where-Object { $_.Contains(\"iam.gserviceaccount.com\") } + + Write-Host \"Generating GCP IAM token ...\" + # Retrieve token for account + $token = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/$serviceAccount/token\" -Headers $header -UseBasicParsing + + $liquibasePassword = $token.access_token + $liquibaseArguments += \"--password=`\"$liquibasePassword`\"\" + } + \"usernamepassword\" { + # Add password + $liquibaseArguments += \"--password=`\"$liquibasePassword`\"\" + + break + } +} + +# Add connection url +$liquibaseArguments += \"--url=`\"$connectionUrl`\"\" + +# Determine if the output variable needs to be set +if ($liquibaseCommand.EndsWith(\"SQL\")) { + # Add the output variable as the command name + $liquibaseArguments += \"--outputFile=`\"$PSScriptRoot/artifacts/$($liquibaseCommand).sql`\"\" + + # Create the folder + if ((Test-Path -Path \"$PSScriptRoot/artifacts\") -eq $false) { + New-Item -Path \"$PSScriptRoot/artifacts\" -ItemType \"Directory\" + } +} + + +# Add the additional switches +foreach ($liquibaseSwitch in $liquibaseAdditionalSwitches) { + $liquibaseArguments += $liquibaseSwitch +} + +switch ($liquibaseCommandStyle) { + \"legacy\" { + # Add the command to execute + $liquibaseArguments += $liquibaseCommand + } + \"modern\" { + # Insert the command at the beginning + $liquibaseArguments = @($liquibaseCommand) + $liquibaseArguments + } +} + +# Add command arguments +$liquibaseArguments += $liquibaseCommandArguments + +# Display what's going to be run +if (![string]::IsNullOrWhitespace($liquibasePassword)) { + $liquibaseDisplayArguments = $liquibaseArguments.PSObject.Copy() + for ($i = 0; $i -lt $liquibaseDisplayArguments.Count; $i++) { + if ($null -ne $liquibaseDisplayArguments[$i]) { + if ($liquibaseDisplayArguments[$i].Contains($liquibasePassword)) { + $liquibaseDisplayArguments[$i] = $liquibaseDisplayArguments[$i].Replace($liquibasePassword, \"****\") + } + } + } + + Write-Host \"Executing the following command: $($liquibaseExecutable.FullName) $liquibaseDisplayArguments\" +} +else { + Write-Host \"Executing the following command: $($liquibaseExecutable.FullName) $liquibaseArguments\" +} + + +# Check to see if it's running in a container (this variable is provided by the build of the container itself) +if ($env:IsContainer) +{ +\t# Download any additional drivers based on the database technology being deployed to + $driverPath = Get-DatabaseJar -DatabaseType $liquibaseDatabaseType + + # Check to see if it's null + if ($null -ne $driverPath) { + # Create folder to hold jar files to override + New-Item -Path \"$PWD/liquibase_libs/\" -ItemType Directory + + # Copy contents into liquibase_libs folder + $driverPaths = $driverPath.Split([IO.Path]::PathSeparator) + + foreach ($driver in $driverPaths) { + # Copy the items + $files = Get-ChildItem -Path $driver + + foreach ($file in $files) { + Write-Host \"Copying $($file.FullName) to $PWD/liquibase_libs/$($file.Name)\" + Copy-Item -Path $file.FullName -Destination \"$PWD/liquibase_libs/$($file.Name)\" + } + } + } +} + + + +# Declare variable to hold output from Tee-Object +$liquibaseCommandOutput; + +# Redirection of stderr to stdout is done different on Windows versus Linux +if ($IsWindows) { + $liquibaseArguments += \"2>&1\" + # Execute Liquibase + & $liquibaseExecutable.FullName $liquibaseArguments | Tee-Object -Variable liquibaseCommandOutput +} + +if ($IsLinux) { + # Execute Liquibase + & $liquibaseExecutable.FullName $liquibaseArguments 2>&1 | Tee-Object -Variable liquibaseCommandOutput +} + +Set-OctopusVariable -name \"LiquibaseCommandOutput\" -value $liquibaseCommandOutput + +# Check exit code +if ($lastExitCode -ne 0) { + # Fail the step + Write-Error \"Execution of Liquibase failed!\" +} + +# Check to see if there were any files output +if ((Test-Path -Path \"$PSScriptRoot/artifacts\") -eq $true) { + # Loop through items + foreach ($item in (Get-ChildItem -Path \"$PSScriptRoot/artifacts\")) { + New-OctopusArtifact -Path $item.FullName -Name $item.Name + } +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "Parameters": [ + { + "Id": "a5f6167a-3e45-4337-ad76-fdacf385ac9c", + "Name": "liquibaseProLicenseKey", + "Label": "Pro license key", + "HelpText": "Enter your Liquibase Pro license key. [Request a free 30-day trial.](https://www.liquibase.com/trial) Leave blank to use the Community Edition.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f56f8e65-bd97-4be4-a9e8-979a5d8e5208", + "Name": "liquibaseDatabaseType", + "Label": "Database type", + "HelpText": "Select the database type to deploy to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Cassandra|Cassandra +CosmosDB|CosmosDB +DB2|DB2 +MariaDB|MariaDB +MongoDB|MongoDB +MySQL|MySQL +Oracle|Oracle +PostgreSQL|PostgreSQL +Snowflake|Snowflake +SqlServer|SqlServer" + } + }, + { + "Id": "e0e2ba23-69f4-4a82-8310-b06f9a485802", + "Name": "liquibaseCommand", + "Label": "Command", + "HelpText": "Use the drop down to select the command to execute. +Commands with `*` have partial functionality with Community edition, full functionality with Pro edition. + +Commands with `**` are Pro only (all Pro only commands require the use of Additional switches). + +Commands with `***` require Additiona switches. + +All commands that end in `SQL` will automatically add the `--outputFile` switch and include the generated output as an artifact.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "changelogSync|changelogSync +changelogSyncSQL|changelogSyncSQL +clearCheckSums|clearCheckSums +deactivateChangeLog|deactivateChangeLog +dropAll|dropAll* +futureRollbackSQL|futureRollbackSQL +history|history +rollbackOneChangeSet|rollbackOneChangeSet** +rollbackOneChangeSetSQL|rollbackOneChangeSetSQL** +rollbackOneUpdate|rollbackOneUpdate** +rollbackOneUpdateSQL|rollbackOneUpdateSQL** +rollbackToDate|rollbackToDate*** +rollbackToDateSQL|rollbackToDateSQL*** +status|status +tag-exists|tag-exists +updateSQL|updateSQL +update|update +updateTestingRollback|updateTestingRollback +validate|validate" + } + }, + { + "Id": "86049fa3-866d-4687-b128-25be358cc5aa", + "Name": "liquibaseCommandStyle", + "Label": "Command style", + "HelpText": "Select the style of command, Legacy or Modern. Legacy will place the Liquibase command at the end of the call, Modern will place it at the beginning + +Legacy: +`liquibase --switch1 command` + +Modern: +`liquibase command --switch1`", + "DefaultValue": "modern", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "legacy|Legacy +modern|Modern" + } + }, + { + "Id": "0af1aaa8-0cfb-4018-85fc-c996fd450d8c", + "Name": "liquibaseCommandArguments", + "Label": "Command Arguments", + "HelpText": "Some commands require additional arguments, e.g. , `status --verbose`, `rollbackOneUpdate --force`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9f2a3e0c-1afd-4472-acc9-66bd8743ec37", + "Name": "liquibaseAdditionalSwitches", + "Label": "Additional switches", + "HelpText": "A list of additional switches to include for the command, one per line. (I.e. `--logLevel=debug`)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "ef523320-65ec-44ce-a0cd-0e65a0fed9f9", + "Name": "liquibaseChangeLogFileName", + "Label": "Change Log file name", + "HelpText": "Name of the changelog file in the package.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2ff32870-827c-45c1-86f8-f31842ef547b", + "Name": "liquibaseChangeset", + "Label": "Changeset package", + "HelpText": "Select the package with the changeset.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "dbb7c649-f87b-42f3-b6c9-6db97df39a2f", + "Name": "liquibaseServerName", + "Label": "Server name", + "HelpText": "Name or IP address of the database server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "481b1bd7-ed0a-40da-8ee6-81397c8d0769", + "Name": "liquibaseServerPort", + "Label": "Server port", + "HelpText": "The port the database server listens on.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "56db5b8b-ceaa-4ee8-b231-df15ea7d9a43", + "Name": "liquibaseAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the database server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +azuremanagedidentity|Azure Managed Identity +gcpserviceaccount|GCP Service Account +usernamepassword|Username\\Password +windowsauthentication|Windows Authentication" + } + }, + { + "Id": "8d4337f3-3e37-40e7-983b-b85cc630a720", + "Name": "liquibaseDatabaseName", + "Label": "Database name", + "HelpText": "Name of the database to deploy to. +- Service name for Oracle +- KeySpace for Cassandra", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0524f7f4-fb8e-4747-8f1b-679237e398a7", + "Name": "liquibaseUsername", + "Label": "Username", + "HelpText": "Username of a user that has permission to make changes to the database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d58b5a73-681b-434a-92a9-b373216fedc3", + "Name": "liquibasePassword", + "Label": "Password", + "HelpText": "Password for the user with permissions to make changes to the database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "bac7d86b-eb37-4b3f-a99c-0779ffae3fca", + "Name": "liquibaseQueryStringParameters", + "Label": "Connection query string parameters", + "HelpText": "Add additional parameters to the connection string URL. Example: ?useUnicode=true or ;AuthMech=1", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8f1c2bc5-6b39-4c95-8eb0-bd97b0fe62f0", + "Name": "liquibaseClassPath", + "Label": "Database driver path", + "HelpText": "Filepath to the location of the .jar driver for the database type.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cae0020d-595b-407c-a96b-02f7a6bc9788", + "Name": "liquibaseExecutablePath", + "Label": "Executable file path", + "HelpText": "File path to the Liquibase executable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2eae1d4e-a12d-47dd-8a43-1a6e49983fd9", + "Name": "liquibaseDownload", + "Label": "Download Liquibase?", + "HelpText": "Use this option to download the software necessary to deploy a Liquibase changeset. This will download Liquibase, java, and the appropriate driver for the selected database type. Using this option overrides the `Database driver path` and `Executable file path` inputs.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "3f37288f-a4df-4ed7-b309-bb361a496572", + "Name": "liquibaseVersion", + "Label": "Liquibase version", + "HelpText": "Used with `Download Liquibase` to specify the version of Liquibase to use.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2024-07-23T23:44:02.126Z", + "OctopusVersion": "2024.2.9313", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "liquibase" +} diff --git a/step-templates/mariadb-add-database-user-to-role.json.human b/step-templates/mariadb-add-database-user-to-role.json.human new file mode 100644 index 000000000..b061f0979 --- /dev/null +++ b/step-templates/mariadb-add-database-user-to-role.json.human @@ -0,0 +1,284 @@ +{ + "Id": "24095ff8-a851-498f-8105-667bd76733eb", + "Name": "MariaDB - Add Database User To Role", + "Description": "Adds a database user to a role", + "ActionType": "Octopus.Script", + "Version": 5, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-UserInRole +{ +\t# Define parameters + param ($UserHostname, + $Username, + $RoleHostName, + $RoleName) + +\t# Execute query + $grants = Invoke-SqlQuery \"SHOW GRANTS FOR '$Username'@'$UserHostName';\" + + # Loop through Grants + foreach ($grant in $grants.ItemArray) + { + # Check grant + if ($grant -eq \"GRANT $RoleName TO '$Username'@'$UserHostName'\") + { + # They're in the group + return $true + } + } + + # Not found + return $false +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Declare initial connection string +$connectionString = \"Server=$addMariaDBServerName;Port=$addMariaDBServerPort;\" + +# Update the connection string based on authentication method +switch ($mariaDbAuthenticationMethod) { + \"awsiam\" { + # Region is part of the RDS endpoint, extract + $region = ($addMariaDBServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $addLoginPasswordWithAddRoleRights = (aws rds generate-db-auth-token --hostname $addMariaDBServerName --region $region --port $addMariaDBServerPort --username $addLoginWithAddRoleRights) + + # Append remaining portion of connection string + $connectionString += \";Uid=$addLoginWithAddRoleRights;Pwd=`\"$addLoginPasswordWithAddRoleRights`\";\" + + break + } + \"usernamepassword\" { + # Append remaining portion of connection string + $connectionString += \";Uid=$addLoginWithAddRoleRights;Pwd=`\"$addLoginPasswordWithAddRoleRights`\";\" + + break + } + \"windowsauthentication\" { + # Append remaining portion of connection string + $connectionString += \";IntegratedSecurity=yes;Uid=$addLoginWithAddRoleRights;\" + + break + } +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +try +{ + # Connect to MySQL + Open-MySqlConnection -ConnectionString $connectionString + + # See if database exists + $userInRole = Get-UserInRole -UserHostname $addUserHostname -Username $addUsername -RoleName $addRoleName + + if ($userInRole -eq $false) + { + # Create database + Write-Output \"Adding user $addUsername@$addUserHostName to role $addRoleName ...\" + $executionResults = Invoke-SqlUpdate \"GRANT $addRoleName TO '$addUsername'@'$addUserHostName';\" + + # See if it was created + $userInRole = Get-UserInRole -UserHostname $addUserHostname -Username $addUsername -RoleName $addRoleName + + # Check array + if ($userInRole -eq $true) + { + # Success + Write-Output \"$addUserName@$addUserHostName added to $addRoleName successfully!\" + } + else + { + # Failed + Write-Error \"Failure adding $addUserName@$addUserHostName to $addRoleName!\" + } + } + else + { + \t# Display message + Write-Output \"User $addUsername@$addUserHostName is already in role $addRoleName\" + } +} +finally +{ + Close-SqlConnection +} + + +" + }, + "Parameters": [ + { + "Id": "b6384c33-5196-40a4-b67f-f904eccbd795", + "Name": "addMariaDBServerName", + "Label": "MariaDB Server name", + "HelpText": "Name of the MariaDB database server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6b8a13d6-7b7a-4cf2-8864-26b0c922a27f", + "Name": "addMariaDBServerPort", + "Label": "Port", + "HelpText": "Port the MariaDB listens on.", + "DefaultValue": "3306", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a8582fbc-4272-4098-8a99-6a28c9259958", + "Name": "addLoginWithAddRoleRights", + "Label": "Login name", + "HelpText": "Login name of a user that can add roles to other users.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d8e07293-a942-4ab3-9ef2-59092cc1fdc6", + "Name": "addLoginPasswordWithAddRoleRights", + "Label": "Login password", + "HelpText": "Password for the login account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "846984d9-3661-4938-9cd7-c6e2daa87c43", + "Name": "addUsername", + "Label": "User name", + "HelpText": "Name of the user to add the role to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ab3c26b1-d64b-4257-bd88-0b316ae21655", + "Name": "addUserHostname", + "Label": "User Hostname", + "HelpText": "Hostname of the user.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "da07dc32-a66d-453d-9dcb-e78ea525a31f", + "Name": "addRoleName", + "Label": "Role name", + "HelpText": "Name of the role to add to the user.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0d29c6ce-724c-47bc-a4ef-42b123cf02bb", + "Name": "mariaDbAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the MariaDB server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +usernamepassword|Username\\Password" + } + } + ], + "LastModifiedBy": "coryreid", + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2022-07-12T19:52:36.677Z", + "OctopusVersion": "2022.3.2617-hotfix.4278", + "Type": "ActionTemplate" + }, + "Category": "mariadb" + } diff --git a/step-templates/mariadb-create-database.json.human b/step-templates/mariadb-create-database.json.human new file mode 100644 index 000000000..367925e9d --- /dev/null +++ b/step-templates/mariadb-create-database.json.human @@ -0,0 +1,238 @@ +{ + "Id": "2bdfe600-e205-43f9-b174-67ee5d36bf5b", + "Name": "MariaDB - Create Database If Not Exists", + "Description": "Creates a MariaDB database if it doesn't already exist.", + "ActionType": "Octopus.Script", + "Version": 6, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled { + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) { + # It is installed + return $true + } + else { + # Module not installed + return $false + } +} + +function Install-PowerShellModule { + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + + # Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + # Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + + # Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-DatabaseExists { + # Define parameters + param ($DatabaseName) + + # Execute query + return Invoke-SqlQuery \"SHOW DATABASES LIKE '$DatabaseName';\" +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) { + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) { + # Use specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + + +# Declare initial connection string +$connectionString = \"Server=$createMariaDBServerName;Port=$createPort;\" + +# Update the connection string based on authentication method +switch ($mariaDbAuthenticationMethod) { + \"awsiam\" { + # Region is part of the RDS endpoint, extract + $region = ($createMariaDBServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $createUserPassword = (aws rds generate-db-auth-token --hostname $createMariaDBServerName --region $region --port $createPort --username $createUsername) + + # Append remaining portion of connection string + $connectionString += \";Uid=$createUsername;Pwd=`\"$createUserPassword`\";\" + + break + } + \"usernamepassword\" { + # Append remaining portion of connection string + $connectionString += \";Uid=$createUsername;Pwd=`\"$createUserPassword`\";\" + + break + } + \"windowsauthentication\" { + # Append remaining portion of connection string + $connectionString += \";IntegratedSecurity=yes;Uid=$createUsername;\" + + break + } +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +try { + # Connect to MySQL + Open-MySqlConnection -ConnectionString $connectionString + + # See if database exists + $databaseExists = Get-DatabaseExists -DatabaseName $createDatabaseName + + if ($databaseExists.ItemArray.Count -eq 0) { + # Create database + Write-Output \"Creating database $createDatabaseName ...\" + $executionResult = Invoke-SqlUpdate \"CREATE DATABASE $createDatabaseName;\" + + # Check result + if ($executionResult -ne 1) { + # Commit transaction + Write-Error \"Create schema failed.\" + } + else { + # See if it was created + $databaseExists = Get-DatabaseExists -DatabaseName $createDatabaseName + + # Check array + if ($databaseExists.ItemArray.Count -eq 1) { + # Success + Write-Output \"$createDatabaseName created successfully!\" + } + else { + # Failed + Write-Error \"$createDatabaseName was not created!\" + } + } + } + else { + # Display message + Write-Output \"Database $createDatabaseName already exists.\" + } +} +finally { + Close-SqlConnection +} +" + }, + "Parameters": [ + { + "Id": "8fc92b80-5122-44a0-b3d8-a1d022a35055", + "Name": "createMariaDBServerName", + "Label": "Server", + "HelpText": "Hostname (or IP) of the MariaDB database server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "df993ccf-71ab-48de-9a67-e2af6653d35e", + "Name": "createUsername", + "Label": "Username", + "HelpText": "Username to use for the connection", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8a07f25d-a7db-466e-a356-9155cbc5f258", + "Name": "createUserPassword", + "Label": "Password", + "HelpText": "Password for the user account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "2af18465-c8d1-48f6-afce-1b1b30ae9559", + "Name": "createDatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to create", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f55e2a34-7a82-4d92-83bb-a19f304774d8", + "Name": "createPort", + "Label": "Port", + "HelpText": "Port for the database instance.", + "DefaultValue": "3306", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "22bcb486-c796-412d-a7bc-e1a3e0d854a3", + "Name": "mariaDbAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the MariaDB server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +usernamepassword|Username\\Password" + } + } + ], + "LastModifiedBy": "coryreid", + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2022-07-12T19:34:19.067Z", + "OctopusVersion": "2022.3.2617-hotfix.4278", + "Type": "ActionTemplate" + }, + "Category": "mariadb" + } diff --git a/step-templates/mariadb-create-user.json.human b/step-templates/mariadb-create-user.json.human new file mode 100644 index 000000000..d70cf1779 --- /dev/null +++ b/step-templates/mariadb-create-user.json.human @@ -0,0 +1,268 @@ +{ + "Id": "5e41412b-0839-4fa8-b7a1-9360115ef303", + "Name": "MariaDB - Create User If Not Exists", + "Description": "Creates a new user account on a MariaDB database server", + "ActionType": "Octopus.Script", + "Version": 5, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-UserExists +{ +\t# Define parameters + param ($Hostname, + $Username) + +\t# Execute query + return Invoke-SqlQuery \"SELECT * FROM mysql.user WHERE Host = '$Hostname' AND User = '$Username';\" +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Declare initial connection string +$connectionString = \"Server=$createMariaDBServerName;Port=$createPort;\" + +# Update the connection string based on authentication method +switch ($mariaDbAuthenticationMethod) { + \"awsiam\" { + # Region is part of the RDS endpoint, extract + $region = ($createMariaDBServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $createUserPassword = (aws rds generate-db-auth-token --hostname $createMariaDBServerName --region $region --port $createPort --username $createUsername) + + # Append remaining portion of connection string + $connectionString += \";Uid=$createUsername;Pwd=`\"$createUserPassword`\";\" + + break + } + \"usernamepassword\" { + # Append remaining portion of connection string + $connectionString += \";Uid=$createUsername;Pwd=`\"$createUserPassword`\";\" + + break + } + \"windowsauthentication\" { + # Append remaining portion of connection string + $connectionString += \";IntegratedSecurity=yes;Uid=$createUsername;\" + + break + } +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +try +{ + # Connect to MySQL + Open-MySqlConnection -ConnectionString $connectionString + + # See if database exists + $userExists = Get-UserExists -Hostname $createUserHostname -Username $createNewUsername + + if ($userExists -eq $null) + { + # Create database + Write-Output \"Creating user $createNewUsername ...\" + $executionResults = Invoke-SqlUpdate \"CREATE USER '$createNewUsername'@'$createUserHostname' IDENTIFIED BY '$createNewUserPassword';\" + + # See if it was created + $userExists = Get-UserExists -Hostname $createUserHostname -Username $createNewUsername + + # Check array + if ($userExists -ne $null) + { + # Success + Write-Output \"$createNewUsername created successfully!\" + } + else + { + # Failed + Write-Error \"$createNewUsername was not created!\" + } + } + else + { + \t# Display message + Write-Output \"User $createNewUsername on $createUserHostname already exists.\" + } +} +finally +{ + Close-SqlConnection +} + + +" + }, + "Parameters": [ + { + "Id": "0fb5e63d-528c-4e7e-841d-6d4bd1ef47a4", + "Name": "createMariaDBServerName", + "Label": "MariaDB Server", + "HelpText": "Host name of the MariaDB server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "47364cd4-6c31-43f6-9585-cc97aca28d3c", + "Name": "createMariaDBServerPort", + "Label": "Port", + "HelpText": "Port number the MySQL server listens on.", + "DefaultValue": "3306", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cb9b74a5-f444-4b8c-b353-0eebd990e0a3", + "Name": "createLoginWithAddUserRights", + "Label": "Login name", + "HelpText": "Login name of a user with rights to create user accounts.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "699b3521-06dc-4e66-a20e-adce0fddab38", + "Name": "createLoginPasswordWithAddUserRights", + "Label": "Login Password", + "HelpText": "Password Login name.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "5b5ec614-a799-407d-870a-d3098794e049", + "Name": "createNewUsername", + "Label": "New user name", + "HelpText": "Name of the new user account to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8fe190a3-b3d5-4d4b-84d2-a4fe5bf2c99f", + "Name": "createNewUserPassword", + "Label": "New user password", + "HelpText": "Password for the new user account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c7fd6115-ec4d-455b-b84b-d6eb19228140", + "Name": "createUserHostname", + "Label": "New user host name", + "HelpText": "Host name that the new user account is allowed to login from. Enter % to allow the account to connect from anywhere.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "37569f0a-6cc7-4914-be50-0cc24484dcd9", + "Name": "mariaDbAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the MariaDB server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +usernamepassword|Username\\Password" + } + } + ], + "LastModifiedBy": "coryreid", + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2022-07-12T19:41:41.956Z", + "OctopusVersion": "2022.3.2617-hotfix.4278", + "Type": "ActionTemplate" + }, + "Category": "mariadb" +} diff --git a/step-templates/medianova-purge.json.human b/step-templates/medianova-purge.json.human new file mode 100644 index 000000000..01e154247 --- /dev/null +++ b/step-templates/medianova-purge.json.human @@ -0,0 +1,79 @@ + +{ + "Id": "dce70842-466e-4ae7-acd4-9aa18bfac065", + "Name": "Medianova - Purge", + "Description": "Allows to purge content using Medianova CDN Purge App", + "ActionType": "Octopus.Script", + "Version": 0, + "Author": "olcay", + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$username = $OctopusParameters['username']\r +$pass = $OctopusParameters['pass']\r +$fileList = $OctopusParameters['fileList']\r +\r +Try \r +{\r + foreach($file in $fileList.Split(\"`n\")){\r + \"https://purge.mncdn.com/?username=$username&pass=$pass&file=$file\"\r + $result = Invoke-WebRequest -UseBasicParsing -Uri \"https://purge.mncdn.com/?username=$username&pass=$pass&file=$file\"\r + }\r +}\r +catch [Exception] {\r +\t\"Error, couldn't finish purge operation. `r`n $_.Exception.ToString()\"\r +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "f633ea69-cf1c-4990-be60-a1efc0162851", + "Name": "username", + "Label": "Username", + "HelpText": "Username of the user with purge priviliges", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "44be4cbd-f9e7-4f42-a6ed-627a56d31e8d", + "Name": "pass", + "Label": "Password", + "HelpText": "Password of the user", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "67f5c81a-1559-4359-a920-e5cd48b5940d", + "Name": "fileList", + "Label": "File Names", + "HelpText": "A row seperated list of files to purge. + +Sample; +/folder-1/file-1.jpg", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + } + ], + "Category": "Medianova", + "HistoryUrl": "https://github.com/OctopusDeploy/Library/commits/master/step-templates/medianova-purge.json", + "Website": "/step-templates/dce70842-466e-4ae7-acd4-9aa18bfac065", + "Logo": "iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAIAAAC1nk4lAAAACXBIWXMAAA9hAAAPYQGoP6dpAAANRUlEQVRo3sVaCXNUVRb2twwJQkg6W2ffTBzCEgyoMOI4BkVAHHFcsGSknBELB4yOog7KFOPo6OAwqAVoUYpsCSFbZyH7vpOtk3SWTncnnd7OfOfd192v1yRNRlK3Ui8v79373XO/c8537rv3kevHZqMFC82baU60OTKZ+AJ3LFayW8lBcrPYaN5KZgvZ7ER28bb4j3Rl5+fxX2U/pnn5mps56LXyzhzNS/cXpLHkAeg+N2iL1VpVa796nX4W7Rq3y1dxx66pJtMcWWXc84PDs0Wl5us3aWiE+8V9Cbrcp91undANl5Ubim7ZrhY5u3J2KF9fD3ztvHNF/L6GThaqa9kQLtBuC5nmqnbtLVOn1cSl1sYkuVpZXPLltCwqq+QZOyRD9g9eyM27lpJVtuVhKiklg5FsVr4vgNvttLDQcubs2ZzcG0mZlfEpyt5qYxM8/vRpddHcXH9q4lKKEjNaXjnEViNf0LOG3see7F8Tow1TTayKklvYuqH7VW2R6q6CPTQ1w0YFJoPR8dGnFXHJdTEJpekPOM59RzOzMlsEaDw2a6DL12rW5zVHJ7r7DFvHzdW5v6bD719Fuv7E6C3r1PNHjrFpyJsedtJ7gnYOMBYWNRQeVR2bQt9eZChWia8j2rLtOxtVCZ3r4mCM2aOFNDHlxi1oDdu0dzU/vQ/vDqyJ0bmRqaTmB66rue7gRdibTn9OxrlgoMfCo8cxVydoXZgKL3esU5fmbaOBYTKbGTcA/fgzlh7P90v9Nux/gXoGnNR3QocTj47p3y7Ek5je8GoVTBDExh6IpXHReXl8Gl285EkPh5PeTnqMKrpw9Xjn/pg6VaKl8ATNGtmcQDZjuPPcHxqj1AMS7iZVQt2WR6iymh+wWtgK6BhPwvwzBrrwQ3FOLmimDeelG1cQwKfJ6yCm17M2DpwmTQ17FPlGj1lj++NPdUWwPXxBgzMAV5mSRU2tMg0QxWrrb6Zn4xUMADTAVJz1azr/Pen1tGCWAqL0gxmCkbcbGrfvbIhJ7FkTPRoeFdjkDBr/FQ9gfa6mZFPvIC+aP9CmuoI9jaokGFXJOVgFK8WwwlQgccdLr9LUNFksAsr4iZNwdiBGg9NgjAp1quHdD2hsnOYtso87pJUxmmh4tPv1P8MHetfGjYZ7U0WnCACua9CyeNNWGtOxpfyAxlRuVTY/sVsTl4ZHAV0b5u5X9ILFKklIA5uZYTbJ20bGazfki/UZk3wAVAGszudfpv4hXlMRxeVQaOEQ9NmXmsTM7jWx2mD2llubKuH20/toUs/v+gFtk/xmSm87+13Jpvz66IQBr/C3KgrIYMvGrTtofJIR4xWDib69gOgO44llgQnxItakYdsOqr7NxLA7XVPg1huppLzvwQ3Dq71B43UXMUSDq3S//gZ7BYbzA1oEbMG//jtzfymsSM7EO0Dj8npBEsQKw2dfkQGBAiCspJtq2vc8nnStjE6aHihenrWeLl3mQCnnYbuL3z05uX6ZzUOscrspJq898Td+xWIPDJqkfhHXMFJjU8vzL5YmpGH1AUj0hU471sbfyt5E7T28MhwfzFRbV5GYIRglBhsNiwTFOyKZ4pYPT5JuWnJNKVbebqjbmI8VcyH2sq6ywWHM584zzfxrDytk0DzjsErrbpfQ6KYQI5se2o6XMYyAhd/10UkTf/wTQjvjsHP+M751DFaRnFh2AOAAVe6sja2OS+o8eAjJn6b11NhSvTG/NUo9uNrDuoFA18QmUVEJr6d/0GZz/TvvUXE5c84sPWSTKIhUpNXNnPpHSdZ6AUvgrlCnU0UVqzDgxlR7B+AJ8GDvtQ5T9UsU78vfAfY3bNwKxFgEeKEL7viqCL/pHf+qik+l1nYlYk/Qprlbzzx7MSOn7Y2j1NVHs3NyPBZRAqxqaR979TCWG6MKHNW79/FSLMjhz3b2m6qYZADyMpuAjnliSiIvuiDKNvanSUTs1ySm08Sk7Mf+QJsGjhdeTWZdVpWxnk59hgwsk0mkQJgTXlxSXv273ZATAF2ekE5n/ksmo+xhkzMdT+xqi5TTk3LRBW7cD57JlUlNBP7mTfk8qEPpeh70sNCwtnHXHpZBEWqGvuMJgiAGcUW4BcvZk+ZpXEdf/LsiN+9mYsaNhx4mrVbWG9DWxbdKkzIQzgXiRcNwENDwh961MUMI0nojkRdolzYVcaCzG8miNZJxgwZIEyMvvALv4WBikZAJohvmaWBk8J33v8rd1PbJKVn94YHp2Ttvvo1pM1PDVctFLEK1S5QiaOrh7hAzDimsOaskBWiOeTYOSUWlZanZAI258gJFqYsyH5z6+BO4I6di4ch4fYE1E9U3XXrjTerqlpcCN3sGNA9sgJFGQwKtlGhIcPTRSU4I3qC9fiAqDEbrf87BxngNAyM2dUTEIqE0bnmULlyiKQOLChETRS1oXrBPTvGfIufBZU9/LlRraHBFxBRygM5+qxSlAUA7JOLOzOqPHENsRkIWvIQDIcVA2rYcOEh1LVJYNDuls9sGjBtjDAz2PvxYj5TYQ254HXkNTsKushhoCTey19hE3d7nnMmZG3AjpTdHJSC9zx1/n/WQSZLODrvHu7B0ZW13zuaQLS3qJtjoRnIWtXbSnMkB3voHLTS7zeltyI69fbWbtyG4Doe7xSoMDzQ10YnledsckM7TM2xaUBkJVVQ0TS3Vm6UMsjomZG4AdGdU/PWcDTQ6zkjI7g1aLl5sVmNrK+9R2KzyBICgTAMv7IpQKwsCoePg2ojW3bv2cl5Efga5WVc01eRtw2pwigmPvht6oH/NzgJJtFgC02PB8v3BV6m+gaOy8Couqo30zXl4FSSEEEOukASTCylXnJKlPXqc+gaoofn2Rg8b68JCDyCYeftLh2jawMsYELRxruqpPeU7HkeK4cnZnUpyxqA//h6M6sVRkWZhUZAP1G/LWN+dvQFZmnVFmCpoFbikFIM+h955n1ObNQjoWWPXzgLEh9bXDkvLvSDHb6z72HjH/gNIGQCkTBmunCeytLLS8coUy22TYSoIScOXX0slkj/QMqf1xp7HClpUiVyy//2fUsXhqqjN1Ndf/9CjIIPQFX4VmXfSXsLWTJDG2x0/XfEokBWgOcoybt5CKEB0RKtMyoQwcs8S0JELy6vK0h9EpvS1qH/r3h3omrhUut0op4LA9GDQIC7cCBat3JxPHd3uwh3VDtLpxUsadapv7RhcyIegPZAWNPHpNDQqr3Zw0ANSlEAChx907X6WN7t4n9cZxfXGhXc/xMINOJ1SbDCsIGhXydOQvdlXlAa0NLtUOPSKCqXO1Acfuythh+SUuqmOAy8jrvndr1iRJjaGAMZXlAYDLRgJcheD3D/8xMQQtTRvXOip6BaC6MoC9d6oiFBPvfgaZ4nlgZZFklrzwEZqapPrALhmTV1T7pa7FEOLilKQ03L8rzRrWiroUY9IrGqLSkR9RUNaTk6NrcjSojIVHuPi9Ao2cKMmNoW+OOMrSoOBVroUuoBGdRw8TCVV0E9CV4haOvh+c8gNWrJMnUY/XiGT2eNrTgDQT7pAu8KtCCbgQ29SFkKhl3aTzLySoDEuxrqemEG1TVyPBgVtV4JWhgWRorn0Wh3jpSsWy9KhTAaqA154JS2HCanY3l0EtHZpHF2CrggRNH912LqDtzvMFof0Q4vSQ7vSjrXcBkdv3H9A3gVf1BF7dj7pFT3uSUO863vzLRalFmsw0A5JmvYVPAMnGAn7v8SEpbf66ITpT09zRgtuaQZtMI4deKXDual1D0FzhL1wSd6RW4QeBqPpyLHGqHioDuRwrw9Fvp+OgtzXKaRPKKI0NgkymKss+6KgkX7+daY6LonLqnsHWt7e7e732t4NABrLoam5kZSBmi90etyd8BeitDo1m2WZb1Lxv2s6MX1z23akvXsFWkji9q2/CSRKPcot1yfK0Y9Ownm9vsT9Yk3SlTEjCNKSKPVIKwrDK0DzhqKFunrL03JEitH94llGbNIajxxVnjnwC9qZxsku9jyNhSd8zg2EuB+3XKqwooxJoFOnlWcOgmoPYe/5BRrW1v726UZVkpBHoZNk+aCxwpXxKfx13bQk0MrvthZq66rKe6Q51E3Eu9nevQlRiiCtOHPgPjvlCARaPIR3Glrqtj+OwhsRkE2+KvIXAM1nDlKzqOeOhygNDtqhtDdwj4wOvXWsKDlTbCyt1A5BENpAlN7YkMcf3hVnDnwD9n0BHFQcZbNwXVhSObj/hfL4tProJIhGGL5/DX+6xTSCbzWFABr91z61lyY9t3eDg/YmiU368AOTI9Q3tY19eLJiZwF/aIxL462ciDjl8Qqls46FrRtzHiVarijtPHSYz7gE0HdLAO1QftCQDoDBBi1ddO78/NuFQ/t+37blUU1yliYuBSESKgdNnEuTrhc5yua3XUnJ7D55SvoqYvUtWPwdcQsczN0H18QRScQjBH+IdLTpWRoYoqYWqtDwRx3XOcDLVxc7NOjn2ny9iPr62cwOezBLLwm0uOGvXJNZJM69zZsXO5K52PW8xX3YLPDP/wBTT3BVqNUZhQAAAABJRU5ErkJggg==", + "LastModifiedBy": "olcay", + "$Meta": { + "ExportedAt": "2017-01-12T12:04:56.746Z", + "OctopusVersion": "3.5.1", + "Type": "ActionTemplate" + } +} diff --git a/step-templates/microsoft-teams-post-a-message.json.human b/step-templates/microsoft-teams-post-a-message.json.human new file mode 100644 index 000000000..7188d00dc --- /dev/null +++ b/step-templates/microsoft-teams-post-a-message.json.human @@ -0,0 +1,182 @@ +{ + "Id": "110a8b1e-4da4-498a-9209-ef8929c31168", + "Name": "Microsoft Teams - Post a message", + "Description": "Posts a message to Microsoft Teams using a general webhook.", + "ActionType": "Octopus.Script", + "Version": 24, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +# Helper functions +function Retry-Command { + [CmdletBinding()] + Param( + [Parameter(Position=0, Mandatory=$true)] + [scriptblock]$ScriptBlock, + + [Parameter(Position=1, Mandatory=$false)] + [int]$Maximum = 5, + + [Parameter(Position=2, Mandatory=$false)] + [int]$Delay = 100 + ) + + Begin { + $count = 0 + } + + Process { + \t$ex=$null + do { + $count++ + + try { + Write-Verbose \"Attempt $count of $Maximum\" + $ScriptBlock.Invoke() + return + } catch { + $ex = $_ + Write-Warning \"Error occurred executing command (on attempt $count of $Maximum): $($ex.Exception.Message)\" + Start-Sleep -Milliseconds $Delay + } + } while ($count -lt $Maximum) + + # Throw an error after $Maximum unsuccessful invocations. Doesn't need + # a condition, since the function returns upon successful invocation. + throw \"Execution failed (after $count attempts): $($ex.Exception.Message)\" + } +} +# End Helper functions +[int]$timeoutSec = $null +[int]$maximum = 1 +[int]$delay = 100 + +if(-not [int]::TryParse($OctopusParameters['Timeout'], [ref]$timeoutSec)) { $timeoutSec = 60 } + + +If ($OctopusParameters[\"TeamsPostMessage.RetryPosting\"] -eq $True) { +\tif(-not [int]::TryParse($OctopusParameters['RetryCount'], [ref]$maximum)) { $maximum = 1 } +\tif(-not [int]::TryParse($OctopusParameters['RetryDelay'], [ref]$delay)) { $delay = 100 } +\t + Write-Verbose \"Setting maximum retries to $maximum using a $delay ms delay\" +} + +$payload = @{ + title = $OctopusParameters['Title'] + text = $OctopusParameters['Body']; + themeColor = $OctopusParameters['Color']; +} + +Retry-Command -Maximum $maximum -Delay $delay -ScriptBlock { +\t# Declare variable for parameters + $invokeParameters = @{} + $invokeParameters.Add(\"Method\", \"POST\") + $invokeParameters.Add(\"Uri\", $OctopusParameters['HookUrl']) + $invokeParameters.Add(\"Body\", ($payload | ConvertTo-Json -Depth 4)) + $invokeParameters.Add(\"ContentType\", \"application/json; charset=utf-8\") + $invokeParameters.Add(\"TimeoutSec\", $timeoutSec) + + # Check for UseBasicParsing + if ((Get-Command Invoke-RestMethod).Parameters.ContainsKey(\"UseBasicParsing\")) + { + \t# Add the basic parsing argument + $invokeParameters.Add(\"UseBasicParsing\", $true) + } + +\t$Response = Invoke-RestMethod @invokeParameters + Write-Verbose \"Response: $Response\" +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "433c8776-2bff-44e0-a903-ab15288cadc8", + "Name": "HookUrl", + "Label": "Webhook Url", + "HelpText": "The specific URL provided by Microsoft Teams when adding an _Incoming WebHook_ connector to a team channel. Copy and paste the full Webhook URL from Microsoft Teams here.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3a51e22f-1c46-4b07-b91e-d36e70578463", + "Name": "Title", + "Label": "Message title", + "HelpText": "The title of the message that will be posted to your Microsoft Teams channel.", + "DefaultValue": "#{Octopus.Project.Name} #{Octopus.Release.Number} deployed to #{Octopus.Environment.Name}#{if Octopus.Deployment.Tenant.Id} for #{Octopus.Deployment.Tenant.Name}#{/if}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "11af257b-4c16-4a2a-8b5f-b42ab493d43d", + "Name": "Body", + "Label": "Message body", + "HelpText": "The message body of post being added to your Microsoft Teams channel.", + "DefaultValue": "For more information, please see [deployment details](#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}#{Octopus.Web.DeploymentLink})!", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "ad6a1f33-949e-4570-966e-1e2bbdfa8042", + "Name": "Color", + "Label": "Color", + "HelpText": "The color to use for the border on the side of the message.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2d51e4e8-1f81-444f-934f-21a3051e7423", + "Name": "Timeout", + "Label": "Timeout in seconds", + "HelpText": "The maximum timeout in seconds for each request.", + "DefaultValue": "60", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b8835767-e7fe-4ef9-9492-893ca98950f6", + "Name": "TeamsPostMessage.RetryPosting", + "Label": "Retry posting message", + "HelpText": "Should retries be made? If this option is enabled, the step will attempt to retry the posting of message to teams up to the set retry count. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "da4ce68b-4813-5cea-c788-372e82643e93", + "Name": "TeamsPostMessage.RetryCount", + "Label": "Retry Count", + "HelpText": "The maximum number of times to retry the post before allowing failure. Default 1", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "16d42450-c6c5-0b26-92a2-0b3e4eefb28e", + "Name": "TeamsPostMessage.RetryDelay", + "Label": "Retry delay in milliseconds", + "HelpText": "The amount of time in milliseconds to wait between retries. Default 100", + "DefaultValue": "100", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2023-08-31T20:39:39.222Z", + "OctopusVersion": "2023.2.13239", + "Type": "ActionTemplate" + }, + "Category": "microsoft-teams" +} diff --git a/step-templates/mongodb-add-update-user-roles.json.human b/step-templates/mongodb-add-update-user-roles.json.human new file mode 100644 index 000000000..0268fd85a --- /dev/null +++ b/step-templates/mongodb-add-update-user-roles.json.human @@ -0,0 +1,254 @@ +{ + "Id": "d93bcfdc-d3f8-4c83-9fd7-d35c9a8d1f2b", + "Name": "MongoDB - Add or update user roles ", + "Description": "Adds roles to an existing user in a MongoDB database.", + "ActionType": "Octopus.Script", + "Version": 2, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-DatabaseUserExists +{ +\t# Define parameters + param ($UserName) + + # Define working variables + $userExists = $false + +\t# Get users for database + $command = @\" +{ usersInfo: 1 } +\"@ + +\t$results = Invoke-MdbcCommand -Command $command + $users = $results[\"users\"] + + # Loop through returned results + foreach ($user in $users) + { + \tif ($user[\"user\"] -eq $UserName) + { + \treturn $true + } + } + + return $false +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"Mdbc\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +# Connect to mongodb instance +$connectionUrl = \"mongodb://$($MongoDBAdminUsername):$($MogoDBAdminUserpassword)@$($MongoDBServerName):$($MongoDBPort)\" + +# Connect to MongoDB server +Connect-Mdbc $connectionUrl $MongoDBDatabaseName + +# Get whether the database exits +if ((Get-DatabaseUserExists -UserName $MongoDBUsername) -eq $true) +{ +\t# Create user + Write-Output \"Adding $MongoDBRoles to $MongoDBUsername.\" + + # Create Roles array for adding + $roles = @() + foreach ($MongoDBRole in $MongoDBRoles.Split(\",\")) + { + \t$roles += @{ + \trole = $MongoDBRole.Trim() + db = $MongoDBDatabaseName + } + } + + # Define create user command + $command = @\" +{ +\tupdateUser: `\"$MongoDBUsername`\" + roles: $(ConvertTo-Json $roles) +} +\"@ + +\t# Create user account + $result = Invoke-MdbcCommand -Command $command + + # Check to make sure it was created successfully + if ($result.ContainsKey(\"ok\")) + { + \tWrite-Output \"Successfully added role(s) $MongoDBRoles to $MongoDBUsername in database $MongoDBDatabaseName.\" + } + else + { + \tWrite-Error \"Failed, $result\" + } +} +else +{ +\tWrite-Error \"Unable to add role(s) to $MongoDBUsername, user does not exist in $MongoDBDatabaseName.\" +} + + + + + + +" + }, + "Parameters": [ + { + "Id": "d6343cb6-40d3-47a8-898f-ff3aeb6f9c1a", + "Name": "MongoDBServerName", + "Label": "Server Name", + "HelpText": "Name or IP address of the MongoDB server instance.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "75eae92c-0081-481b-b79a-73524a798349", + "Name": "MongoDBPort", + "Label": "Port", + "HelpText": "Port number the MongoDB instance is listening on.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "dbb4ef7a-ab79-4036-ac7b-466d683341f5", + "Name": "MongoDBDatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to use.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0f0612d3-e039-4940-bcb7-a3333266ae95", + "Name": "MongoDBAdminUsername", + "Label": "Admin Username", + "HelpText": "User account with rights to query the users in the database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fbdb0e70-33d5-41a6-b316-66f1be7e5a18", + "Name": "MogoDBAdminUserpassword", + "Label": "Admin Password", + "HelpText": "Password for the admin account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c151f9f5-36f3-47ab-80b7-7dcb18bb7ff8", + "Name": "MongoDBUsername", + "Label": "Username", + "HelpText": "Username to add roles to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c705b32d-c416-40fc-869d-88fc307fe94f", + "Name": "MongoDBRoles", + "Label": "Roles", + "HelpText": "A comma-delimited list of roles to add the user account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-12-02T20:31:19.166Z", + "OctopusVersion": "2020.5.0", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "mongodb" + } diff --git a/step-templates/mongodb-atlas-create-snapshot.json.human b/step-templates/mongodb-atlas-create-snapshot.json.human new file mode 100644 index 000000000..309070722 --- /dev/null +++ b/step-templates/mongodb-atlas-create-snapshot.json.human @@ -0,0 +1,155 @@ +{ + "Id": "6a08c1c3-d96b-4fc0-9677-6008fe831d1c", + "Name": "MongoDB Atlas - Create snapshot", + "Description": "Takes one on-demand snapshot for the specified cluster. To use this resource, the requesting API Key must have the Project Atlas Admin role and an entry for the project access list.", + "ActionType": "Octopus.Script", + "Version": 1, + "Author": "claude-uceda", + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$project_source = $OctopusParameters[\"matlas-project-id\"] +$cluster_source = $OctopusParameters[\"matlas-cluster-name\"] +$retention_in_days = $OctopusParameters[\"matlas-retention-in-days\"] +$snapshot_description = $OctopusParameters[\"matlas-snapshot-description\"] +$check_delay_seconds = $OctopusParameters[\"matlas-check-delay-seconds\"] + +$login = $OctopusParameters[\"matlas-public-key\"] +$secret = $OctopusParameters[\"matlas-private-key\"] + +$snapshot_description_json = $snapshot_description | ConvertTo-Json +$check_delay_seconds_nb = ($check_delay_seconds -as [int]) +$retention_in_days_nb = ($retention_in_days -as [int]) + +function Check-Required($name, $value) { +\tif($value -eq $null -or $value -eq ''){ + \tWrite-Error -Message \"Missing parameter or invalid value for '$name'. ($value)\" -ErrorAction Stop + } +} + +Check-Required 'matlas-public-key' $login +Check-Required 'matlas-private-key' $secret +Check-Required 'matlas-project-id' $project_source +Check-Required 'matlas-cluster-name' $cluster_source +Check-Required 'matlas-check-delay-seconds' $check_delay_seconds_nb +Check-Required 'matlas-retention-in-days' $retention_in_days_nb + +Write-Host \"Creating snapshot of $($project_source)/$($cluster_source) using $login.\" + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +function Invoke-Api($uri, $method, $content) {\t + +\t$securedPassword = ConvertTo-SecureString -String $secret -AsPlainText -Force\t +\t$credentials = New-Object System.Management.Automation.PSCredential ($login, $securedPassword) + +\ttry { +\t\treturn Invoke-RestMethod -Uri $uri -Method $method -Credential $credentials -ContentType \"application/json\" -Body $content +\t} +\tcatch { +\t\tWrite-Error -Message $_ -ErrorAction Stop +\t} +} + +$root = \"https://cloud.mongodb.com/api/atlas/v1.0\" + +$uri = New-Object System.Uri(\"$root/groups/$project_source/clusters/$cluster_source/backup/snapshots\") +$request = \"{`\"description`\": $snapshot_description_json, `\"retentionInDays`\": $retention_in_days_nb}\" +$snapshot = Invoke-Api $uri \"POST\" $request + +$uri = New-Object System.Uri(\"$root/groups/$project_source/clusters/$cluster_source/backup/snapshots/$($snapshot.id)\") +while ($snapshot.status -eq \"queued\" -or $snapshot.status -eq \"inProgress\") { + +\tWrite-Host \"Waiting for snapshot to complete.\"\t +\tStart-Sleep -s $check_delay_seconds_nb +\t$snapshot = Invoke-Api $uri \"GET\" +} + +Write-Host \"Snapshot $($snapshot.status). Id : '$($snapshot.id)'.\" + +Set-OctopusVariable -name \"matlas-snapshot-id\" -value $snapshot.id +Set-OctopusVariable -name \"matlas-snapshot-status\" -value $snapshot.status +" + }, + "Parameters": [ + { + "Id": "8e035a62-ee27-49ca-a930-4b428b4905da", + "Name": "matlas-public-key", + "Label": "Public key", + "HelpText": "Mongo atlas public key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1138550e-5ad2-4e4a-b4e3-22a91871ce1b", + "Name": "matlas-private-key", + "Label": "Private key", + "HelpText": "Mongo atlas private key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "02556d4a-0a5f-4a1f-a243-b4bc5d0ef761", + "Name": "matlas-project-id", + "Label": "Project id", + "HelpText": "Project/group id of the cluster.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ec84fdda-a234-492e-ab69-e4a09e94c372", + "Name": "matlas-cluster-name", + "Label": "Cluster name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5f226dcb-842e-4979-a32b-085ab1aaeb0c", + "Name": "matlas-snapshot-description", + "Label": "Snapshot description", + "HelpText": "Human-readable phrase or sentence that explains the purpose of the snapshot.", + "DefaultValue": "Created by Octopus's deployment.", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b97be633-21ab-42a3-8907-21cac2c29962", + "Name": "matlas-retention-in-days", + "Label": "Snapshot retention days", + "HelpText": "Number of days that MongoDB Cloud should retain the on-demand snapshot. Must be at least 1", + "DefaultValue": "3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "199300ca-64bc-4a1c-bbb8-8ee10ff653d6", + "Name": "matlas-check-delay-seconds", + "Label": "Status check delay", + "HelpText": "Delay in seconds between each statuses check.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2022-10-03T10:50:33.391Z", + "OctopusVersion": "2020.6.5394", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "claude-uceda", + "Category": "mongodb" +} diff --git a/step-templates/mongodb-atlas-pause-resume-cluster.json.human b/step-templates/mongodb-atlas-pause-resume-cluster.json.human new file mode 100644 index 000000000..9c99617f8 --- /dev/null +++ b/step-templates/mongodb-atlas-pause-resume-cluster.json.human @@ -0,0 +1,153 @@ +{ + "Id": "2339fa87-ba06-4014-b918-ec1bdc4690e4", + "Name": "MongoDB Atlas - Pause or Resume cluster", + "Description": "Allow the user to pause/resume the asked cluster.", + "ActionType": "Octopus.Script", + "Version": 1, + "Author": "claude-uceda", + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$cluster = $OctopusParameters[\"matlas-cluster-name\"] +$project = $OctopusParameters[\"matlas-project-id\"] +$check_delay_seconds = $OctopusParameters[\"matlas-check-delay-seconds\"] + +$login = $OctopusParameters[\"matlas-public-key\"] +$secret = $OctopusParameters[\"matlas-private-key\"] + +$pause = [System.Convert]::ToBoolean($OctopusParameters[\"matlas-pause\"]) +$check_delay_seconds_nb = ($check_delay_seconds -as [int]) + +function Check-Required($name, $value) { +\tif($value -eq $null -or $value -eq ''){ + \tWrite-Error -Message \"Missing parameter or invalid value for '$name'. ($value)\" -ErrorAction Stop + } +} + +Check-Required 'matlas-public-key' $login +Check-Required 'matlas-private-key' $secret +Check-Required 'matlas-project-id' $project +Check-Required 'matlas-cluster-name' $cluster +Check-Required 'matlas-check-delay-seconds' $check_delay_seconds_nb + +$action = \"Pausing\" +if($pause -eq $false){ +\t$action = \"Resuming\" +} + +Write-Host \"$action $($project)/$($cluster) using $login.\" + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +function Invoke-Api($uri, $method, $content) { + +\t$securedPassword = ConvertTo-SecureString -String $secret -AsPlainText -Force\t +\t$credentials = New-Object System.Management.Automation.PSCredential ($login, $securedPassword) + +\ttry { +\t\treturn Invoke-RestMethod -Uri $uri -Method $method -Credential $credentials -ContentType \"application/json\" -Body $content +\t} +\tcatch { +\t\tWrite-Error -Message $_ -ErrorAction Stop +\t} +} + + +$root = \"https://cloud.mongodb.com/api/atlas/v1.0\" +$uri = New-Object System.Uri(\"$root/groups/$project/clusters/$cluster\") +$data = Invoke-Api $uri \"GET\" + +if ($data.paused -ne $pause) {\t +\t$value = $pause.ToString().ToLower() +\t$data = Invoke-Api $uri \"PATCH\" \"{`\"paused`\": $value}\"\t +\t +\twhile ($data.stateName -eq \"REPAIRING\" -or $data.stateName -eq \"UPDATING\") { + +\t\tWrite-Host \"Waiting for change to be applied. Cluster status : $($data.stateName).\"\t\t +\t\tStart-Sleep -s $check_delay_seconds_nb\t +\t\t$data = Invoke-Api $uri \"GET\" +\t}\t + +\tWrite-Host \"Change applied. $Cluster status : $($data.stateName).\" +} +else { +\t +\t$action = If ($pause) { \"paused\" } Else { \"running\" } +\tWrite-Host \"Cluster already $action, no change applied. $Cluster status : $($data.stateName).\" +} +" + }, + "Parameters": [ + { + "Id": "7b92df2e-5469-4e97-a379-ccfe6eb942d9", + "Name": "matlas-public-key", + "Label": "Public key", + "HelpText": "Mongo atlas public key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "51ced2a8-b158-4c0e-9244-081d429a1814", + "Name": "matlas-private-key", + "Label": "Private key", + "HelpText": "Mongo atlas private key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "3f62699d-32c3-4eb0-8925-f6ad2e288de9", + "Name": "matlas-project-id", + "Label": "Project id", + "HelpText": "Project/group id of the cluster.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "215ed2bf-a5fd-4860-9006-c2525f96815e", + "Name": "matlas-cluster-name", + "Label": "Cluster name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f1eb059b-d8e7-46c8-a084-77b0d3a872a4", + "Name": "matlas-pause", + "Label": "Action", + "HelpText": "If the cluster needs to be paused or resumed.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "True|Pause +False|Resume" + } + }, + { + "Id": "e1975200-57aa-4159-a1cc-bb1d5fbfca2e", + "Name": "matlas-check-delay-seconds", + "Label": "Status check delay", + "HelpText": "Delay in seconds between each statuses check.", + "DefaultValue": "15", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2022-10-03T10:51:25.583Z", + "OctopusVersion": "2020.6.5394", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "claude-uceda", + "Category": "mongodb" +} diff --git a/step-templates/mongodb-atlas-restore-snapshot.json.human b/step-templates/mongodb-atlas-restore-snapshot.json.human new file mode 100644 index 000000000..65b4cab65 --- /dev/null +++ b/step-templates/mongodb-atlas-restore-snapshot.json.human @@ -0,0 +1,155 @@ +{ + "Id": "bf062dfe-de07-462c-8e7d-ec062b29e550", + "Name": "MongoDB Atlas - Restore cluster from latest snapshot", + "Description": "Allow the user to restore a cluster from the latest snapshot.", + "ActionType": "Octopus.Script", + "Version": 1, + "Author": "claude-uceda", + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$project_source = $OctopusParameters[\"matlas-project-source\"] +$cluster_source = $OctopusParameters[\"matlas-cluster-source\"] +$project_target = $OctopusParameters[\"matlas-project-target\"] +$cluster_target = $OctopusParameters[\"matlas-cluster-target\"] +$check_delay_seconds = $OctopusParameters[\"matlas-check-delay-seconds\"] + +$login = $OctopusParameters[\"matlas-public-key\"] +$secret = $OctopusParameters[\"matlas-private-key\"] + +$check_delay_seconds_nb = ($check_delay_seconds -as [int]) + +function Check-Required($name, $value) { +\tif($value -eq $null -or $value -eq ''){ + \tWrite-Error -Message \"Missing parameter or invalid value for '$name'. ($value)\" -ErrorAction Stop + } +} + +Check-Required 'matlas-public-key' $login +Check-Required 'matlas-private-key' $secret +Check-Required 'matlas-project-source' $project_source +Check-Required 'matlas-cluster-source' $cluster_source +Check-Required 'matlas-project-target' $project_target +Check-Required 'matlas-cluster-target' $cluster_target +Check-Required 'matlas-check-delay-seconds' $check_delay_seconds_nb + +Write-Host \"Restoring from $($project_source)/$($cluster_source) to $($project_target)/$($cluster_target) using $login.\" + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +function Invoke-Api($uri, $method, $content) {\t + +\t$securedPassword = ConvertTo-SecureString -String $secret -AsPlainText -Force\t +\t$credentials = New-Object System.Management.Automation.PSCredential ($login, $securedPassword) + +\ttry { +\t\treturn Invoke-RestMethod -Uri $uri -Method $method -Credential $credentials -ContentType \"application/json\" -Body $content +\t} +\tcatch { +\t\tWrite-Error -Message $_ -ErrorAction Stop +\t} +} + +$root = \"https://cloud.mongodb.com/api/atlas/v1.0\" +$uri = New-Object System.Uri(\"$root/groups/$project_source/clusters/$cluster_source/backup/snapshots?itemsPerPage=5\") +$results = Invoke-Api $uri \"GET\" +$snapshots = $results.results | Where-Object { $_.status -eq \"completed\" } +$snapshot = $snapshots[0] + +$uri = New-Object System.Uri(\"$root/groups/$project_source/clusters/$cluster_source/backup/restoreJobs\") +$request = \"{`\"deliveryType`\":`\"automated`\", `\"snapshotId`\":`\"$($snapshot.id)`\", `\"targetClusterName`\":`\"$cluster_target`\", `\"targetGroupId`\":`\"$project_target`\"}\" +$job = Invoke-Api $uri \"POST\" $request + +$uri = New-Object System.Uri(\"$root/groups/$project_source/clusters/$cluster_source/backup/restoreJobs/$($job.id)\") +while ($null -eq $job.finishedAt -or $job.finishedAt -eq \"\") { + +\tWrite-Host \"Waiting for restore to complete.\"\t +\tStart-Sleep -s $check_delay_seconds_nb +\t$job = Invoke-Api $uri \"GET\" +} + +Write-Host \"Restore completed.\" +" + }, + "Parameters": [ + { + "Id": "0cfd5665-9bfc-413a-bc4a-7778a860b7f0", + "Name": "matlas-public-key", + "Label": "Public key", + "HelpText": "Mongo atlas public key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "61cff00c-69b5-44b5-873d-742430ca2beb", + "Name": "matlas-private-key", + "Label": "Private key", + "HelpText": "Mongo atlas private key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "3d86c70a-2a6c-4cb7-a04e-f7f43838554c", + "Name": "matlas-project-source", + "Label": "Project source's id", + "HelpText": "Project id of the cluster that will be the snapshot source.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2a32df2e-641d-41a0-9e56-dba341efe58d", + "Name": "matlas-cluster-source", + "Label": "Cluster source's name", + "HelpText": "Cluster that will be used to get the snapshot.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bff9846a-980e-4a8f-b6bb-b43de82397c1", + "Name": "matlas-project-target", + "Label": "Project target's id", + "HelpText": "Project id of the cluster that will used to restore the snapshot.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e2aaa07d-9122-47ee-b3e9-008949ed5709", + "Name": "matlas-cluster-target", + "Label": "Cluster target's name", + "HelpText": "Cluster that will be used to restore the snapshot.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ac4c10c6-e487-4cec-902d-7cfaab7a3934", + "Name": "matlas-check-delay-seconds", + "Label": "Status check delay", + "HelpText": "Delay in seconds between each statuses check.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2022-10-03T10:52:04.423Z", + "OctopusVersion": "2020.6.5394", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "claude-uceda", + "Category": "mongodb" +} diff --git a/step-templates/mongodb-create-database.json.human b/step-templates/mongodb-create-database.json.human new file mode 100644 index 000000000..c1313cf2e --- /dev/null +++ b/step-templates/mongodb-create-database.json.human @@ -0,0 +1,212 @@ +{ + "Id": "4de90708-1f76-4103-ab27-e9fd1916fa07", + "Name": "MongoDB - Create Database if not exists", + "Description": "Creates a new database on a MongoDB server with an initial collection.", + "ActionType": "Octopus.Script", + "Version": 3, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-DatabaseExists +{ +\t# Define parameters + param ($DatabaseName) + +\t# Execute query + $mongodbDatabases = Get-MdbcDatabase + + return $mongodbDatabases.DatabaseNamespace -contains $DatabaseName +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"Mdbc\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +# Connect to mongodb instance +$connectionUrl = \"mongodb://$($MongoDBUsername):$($MogoDBUserpassword)@$($MongoDBServerName):$($MongoDBPort)\" + +# Connect to MongoDB server +Connect-Mdbc $connectionUrl \"admin\" + +# Get whether the database exits +if ((Get-DatabaseExists -DatabaseName $MongoDBDatabaseName) -ne $true) +{ +\t# Create database + Write-Output \"Database $MongoDBDatabaseName doesn't exist.\" + Connect-Mdbc $connectionUrl \"$MongoDBDatabaseName\" + + # Databases don't get created unless some data has been added + Add-MdbcCollection $MongoDBInitialCollection + + # Check to make sure it was successful + if ((Get-DatabaseExists -DatabaseName $MongoDBDatabaseName)) + { + \t# Display success + Write-Output \"$MongoDBDatabaseName created successfully.\" + } + else + { + \tWrite-Error \"Failed to create $MongoDBDatabaseName!\" + } +} +else +{ +\tWrite-Output \"Database $MongoDBDatabaseName already exists.\" +} + + + + + + +" + }, + "Parameters": [ + { + "Id": "d6343cb6-40d3-47a8-898f-ff3aeb6f9c1a", + "Name": "MongoDBServerName", + "Label": "Server Name", + "HelpText": "Name or IP address of the MongoDB server instance.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "75eae92c-0081-481b-b79a-73524a798349", + "Name": "MongoDBPort", + "Label": "Port", + "HelpText": "Port number the MongoDB instance is listening on.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "dbb4ef7a-ab79-4036-ac7b-466d683341f5", + "Name": "MongoDBDatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8fabfc02-2f33-4663-9af3-b8c2f74e0175", + "Name": "MongoDBInitialCollection", + "Label": "Initial Collection", + "HelpText": "Name of the Initial Collection in the database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0f0612d3-e039-4940-bcb7-a3333266ae95", + "Name": "MongoDBUsername", + "Label": "Username", + "HelpText": "Username to connect with.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fbdb0e70-33d5-41a6-b316-66f1be7e5a18", + "Name": "MogoDBUserpassword", + "Label": "Password", + "HelpText": "Password for the user to connect with.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2020-12-02T20:27:29.307Z", + "OctopusVersion": "2020.5.0", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "Category": "mongodb" + } diff --git a/step-templates/mongodb-create-user.json.human b/step-templates/mongodb-create-user.json.human new file mode 100644 index 000000000..0c1079aea --- /dev/null +++ b/step-templates/mongodb-create-user.json.human @@ -0,0 +1,245 @@ +{ + "Id": "d7504483-0d48-49b5-b75c-b65dde8fc2bf", + "Name": "MongoDB - Create User if not exists", + "Description": "Creates a new database user on a MongoDB server.", + "ActionType": "Octopus.Script", + "Version": 2, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-DatabaseUserExists +{ +\t# Define parameters + param ($UserName) + + # Define working variables + $userExists = $false + +\t# Get users for database + $command = @\" +{ usersInfo: 1 } +\"@ + +\t$results = Invoke-MdbcCommand -Command $command + $users = $results[\"users\"] + + # Loop through returned results + foreach ($user in $users) + { + \tif ($user[\"user\"] -eq $UserName) + { + \treturn $true + } + } + + return $false +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"Mdbc\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +# Connect to mongodb instance +$connectionUrl = \"mongodb://$($MongoDBAdminUsername):$($MogoDBAdminUserpassword)@$($MongoDBServerName):$($MongoDBPort)\" + +# Connect to MongoDB server +Connect-Mdbc $connectionUrl $MongoDBDatabaseName + +# Get whether the database exits +if ((Get-DatabaseUserExists -UserName $MongoDBUsername) -ne $true) +{ +\t# Create user + Write-Output \"User $MongoDBUsername doesn't exist in database $MongoDBDatabaseName.\" + + # Define create user command + $command = @\" +{ +\tcreateUser: `\"$MongoDBUsername`\" + pwd: `\"$MongoDBUserPassword`\" + roles: [] +} +\"@ + +\t# Create user account + $result = Invoke-MdbcCommand -Command $command + + # Check to make sure it was created successfully + if ($result.ContainsKey(\"ok\")) + { + \tWrite-Output \"User $MongoDBUsername successfully created in database $MongoDBDatabaseName.\" + } + else + { + \tWrite-Error \"Failed, $result\" + } +} +else +{ +\tWrite-Output \"User $MongoDBUsername already exists in database $MongoDBDatabaseName.\" +} + + + + + + +" + }, + "Parameters": [ + { + "Id": "d6343cb6-40d3-47a8-898f-ff3aeb6f9c1a", + "Name": "MongoDBServerName", + "Label": "Server Name", + "HelpText": "Name or IP address of the MongoDB server instance.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "75eae92c-0081-481b-b79a-73524a798349", + "Name": "MongoDBPort", + "Label": "Port", + "HelpText": "Port number the MongoDB instance is listening on.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "dbb4ef7a-ab79-4036-ac7b-466d683341f5", + "Name": "MongoDBDatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to add the user to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0f0612d3-e039-4940-bcb7-a3333266ae95", + "Name": "MongoDBAdminUsername", + "Label": "Admin Username", + "HelpText": "User account with rights to query the users in the database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fbdb0e70-33d5-41a6-b316-66f1be7e5a18", + "Name": "MogoDBAdminUserpassword", + "Label": "Admin Password", + "HelpText": "Password for the admin account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c151f9f5-36f3-47ab-80b7-7dcb18bb7ff8", + "Name": "MongoDBUsername", + "Label": "Username", + "HelpText": "Username to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3cb55ad5-8dc3-4f12-a35f-aaa30957313b", + "Name": "MongoDBUserPassword", + "Label": "Password", + "HelpText": "Password for the account to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2020-12-02T20:29:45.311Z", + "OctopusVersion": "2020.5.0", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "mongodb" + } diff --git a/step-templates/msmq-create-transactional-queue.json.human b/step-templates/msmq-create-transactional-queue.json.human new file mode 100644 index 000000000..1a685e643 --- /dev/null +++ b/step-templates/msmq-create-transactional-queue.json.human @@ -0,0 +1,257 @@ +{ + "Id": "4c22f201-c634-4a22-aa55-ae24dc83d588", + "Name": "MSMQ - Create Transactional Queue", + "Description": "Create one or more MSMQ transactional queues and configure permissions.", + "ActionType": "Octopus.Script", + "Version": 10, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$MSMQQueues = $OctopusParameters['MSMQQueues'] +$MSMQResetPermissions = $OctopusParameters['MSMQResetPermissions'] +$MSMQResetDomains = $OctopusParameters['MSMQResetDomains'] +$MSMQUsers = $OctopusParameters['MSMQUsers'] +$MSMQPermAllow = $OctopusParameters['MSMQPermAllow'] +$MSMQPermDeny = $OctopusParameters['MSMQPermDeny'] +$MSMQAdminUsers = $OctopusParameters['MSMQAdminUsers'] +$MSMQPermAdminAllow = $OctopusParameters['MSMQPermAdminAllow'] +$MSMQPermAdminDeny = $OctopusParameters['MSMQPermAdminDeny'] + +Write-Verbose \"`$MSMQQueues = $MSMQQueues\" +Write-Verbose \"`$MSMQResetPermissions = $MSMQResetPermissions\" +Write-Verbose \"`$MSMQResetDomains = $MSMQResetDomains\" +Write-Verbose \"`$MSMQUsers = $MSMQUsers\" +Write-Verbose \"`$MSMQPermAllow = $MSMQPermAllow\" +Write-Verbose \"`$MSMQPermDeny = $MSMQPermDeny\" +Write-Verbose \"`$MSMQAdminUsers = $MSMQAdminUsers\" +Write-Verbose \"`$MSMQPermAdminAllow = $MSMQPermAdminAllow\" +Write-Verbose \"`$MSMQPermAdminDeny = $MSMQPermAdminDeny\" + +#Split the Queues into an array +$arrQueues = $MSMQQueues.split(\";\") +foreach ($Queue in $arrQueues) +{ + #Does Queue Exists Already? + $thisQueue = Get-MSMQQueue $Queue + if (!$thisQueue) + { + #not found, create + Write-Output \"Creating Queue: \" $Queue + New-MsmqQueue -Name \"$Queue\" -Label \"private$\\$Queue\" -Transactional | Out-Null + $thisQueue = Get-MSMQQueue $Queue + } + else + { + Write-Output \"Queue Exists: \" $thisQueue.QueueName + + if($MSMQResetPermissions -eq \"True\") + { + foreach($domain in $MSMQResetDomains.split(\";\")) + { + # reset permissions + $QueuePermissions = $thisQueue | Get-MsmqQueueACL + foreach ($AccessItem in $MSMQQueuePermissions) + { + $userName = [Environment]::UserName + if($AccessItem.AccountName -NotLike \"*$userName\") # not current user + { + $domain = \"$($domain)*\" #append * to end of domain + if ($AccessItem.AccountName -Like \"$($domain)*\") + { + Write-Output \"Removing Permissions $($AccessItem.Right) for $($AccessItem.AccountName)\" + Try + { + $thisQueue | Set-MsmqQueueACL -UserName $AccessItem.AccountName -Remove $AccessItem.Right | Out-Null + } + Catch + { + Write-Output \"Could not set permissions item $_.Exception.Message\" + Break + } + } + } + } + } + } + } + + #set acl for users + $arrUsers = $MSMQUsers.split(\";\") + foreach ($User in $arrUsers) + { + if ($User) + { + Write-Output \"Adding ACL for User: \" $User + + #allows + if ($MSMQPermAllow) + { + $arrPermissions = $MSMQPermAllow.split(\";\") + foreach ($Permission in $arrPermissions) + { + $thisQueue | Set-MsmqQueueAcl -UserName $User -Allow $Permission | Out-Null + Write-Output \"ACL Allow set: $Permission\" + } + } + + #denies + if ($MSMQPermDeny) + { + $arrPermissions = $MSMQPermDeny.split(\";\") + foreach ($Permission in $arrPermissions) + { + $thisQueue | Set-MsmqQueueAcl -UserName $User -Deny $Permission | Out-Null + Write-Output \"ACL Deny set: $Permission\" + } + } + } + } + + + $arrAdminUsers = $MSMQAdminUsers.split(\";\") + foreach ($User in $arrAdminUsers) + { + if ($User) + { + Write-Output \"Adding ACL for Admin User: \" $User + + #allows + if ($MSMQPermAdminAllow) + { + $arrPermissions = $MSMQPermAdminAllow.split(\";\") + foreach ($Permission in $arrPermissions) + { + $thisQueue | Set-MsmqQueueAcl -UserName $User -Allow $Permission | Out-Null + Write-Output \"ACL Allow admin set: $Permission\" + } + } + + #denies + if ($MSMQPermAdminDeny) + { + $arrPermissions = $MSMQPermAdminDeny.split(\";\") + foreach ($Permission in $arrPermissions) + { + $thisQueue | Set-MsmqQueueAcl -UserName $User -Deny $Permission | Out-Null + Write-Output \"ACL Deny admin set: $Permission\" + } + } + } + } +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "35154cac-d005-4a7b-85c9-8eab276e726b", + "Name": "MSMQQueues", + "Label": "Queue names", + "HelpText": "Queue names, separated by semicolons. Example: _Queue1;Queue2;Queue3_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "6f3a90b7-df04-4884-9cd1-f04ad7a1f97b", + "Name": "MSMQUsers", + "Label": "Queue users", + "HelpText": "Users with access to the queue separated by semicolons. Example: _DOMAIN\\User1;DOMAIN\\User2_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "3e2e1512-6f75-4e36-80cc-d350a3563a09", + "Name": "MSMQPermAllow", + "Label": "Allowed permissions", + "HelpText": "Permissions granted to the queue users, separated by semicolons. Example: _DeleteMessage;PeekMessage;ReceiveMessage_", + "DefaultValue": "DeleteMessage;PeekMessage;ReceiveMessage;WriteMessage", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2063989e-587f-4787-b3ce-6865501316b2", + "Name": "MSMQPermDeny", + "Label": "Denied permissions", + "HelpText": "Denied permissions, separated by semicolons: _TakeQueueOwnership_", + "DefaultValue": "TakeQueueOwnership", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "42ec591d-9631-4ff4-ac56-94a1ac7c5994", + "Name": "MSMQAdminUsers", + "Label": "Admin queue users", + "HelpText": "Users with access to the queue separated by semicolons. Example: _DOMAIN\\User1;DOMAIN\\User2_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "47dee55f-9d5a-4798-a61c-b4c7350774a5", + "Name": "MSMQPermAdminAllow", + "Label": "Allowed admin permissions", + "HelpText": "Permissions granted to the queue admin users, separated by semicolons. Example: _DeleteMessage;PeekMessage;ReceiveMessage_", + "DefaultValue": "FullControl", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "949b3efb-5fb7-4350-8336-1dfff67172aa", + "Name": "MSMQPermAdminDeny", + "Label": "Denied admin permissions", + "HelpText": "Denied permissions, separated by semicolons: _TakeQueueOwnership_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2be54d36-3b81-4a39-8685-adfe8d98ebbb", + "Name": "MSMQResetPermissions", + "Label": "Reset Permissions", + "HelpText": "Remove all existing permissions from the Queue if it already exists", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "54143721-075f-4270-89c1-154719ee0b3c", + "Name": "MSMQResetDomains", + "Label": "Reset Permissions Domains", + "HelpText": "This is used if Reset Permissions is set. +Example: YOURDOMAIN1;YOURDOMAIN2", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2018-03-28T07:21:22.039Z", + "OctopusVersion": "2018.2.8", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/mulesoft-cloudhub.json.human b/step-templates/mulesoft-cloudhub.json.human new file mode 100644 index 000000000..8760797fb --- /dev/null +++ b/step-templates/mulesoft-cloudhub.json.human @@ -0,0 +1,382 @@ +{ + "Id": "35450be7-a9a2-415f-82b4-6503ca148f22", + "Name": "Mulesoft - Deploy to Cloudhub", + "Description": "Deploys a Mulesoft API to Cloudhub", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "a8d60939-169c-4026-a9b3-3789b2bb0152", + "Name": "Mulesoft.Asset", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "False", + "SelectionMode": "deferred", + "PackageParameterName": "Mulesoft.Asset.File", + "Purpose": "" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Function Get-NpmExecutable +{ +\t# Define parameters + param ( + \t$NodeVersion = \"18.16.0\" + ) + + # Declare local variables + $npmDownloadUrl = \"https://nodejs.org/dist/v$NodeVersion/\" + $downloadFileName = [string]::Empty + $npmExecutable = [string]::Empty + + # Assign download url + if ($IsWindows) + { + \t$downloadFileName += \"node-v$($NodeVersion)-win-x64.zip\" + } + else + { + \t$downloadFileName += \"node-v$($NodeVersion)-linux-x64.tar.xz\" + } + +\t# Create folder for npm + if ((Test-Path -Path \"$PWD/npm\") -eq $false) + { + \tNew-Item -Path \"$PWD/npm\" -ItemType \"Directory\" + } + + # Download npm binary + Write-Host \"Downloading $(($npmDownloadUrl + $downloadFileName)) ...\" + Invoke-WebRequest -Uri ($npmDownloadUrl + $downloadFileName) -Outfile \"$PWD/$downloadFileName\" + + Write-Output \"Extracting $downloadFileName ... \" + + if ($IsWindows) + { + # Extract + Expand-Archive -Path \"$PWD/$downloadFileName\" -DestinationPath \"$PWD/npm\" + + # Find the executable + $npmExecutable = Get-ChildItem -Path \"$PWD/npm/$($downloadFileName.Replace('.zip', ''))\" | Where-Object {$_.Name -eq \"npm.cmd\"} + } + + if ($IsLinux) + { + # Extract archive + tar -xf \"$PWD/$downloadFileName\" --directory \"$PWD/npm\" + + # Find the executable + $npmExecutable = Get-ChildItem -Path \"$PWD/npm/$($downloadFileName.Replace('.tar.xz', ''))/bin\" | Where-Object {$_.Name -eq \"npm\"} + } + + # Insert location of executable into PATH environment variable so it can be called from anywhere + $env:PATH = \"$($npmExecutable.Directory)$([IO.Path]::PathSeparator)\" + $env:PATH +} + +Function Install-MulesoftCLI +{ +\t# Define parameters + param ( + \t$CLIVersion = \"4\" + ) +\t + # Run npm command to install pluguin + Write-Host \"Installing anypoint-cli-v$($CLIVersion) node module ...\" + + # Adjust install command based on operating system + if ($IsWindows) + { + \t& npm install -g \"anypoint-cli-v$($CLIVersion)\" \"2>&1\" + } + else + { + \t& npm install -g \"anypoint-cli-v$($CLIVersion)\" 2>&1 + } + +\t# Check exit code +\tif ($lastExitCode -ne 0) +\t{ +\t\t# Fail the step + \tWrite-Error \"Installation failed!\" +\t} +} + +Function Deploy-MulesoftApplication +{ +\t# Define parameters + param ( + \t$AssetFilePath, + $ApplicationName, + $RuntimeVersion, + $NumberOfWorkers, + $WorkerSize, + $Region + ) + + # Replace path seperator + if ($AssetFilePath.Contains(\"\\\")) + { + \t# Replace them with forward slash + $AssetFilePath = $AssetFilePath.Replace(\"\\\", \"/\") + } + + # Check to see if application already exists + $applicationList = (anypoint-cli-v4 runtime-mgr:cloudhub-application:list --output json | ConvertFrom-JSON) + $deployResults = $null + + if ($null -eq ($applicationList | Where-Object {$_.domain -eq $ApplicationName})) + { + \t# Deploy the application to cloud hub + Write-Host \"Deploying new application ...\" + \t$deployResults = anypoint-cli-v4 runtime-mgr:cloudhub-application:deploy $ApplicationName $AssetFilePath --output json --runtime $RuntimeVersion --workers $NumberOfWorkers --workerSize $WorkerSize --region $Region + } + else + { + \t# Update the application + Write-Host \"Updating existing application ...\" + $deployResults = anypoint-cli-v4 runtime-mgr:cloudhub-application:modify $ApplicationName $AssetFilePath --output json --runtime $RuntimeVersion --workers $NumberOfWorkers --workerSize $WorkerSize --region $Region + } + + # Display results + Write-Host \"Results:\" + $deployResults +} + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +if ($IsWindows) +{ +\t# Disable progress bar for faster installation + $ProgressPreference = 'SilentlyContinue' +} + +# Fix ANSI Color on PWSH Core issues when displaying objects +if ($PSEdition -eq \"Core\") { + $PSStyle.OutputRendering = \"PlainText\" +} + +# Get parameters +$downloadUtils = [System.Convert]::ToBoolean(\"$($OctopusParameters['Mulesoft.Download'])\") + +# Check to see if we need to download utilities +if ($downloadUtils) +{ +\tGet-NpmExecutable -NodeVersion $OctopusParameters['Mulesoft.Node.CLI.Version'] +\tInstall-MulesoftCLI -CLIVersion $OctopusParameters['Mulesoft.Anypoint.CLI.Version'] +} + +# Set environment variables +$env:ANYPOINT_CLIENT_ID = $OctopusParameters['Mulesoft.Anypoint.Client.Id'] +$env:ANYPOINT_CLIENT_SECRET = $OctopusParameters['Mulesoft.Anypoint.Client.Secret'] +$env:ANYPOINT_ORG = $OctopusParameters['Mulesoft.Anypoint.Organization.Id'] +$env:ANYPOINT_ENV = $OctopusParameters['Mulesoft.Anypoint.Environment'] + +# Set global variables +$mulesoftOrganizationId = $OctopusParameters['Mulesoft.Anypoint.Organization.Id'] +$mulesoftAssetVersionNumber = $OctopusParameters['Octopus.Action.Package[Mulesoft.Asset].PackageVersion'] +$mulesoftAssetArtifactId = $OctopusParameters['Octopus.Action.Package[Mulesoft.Asset].PackageId'] +$mulesoftApplicationName = $OctopusParameters['Mulesoft.Anypoint.Application.Name'].ToLower() +$mulesoftRuntimeVersion = $OctopusParameters['Mulesoft.Anypoint.Runtime.Version'] +$mulesoftNumberOfWorkers = $OctopusParameters['Mulesfot.Anypoint.Worker.Count'] +$mulesoftWorkerSize = $OctopusParameters['Mulesoft.Anypoint.Worker.Size'] +$mulesoftRegion = $OctopusParameters['Mulesoft.Anypoint.Region'] + +# Check optional parameters +if ([string]::IsNullOrWhitespace($mulesoftNumberOfWorkers)) +{ +\t$mulesoftNumberOfWorkers = \"1\" +} + +if ([string]::IsNullOrWhitespace($mulesoftWorkerSize)) +{ +\t$mulesoftWorkerSize = \"1\" +} + +# Display variable values +Write-Host \"================== Deploying to CloudHub with the following options ==================\" +Write-Host \"Organization Id/Group Id: $mulesoftOrganizationId\" +Write-Host \"Artifact Id: $mulesoftAssetArtifactId\" +Write-Host \"Version number: $mulesoftAssetVersionNumber\" +Write-Host \"Application Name: $mulesoftApplicationname\" +Write-Host \"Environment: $($env:ANYPOINT_ENV)\" +Write-Host \"Runtime version: $mulesoftRuntimeVersion\" +Write-Host \"Number of workers: $mulesoftNumberOfWorkers\" +Write-Host \"Worker size: $mulesoftWorkerSize\" +Write-Host \"Region: $mulesoftRegion\" +Write-Host \"=======================================================================================\" + +# Get file properties +$mulesoftApplicationFileExtension = [System.IO.Path]::GetExtension(\"$PWD/$($OctopusParameters['Octopus.Action.Package[Mulesoft.Asset].PackageFileName'])\") + +# Rename the file to the original +Rename-Item -Path \"$PWD/$($OctopusParameters['Octopus.Action.Package[Mulesoft.Asset].PackageFileName'])\" -NewName \"$($mulesoftAssetArtifactId).$($mulesoftAssetVersionNumber)$mulesoftApplicationFileExtension\" +$mulesoftApplicationFilePath = \"$PWD/$($mulesoftAssetArtifactId).$($mulesoftAssetVersionNumber)$mulesoftApplicationFileExtension\" + +# Upload asset to exchange +Deploy-MulesoftApplication -AssetFilePath $mulesoftApplicationFilePath -ApplicationName $mulesoftApplicationName -Region $mulesoftRegion -RuntimeVersion $mulesoftRuntimeVersion -NumberOfWorkers $mulesoftNumberOfWorkers -Workersize $mulesoftWorkerSize + +" + }, + "Parameters": [ + { + "Id": "e73b1ba3-df1f-490f-98d3-e564f6c1904a", + "Name": "Mulesoft.Anypoint.Organization.Id", + "Label": "Anypoint OrganizationID", + "HelpText": "The Organization ID of your Anypoint account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "680c0b8c-a219-466a-840d-a5828247d2e8", + "Name": "Mulesoft.Anypoint.Client.Id", + "Label": "Anypoint Client ID", + "HelpText": "Client ID of the Anypoint user used for deployment.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "f71f9285-acee-4461-83fc-6f281c78ee87", + "Name": "Mulesoft.Anypoint.Client.Secret", + "Label": "Anypoint Client Secret", + "HelpText": "Client Secret of the Anypoint user used for deployment.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "4a7f78d5-3e68-4d04-aa88-327903d86d5c", + "Name": "Mulesoft.Anypoint.Environment", + "Label": "Anypoint Environment", + "HelpText": "Environment name to target.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "669bd6f1-a55e-4017-bc41-7008a166727c", + "Name": "Mulesoft.Anypoint.Application.Name", + "Label": "Anypoint Application Name", + "HelpText": "Name of the deployed application.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3ad06081-5a16-4fff-8d6d-dd2e9b5ea435", + "Name": "Mulesoft.Asset.File", + "Label": "Mulesoft Asset File", + "HelpText": "Select the package containing the Mulesoft API to upload to Exchange.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "291892c6-9fb0-4fc6-a3a5-d10bcdf70338", + "Name": "Mulesoft.Anypoint.Runtime.Version", + "Label": "Runtime version", + "HelpText": "The version of the runtime to use for your application.", + "DefaultValue": "4.4.0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d636b68a-dd56-4201-b2af-bb8faecc17ea", + "Name": "Mulesfot.Anypoint.Worker.Count", + "Label": "Number of Workers", + "HelpText": "(Optional) \t +Number of workers. (This value is '1' by default)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ace7c127-6adf-4df3-8e13-e43d596490f4", + "Name": "Mulesoft.Anypoint.Worker.Size", + "Label": "Worker Size", + "HelpText": "(Optional) Size of the workers in vCores. (This value is '1' by default)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a5aa6973-a64a-479e-a080-7fa684f00d45", + "Name": "Mulesoft.Anypoint.Region", + "Label": "Region", + "HelpText": "Name of the region to deploy to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7f527d06-c329-4883-a5cf-15f233fbe319", + "Name": "Mulesoft.Anypoint.CLI.AdditionalArguments", + "Label": "Additional CLI arguments", + "HelpText": "A comma delimited list of additional arguments to add to the CLI.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ada95ba7-64d4-4b0e-a287-b2dd6fac5718", + "Name": "Mulesoft.Node.CLI.Version", + "Label": "NodeJS version", + "HelpText": "Use to specify which version of the NodeJS CLI to use when choosing the Download Node option", + "DefaultValue": "18.16.0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9b34443c-bd76-4aa4-b036-10d930d619eb", + "Name": "Mulesoft.Anypoint.CLI.Version", + "Label": "Anypoint CLI Version", + "HelpText": "Specify the version of the CLI being used.", + "DefaultValue": "4", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "aa635c7b-bedb-4000-95a0-3c399721ce2b", + "Name": "Mulesoft.Download", + "Label": "Download NodeJS and Anypoint CLI?", + "HelpText": "Tick the box to dynamically download the NodeJS and Anypoint CLI utilities to deploy.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-07-20T17:36:01.897Z", + "OctopusVersion": "2023.2.12998", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "mulesoft" +} diff --git a/step-templates/mysql-add-database-user-to-role.json.human b/step-templates/mysql-add-database-user-to-role.json.human new file mode 100644 index 000000000..fe484cb46 --- /dev/null +++ b/step-templates/mysql-add-database-user-to-role.json.human @@ -0,0 +1,391 @@ +{ + "Id": "fc7272be-779c-4ef2-8051-0e7271471328", + "Name": "MySQL - Add Database User To Role", + "Description": "Adds a database user to a role", + "Author": "twerthi", + "ActionType": "Octopus.Script", + "Version": 7, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define variables +$connectionName = \"OctopusDeploy\" + +# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-UserInRole +{ +\t# Define parameters + param ($UserHostname, + $Username, + $RoleHostName, + $RoleName) + +\t# Execute query + $grants = Invoke-SqlQuery \"SHOW GRANTS FOR '$Username'@'$UserHostName';\" -ConnectionName $connectionName + + # Loop through Grants + foreach ($grant in $grants.ItemArray) + { + # Check grant + if ($grant -eq \"GRANT ``$RoleName``@``$RoleHostName`` TO ``$Username``@``$UserHostName``\") + { + # They're in the group + return $true + } + } + + # Not found + return $false +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Import from specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Declare connection string +$connectionString = \"Server=$addMySQLServerName;Port=$addMySQLServerPort;\" + +# Customize connection string based on authentication method +switch ($mySqlAuthenticationMethod) { + \"awsiam\" { + # Region is part of the RDS endpoint, extract + $region = ($addMySQLServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $addLoginPasswordWithAddRoleRights = (aws rds generate-db-auth-token --hostname $addMySQLServerName --region $region --port $addMySQLServerPort --username $addLoginWithAddRoleRights) + + # Append remaining portion of connection string + $connectionString += \";Uid=$addLoginWithAddRoleRights;Pwd=`\"$addLoginPasswordWithAddRoleRights`\";\" + + break + } + + \"usernamepassword\" { + # Append remaining portion of connection string + $connectionString += \";Uid=$addLoginWithAddRoleRights;Pwd=`\"$addLoginPasswordWithAddRoleRights`\";\" + + break + } + + \"windowsauthentication\" { + # Append remaining portion of connection string + $connectionString += \";IntegratedSecurity=yes;Uid=$addLoginWithAddRoleRights;\" + + break + } + + \"azuremanagedidentity\" { + Write-Host \"Generating Azure Managed Identity token ...\" + $token = Invoke-RestMethod -Method GET -Uri \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://ossrdbms-aad.database.windows.net\" -Headers @{\"MetaData\" = \"true\" } + + $addLoginPasswordWithAddRoleRights = $token.access_token + + $connectionString += \";Uid=$addLoginWithAddRoleRights;Pwd=`\"$addLoginPasswordWithAddRoleRights`\";\" + + break + } + + \"gcpserviceaccount\" { + # Define header + $header = @{ \"Metadata-Flavor\" = \"Google\" } + + # Retrieve service accounts + $serviceAccounts = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\" -Headers $header + + # Results returned in plain text format, get into array and remove empty entries + $serviceAccounts = $serviceAccounts.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + + # Retreive the specific service account assigned to the VM + $serviceAccount = $serviceAccounts | Where-Object { $_.Contains(\"iam.gserviceaccount.com\") } + + if ([string]::IsNullOrWhiteSpace(($addLoginWithAddRoleRights))) { + $addLoginWithAddRoleRights = $serviceAccount.SubString(0, $serviceAccount.IndexOf(\".gserviceaccount.com\")) + } + + Write-Host \"Generating GCP IAM token ...\" + # Retrieve token for account + $token = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/$serviceAccount/token\" -Headers $header + + $addLoginPasswordWithAddRoleRights = $token.access_token + $connectionString += \";Uid=$addLoginWithAddRoleRights;Pwd=`\"$addLoginPasswordWithAddRoleRights`\";\" + + break + } +} + + +# Import the module +Import-Module -Name $PowerShellModuleName + +try +{ + if ($addUseSSL -eq \"True\") + { + \t# Append to connection string + $connectionString += \"SslMode=Required;\" + } + else + { + \t# Disable SSL + $connectionString += \"SslMode=none;\" + } + + if (![string]::IsNullOrWhitespace($mysqlAdditionalParameters)) + { + foreach ($parameter in $mysqlAdditionalParameters.Split(\",\")) + { + # Check for delimiter + if (!$connectionString.EndsWith(\";\") -and !$parameter.StartsWith(\";\")) + { + # Append delimeter + $connectionString +=\";\" + } + + $connectionString += $parameter.Trim() + } + } + + + Open-MySqlConnection -ConnectionString $connectionString -ConnectionName $connectionName + + + # See if database exists + $userInRole = Get-UserInRole -UserHostname $addUserHostname -Username $addUsername -RoleHostName $addRoleHostName -RoleName $addRoleName + + if ($userInRole -eq $false) + { + # Create database + Write-Output \"Adding user $addUsername@$addUserHostName to role $addRoleName@$addRoleHostName ...\" + $executionResults = Invoke-SqlUpdate \"GRANT '$addRoleName'@'$addRoleHostName' TO '$addUsername'@'$addUserHostName';\" -ConnectionName $connectionName + + # See if it was created + $userInRole = Get-UserInRole -UserHostname $addUserHostname -Username $addUsername -RoleHostName $addRoleHostName -RoleName $addRoleName + + # Check array + if ($userInRole -eq $true) + { + # Success + Write-Output \"$addUserName@$addUserHostName added to $addRoleName@$addRoleHostName successfully!\" + } + else + { + # Failed + Write-Error \"Failure adding $addUserName@$addUserHostName to $addRoleName@$addRoleHostName!\" + } + } + else + { + \t# Display message + Write-Output \"User $addUsername@$addUserHostName is already in role $addRoleName@$addRoleHostName\" + } +} +finally +{ +\t# Close connection if open + if ((Test-SqlConnection -ConnectionName $connectionName) -eq $true) + { + \tClose-SqlConnection -ConnectionName $connectionName + } +} +" + }, + "Parameters": [ + { + "Id": "80bf18c2-a2ea-4499-a33d-eea226b9727a", + "Name": "addMySQLServerName", + "Label": "MySQL Server name", + "HelpText": "Name of the MySQL database server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2eba54c0-293c-4009-aef0-077b91b568b4", + "Name": "addMySQLServerPort", + "Label": "Port", + "HelpText": "Port the MySQL listens on.", + "DefaultValue": "3306", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c8732b96-bedd-4e4a-be2d-941315e1bddd", + "Name": "addLoginWithAddRoleRights", + "Label": "Login name", + "HelpText": "Login name of a user that can add roles to other users.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4b669f8a-7309-4aed-81ff-cd5b3ebf5342", + "Name": "addLoginPasswordWithAddRoleRights", + "Label": "Login password", + "HelpText": "Password for the login account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "7db3f187-ca97-42cc-a10e-fa5cc3f1382c", + "Name": "addUsername", + "Label": "User name", + "HelpText": "Name of the user to add the role to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "052563dd-c831-4f09-8d59-1e54bc30afa0", + "Name": "addUserHostname", + "Label": "User Hostname", + "HelpText": "Hostname for the user account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "885c5057-f859-4bed-a765-03c568f9e9a2", + "Name": "addRoleName", + "Label": "Role name", + "HelpText": "Name of the role to add to the user.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "302ac0ce-3128-45bc-8006-ac92a59b7141", + "Name": "addRoleHostName", + "Label": "Role hostname", + "HelpText": "Hostname of the role.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a54387ea-9e98-45a2-93e9-214ff2fcf67f", + "Name": "addUseSSL", + "Label": "Use SSL", + "HelpText": "Check this box to force the use of SSL.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "89de7391-de67-46cf-b19c-a7b219280dc9", + "Name": "mySqlAuthenticationMethod", + "Label": "MySQL Authentication Method", + "HelpText": "Authentication method used to connect with MySQL. Options include standard Username/Password, Windows Authentication, [AWS IAM Authentication](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html), [Azure Managed Identity](https://docs.microsoft.com/en-us/azure/mysql/single-server/how-to-connect-with-managed-identity), and [Google Cloud IAM for Cloud SQL ](https://cloud.google.com/sql/docs/mysql/iam-overview)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS IAM +usernamepassword|Username/password +windowsauthentication|Windows Authentication +azuremanagedidentity|Azure Managed Identity +gcpserviceaccount|GCP IAM" + } + }, + { + "Id": "a97452b5-f7ef-4b72-ab2f-b440cda16343", + "Name": "mysqlAdditionalParameters", + "Label": "Additional connection string parameters", + "HelpText": "A comma-delimited list of additional parameters to add to the connection string. ex `AllowPublicKeyRetrieval=True`\"", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2024-03-22T16:19:47.074Z", + "OctopusVersion": "2024.1.12087", + "Type": "ActionTemplate" + }, + "Category": "mysql" +} diff --git a/step-templates/mysql-backup-database.json.human b/step-templates/mysql-backup-database.json.human new file mode 100644 index 000000000..33249a2a0 --- /dev/null +++ b/step-templates/mysql-backup-database.json.human @@ -0,0 +1,245 @@ +{ + "Id": "4fa6d062-d4da-4a02-849e-dec804554453", + "Name": "MySQL - Backup Database", + "Description": "Backs up a MySQL database on a windows instance hosting MySQL. ", + "ActionType": "Octopus.Script", + "Version": 1, + "Author": "adamoctoclose", + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-DatabaseExists +{ +\t# Define parameters + param ($DatabaseName) + +\t# Execute query + return Invoke-SqlQuery \"SHOW DATABASES LIKE '$DatabaseName';\" +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Import from temp location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Import the module +Import-Module -Name $PowerShellModuleName + + +# Create credential object for the connection +#$SecurePassword = ConvertTo-SecureString $BackupMySQL_Password -AsPlainText -Force +#$ServerCredential = New-Object System.Management.Automation.PSCredential ($BackupMySQL_Username, $BackupMySQL_Password) + +try +{ + + +\t# Connect to MySQL + $connectionString = \"Server=$BackupMySQL_ServerName;Port=$BackupMySQL_Port;Uid=$BackupMySQL_Username;Pwd=$BackupMySQL_Password;\" + + if ($BackupMySQL_UseSSL -eq \"True\") + { +\t\t# Append to connection string + $connectionString += \"SslMode=Required;\" + } + else + { + \t# Disable ssl + $connectionString += \"SslMode=none;\" + } + + Open-MySqlConnection -ConnectionString $connectionString + + # See if database exists + $databaseExists = Get-DatabaseExists -DatabaseName $BackupMySQL_DatabaseName + + if ($databaseExists.ItemArray.Count -eq 0) + { + # Display message + Write-Error \"Database $BackupMySQL_DatabaseName doesn't exist.\" + + } + else + { + + cd $BackupMySQL_MySQLPath + + $backupname = '{0}{1}-{2}.sql' -f ($BackupMySQL_BackupDirectory, $BackupMySQL_DatabaseName ,(Get-Date -Format FileDatetime)) + + .\\mysqldump.exe --databases $BackupMySQL_DatabaseName > $backupname + + # Success + Write-Output \"$BackupMySQL_DatabaseName was backed up!\" + } +} +finally +{ + Close-SqlConnection +} +" +}, +"Parameters": [ + { + "Id": "7030ad54-4e10-4854-998a-ca91e3b490a3", + "Name": "BackupMySQL_ServerName", + "Label": "Server", + "HelpText": "Name or IP of the MySQL server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "07dbf251-a982-4cc5-b6c1-dc0af50c519c", + "Name": "BackupMySQL_Username", + "Label": "Username", + "HelpText": "\tUsername with rights to dump database", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ac42a150-b7e7-4414-9dec-f18e105eaedf", + "Name": "BackupMySQL_Password", + "Label": "Password", + "HelpText": "Password for the user account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "62ffb949-c5db-4151-bb29-47a37586bda5", + "Name": "BackupMySQL_DatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to backup", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c54832b1-571f-4475-9159-6a7342a67982", + "Name": "BackupMySQL_Port", + "Label": "Port", + "HelpText": "Port number for the MySQL server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a33dc26c-15b3-4b5e-a62f-7a5e97576bcc", + "Name": "BackupMySQL_UseSSL", + "Label": "Use SSL", + "HelpText": "Check this box to force the use of SSL.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "fc2c1a2f-43ba-4878-8e74-b88dad96c5d2", + "Name": "BackupMySQL_MySQLPath", + "Label": "MySQL Path", + "HelpText": "Path to binaries e.g. C:\\Program Files\\MySQL\\MySQL Server 5.6\\bin +If binaries don't exist on the target machine the backup will fail.", + "DefaultValue": "C:\\Program Files\\MySQL\\MySQL Server 5.6\\bin", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "39063215-b069-4e51-b202-65060db9fd20", + "Name": "BackupMySQL_BackupDirectory", + "Label": "Backup Directory", + "HelpText": "Location to store backup file", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } +], + "$Meta": { + "ExportedAt": "2020-08-11T15:11:43.135Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "adamoctoclose", + "Category": "mysql" +} diff --git a/step-templates/mysql-create-database.json.human b/step-templates/mysql-create-database.json.human new file mode 100644 index 000000000..3e7e5fcc1 --- /dev/null +++ b/step-templates/mysql-create-database.json.human @@ -0,0 +1,335 @@ +{ + "Id": "4a222ac3-ff4b-4328-8778-1c44eebdedde", + "Name": "MySQL - Create Database If Not Exists", + "Description": "Creates a MySQL database if it doesn't already exist. This template is also compatible with MariaDB. + +Note - this template will install the Nuget package provider if it's not already present.", + "ActionType": "Octopus.Script", + "Author": "twerthi", + "Version": 9, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define variables +$connectionName = \"OctopusDeploy\" + +# Define functions +function Get-ModuleInstalled { + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) { + # It is installed + return $true + } + else { + # Module not installed + return $false + } +} + +function Install-PowerShellModule { + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + + # Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + # Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + + # Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-DatabaseExists { + # Define parameters + param ($DatabaseName) + + # Execute query + return Invoke-SqlQuery \"SHOW DATABASES LIKE '$DatabaseName';\" -ConnectionName $connectionName +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) { + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) { + # Import from temp location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Declare connection string +$connectionString = \"Server=$createMySQLServerName;Port=$createPort;\" + +# Customize connection string based on authentication method +switch ($mySqlAuthenticationMethod) { + \"awsiam\" { + # Region is part of the RDS endpoint, extract + $region = ($createMySQLServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $createUserPassword = (aws rds generate-db-auth-token --hostname $createMySQLServerName --region $region --port $createPort --username $createUsername) + + # Append remaining portion of connection string + $connectionString += \";Uid=$createUsername;Pwd=`\"$createUserPassword`\";\" + + break + } + + \"usernamepassword\" { + # Append remaining portion of connection string + $connectionString += \";Uid=$createUsername;Pwd=`\"$createUserPassword`\";\" + + break + } + + \"windowsauthentication\" { + # Append remaining portion of connection string + $connectionString += \";IntegratedSecurity=yes;Uid=$createUsername;\" + + break + } + + \"azuremanagedidentity\" { + Write-Host \"Generating Azure Managed Identity token ...\" + $token = Invoke-RestMethod -Method GET -Uri \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://ossrdbms-aad.database.windows.net\" -Headers @{\"MetaData\" = \"true\" } + + $createUserPassword = $token.access_token + + $connectionString += \";Uid=$createUsername;Pwd=`\"$createUserPassword`\";\" + + break + } + + \"gcpserviceaccount\" { + # Define header + $header = @{ \"Metadata-Flavor\" = \"Google\" } + + # Retrieve service accounts + $serviceAccounts = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\" -Headers $header + + # Results returned in plain text format, get into array and remove empty entries + $serviceAccounts = $serviceAccounts.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + + # Retreive the specific service account assigned to the VM + $serviceAccount = $serviceAccounts | Where-Object { $_.Contains(\"iam.gserviceaccount.com\") } + + if ([string]::IsNullOrWhiteSpace(($createUsername))) { + $createUsername = $serviceAccount.SubString(0, $serviceAccount.IndexOf(\".gserviceaccount.com\")) + } + + Write-Host \"Generating GCP IAM token ...\" + # Retrieve token for account + $token = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/$serviceAccount/token\" -Headers $header + + $createUserPassword = $token.access_token + $connectionString += \";Uid=$createUsername;Pwd=`\"$createUserPassword`\";\" + + break + } +} + + +# Import the module +Import-Module -Name $PowerShellModuleName + + +try { + # Connect to MySQL + $connectionString = \"Server=$createMySQLServerName;Port=$createPort;Uid=$createUsername;Pwd=$createUserPassword;\" + if ($createUseSSL -eq \"True\") { + # Append to connection string + $connectionString += \"SslMode=Required;\" + } + else { + # Disable ssl + $connectionString += \"SslMode=none;\" + } + + if (![string]::IsNullOrWhitespace($mysqlAdditionalParameters)) + { + foreach ($parameter in $mysqlAdditionalParameters.Split(\",\")) + { + # Check for delimiter + if (!$connectionString.EndsWith(\";\") -and !$parameter.StartsWith(\";\")) + { + # Append delimeter + $connectionString +=\";\" + } + + $connectionString += $parameter.Trim() + } + } + + Open-MySqlConnection -ConnectionString $connectionString -ConnectionName $connectionName + + # See if database exists + $databaseExists = Get-DatabaseExists -DatabaseName $createDatabaseName + + if ($databaseExists.ItemArray.Count -eq 0) { + # Create database + Write-Output \"Creating database $createDatabaseName ...\" + $executionResult = Invoke-SqlUpdate \"CREATE DATABASE $createDatabaseName;\" -ConnectionName $connectionName + + # Check result + if ($executionResult -ne 1) { + # Commit transaction + Write-Error \"Create schema failed.\" + } + else { + # See if it was created + $databaseExists = Get-DatabaseExists -DatabaseName $createDatabaseName + + # Check array + if ($databaseExists.ItemArray.Count -eq 1) { + # Success + Write-Output \"$createDatabaseName created successfully!\" + } + else { + # Failed + Write-Error \"$createDatabaseName was not created!\" + } + } + } + else { + # Display message + Write-Output \"Database $createDatabaseName already exists.\" + } +} +finally { +\t# Close connection if open + if ((Test-SqlConnection -ConnectionName $connectionName) -eq $true) + { + \tClose-SqlConnection -ConnectionName $connectionName + } +}" + }, + "Parameters": [ + { + "Id": "987155d9-f852-415d-b89d-cd74618d14bb", + "Name": "createMySQLServerName", + "Label": "Server", + "HelpText": "Hostname (or IP) of the MySQL database server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "aff09ee3-b7c4-4a88-a4e3-70ad0eee04e9", + "Name": "createUsername", + "Label": "Username", + "HelpText": "Username to use for the connection", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a08b0359-560f-49cb-bc71-d759d49a06fc", + "Name": "createUserPassword", + "Label": "Password", + "HelpText": "Password for the user account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e17c8ac9-6b4a-4321-975b-452e2015bc4a", + "Name": "createDatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to create", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a1d735f0-3ec2-45a1-af43-d0af7bf13f68", + "Name": "createPort", + "Label": "Port", + "HelpText": "Port for the database instance.", + "DefaultValue": "3306", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ddd1ca9c-04fe-4a43-84f7-dc8d52adb063", + "Name": "createUseSSL", + "Label": "Use SSL", + "HelpText": "Check this box to force the use of SSL.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "4090f423-b47f-4bac-ab38-ab45c92e0ab5", + "Name": "mySqlAuthenticationMethod", + "Label": "MySQL Authentication Method", + "HelpText": "Authentication method used to connect with MySQL. Options include standard Username/Password, Windows Authentication, [AWS IAM Authentication](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html), [Azure Managed Identity](https://docs.microsoft.com/en-us/azure/mysql/single-server/how-to-connect-with-managed-identity), and [Google Cloud IAM for Cloud SQL ](https://cloud.google.com/sql/docs/mysql/iam-overview)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS IAM +usernamepassword|Username/password +windowsauthentication|Windows Authentication +azuremanagedidentity|Azure Managed Identity +gcpserviceaccount|GCP IAM" + } + }, + { + "Id": "273612dc-5c75-4591-9da9-dc8e40b7bf39", + "Name": "mysqlAdditionalParameters", + "Label": "Additional connection string parameters", + "HelpText": "A comma-delimited list of additional parameters to add to the connection string. ex `AllowPublicKeyRetrieval=True`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2024-03-20T19:31:22.096Z", + "OctopusVersion": "2024.2.2075", + "Type": "ActionTemplate" + }, + "Category": "mysql" +} diff --git a/step-templates/mysql-create-user.json.human b/step-templates/mysql-create-user.json.human new file mode 100644 index 000000000..09177573b --- /dev/null +++ b/step-templates/mysql-create-user.json.human @@ -0,0 +1,365 @@ +{ + "Id": "d5e87b36-da2b-4771-9394-0dbdc9587dd4", + "Name": "MySQL - Create User If Not Exists", + "Description": "Creates a new user account on a MySQL database server", + "ActionType": "Octopus.Script", + "Version": 7, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define variables +$connectionName = \"OctopusDeploy\" + +# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-UserExists +{ +\t# Define parameters + param ($Hostname, + $Username) + +\t# Execute query + return Invoke-SqlQuery \"SELECT * FROM mysql.user WHERE Host = '$Hostname' AND User = '$Username';\" -ConnectionName $connectionName +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Declare connection string +$connectionString = \"Server=$createMySQLServerName;Port=$createPort;\" +$connectionString = \"Server=$createMySQLServerName;Port=$createMySQLServerPort;Uid=$createLoginWithAddUserRights;Pwd=$createLoginPasswordWithAddUserRights;\" + + +# Customize connection string based on authentication method +switch ($mySqlAuthenticationMethod) { + \"awsiam\" { + # Region is part of the RDS endpoint, extract + $region = ($createMySQLServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $createLoginPasswordWithAddUserRights = (aws rds generate-db-auth-token --hostname $createMySQLServerName --region $region --port $createPort --username $createLoginWithAddUserRights) + + # Append remaining portion of connection string + $connectionString += \";Uid=$createLoginWithAddUserRights;Pwd=`\"$createLoginPasswordWithAddUserRights`\";\" + + break + } + + \"usernamepassword\" { + # Append remaining portion of connection string + $connectionString += \";Uid=$createLoginWithAddUserRights;Pwd=`\"$createLoginPasswordWithAddUserRights`\";\" + + break + } + + \"windowsauthentication\" { + # Append remaining portion of connection string + $connectionString += \";IntegratedSecurity=yes;Uid=$createLoginWithAddUserRights;\" + + break + } + + \"azuremanagedidentity\" { + Write-Host \"Generating Azure Managed Identity token ...\" + $token = Invoke-RestMethod -Method GET -Uri \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://ossrdbms-aad.database.windows.net\" -Headers @{\"MetaData\" = \"true\" } + + $createLoginPasswordWithAddUserRights = $token.access_token + + $connectionString += \";Uid=$createLoginWithAddUserRights;Pwd=`\"$createLoginPasswordWithAddUserRights`\";\" + + break + } + + \"gcpserviceaccount\" { + # Define header + $header = @{ \"Metadata-Flavor\" = \"Google\" } + + # Retrieve service accounts + $serviceAccounts = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\" -Headers $header + + # Results returned in plain text format, get into array and remove empty entries + $serviceAccounts = $serviceAccounts.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + + # Retreive the specific service account assigned to the VM + $serviceAccount = $serviceAccounts | Where-Object { $_.Contains(\"iam.gserviceaccount.com\") } + + if ([string]::IsNullOrWhiteSpace(($createLoginWithAddUserRights))) + { + $createLoginWithAddUserRights = $serviceAccount.SubString(0, $serviceAccount.IndexOf(\".gserviceaccount.com\")) + } + + Write-Host \"Generating GCP IAM token ...\" + # Retrieve token for account + $token = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/$serviceAccount/token\" -Headers $header + + $createLoginPasswordWithAddUserRights = $token.access_token + $connectionString += \";Uid=$createLoginWithAddUserRights;Pwd=`\"$createLoginPasswordWithAddUserRights`\";\" + + break + } +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +try +{ +\t# Connect to MySQL + if ($createUseSSL -eq \"True\") + { + \t# Append to connection string + $connectionString += \"SslMode=Required;\" + } + else + { + \t# Disable ssl + $connectionString += \"SslMode=none;\" + } + + if (![string]::IsNullOrWhitespace($mysqlAdditionalParameters)) + { + foreach ($parameter in $mysqlAdditionalParameters.Split(\",\")) + { + # Check for delimiter + if (!$connectionString.EndsWith(\";\") -and !$parameter.StartsWith(\";\")) + { + # Append delimeter + $connectionString +=\";\" + } + + $connectionString += $parameter.Trim() + } + } + +\tOpen-MySqlConnection -ConnectionString $connectionString -ConnectionName $connectionName + + # See if database exists + $userExists = Get-UserExists -Hostname $createUserHostname -Username $createNewUsername + + if ($userExists -eq $null) + { + # Create database + Write-Output \"Creating user $createNewUsername ...\" + $executionResults = Invoke-SqlUpdate \"CREATE USER '$createNewUsername'@'$createUserHostname' IDENTIFIED BY '$createNewUserPassword';\" -ConnectionName $connectionName + + # See if it was created + $userExists = Get-UserExists -Hostname $createUserHostname -Username $createNewUsername + + # Check array + if ($userExists -ne $null) + { + # Success + Write-Output \"$createNewUsername created successfully!\" + } + else + { + # Failed + Write-Error \"$createNewUsername was not created!\" + } + } + else + { + \t# Display message + Write-Output \"User $createNewUsername on $createUserHostname already exists.\" + } +} +finally +{ +\t# Close connection if open + if ((Test-SqlConnection -ConnectionName $connectionName) -eq $true) + { + \tClose-SqlConnection -ConnectionName $connectionName + } +} +" + }, + "Parameters": [ + { + "Id": "0fb5e63d-528c-4e7e-841d-6d4bd1ef47a4", + "Name": "createMySQLServerName", + "Label": "MySQL Server", + "HelpText": "Host name of the MySQL server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "47364cd4-6c31-43f6-9585-cc97aca28d3c", + "Name": "createMySQLServerPort", + "Label": "Port", + "HelpText": "Port number the MySQL server listens on.", + "DefaultValue": "3306", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cb9b74a5-f444-4b8c-b353-0eebd990e0a3", + "Name": "createLoginWithAddUserRights", + "Label": "Login name", + "HelpText": "Login name of a user with rights to create user accounts.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "699b3521-06dc-4e66-a20e-adce0fddab38", + "Name": "createLoginPasswordWithAddUserRights", + "Label": "Login Password", + "HelpText": "Password Login name.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "5b5ec614-a799-407d-870a-d3098794e049", + "Name": "createNewUsername", + "Label": "New user name", + "HelpText": "Name of the new user account to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8fe190a3-b3d5-4d4b-84d2-a4fe5bf2c99f", + "Name": "createNewUserPassword", + "Label": "New user password", + "HelpText": "Password for the new user account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c7fd6115-ec4d-455b-b84b-d6eb19228140", + "Name": "createUserHostname", + "Label": "New user host name", + "HelpText": "Host name that the new user account is allowed to login from. Enter % to allow the account to connect from anywhere.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "26090b1e-7f56-4f49-990d-5d8022417f13", + "Name": "createUseSSL", + "Label": "Use SSL", + "HelpText": "Check this box to force the use of SSL.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "f6ad5f4c-509a-4533-82d4-b9fad04988f3", + "Name": "mySqlAuthenticationMethod", + "Label": "MySQL Authentication Method", + "HelpText": "Authentication method used to connect with MySQL. Options include standard Username/Password, Windows Authentication, [AWS IAM Authentication](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html), [Azure Managed Identity](https://docs.microsoft.com/en-us/azure/mysql/single-server/how-to-connect-with-managed-identity), and [Google Cloud IAM for Cloud SQL ](https://cloud.google.com/sql/docs/mysql/iam-overview)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS IAM +usernamepassword|Username/password +windowsauthentication|Windows Authentication +azuremanagedidentity|Azure Managed Identity +gcpserviceaccount|GCP IAM" + } + }, + { + "Id": "5a8ca84d-ad02-46ac-b8f2-f19191fe9cc5", + "Name": "mysqlAdditionalParameters", + "Label": "Additional connection string parameters", + "HelpText": "A comma-delimited list of additional parameters to add to the connection string. ex `AllowPublicKeyRetrieval=True`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "twerthi", + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2024-03-22T16:22:09.124Z", + "OctopusVersion": "2024.1.12087", + "Type": "ActionTemplate" + }, + "Category": "mysql" +} diff --git a/step-templates/mysql-execute-command.json.human b/step-templates/mysql-execute-command.json.human new file mode 100644 index 000000000..2f310c5ae --- /dev/null +++ b/step-templates/mysql-execute-command.json.human @@ -0,0 +1,284 @@ +{ + "Id": "f035cbb1-3400-4808-bfde-d5a8aaf36cf1", + "Name": "MySQL - Execute Command", + "Description": "Enables ad-hoc command execution on a MySQL server", + "ActionType": "Octopus.Script", + "Version": 3, + "Author": "coryreid", + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled { + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) { + # It is installed + return $true + } + else { + # Module not installed + return $false + } +} + +function Install-PowerShellModule { + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + + # Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + # Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + + # Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) { + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) { + # Use specific version + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +# Get whether trust certificate is necessary +$mySqlTrustSSL = [System.Convert]::ToBoolean(\"$mySqlTrustSSL\") + +try { + # Declare initial connection string + $connectionString = \"Server=$mySqlServerName;Port=$mySqlServerPort;Database=$mySqlDatabaseName;\" + + # Check to see if we need to trust the ssl cert + if ($mySqlTrustSSL -eq $true) { + # Append SSL connection string components + $connectionString += \"SslMode=Required;\" + } + + # Update the connection string based on authentication method + switch ($mySqlAuthenticationMethod) { + \"azuremanagedidentity\" { + # Get login token + Write-Host \"Generating Azure Managed Identity token ...\" + $token = Invoke-RestMethod -Method GET -Uri \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://ossrdbms-aad.database.windows.net\" -Headers @{\"MetaData\" = \"true\" } + + # Append remaining portion of connection string + $connectionString += \";Uid=$mySqlUsername;Pwd=`\"$($token.access_token)`\";\" + + break + } + \"awsiam\" { + # Region is part of the RDS endpoint, extract + $region = ($mySqlServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $mySqlPassword = (aws rds generate-db-auth-token --hostname $mySqlServerName --region $region --port $mySqlServerPort --username $mySqlUsername) + + # Append remaining portion of connection string + $connectionString += \";Uid=$mySqlUsername;Pwd=`\"$mySqlPassword`\";\" + + break + } + \"gcpserviceaccount\" { + # Define header + $header = @{ \"Metadata-Flavor\" = \"Google\" } + + # Retrieve service accounts + $serviceAccounts = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\" -Headers $header + + # Results returned in plain text format, get into array and remove empty entries + $serviceAccounts = $serviceAccounts.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + + # Retreive the specific service account assigned to the VM + $serviceAccount = $serviceAccounts | Where-Object { $_.Contains(\"iam.gserviceaccount.com\") } + + Write-Host \"Generating GCP IAM token ...\" + # Retrieve token for account + $token = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/$serviceAccount/token\" -Headers $header + + # Check to see if there was a username provided + if ([string]::IsNullOrWhitespace($mySqlUsername)) { + # Use the service account name, but strip off the .gserviceaccount.com part + $mySqlUsername = $serviceAccount.SubString(0, $serviceAccount.IndexOf(\".gserviceaccount.com\")) + } + + # Append remaining portion of connection string + $connectionString += \";Uid=$mySqlUsername;Pwd=`\"$($token.access_token)`\";\" + + break + } + \"usernamepassword\" { + # Append remaining portion of connection string + $connectionString += \";Uid=$mySqlUsername;Pwd=`\"$mySqlPassword`\";\" + + break + } + + \"windowsauthentication\" { + # Append remaining portion of connection string + $connectionString += \";Integrated Security=True;\" + } + } + + # Open connection + Open-MySqlConnection -ConnectionString $connectionString + + # Execute the statement + $executionResult = Invoke-SqlUpdate -Query \"$mySqlCommand\" -CommandTimeout $mySqlCommandTimeout + + # Display the result + Get-SqlMessage +} +finally { + Close-SqlConnection +} + + +" + }, + "Parameters": [ + { + "Id": "c1126889-66f0-4bcf-8fca-8164097e73ee", + "Name": "mySqlServerName", + "Label": "Server", + "HelpText": "Hostname (or IP) of the MySQL database server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "28f4f93c-c6a0-40fd-a0db-5d6830c9779a", + "Name": "mySqlAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the MySQL server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +azuremanagedidentity|Azure Managed Identity +gcpserviceaccount|GCP Service Account +usernamepassword|Username\\Password +windowsauthentication|Windows Authentication" + } + }, + { + "Id": "a8563d9f-6ce3-4965-9f04-9fab03ec9385", + "Name": "mySqlUsername", + "Label": "Username", + "HelpText": "Username to use for the connection", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "19693759-b161-454f-86cc-535788da366e", + "Name": "mySqlPassword", + "Label": "Password", + "HelpText": "Password for the user account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "a5e1378b-990a-4ffe-a675-16d6c5f6b802", + "Name": "mySqlDatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to execute against.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b2206d1e-cf1e-4305-b58a-0abf835ccc9a", + "Name": "mySqlServerPort", + "Label": "Server Port", + "HelpText": "Port for the database instance.", + "DefaultValue": "3306", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fda16aec-ebcc-47f2-9294-7b37b985209c", + "Name": "mySqlTrustSSL", + "Label": "Trust SSL Certificate", + "HelpText": "Force trusting an SSL Certificate.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "786d615c-e7cd-4f1d-adb0-7787bd26c476", + "Name": "mySqlCommandTimeout", + "Label": "Command Timeout", + "HelpText": "Timeout value (in seconds) for SQL commands.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0b10867f-b061-4cab-8044-175e6469e1e1", + "Name": "mySqlCommand", + "Label": "Command", + "HelpText": "SQL statement(s) to execute.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2022-06-22T18:37:20.847Z", + "OctopusVersion": "2022.3.1455", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "coryreid", + "Category": "mysql" +} diff --git a/step-templates/netscaler-adc-enable-or-disable-service.json.human b/step-templates/netscaler-adc-enable-or-disable-service.json.human new file mode 100644 index 000000000..1022161e0 --- /dev/null +++ b/step-templates/netscaler-adc-enable-or-disable-service.json.human @@ -0,0 +1,375 @@ +{ + "Id": "1bd58988-3e56-4cfb-8f87-3769024fcdc3", + "Name": "NetScaler ADC - Enable or Disable Service", + "Description": "Enables or disables a load balancing service i Citrix NetScaler ADC. For documentation, see https://github.com/jnus/NetScalerPSLib +", + "ActionType": "Octopus.Script", + "Version": 21, + "Properties": { + "Octopus.Action.Script.ScriptBody": "param(\r + [string]$NSAddress,\r + [string]$ServiceName,\r + [string]$Action,\r + [string]$NSUserName,\r + [string]$NSPassword,\r + [string]$NSProtocol\r +)\r +\r +\r +$ErrorActionPreference = \"Stop\"\r +\r +function Connect-NSAppliance {\r + <#\r + .SYNOPSIS\r + Connect to NetScaler Appliance\r + .DESCRIPTION\r + Connect to NetScaler Appliance. A custom web request session object will be returned\r + .PARAMETER NSAddress\r + NetScaler Management IP address\r + .PARAMETER NSName\r + NetScaler DNS name or FQDN\r + .PARAMETER NSUserName\r + UserName to access the NetScaler appliance\r + .PARAMETER NSPassword\r + Password to access the NetScaler appliance\r + .PARAMETER Timeout\r + Timeout in seconds to for the token of the connection to the NetScaler appliance. 900 is the default admin configured value.\r + .EXAMPLE\r + $Session = Connect-NSAppliance -NSAddress 10.108.151.1\r + .EXAMPLE\r + $Session = Connect-NSAppliance -NSName mynetscaler.mydomain.com\r + .OUTPUTS\r + CustomPSObject\r + .NOTES\r + Copyright (c) Citrix Systems, Inc. All rights reserved.\r + #>\r + [CmdletBinding()]\r + param (\r + [Parameter(Mandatory=$true,ParameterSetName='Address')] [string]$NSAddress,\r + [Parameter(Mandatory=$true,ParameterSetName='Name')] [string]$NSName,\r + [Parameter(Mandatory=$false)] [string]$NSUserName=\"nsroot\",\r + [Parameter(Mandatory=$false)] [string]$NSPassword=\"nsroot\",\r + [Parameter(Mandatory=$false)] [int]$Timeout=900\r + )\r + Write-Verbose \"$($MyInvocation.MyCommand): Enter\"\r +\r + if ($PSCmdlet.ParameterSetName -eq 'Address') {\r + Write-Verbose \"Validating IP Address\"\r + $IPAddressObj = New-Object -TypeName System.Net.IPAddress -ArgumentList 0\r + if (-not [System.Net.IPAddress]::TryParse($NSAddress,[ref]$IPAddressObj)) {\r + throw \"'$NSAddress' is an invalid IP address\"\r + }\r + $nsEndpoint = $NSAddress\r + } elseif ($PSCmdlet.ParameterSetName -eq 'Name') {\r + $nsEndpoint = $NSName\r + }\r +\r +\r + $login = @{\"login\" = @{\"username\"=$NSUserName;\"password\"=$NSPassword;\"timeout\"=$Timeout}}\r + $loginJson = ConvertTo-Json $login\r +\r + try {\r + Write-Verbose \"Calling Invoke-RestMethod for login\"\r + $response = Invoke-RestMethod -Uri \"$($Script:NSURLProtocol)://$nsEndpoint/nitro/v1/config/login\" -Body $loginJson -Method POST -SessionVariable saveSession -ContentType application/json\r +\r + if ($response.severity -eq \"ERROR\") {\r + throw \"Error. See response: `n$($response | fl * | Out-String)\"\r + } else {\r + Write-Verbose \"Response:`n$(ConvertTo-Json $response | Out-String)\"\r + }\r + }\r + catch [Exception] {\r + throw $_\r + }\r +\r +\r + $nsSession = New-Object -TypeName PSObject\r + $nsSession | Add-Member -NotePropertyName Endpoint -NotePropertyValue $nsEndpoint -TypeName String\r + $nsSession | Add-Member -NotePropertyName WebSession -NotePropertyValue $saveSession -TypeName Microsoft.PowerShell.Commands.WebRequestSession\r +\r + Write-Verbose \"$($MyInvocation.MyCommand): Exit\"\r +\r + return $nsSession\r +}\r +\r +function Set-NSMgmtProtocol {\r + <#\r + .SYNOPSIS\r + Set $Script:NSURLProtocol, this will be used for all subsequent invocation of NITRO APIs\r + .DESCRIPTION\r + Set $Script:NSURLProtocol\r + .PARAMETER Protocol\r + Protocol, acceptable values are \"http\" and \"https\"\r + .EXAMPLE\r + Set-Protocol -Protocol https\r + .NOTES\r + Copyright (c) Citrix Systems, Inc. All rights reserved.\r + #>\r + [CmdletBinding()]\r + param (\r + [Parameter(Mandatory=$true)] [ValidateSet(\"http\",\"https\")] [string]$Protocol\r + )\r +\r + Write-Verbose \"$($MyInvocation.MyCommand): Enter\"\r +\r + $Script:NSURLProtocol = $Protocol\r +\r + Write-Verbose \"$($MyInvocation.MyCommand): Exit\"\r +}\r +\r +function Invoke-NSNitroRestApi {\r + <#\r + .SYNOPSIS\r + Invoke NetScaler NITRO REST API\r + .DESCRIPTION\r + Invoke NetScaler NITRO REST API\r + .PARAMETER NSSession\r + An existing custom NetScaler Web Request Session object returned by Connect-NSAppliance\r + .PARAMETER OperationMethod\r + Specifies the method used for the web request\r + .PARAMETER ResourceType\r + Type of the NS appliance resource\r + .PARAMETER ResourceName\r + Name of the NS appliance resource, optional\r + .PARAMETER Action\r + Name of the action to perform on the NS appliance resource\r + .PARAMETER Payload\r + Payload of the web request, in hashtable format\r + .PARAMETER GetWarning\r + Switch parameter, when turned on, warning message will be sent in 'message' field and 'WARNING' value is set in severity field of the response in case there is a warning.\r + Turned off by default\r + .PARAMETER OnErrorAction\r + Use this parameter to set the onerror status for nitro request. Applicable only for bulk requests.\r + Acceptable values: \"EXIT\", \"CONTINUE\", \"ROLLBACK\", default to \"EXIT\"\r + .EXAMPLE\r + Invoke NITRO REST API to add a DNS Server resource.\r + $payload = @{ip=\"10.8.115.210\"}\r + Invoke-NSNitroRestApi -NSSession $Session -OperationMethod POST -ResourceType dnsnameserver -Payload $payload -Action add\r + .OUTPUTS\r + Only when the OperationMethod is GET:\r + PSCustomObject that represents the JSON response content. This object can be manipulated using the ConvertTo-Json Cmdlet.\r + .NOTES\r + Copyright (c) Citrix Systems, Inc. All rights reserved.\r + #>\r + [CmdletBinding()]\r + param (\r + [Parameter(Mandatory=$true)] [PSObject]$NSSession,\r + [Parameter(Mandatory=$true)] [ValidateSet(\"DELETE\",\"GET\",\"POST\",\"PUT\")] [string]$OperationMethod,\r + [Parameter(Mandatory=$true)] [string]$ResourceType,\r + [Parameter(Mandatory=$false)] [string]$ResourceName,\r + [Parameter(Mandatory=$false)] [string]$Action,\r + [Parameter(Mandatory=$false)] [ValidateScript({$OperationMethod -eq \"GET\"})] [hashtable]$Arguments=@{},\r + [Parameter(Mandatory=$false)] [ValidateScript({$OperationMethod -ne \"GET\"})] [hashtable]$Payload=@{},\r + [Parameter(Mandatory=$false)] [switch]$GetWarning=$false,\r + [Parameter(Mandatory=$false)] [ValidateSet(\"EXIT\", \"CONTINUE\", \"ROLLBACK\")] [string]$OnErrorAction=\"EXIT\"\r + )\r +\r + Write-Verbose \"$($MyInvocation.MyCommand): Enter\"\r +\r + Write-Verbose \"Building URI\"\r + $uri = \"$($Script:NSURLProtocol)://$($NSSession.Endpoint)/nitro/v1/config/$ResourceType\"\r + if (-not [string]::IsNullOrEmpty($ResourceName)) {\r + $uri += \"/$ResourceName\"\r + }\r + if ($OperationMethod -ne \"GET\") {\r + if (-not [string]::IsNullOrEmpty($Action)) {\r + $uri += \"?action=$Action\"\r + }\r + } else {\r + if ($Arguments.Count -gt 0) {\r + $uri += \"?args=\"\r + $argsList = @()\r + foreach ($arg in $Arguments.GetEnumerator()) {\r + $argsList += \"$($arg.Name):$([System.Uri]::EscapeDataString($arg.Value))\"\r + }\r + $uri += $argsList -join ','\r + }\r + #TODO: Add filter, view, and pagesize\r + }\r + Write-Verbose \"URI: $uri\"\r +\r + if ($OperationMethod -ne \"GET\") {\r + Write-Verbose \"Building Payload\"\r + $warning = if ($GetWarning) { \"YES\" } else { \"NO\" }\r + $hashtablePayload = @{}\r + $hashtablePayload.\"params\" = @{\"warning\"=$warning;\"onerror\"=$OnErrorAction;<#\"action\"=$Action#>}\r + $hashtablePayload.$ResourceType = $Payload\r + $jsonPayload = ConvertTo-Json $hashtablePayload -Depth ([int]::MaxValue)\r + Write-Verbose \"JSON Payload:`n$jsonPayload\"\r + }\r +\r + try {\r + Write-Verbose \"Calling Invoke-RestMethod\"\r + $restParams = @{\r + Uri = $uri\r + ContentType = \"application/json\"\r + Method = $OperationMethod\r + WebSession = $NSSession.WebSession\r + ErrorVariable = \"restError\"\r + }\r +\r + if ($OperationMethod -ne \"GET\") {\r + $restParams.Add(\"Body\",$jsonPayload)\r + }\r +\r + $response = Invoke-RestMethod @restParams\r +\r + if ($response) {\r + if ($response.severity -eq \"ERROR\") {\r + throw \"Error. See response: `n$($response | fl * | Out-String)\"\r + } else {\r + Write-Verbose \"Response:`n$(ConvertTo-Json $response | Out-String)\"\r + }\r + }\r + }\r + catch [Exception] {\r + if ($ResourceType -eq \"reboot\" -and $restError[0].Message -eq \"The underlying connection was closed: The connection was closed unexpectedly.\") {\r + Write-Verbose \"Connection closed due to reboot\"\r + } else {\r + throw $_\r + }\r + }\r +\r + Write-Verbose \"$($MyInvocation.MyCommand): Exit\"\r +\r + if ($OperationMethod -eq \"GET\") {\r + return $response\r + }\r +}\r +\r +$psver = $PSVersionTable.PSVersion.Major\r +if ($psver -eq \"1\" -or $psver -eq \"2\") {\r + Write-Error \"NetScaler ADC Enable Disable Service requires PowerShell v3 or newer. Installed version v$psver\"\r + return -1\r +}\r +\r +$NSAddress = $OctopusParameters['HostName']\r +$NSUserName = $OctopusParameters['Username']\r +$NSPassword = $OctopusParameters['Password']\r +$NSProtocol=\"http\"\r +$Action = $OctopusParameters['EnableOrDisable']\r +$ServiceName = $OctopusParameters['ServiceName']\r +$GracefulShutdown = $OctopusParameters['Graceful']\r +$GraceFulShutdownDelay = $OctopusParameters['GracefulDelay']\r +\r +if(!$NSAddress) {\r + Write-Error \"No NetScaler address specified. Please specify an address\"\r + exit -2\r +}\r +\r +if(!$NSUserName) {\r + Write-Error \"No username specified. Please specify a username\"\r + exit -2\r +}\r +\r +if(!$NSPassword) {\r + Write-Error \"No password specified. Please specify a password\"\r + exit -2\r +}\r +\r +if(!$Action) {\r + Write-Error \"No action set. Action must either be enable or disable. Please select an action\"\r + exit -2\r +}\r +\r +if(!$GracefulShutdown) {\r + Write-Error \"Graceful shutdown not selected. Must either be yes or no. Please select an option\"\r + exit -2\r +}\r +\r +if(!$ServiceName) {\r + Write-Error \"Service name must be specified. Please specify service name\"\r + exist -2\r +}\r +\r +\r +Set-NSMgmtProtocol -Protocol $NSProtocol\r +$myNSSession = Connect-NSAppliance -NSAddress $NSAddress -NSUserName $NSUserName -NSPassword $NSPassword\r +$payload = @{name=$ServiceName}\r +if($Action -eq \"disable\") {\r + $payload = @{name=$ServiceName;graceful=$GracefulShutdown;delay=$GraceFulShutdownDelay}\r +}\r +\r +Invoke-NSNitroRestApi -NSSession $myNSSession -OperationMethod POST -ResourceType service -Payload $payload -Action $Action\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "EnableOrDisable", + "Label": "Service status", + "HelpText": "Option whether to disable og enable a Load Balancing Service", + "DefaultValue": "enable", + "DisplaySettings": { + "Octopus.SelectOptions": "enable|Enable +disable|Disable", + "Octopus.ControlType": "Select" + } + }, + { + "Name": "ServiceName", + "Label": "LB Service Name", + "HelpText": "Load Balancing Service Name to enable or diable", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HostName", + "Label": "NetScaler ADC host name", + "HelpText": "Address of the primary NetScaler ADC instance", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Username", + "Label": "NetScaler ADC username", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Password", + "Label": "NetScaler ADC password", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Graceful", + "Label": "Graceful shutdown", + "HelpText": "The service is disabled only when all the current active client connections are closed by either the server or the client", + "DefaultValue": "Yes", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "YES|Yes +NO|No" + } + }, + { + "Name": "GracefulDelay", + "Label": "Graceful Delay (s)", + "HelpText": "The time in seconds after which the service is marked as OUT OF SERVICE.", + "DefaultValue": "300", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-01-29T21:49:05.609+00:00", + "LastModifiedBy": "jasper@sovs.net", + "$Meta": { + "ExportedAt": "2015-01-29T21:49:10.234+00:00", + "OctopusVersion": "2.6.0.778", + "Type": "ActionTemplate" + }, + "Category": "netScaler" +} diff --git a/step-templates/network-add-ssl-certificate-binding.json.human b/step-templates/network-add-ssl-certificate-binding.json.human new file mode 100644 index 000000000..1d7bab5d0 --- /dev/null +++ b/step-templates/network-add-ssl-certificate-binding.json.human @@ -0,0 +1,72 @@ +{ + "Id": "5a4857cf-dddc-4a08-a32e-8dfe018d986a", + "Name": "Network - Add SSL Certificate Binding", + "Description": "Creates (replaces) an SSL certificate binding to a specific hostname and port using NETSH.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$hostnameport = $OctopusParameters['HostnamePort'] +$certhash = $OctopusParameters['CertHash'] +$appid = $OctopusParameters['AppId'] +$certstore = $OctopusParameters['CertStore'] + +$delcert = \"http delete sslcert hostnameport=\"\"$hostnameport\"\"\" +write-host \"Removing Cert: $delcert\" +$delcert | netsh | out-host + +$addcert = \"http add sslcert hostnameport=\"\"$hostnameport\"\" certhash=\"\"$certhash\"\" appid=\"\"$appid\"\" certstore=$certstore\" +write-host \"Creating Certificate Binding: $addcert\" +$addcert | netsh | out-host", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "HostnamePort", + "Label": "Hostname and Port", + "HelpText": "The hostname and port to bind to. Example: example.com:443", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "CertHash", + "Label": "Cert Hash", + "HelpText": "The certificate thumbprint (no spaces). Example: b087166a9f5cd6d75e5ba91105baa022658460de", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AppId", + "Label": "App Id", + "HelpText": "The application identifier, can be any GUID value. Example: {06aabebd-3a91-4b80-8a15-adfd3c8a0b14}", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "CertStore", + "Label": "Certificate Store", + "HelpText": "The certificate store where the certificate lives. Example: My", + "DefaultValue": "My", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2017-07-04T10:34:05.300Z", + "OctopusVersion": "3.3.14", + "Type": "ActionTemplate" + }, + "Category": "http" +} diff --git a/step-templates/network-add-url-reservation.json.human b/step-templates/network-add-url-reservation.json.human new file mode 100644 index 000000000..4a441eea8 --- /dev/null +++ b/step-templates/network-add-url-reservation.json.human @@ -0,0 +1,61 @@ +{ + "Id": "45eea99e-061c-45d2-bcc0-9320269a4ee4", + "Name": "Network - Add URL Reservation", + "Description": "Creates an HTTP URL reservation (ACL) using NETSH.", + "ActionType": "Octopus.Script", + "Version": 11, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$url = $OctopusParameters['Url'] +$user = $OctopusParameters['User'] +$delegate = if ('True' -eq $OctopusParameters['Delegate']) { 'yes' } else { 'no'} + +$delacl = \"http delete urlacl url=$url\" +$addacl = \"http add urlacl url=$url user=\"\"$user\"\" delegate=$delegate\" + +write-host \"Removing ACL: $delacl\" +$delacl | netsh | out-host + +write-host \"Creating ACL: $addacl\" +$addacl | netsh | out-host +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Url", + "Label": "URL", + "HelpText": "The URL to reserve. Example: _http://+:8080_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Delegate", + "Label": "Allow delegation", + "HelpText": "Indicates whether or not the user can delegate URLs. Example: _True_ or _False_", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "User", + "Label": "User", + "HelpText": "Specifies the user or group name. Example: _DOMAIN\\user_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-27T18:53:32.929+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "http" +} diff --git a/step-templates/newrelic-complete-deployment.json.human b/step-templates/newrelic-complete-deployment.json.human new file mode 100644 index 000000000..1a5c67601 --- /dev/null +++ b/step-templates/newrelic-complete-deployment.json.human @@ -0,0 +1,168 @@ +{ + "Id": "7c88ea1e-de71-452d-be7e-b99dda397ba7", + "Name": "New Relic - Complete Deployment", + "Description": "Notifies [New Relic](https://newrelic.com) of a deployment. +Sends the revision/version number, deployer, etc from the Octopus deployment.", + "ActionType": "Octopus.Script", + "Version": 27, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Minimum PowerShell version for ConvertTo-Json is 3 + +Add-Type -AssemblyName System.Web + +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 + +$apiKey = $OctopusParameters['ApiKey'] +$user = $OctopusParameters['User'] +$appId = $OctopusParameters['AppId'] + +#https://octopus.com/docs/deployment-process/variables/system-variables +$releaseNumber = $OctopusParameters['Octopus.Release.Number'] +$releaseNotes = $OctopusParameters['Octopus.Release.Notes'] +$machineName = $OctopusParameters['Octopus.Machine.Name'] +$projectName = $OctopusParameters['Octopus.Project.Name'] +$deploymentLink = $OctopusParameters['Octopus.Web.DeploymentLink'] + +## -------------------------------------------------------------------------------------- +## Helpers +## -------------------------------------------------------------------------------------- +# Helper for validating input parameters +function Validate-Parameter([string]$foo, [string[]]$validInput, $parameterName) { + Write-Host \"${parameterName}: $foo\" + if (! $foo) { + throw \"No value was set for $parameterName, and it cannot be empty\" + } + + if ($validInput) { + if (! $validInput -contains $foo) { + throw \"'$input' is not a valid input for '$parameterName'\" + } + } +} + +## -------------------------------------------------------------------------------------- +## Configuration +## -------------------------------------------------------------------------------------- +Validate-Parameter $apiKey -parameterName \"Api Key\" + +if (!$appId) { + Write-Host \"NewRelic Deploy - AppId has not been set yet. Skipping call to API.\" + exit 0 +} + +if ($appId -eq 0) { + Write-Host \"NewRelic Deploy - AppId has been set to zero. Skipping call to API.\" + exit 0 +} + +$userText = $((\" by user $user\", \"\")[!$user]) +Write-Host (\"NewRelic Deploy - Notify deployment{0} - App {1} - Revision {2}\" -f $userText, $appId, $revision) + + +# https://rpm.newrelic.com/api/explore/application_deployments/create?application_id=1127348 +$deployment = New-Object -TypeName PSObject +$deployment | Add-Member -MemberType NoteProperty -Name \"user\" -Value $user +$deployment | Add-Member -MemberType NoteProperty -Name \"revision\" -Value $releaseNumber +$deployment | Add-Member -MemberType NoteProperty -Name \"changelog\" -Value $releaseNotes +$deployment | Add-Member -MemberType NoteProperty -Name \"description\" -Value \"Octopus deployment of $projectName to $machineName. ($deploymentLink)\" + +$deploymentContainer = New-Object -TypeName PSObject +$deploymentContainer | Add-Member -MemberType NoteProperty -Name \"deployment\" -Value $deployment + +$post = $deploymentContainer | ConvertTo-Json +Write-Debug $post + +# in production, we need to +#Create a URI instance since the HttpWebRequest.Create Method will escape the URL by default. +$URL = \"https://api.newrelic.com/v2/applications/$appId/deployments.json\" +$URI = New-Object System.Uri($URL,$true) + +#Create a request object using the URI +$request = [System.Net.HttpWebRequest]::Create($URI) +$request.Method = \"POST\" +$request.Headers.Add(\"X-Api-Key\",\"$apiKey\"); +$request.ContentType = \"application/json\" + +#Build up a nice User Agent +$request.UserAgent = $( +\"{0} (PowerShell {1}; .NET CLR {2}; {3})\" -f $UserAgent, +$(if($Host.Version){$Host.Version}else{\"1.0\"}), +[Environment]::Version, +[Environment]::OSVersion.ToString().Replace(\"Microsoft Windows \", \"Win\") +) +$ReturnCode = 0 +try { + Write-Host \"Posting data to $URL\" + #Create a new stream writer to write the xml to the request stream. + $stream = New-Object IO.StreamWriter $request.GetRequestStream() + $stream.AutoFlush = $True + $PostStr = [System.Text.Encoding]::UTF8.GetBytes($Post) + $stream.Write($PostStr, 0,$PostStr.length) + $stream.Close() + + #Make the request and get the response + $response = $request.GetResponse() + + if ([int]$response.StatusCode -eq 201) { + Write-Host \"NewRelic Deploy - API called succeeded - HTTP $($response.StatusCode).\" + } else { + Write-Host \"NewRelic Deploy - API called failed - HTTP $($response.StatusCode).\" + $ReturnCode = 1 + } + $response.Close() +} catch { + $ErrorMessage = $_.Exception.Message + $res = $_.Exception.Response + Write-Host \"NewRelic Deploy - API called failed - $ErrorMessage\" + $ReturnCode = 1 +} +exit $ReturnCode +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "caf3f015-671f-4600-bce0-ebcc7d5957fe", + "Name": "ApiKey", + "Label": "Api Key", + "HelpText": "Your New Relic API key.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3748da93-614d-4040-8c63-7c86a260c79e", + "Name": "AppId", + "Label": "App ID", + "HelpText": "The ID of the application in New Relic RPM.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "719ab099-a361-4a58-ad34-b1d4acb70dd2", + "Name": "User", + "Label": "User (optional)", + "HelpText": "The name of the user/process that triggered this deployment.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2024-04-16T20:50:00.0000000+03:00", + "LastModifiedBy": "Softer", + "$Meta": { + "ExportedAt": "2024-04-16T20:50:00.0000000+03:00", + "OctopusVersion": "2019.12.0", + "Type": "ActionTemplate" + }, + "Category": "newrelic" +} diff --git a/step-templates/nssm-windows-service-create.json.human b/step-templates/nssm-windows-service-create.json.human new file mode 100644 index 000000000..7d3598a7d --- /dev/null +++ b/step-templates/nssm-windows-service-create.json.human @@ -0,0 +1,248 @@ +{ + "Id": "fc54e757-fbd9-40b1-98fa-7b2e16c649de", + "Name": "NSSM Windows Service - Create", + "Description": "Create Windows Service using NSSM and powershell script. + +Visit https://nssm.cc/usage for more information. + +NOTE: This site may be blocked due to .cc domain", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$serviceStopStepAdded = $OctopusParameters['ServiceStopStepAdded']\r +$serviceName = $OctopusParameters['ServiceNameValue']\r +$displayName = $OctopusParameters['ServiceDisplayNameValue']\r +$startupType = $OctopusParameters['StartupTypeValue']\r +$description = $OctopusParameters['ServiceDescriptionValue']\r +$serviceExecutable = $OctopusParameters['ServiceExecutableValue']\r +$serviceExecutableArgs = $OctopusParameters['serviceExecutableArgsValue']\r +$serviceAppDirectory = $OctopusParameters['ServiceAppDirectoryValue']\r +$serviceUserAccount = $OctopusParameters['serviceUserAccountValue']\r +$serviceUserPassword = $OctopusParameters['serviceUserPasswordValue']\r +$dependsOn = $OctopusParameters['DependsOnValue']\r +$serviceErrorLogFile = $OctopusParameters['serviceErrorLogFileValue']\r +$serviceOutputLogFile = $OctopusParameters['serviceOutputLogFileValue']\r +$nssmExecutable = $OctopusParameters['NSSMExecutableValue']\r +\r +if($serviceStopStepAdded -ne 'True'){\r + Write-Host Please add a step to stop the windows service as the first step!\r + Write-Host If already added, make sure to check the checkbox - Step to stop service added as first step? - in NSSM Windows Service Setup\r + return\r +}\r +\r +Write-Host Installing service $serviceName -foreground \"green\"\r +Write-Host \"NSSM path\" $serviceAppDirectory\r +Write-Host $serviceName\r +Write-Host $serviceExecutable\r +Write-Host $serviceExecutableArgs\r +Write-Host $serviceAppDirectory\r +Write-Host $serviceErrorLogFile\r +Write-Host $serviceOutputLogFile\r +Write-Host $serviceUserAccount\r +Write-Host $serviceUserPassword\r +\r +push-location\r +Set-Location $serviceAppDirectory\r +\r +$service = Get-Service $serviceName -ErrorAction SilentlyContinue\r +\r +if($service) {\r + Write-host service $service.Name is $service.Status\r + Write-Host Removing $serviceName service \r + if($service.Status -ne 'Stopped'){\r + &$nssmExecutable stop $serviceName\r + }\r + &$nssmExecutable remove $serviceName confirm\r +}\r +\r +Write-Host Installing $serviceName as a service\r +&$nssmExecutable install $serviceName $serviceExecutable $serviceExecutableArgs\r +\r +if($displayName){\r + &$nssmExecutable set $serviceName DisplayName $displayName\r +} \r +\r +if($startupType){\r + &$nssmExecutable set $serviceName Start $startupType\r +}\r +\r +if($description){\r + &$nssmExecutable set $serviceName Description $description\r +}\r +\r +if($dependsOn){\r + &$nssmExecutable set $serviceName DependOnService $dependsOn\r +}\r +\r +# setting log file \r +if($serviceErrorLogFile){\r + &$nssmExecutable set $serviceName AppStderr $serviceErrorLogFile\r + &$nssmExecutable set $serviceName AppStderrCreationDisposition 2\r +}\r +\r +if($serviceOutputLogFile){\r + &$nssmExecutable set $serviceName AppStdout $serviceOutputLogFile\r + &$nssmExecutable set $serviceName AppStdoutCreationDisposition 2\r +}\r +\r +# setting app directory\r +if($serviceAppDirectory) {\r + Write-host setting app directory to $serviceAppDirectory -foreground \"green\"\r + &$nssmExecutable set $serviceName AppDirectory $serviceAppDirectory\r +}\r +\r +# setting user account\r +if($serviceUserAccount -And $serviceUserPassword) {\r + &$nssmExecutable set $serviceName ObjectName $serviceUserAccount $serviceUserPassword\r +}\r +\r +#start service right away\r +&$nssmExecutable start $serviceName\r +pop-location", + "Octopus.Action.Script.ScriptFileName": "", + "Octopus.Action.Package.FeedId": "", + "Octopus.Action.Package.PackageId": "" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ServiceStopStepAdded", + "Label": "Step to stop service added as first step?", + "HelpText": "Ensure that a step to stop the service added as first step", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "ServiceNameValue", + "Label": "ServiceName", + "HelpText": "[Required] Service Name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ServiceDisplayNameValue", + "Label": "DisplayName", + "HelpText": "Display Name for the service", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "StartupTypeValue", + "Label": "StartupType", + "HelpText": "StartupType (check https://nssm.cc/usage for the Valid values)", + "DefaultValue": "SERVICE_AUTO_START", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ServiceDescriptionValue", + "Label": "Description", + "HelpText": "Description", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ServiceExecutableValue", + "Label": "Service Executable", + "HelpText": "[Required] Path of the executable to run as Windows Service", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "serviceExecutableArgsValue", + "Label": "Service Executable Arguments", + "HelpText": "Any arguments to be passed to Service Executable", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ServiceAppDirectoryValue", + "Label": "Service App Directory", + "HelpText": "[Required] Directory path of the Service Executable", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "serviceUserAccountValue", + "Label": "Service User Account", + "HelpText": "[Required] User Account to run Service as.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "serviceUserPasswordValue", + "Label": "Service User Password", + "HelpText": "[Required] Password for the User Account to run service as.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "DependsOnValue", + "Label": "Depends On", + "HelpText": "Any services or service groups which must be started before the the service can run. +(check https://nssm.cc/usage for more info)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "serviceErrorLogFileValue", + "Label": "Service Error Log File", + "HelpText": "Capture error log messages generated by the application", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "serviceOutputLogFileValue", + "Label": "Service Output Log File", + "HelpText": "Capture output log messages generated by the application", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NSSMExecutableValue", + "Label": "NSSM Executable", + "HelpText": "Allows you to override the NSSM.exe installation location.", + "DefaultValue": ".\ +ssm.exe", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2017-05-12T22:50:12.920+00:00", + "LastModifiedBy": "1FastSTi", + "$Meta": { + "ExportedAt": "2017-05-12T22:50:16.405Z", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/octopus-add-runbook-to-project-azure.json.human b/step-templates/octopus-add-runbook-to-project-azure.json.human new file mode 100644 index 000000000..58816a343 --- /dev/null +++ b/step-templates/octopus-add-runbook-to-project-azure.json.human @@ -0,0 +1,189 @@ +{ + "Id": "9b206752-5a8c-40dd-84a8-94f08a42955c", + "Name": "Octopus - Add Runbook to Project (Azure Backend)", + "Description": "This step exposes the fields required to deploy a runbook serialized with [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport) using Terraform to a project. + +This step configures a Terraform Azure backend. + +It is recommended that this step be run with the `octopuslabs/terraform-workertools` worker image.", + "ActionType": "Octopus.TerraformApply", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "093b1515-15a9-4446-8dc2-6297018a77e7", + "Name": "", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "SelectionMode": "deferred", + "PackageParameterName": "OctoterraApply.Terraform.Package.Id" + } + } + ], + "GitDependencies": [], + "Properties": { + "Octopus.Action.GoogleCloud.UseVMServiceAccount": "False", + "Octopus.Action.GoogleCloud.ImpersonateServiceAccount": "False", + "Octopus.Action.Terraform.GoogleCloudAccount": "False", + "Octopus.Action.Terraform.AzureAccount": "True", + "Octopus.Action.Terraform.ManagedAccount": "None", + "Octopus.Action.Terraform.AllowPluginDownloads": "True", + "Octopus.Action.Script.ScriptSource": "Package", + "Octopus.Action.Terraform.RunAutomaticFileSubstitution": "False", + "Octopus.Action.Terraform.PlanJsonOutput": "False", + "Octopus.Action.Terraform.Workspace": "#{OctoterraApply.Terraform.Workspace.Name}", + "Octopus.Action.Terraform.AdditionalInitParams": "-backend-config=\"resource_group_name=#{OctoterraApply.Azure.Storage.ResourceGroup}\" -backend-config=\"storage_account_name=#{OctoterraApply.Azure.Storage.AccountName}\" -backend-config=\"container_name=#{OctoterraApply.Azure.Storage.Container}\" -backend-config=\"key=#{OctoterraApply.Azure.Storage.Key}\" #{if OctoterraApply.Terraform.AdditionalInitParams}#{OctoterraApply.Terraform.AdditionalInitParams}#{/if}", + "Octopus.Action.Terraform.AdditionalActionParams": "-var=octopus_server=#{OctoterraApply.Octopus.ServerUrl} -var=octopus_apikey=#{OctoterraApply.Octopus.ApiKey} -var=octopus_space_id=#{OctoterraApply.Octopus.SpaceID} \"-var=parent_project_name=#{OctoterraApply.Octopus.Project}\" #{if OctoterraApply.Terraform.AdditionalApplyParams}#{OctoterraApply.Terraform.AdditionalApplyParams}#{/if}", + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.Terraform.TemplateDirectory": "space_population", + "Octopus.Action.Terraform.FileSubstitution": "**/project_variable_sensitive*.tf", + "Octopus.Action.AzureAccount.Variable": "OctoterraApply.Azure.Account" + }, + "Parameters": [ + { + "Id": "78dee77f-10a8-465c-9134-cda7edb6e794", + "Name": "OctoterraApply.Terraform.Workspace.Name", + "Label": "Terraform Workspace", + "HelpText": "The name of the Terraform workspace. This must be unique for every project this module is deployed to. The default value is based on the space ID that the module is applied to: `#{OctoterraApply.Octopus.SpaceID}`. Leave this as the default value unless you have a specific reason to change it.", + "DefaultValue": "#{OctoterraApply.Octopus.SpaceID}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6c05bc2b-1326-4e6b-a6ea-16d09d6a6abe", + "Name": "OctoterraApply.Terraform.Package.Id", + "Label": "Terraform Module Package", + "HelpText": "The package created by [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport). It must include the `space_population` directory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "ecadd361-bdf6-45d5-92bb-2dba7ebf4163", + "Name": "OctoterraApply.Octopus.ServerUrl", + "Label": "Octopus Server URL", + "HelpText": "The Octopus server URL.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c43aff0c-1321-42d6-a08b-a22426239e30", + "Name": "OctoterraApply.Octopus.ApiKey", + "Label": "Octopus API key", + "HelpText": "The Octopus API key. See the [documentation](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) for details on creating an API key.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "4b669ef2-11e3-4ead-b613-60b9832ec23e", + "Name": "OctoterraApply.Octopus.SpaceID", + "Label": "Octopus Space ID", + "HelpText": "The Space ID to deploy the Terraform module into. The [Octopus - Lookup Space ID](https://library.octopus.com/step-templates/324f747e-e2cd-439d-a660-774baf4991f2/actiontemplate-octopus-lookup-space-id) step can be used to convert a space name to an ID.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b54953d9-a441-485c-97f8-937ba0e77c32", + "Name": "OctoterraApply.Octopus.Project", + "Label": "Octopus Project Name", + "HelpText": "The name of the project to import the runbook into", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f827567a-9807-4d3a-b16e-2845112d1873", + "Name": "OctoterraApply.Azure.Account", + "Label": "Azure Account Variable", + "HelpText": "The Azure account variable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "b2d855be-4f8f-4c7c-9e28-49c5539c1df5", + "Name": "OctoterraApply.Azure.Storage.ResourceGroup", + "Label": "Azure Backend Resource Group", + "HelpText": "The name of the resource group holding the Azure storage account. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure storage accounts as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d3098333-99ac-463f-83eb-f66aca3d1055", + "Name": "OctoterraApply.Azure.Storage.AccountName", + "Label": "Azure Storage Account Name", + "HelpText": "The name of the Azure storage account used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure storage accounts as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7c816136-7917-4b7d-9a5a-580c6466c713", + "Name": "OctoterraApply.Azure.Storage.Container", + "Label": "Azure Storage Container", + "HelpText": "The name of the Azure storage account container used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure storage accounts as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b793155e-76c5-4781-a075-d1c5182c3b5f", + "Name": "OctoterraApply.Azure.Storage.Key", + "Label": "Azure Storage Key", + "HelpText": "The file used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure as a backend. The combination of the workspace name and this key must be unique. + +The default value is the name of the runbook and a prefix to indicate the type of resource: `Runbook_#{Octopus.Action.Package.PackageId}`.", + "DefaultValue": "Runbook_#{Octopus.Action.Package.PackageId}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "213f14e9-3856-419f-9de1-1e0e755c82db", + "Name": "OctoterraApply.Terraform.AdditionalApplyParams", + "Label": "Terraform Additional Apply Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform apply` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/apply) for details on the `apply` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "24e0186c-bb65-4c77-b9d8-4f4d19523621", + "Name": "OctoterraApply.Terraform.AdditionalInitParams", + "Label": "Terraform Additional Init Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform init` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/init) for details on the `init` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.TerraformApply", + "$Meta": { + "ExportedAt": "2023-12-20T23:32:58.875Z", + "OctopusVersion": "2024.1.5406", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-add-runbook-to-project.json.human b/step-templates/octopus-add-runbook-to-project.json.human new file mode 100644 index 000000000..177b45133 --- /dev/null +++ b/step-templates/octopus-add-runbook-to-project.json.human @@ -0,0 +1,179 @@ +{ + "Id": "8b8b0386-78f8-42c2-baea-2fdb9a57c32d", + "Name": "Octopus - Add Runbook to Project (S3 Backend)", + "Description": "This step exposes the fields required to deploy a runbook serialized with [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport) using Terraform to a project. + +This step configures a Terraform S3 backend. + +It is recommended that this step be run with the `octopuslabs/terraform-workertools` worker image.", + "ActionType": "Octopus.TerraformApply", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "093b1515-15a9-4446-8dc2-6297018a77e7", + "Name": "", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "SelectionMode": "deferred", + "PackageParameterName": "OctoterraApply.Terraform.Package.Id" + } + } + ], + "Properties": { + "Octopus.Action.GoogleCloud.UseVMServiceAccount": "False", + "Octopus.Action.GoogleCloud.ImpersonateServiceAccount": "False", + "Octopus.Action.Terraform.GoogleCloudAccount": "False", + "Octopus.Action.Terraform.AzureAccount": "False", + "Octopus.Action.Terraform.ManagedAccount": "AWS", + "Octopus.Action.Terraform.AllowPluginDownloads": "True", + "Octopus.Action.Script.ScriptSource": "Package", + "Octopus.Action.Terraform.RunAutomaticFileSubstitution": "False", + "Octopus.Action.Terraform.PlanJsonOutput": "False", + "Octopus.Action.Terraform.Workspace": "#{OctoterraApply.Terraform.Workspace.Name}", + "Octopus.Action.Terraform.AdditionalInitParams": "-backend-config=\"bucket=#{OctoterraApply.AWS.S3.BucketName}\" -backend-config=\"region=#{OctoterraApply.AWS.S3.BucketRegion}\" -backend-config=\"key=#{OctoterraApply.AWS.S3.BucketKey}\" #{if OctoterraApply.Terraform.AdditionalInitParams}#{OctoterraApply.Terraform.AdditionalInitParams}#{/if}", + "Octopus.Action.Terraform.AdditionalActionParams": "-var=octopus_server=#{OctoterraApply.Octopus.ServerUrl} -var=octopus_apikey=#{OctoterraApply.Octopus.ApiKey} -var=octopus_space_id=#{OctoterraApply.Octopus.SpaceID} \"-var=parent_project_name=#{OctoterraApply.Octopus.Project}\" #{if OctoterraApply.Terraform.AdditionalApplyParams}#{OctoterraApply.Terraform.AdditionalApplyParams}#{/if}", + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.AwsAccount.Variable": "OctoterraApply.AWS.Account", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.Aws.Region": "#{OctoterraApply.AWS.S3.BucketRegion}", + "Octopus.Action.Terraform.TemplateDirectory": "space_population", + "Octopus.Action.Terraform.FileSubstitution": "" + }, + "Parameters": [ + { + "Id": "95860b77-2c38-492c-bb84-ca1fbb4e4b72", + "Name": "OctoterraApply.Terraform.Workspace.Name", + "Label": "Terraform Workspace", + "HelpText": "The name of the Terraform workspace. This must be unique for every project this module is deployed to. The default value is based on the space ID and project name that the module is applied to: `#{OctoterraApply.Octopus.SpaceID}_#{OctoterraApply.Octopus.Project | Replace \"[^A-Za-z0-9]\" \"_\"}`. Leave this as the default value unless you have a specific reason to change it.", + "DefaultValue": "#{OctoterraApply.Octopus.SpaceID}_#{OctoterraApply.Octopus.Project | Replace \"[^A-Za-z0-9]\" \"_\"}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1acabadc-d1d6-477a-88ff-ae5a302a9d77", + "Name": "OctoterraApply.Terraform.Package.Id", + "Label": "Terraform Module Package", + "HelpText": "The package created by [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport). It must include the `space_population` directory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "1092f0a5-3e57-4009-9ed7-ee93e36e40cb", + "Name": "OctoterraApply.Octopus.ServerUrl", + "Label": "Octopus Server URL", + "HelpText": "The Octopus server URL.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "03191aa9-d41f-4ae8-96ca-a35da790043a", + "Name": "OctoterraApply.Octopus.ApiKey", + "Label": "Octopus API key", + "HelpText": "The Octopus API key. See the [documentation](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) for details on creating an API key.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "59c7c097-7fd8-46e7-996c-64f74a80ef02", + "Name": "OctoterraApply.Octopus.SpaceID", + "Label": "Octopus Space ID", + "HelpText": "The Space ID to deploy the Terraform module into. The [Octopus - Lookup Space ID](https://library.octopus.com/step-templates/324f747e-e2cd-439d-a660-774baf4991f2/actiontemplate-octopus-lookup-space-id) step can be used to convert a space name to an ID.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ea0dd836-7490-45c6-8e5c-321569e8d07d", + "Name": "OctoterraApply.Octopus.Project", + "Label": "Octopus Project Name", + "HelpText": "The name of the project to import the runbook into", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bc23db0c-381b-4796-9350-0f5a3cba0a66", + "Name": "OctoterraApply.AWS.Account", + "Label": "AWS Account Variable", + "HelpText": "The AWS account variable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "553c3ed3-11cf-4b54-bd78-847062e64828", + "Name": "OctoterraApply.AWS.S3.BucketName", + "Label": "AWS S3 Bucket Name", + "HelpText": "The name of the S3 bucket used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/s3) for details on using S3 as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f21cd6e6-82c2-4b5f-8739-8e0a02be8deb", + "Name": "OctoterraApply.AWS.S3.BucketRegion", + "Label": "AWS S3 Bucket Region", + "HelpText": "The AWS region hosting the S3 bucket. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/s3) for details on using S3 as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "107214e9-b237-4255-894a-95163b28c1fe", + "Name": "OctoterraApply.AWS.S3.BucketKey", + "Label": "AWS S3 Bucket Key", + "HelpText": "The S3 file used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/s3) for details on using S3 as a backend. The combination of the workspace name and this key must be unique. + +The default value is the name of the runbook (based on the name of the package) and a prefix to indicate the type of resource: `Runbook_#{Octopus.Action.Package.PackageId}`.", + "DefaultValue": "Runbook_#{Octopus.Action.Package.PackageId}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a4548b85-2a8c-4ab2-a604-ab1e9f7ef5ea", + "Name": "OctoterraApply.Terraform.AdditionalApplyParams", + "Label": "Terraform Additional Apply Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform apply` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/apply) for details on the `apply` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fc28012d-5e51-4907-bae4-552697226fde", + "Name": "OctoterraApply.Terraform.AdditionalInitParams", + "Label": "Terraform Additional Init Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform init` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/init) for details on the `init` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.TerraformApply", + "$Meta": { + "ExportedAt": "2023-11-09T01:10:53.027Z", + "OctopusVersion": "2024.1.895", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-apply-octoterra-module-azure.json.human b/step-templates/octopus-apply-octoterra-module-azure.json.human new file mode 100644 index 000000000..fbaadb9fc --- /dev/null +++ b/step-templates/octopus-apply-octoterra-module-azure.json.human @@ -0,0 +1,179 @@ +{ + "Id": "c15be981-3138-47c8-a935-ab388b7840be", + "Name": "Octopus - Populate Octoterra Space (Azure Backend)", + "Description": "This step exposes the fields required to deploy a project or space serialized with [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport) using Terraform. + +This step configures a Terraform Azure backend. + +It is recommended that this step be run with the `octopuslabs/terraform-workertools` worker image.", + "ActionType": "Octopus.TerraformApply", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "093b1515-15a9-4446-8dc2-6297018a77e7", + "Name": "", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": null, + "Properties": { + "SelectionMode": "deferred", + "PackageParameterName": "OctoterraApply.Terraform.Package.Id" + } + } + ], + "GitDependencies": [], + "Properties": { + "Octopus.Action.GoogleCloud.UseVMServiceAccount": "True", + "Octopus.Action.GoogleCloud.ImpersonateServiceAccount": "False", + "Octopus.Action.Terraform.GoogleCloudAccount": "False", + "Octopus.Action.Terraform.AzureAccount": "True", + "Octopus.Action.Terraform.ManagedAccount": "None", + "Octopus.Action.Terraform.AllowPluginDownloads": "True", + "Octopus.Action.Script.ScriptSource": "Package", + "Octopus.Action.Terraform.RunAutomaticFileSubstitution": "False", + "Octopus.Action.Terraform.PlanJsonOutput": "False", + "Octopus.Action.Terraform.Workspace": "#{OctoterraApply.Terraform.Workspace.Name}", + "Octopus.Action.Terraform.AdditionalInitParams": "-backend-config=\"resource_group_name=#{OctoterraApply.Azure.Storage.ResourceGroup}\" -backend-config=\"storage_account_name=#{OctoterraApply.Azure.Storage.AccountName}\" -backend-config=\"container_name=#{OctoterraApply.Azure.Storage.Container}\" -backend-config=\"key=#{OctoterraApply.Azure.Storage.Key}\" #{if OctoterraApply.Terraform.AdditionalInitParams}#{OctoterraApply.Terraform.AdditionalInitParams}#{/if}", + "Octopus.Action.Terraform.AdditionalActionParams": "-var=octopus_server=#{OctoterraApply.Octopus.ServerUrl} -var=octopus_apikey=#{OctoterraApply.Octopus.ApiKey} -var=octopus_space_id=#{OctoterraApply.Octopus.SpaceID} #{if OctoterraApply.Terraform.AdditionalApplyParams}#{OctoterraApply.Terraform.AdditionalApplyParams}#{/if}", + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.Terraform.TemplateDirectory": "space_population", + "Octopus.Action.Terraform.FileSubstitution": "**/project_variable_sensitive*.tf", + "Octopus.Action.AzureAccount.Variable": "OctoterraApply.Azure.Account" + }, + "Parameters": [ + { + "Id": "fc203025-f9f8-421d-a4d8-963347555a7b", + "Name": "OctoterraApply.Terraform.Workspace.Name", + "Label": "Terraform Workspace", + "HelpText": "The name of the Terraform workspace. This must be unique for every project this module is deployed to. The default value is based on the space ID that the module is applied to: `#{OctoterraApply.Octopus.SpaceID}`. Leave this as the default value unless you have a specific reason to change it.", + "DefaultValue": "#{OctoterraApply.Octopus.SpaceID}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f3581f20-f65f-4819-9060-aca5d0e5dc85", + "Name": "OctoterraApply.Terraform.Package.Id", + "Label": "Terraform Module Package", + "HelpText": "The package created by [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport). It must include the `space_population` directory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "6e9b0c07-703a-4c1f-a5f6-83084fb676d8", + "Name": "OctoterraApply.Octopus.ServerUrl", + "Label": "Octopus Server URL", + "HelpText": "The Octopus server URL.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "09c3a07e-1c63-4342-8b7f-1d596cc26c11", + "Name": "OctoterraApply.Octopus.ApiKey", + "Label": "Octopus API key", + "HelpText": "The Octopus API key. See the [documentation](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) for details on creating an API key.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "a320bade-7813-4326-8335-b99ffe525871", + "Name": "OctoterraApply.Octopus.SpaceID", + "Label": "Octopus Space ID", + "HelpText": "The Space ID to deploy the Terraform module into. The [Octopus - Lookup Space ID](https://library.octopus.com/step-templates/324f747e-e2cd-439d-a660-774baf4991f2/actiontemplate-octopus-lookup-space-id) step can be used to convert a space name to an ID.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "19fef90b-d94d-4a45-8cc5-6da4925a4b23", + "Name": "OctoterraApply.Azure.Account", + "Label": "Azure Account Variable", + "HelpText": "The Azure account variable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "9442e8fb-056b-4a24-a129-adfe9726ea8d", + "Name": "OctoterraApply.Azure.Storage.ResourceGroup", + "Label": "Azure Backend Resource Group", + "HelpText": "The name of the resource group holding the Azure storage account. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure storage accounts as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d3098333-99ac-463f-83eb-f66aca3d1055", + "Name": "OctoterraApply.Azure.Storage.AccountName", + "Label": "Azure Storage Account Name", + "HelpText": "The name of the Azure storage account used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure storage accounts as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "514db3ac-89a7-4537-abfc-cf7bf7c6ac8c", + "Name": "OctoterraApply.Azure.Storage.Container", + "Label": "Azure Storage Container", + "HelpText": "The name of the Azure storage account container used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure storage accounts as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e34b942b-f99b-48d1-9989-6810a2d0a71b", + "Name": "OctoterraApply.Azure.Storage.Key", + "Label": "Azure Storage Key", + "HelpText": "The file used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure as a backend. The combination of the workspace name and this key must be unique. + +The default value is the name of the project and a prefix to indicate the type of resource: `Project_#{Octopus.Project.Name | Replace \"[^A-Za-z0-9]\" \"_\"}`.", + "DefaultValue": "Project_#{Octopus.Project.Name | Replace \"[^A-Za-z0-9]\" \"_\"}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "eef59864-851c-4f6a-bb55-f4a58e1ca2b2", + "Name": "OctoterraApply.Terraform.AdditionalApplyParams", + "Label": "Terraform Additional Apply Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform apply` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/apply) for details on the `apply` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "dae7e399-1bdc-49c2-90d1-b7b2560b379a", + "Name": "OctoterraApply.Terraform.AdditionalInitParams", + "Label": "Terraform Additional Init Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform init` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/init) for details on the `init` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.TerraformApply", + "$Meta": { + "ExportedAt": "2023-12-20T23:12:15.992Z", + "OctopusVersion": "2024.1.5406", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-apply-octoterra-module-s3.json.human b/step-templates/octopus-apply-octoterra-module-s3.json.human new file mode 100644 index 000000000..15e53098c --- /dev/null +++ b/step-templates/octopus-apply-octoterra-module-s3.json.human @@ -0,0 +1,169 @@ +{ + "Id": "14d51af4-1c3d-4d41-9044-4304111d0cd8", + "Name": "Octopus - Populate Octoterra Space (S3 Backend)", + "Description": "This step exposes the fields required to deploy a project or space serialized with [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport) using Terraform. + +This step configures a Terraform S3 backend. + +It is recommended that this step be run with the `octopuslabs/terraform-workertools` worker image.", + "ActionType": "Octopus.TerraformApply", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "093b1515-15a9-4446-8dc2-6297018a77e7", + "Name": "", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "SelectionMode": "deferred", + "PackageParameterName": "OctoterraApply.Terraform.Package.Id" + } + } + ], + "Properties": { + "Octopus.Action.GoogleCloud.UseVMServiceAccount": "True", + "Octopus.Action.GoogleCloud.ImpersonateServiceAccount": "False", + "Octopus.Action.Terraform.GoogleCloudAccount": "False", + "Octopus.Action.Terraform.AzureAccount": "False", + "Octopus.Action.Terraform.ManagedAccount": "AWS", + "Octopus.Action.Terraform.AllowPluginDownloads": "True", + "Octopus.Action.Script.ScriptSource": "Package", + "Octopus.Action.Terraform.RunAutomaticFileSubstitution": "False", + "Octopus.Action.Terraform.PlanJsonOutput": "False", + "Octopus.Action.Terraform.Workspace": "#{OctoterraApply.Terraform.Workspace.Name}", + "Octopus.Action.Terraform.AdditionalInitParams": "-backend-config=\"bucket=#{OctoterraApply.AWS.S3.BucketName}\" -backend-config=\"region=#{OctoterraApply.AWS.S3.BucketRegion}\" -backend-config=\"key=#{OctoterraApply.AWS.S3.BucketKey}\" #{if OctoterraApply.Terraform.AdditionalInitParams}#{OctoterraApply.Terraform.AdditionalInitParams}#{/if}", + "Octopus.Action.Terraform.AdditionalActionParams": "-var=octopus_server=#{OctoterraApply.Octopus.ServerUrl} -var=octopus_apikey=#{OctoterraApply.Octopus.ApiKey} -var=octopus_space_id=#{OctoterraApply.Octopus.SpaceID} #{if OctoterraApply.Terraform.AdditionalApplyParams}#{OctoterraApply.Terraform.AdditionalApplyParams}#{/if}", + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.AwsAccount.Variable": "OctoterraApply.AWS.Account", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.Aws.Region": "#{OctoterraApply.AWS.S3.BucketRegion}", + "Octopus.Action.Terraform.TemplateDirectory": "space_population", + "Octopus.Action.Terraform.FileSubstitution": "**/project_variable_sensitive*.tf" + }, + "Parameters": [ + { + "Id": "27254625-8cfd-4918-b16b-68ac26a25d37", + "Name": "OctoterraApply.Terraform.Workspace.Name", + "Label": "Terraform Workspace", + "HelpText": "The name of the Terraform workspace. This must be unique for every project this module is deployed to. The default value is based on the space ID that the module is applied to: `#{OctoterraApply.Octopus.SpaceID}`. Leave this as the default value unless you have a specific reason to change it.", + "DefaultValue": "#{OctoterraApply.Octopus.SpaceID}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6c8ac9fd-24e2-4358-a582-0b3104857c56", + "Name": "OctoterraApply.Terraform.Package.Id", + "Label": "Terraform Module Package", + "HelpText": "The package created by [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport). It must include the `space_population` directory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "2c9f6df2-0097-4a40-b649-314eaf3f2fcc", + "Name": "OctoterraApply.Octopus.ServerUrl", + "Label": "Octopus Server URL", + "HelpText": "The Octopus server URL.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d48bd55c-2b47-41c7-bc4d-9b308a87c0bc", + "Name": "OctoterraApply.Octopus.ApiKey", + "Label": "Octopus API key", + "HelpText": "The Octopus API key. See the [documentation](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) for details on creating an API key.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "72953e6d-1a45-4ee1-9878-620dd3a01655", + "Name": "OctoterraApply.Octopus.SpaceID", + "Label": "Octopus Space ID", + "HelpText": "The Space ID to deploy the Terraform module into. The [Octopus - Lookup Space ID](https://library.octopus.com/step-templates/324f747e-e2cd-439d-a660-774baf4991f2/actiontemplate-octopus-lookup-space-id) step can be used to convert a space name to an ID.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8e6960a1-5933-4324-88a8-7a8fc144d272", + "Name": "OctoterraApply.AWS.Account", + "Label": "AWS Account Variable", + "HelpText": "The AWS account variable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "cf0a0548-fe36-42d2-a008-ec6020a1062d", + "Name": "OctoterraApply.AWS.S3.BucketName", + "Label": "AWS S3 Bucket Name", + "HelpText": "The name of the S3 bucket used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/s3) for details on using S3 as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "28c46172-2154-461e-aabd-1c1c30591297", + "Name": "OctoterraApply.AWS.S3.BucketRegion", + "Label": "AWS S3 Bucket Region", + "HelpText": "The AWS region hosting the S3 bucket. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/s3) for details on using S3 as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b36ce7f6-deb9-4252-a2c9-790b2d10ddaf", + "Name": "OctoterraApply.AWS.S3.BucketKey", + "Label": "AWS S3 Bucket Key", + "HelpText": "The S3 file used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/s3) for details on using S3 as a backend. The combination of the workspace name and this key must be unique. + +The default value is the name of the project and a prefix to indicate the type of resource: `Project_#{Octopus.Project.Name | Replace \"[^A-Za-z0-9]\" \"_\"}`.", + "DefaultValue": "Project_#{Octopus.Project.Name | Replace \"[^A-Za-z0-9]\" \"_\"}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "74bccb6c-0959-4183-ad25-d83d9a6356b3", + "Name": "OctoterraApply.Terraform.AdditionalApplyParams", + "Label": "Terraform Additional Apply Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform apply` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/apply) for details on the `apply` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8ff57a5d-9a31-4360-9c1f-06ccf2fb1d21", + "Name": "OctoterraApply.Terraform.AdditionalInitParams", + "Label": "Terraform Additional Init Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform init` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/init) for details on the `init` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.TerraformApply", + "$Meta": { + "ExportedAt": "2023-10-11T09:11:08.244Z", + "OctopusVersion": "2023.4.5160", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-artifact-collect.json.human b/step-templates/octopus-artifact-collect.json.human new file mode 100644 index 000000000..447aace74 --- /dev/null +++ b/step-templates/octopus-artifact-collect.json.human @@ -0,0 +1,207 @@ +{ + "Id": "7d09ccec-91b7-4c0c-95d7-27a42b21eb5a", + "Name": "Artifact Collect", + "Description": "Collect artifacts easily and safely.", + "ActionType": "Octopus.Script", + "Version": 15, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "try{ + #region Types - Constants + Add-Type -assembly \"system.io.compression.filesystem\" + $shaloMaskString = '********' + #endregion + + #region Params + $shaloArtifactPath = $OctopusParameters['shaloArtifactPath'] + Write-Host \" Artifact path: [$shaloArtifactPath]\" + + $shaloCollectedArtifactName = $OctopusParameters['shaloCollectedArtifactName'] + Write-Host \" Artifact name: [$shaloCollectedArtifactName]\" + + $shaloArtifactTempPath = $OctopusParameters['shaloArtifactTempPath'] + if($shaloArtifactTempPath.Length -eq 0){ + Write-Error \" Artifact Temporal path not set.\" + exit 1 + } + Write-Host \" Artifact Temporal path: [$shaloArtifactTempPath]\" + + + + $shaloCompressionLevel = $OctopusParameters['shaloCompressionLevel'] + switch($shaloCompressionLevel) { + 'Optimal' {$shaloCompressionLevel = [System.IO.Compression.CompressionLevel]::Optimal} + 'Fastest' {$shaloCompressionLevel = [System.IO.Compression.CompressionLevel]::Fastest} + 'NoCompression' {$shaloCompressionLevel = [System.IO.Compression.CompressionLevel]::NoCompression} + } + Write-Host \" Artifact compresion Level: [$shaloCompressionLevel]\" + + $shaloMaskFilers = $OctopusParameters['shaloMaskFilers'] + $shaloMaskKeys = $OctopusParameters['shaloMaskKeys'] + if($shaloMaskFilers.Length -gt 0 -and $shaloMaskKeys.Length -gt 0){ + Write-Host \" Scrub sensitive values from this file extensions : [$shaloMaskFilers]\" + $shaloMaskFilers = $shaloMaskFilers.Split(',') + $shaloMaskKeys = $shaloMaskKeys.Split(',') + } + #endregion + + + if(Test-Path -Path $shaloArtifactPath){ + Write-Host '' + #region Create Temporal Artifact + $shaloArtifactObject = Get-Item $shaloArtifactPath + try{ + \tWrite-Host ' Cleaning Artifact Temporal Folder' + Remove-Item -Path $shaloArtifactTempPath -Force -Recurse + }catch{ + } + if($shaloArtifactObject.PSIsContainer){ + Write-Host ' Artifact type: [Directory]' + Copy-Item -Path $shaloArtifactPath -Destination $shaloArtifactTempPath -Recurse -Force + }else{ + Write-Host ' Artifact type: [File]' + New-Item -Path $shaloArtifactTempPath -Force -ItemType Directory + Copy-Item -Path $shaloArtifactObject.FullName -Destination ($shaloArtifactTempPath + \"\\\" + $shaloArtifactObject.Name) -Force + } + Write-Host ' Temporal artifact created' + #endregion + + #region Apply Mask + Write-Host '' + Write-Host ' Masking sensitive data' + if($shaloMaskFilers.Length -gt 0 -and $shaloMaskKeys.Length -gt 0){ + $shaloArtifactTempObjects = Get-ChildItem -Path $shaloArtifactTempPath -Force -Recurse + foreach($shaloItem in $shaloArtifactTempObjects){ + if($shaloMaskFilers.Trim() -contains $shaloItem.Extension){ + foreach($ShaloKey in $shaloMaskKeys){ + (Get-Content $shaloItem.FullName) -replace $ShaloKey.Trim(), $shaloMaskString| Set-Content $shaloItem.FullName + } + } + } + } + #endregion + + #region Compress and Collect + Write-Host '' + Write-Host '' + Write-Host ' Compressing artifact...' + Write-Host \" Artifact Temporal Path [$shaloArtifactTempPath]\" + Compress-Archive -Path $shaloArtifactTempPath -DestinationPath \"$shaloArtifactTempPath\\$shaloCollectedArtifactName.zip\" -Force -CompressionLevel $shaloCompressionLevel + Write-Host ' Artifact compressed' + + + Write-Host '' + Write-Host '' + Write-Host ' Collecting artifact...' + Write-Host \" Artifact Path [$shaloArtifactTempPath\\$shaloCollectedArtifactName.zip]\" + $Shalohash = Get-FileHash \"$shaloArtifactTempPath\\$shaloCollectedArtifactName.zip\" -Algorithm MD5 | Select Hash + Write-Host ' MD5 hash [' $Shalohash.Hash ']' + New-OctopusArtifact -Path \"$shaloArtifactTempPath\\$shaloCollectedArtifactName.zip\" -Name \"$shaloCollectedArtifactName.zip\" + Write-Host ' Artifact Collected' + + Write-Host '' + Write-Host '' + }else{ + Write-Host '' + Write-Host '' + Write-Warning ' Artifact not found!' + } + + Write-Host '' + Write-Host '' + Write-Host 'Done!' + +}catch{ + $ErrorMessage = $_.Exception.Message + $FailedItem = $_.Exception.ItemName + Write-Error \"We failed to processing $FailedItem. The error message was $ErrorMessage\" + exit 1 +}" + }, + "Parameters": [ + { + "Id": "1e50ae45-2a45-43c5-afdb-ac69f2527a37", + "Name": "shaloArtifactPath", + "Label": "Artifact path", + "HelpText": "Folder path or file full name + +Example: +* C:\\somepath +* C:\\somepath\\file.log", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bf2684fa-1ddf-4f46-9763-949962c543f3", + "Name": "shaloArtifactTempPath", + "Label": "Artifact temporal path", + "HelpText": "Temporal path that will use to compress files and mask sensitive data", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "380865db-7f30-43bc-9425-6af65a2b9e8d", + "Name": "shaloCompressionLevel", + "Label": "Compression Level", + "HelpText": null, + "DefaultValue": "Optimal", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Optimal|Optimal +Fastest|Fastest +NoCompression|No Compression" + } + }, + { + "Id": "5161cd62-df8b-4037-83b1-a674f695839b", + "Name": "shaloMaskFilers", + "Label": "Files with sensitive data", + "HelpText": "__Optional__ +Comma separated file extensions witch contains sensitive data. + +Example: .txt, .config", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2a205ab4-1790-436e-b000-42706ea0524b", + "Name": "shaloMaskKeys", + "Label": "Keys words that will be masked", + "HelpText": "__Optional__ +Comma separated sensitive words + +Example: username, password, 1234-4567-8912-3456", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "9a5036b8-dc0e-412a-a7ea-4aa2120d8e0d", + "Name": "shaloCollectedArtifactName", + "Label": "Collected artifact Name", + "HelpText": "Artifact Name + +Example: DeploymentLogs.#{Octopus.Release.Number}", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2019-05-29T23:09:57.329Z", + "OctopusVersion": "2019.3.1", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} diff --git a/step-templates/octopus-certificate-expiry-check.json.human b/step-templates/octopus-certificate-expiry-check.json.human new file mode 100644 index 000000000..70df5825b --- /dev/null +++ b/step-templates/octopus-certificate-expiry-check.json.human @@ -0,0 +1,147 @@ +{ + "Id": "8ca02a74-b2a1-482e-a2b3-5493016513ba", + "Name": "Octopus - Check Certficate Expiry", + "Description": "Checks for certificates stored in the [Octopus Certificate library](https://octopus.com/docs/deployment-examples/certificates) which are due to expire within N days. + +#### Output variable usage + +An [Output variable](https://octopus.com/docs/projects/variables/output-variables) named `ExpiringCertificateJson` is created with a JSON array of all of the matching expiring certificates with the following properties: +- Certificate Name +- Certificate Thumbprint +- Certificate SubjectCommonName +- Certificate Issuer +- Certificate NotAfter + +--- +The Output variable can then be used in a subsequent step. Consider a step named `Check Expiring Certs` which uses this step template. + +Adding the following PowerShell script to a subsequent step would iterate over the expiring certificates and highlight them in the Octopus Deployment log: + +```powershell + +Write-Highlight \"Expiring Certificates:\" +#{each cert in Octopus.Action[Check Expiring Certs].Output.ExpiringCertificateJson} +Write-Highlight \"- #{cert.Name} (#{cert.Thumbprint}) expires #{cert.NotAfter}\" +#{/each} +``` +**Note:** If the Output variable is empty, this indicates that there were no matching certificates that meet the specified expiry window. + +#### Pre-requisites +- Access to the Octopus Server from where the script template runs (e.g. deployment target or worker) is required. +- An Octopus Server running **2019.1** or greater, as Space support is required.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +function Get-OctopusCertificates { + Write-Debug \"Entering: Get-OctopusCertificates\" + + $octopus_uri = $OctopusParameters[\"Certificate.Expiry.Check.OctopusServerUrl\"].Trim('/') + $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"] + $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"Certificate.Expiry.Check.ApiKey\"] } + $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($OctopusParameters[\"Certificate.Expiry.Check.CertificateDomain\"])\" + + try { + # Get a list of certificates that match our domain search criteria. + $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items + + return $certificates_search | Where-Object { + $null -eq $_.ReplacedBy -and + $null -eq $_.Archived + } + } + catch { + Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message).\" + exit 1 + } +} + +Write-Host \"Checking for existing certificates in the Octopus Deploy Certificates Store.\" +$certificates = Get-OctopusCertificates + +if ($certificates) { + + # Handle weird behavior between Powershell 5 and Powershell 6+ + $certificate_count = 1 + if ($certificates.Count -ge 1) { + $certificate_count = $certificates.Count + } + + Write-Host \"Found $certificate_count matching domain: $($OctopusParameters[\"Certificate.Expiry.Check.CertificateDomain\"]).\" + Write-Host \"Checking to see if any expire within $($OctopusParameters[\"Certificate.Expiry.Check.Days\"]) days.\" + + # Check Expiry Dates + $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"Certificate.Expiry.Check.Days\"]) } + + if ($expiring_certificates) { + Write-Host \"Found certificates that expire with $($OctopusParameters[\"Certificate.Expiry.Check.Days\"]) days.\" + + $expiring_certs_json = $expiring_certificates | select Name, Thumbprint, SubjectCommonName, Issuer, NotAfter | ConvertTo-Json + Set-OctopusVariable -name \"ExpiringCertificateJson\" -value $expiring_certs_json + + } + else { + \tSet-OctopusVariable -name \"ExpiringCertificateJson\" -value \"\" + Write-Host \"Nothing to do here...\" + } + + exit 0 +} +" + }, + "Parameters": [ + { + "Id": "5fd60708-3183-4bfa-b042-a7ce6f8cb1a8", + "Name": "Certificate.Expiry.Check.OctopusServerUrl", + "Label": "Octopus Url", + "HelpText": "Provide the URL of your Octopus Server. The default is `#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}`. Cloud instances should use `Octopus.Web.ServerUri`. See [System Variables - Server](https://octopus.com/docs/projects/variables/system-variables#Systemvariables-Server) for more info.", + "DefaultValue": "#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "33fe56db-a089-4979-b103-a502b6e442ba", + "Name": "Certificate.Expiry.Check.ApiKey", + "Label": "Octopus API Key", + "HelpText": "An Octopus Deploy API key with access to change Certificates in the Certificate Store. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "00fc5429-bec2-44bc-bdb8-72c578d08d48", + "Name": "Certificate.Expiry.Check.Days", + "Label": "Expiring within N days", + "HelpText": "Enter the number of days to check within the certificates expire", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a20adf80-285c-4690-aa1e-39b3926dd310", + "Name": "Certificate.Expiry.Check.CertificateDomain", + "Label": "Certificate Domain to check", + "HelpText": "*Optional* certificate domain to search for. If this parameter is blank or empty, all certificates will be checked for expiry.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedAt": "2020-08-28T08:55:06.515Z", + "LastModifiedBy": "benjimac93", + "$Meta": { + "ExportedAt": "2021-08-23T12:40:10.975Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} diff --git a/step-templates/octopus-chain-deployment.json.human b/step-templates/octopus-chain-deployment.json.human new file mode 100644 index 000000000..0f0b3b316 --- /dev/null +++ b/step-templates/octopus-chain-deployment.json.human @@ -0,0 +1,1293 @@ +{ + "Id": "18392835-d50e-4ce9-9065-8e15a3c30954", + "Name": "Chain Deployment", + "Description": "Triggers a deployment of another project in Octopus", + "ActionType": "Octopus.Script", + "Version": 26, + "Packages": [], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptBody": "<# +----- Chain Deployment ----- +Authors & Credits + Paul Marston @paulmarsy (paul@marston.me) + Joe Waid @joewaid + Henrik Andersson @alfhenrik + Damian Brady @Damovisa + Aaron Burke @aburke-incomm (aburke@incomm.com) + Bob Hindy @bstr413 + Leo De Oliveira Dias @LeoDOD +Links + https://library.octopus.com/step-templates/18392835-d50e-4ce9-9065-8e15a3c30954 + https://github.com/OctopusDeploy/Library/commits/master/step-templates/octopus-chain-deployment.json + +----- Advanced Configuration Settings ----- +Variable names can use either of the following two formats: + Octopus.Action. - will apply to all steps in the deployment, e.g. + Octopus.Action.DebugLogging + Octopus.Action[Step Name]. - will apply to 'step name' alone, e.g. + Octopus.Action[Provision Virtual Machine].DeploymentRetryCount + +Available Settings: + - DebugLogging - set to 'True' or 'False' to log all GET web requests + - GuidedFailureMessage - will change the note used when submitting guided failure actions, the following variables will be replaced in the text: + #{GuidedFailureActionIndex} - The current count of interrupts for that step e.g. 1 + #{GuidedFailureAction} - The action being submitted by the step e.g. Retry + - DeploymentRetryCount - will override the number of times a deployment will be retried when unsuccessful and enable retrying when the failure option is set for a different option, default is 1 + - StepRetryCount - will override the number of times a deployment step will be retried before before submitting Ignore or Abort, default is 1 + - RetryWaitPeriod - an additional delay in seconds wait before retrying a failed step/deployment, default is 0 + - QueueTimeout - when scheduling a deployment for later a timeout must be provided, this allows a custom value, default is 30:00, format is hh:mm + - OctopusServerUrl - will override the base url used for all webrequests, making it possible to chain deployments on a different Octopus instance/server, or as a workaround for misconfigured node settings + +----- Changelog ----- +25. Feb 9, 2023 - Bob Hindy @bstr413 +\t- Fixed issue caused by version 23 where script would not work with on premise Octopus servers. (Reverted most of the changes 60ae653 and d614a2d made to this step template.) +24. Sept 13, 2021 - Mark Harrison @harrisonmeister + - Fixed issue where the Invoke-OctopusApi function would error with 404: NotFound when running Chain deployment on an Octopus instance + that runs under either a \"virtual directory\" / route prefix other than the route e.g https://my.octopus.app/octo/ +23. Aug 23rd, 2021 - Ben Macpherson benjimac93 + - Use Octopus.Web.ServerUri in place of Octopus.Web.BaseUrl if present. +22. Dec 31, 2020 - Josh Slaughter @joshgk00 +\t- Fixed an issue where the script was unable to create a release if Chained project contained a step with multiple package references +20. Sept 3, 2020 - Mark Harrison @harrisonmeister +\t- Included setting to TLS 1.2. +19. July 17, 2020 - Aaron Burke @aburke-incomm +\t- Update script handle Regex for Channel Tags in the CreateRelease Function +17. December 18, 2018 - Jim Burger @burgomg +\t- Added Spaces compatibility +16. November 22, 2018 - Patrick Kearney @patrickkearney + - Fixed an issue where the step was unable to pass a form variable containing an \"=\" in the value. +15. July 17, 2017 - Robert Glickman @robertglickman + - Fixed an issue where the step would fail in Octopus 3.15+ due to templated URIs not being handled +14. May 5, 2017 - Paul Marston @paulmarsy (paul@marston.me) + - Improved step parameter metadata & validation + - Added changelog, documentation of advanced settings + - Supports deploying to multiple environments in one step by specifying a lifecycle phase name e.g. 'Dev' + - Automated retry of the entire deployment as an additional failure handling option + - Number of step/deployment retries is configurable using a settings variable + - Supports Octopus scheduled deployments (can be used for reoccuring scheduled deploys, or autonomous deployment retry) + - Individual tenants as well as tenant tags can be deployed to + - Fixing a bug where Guided Failure is always evaluated to true + - Improved identification of valid environment&tenant promotions by using the 'deployment template' api + - If a release version has already been created, it will be used rather than erroring trying to recreate it + - Using 'Fail-Step' for better error logging + - Fixed a bug where log messages with an identical timestamp were repeatedly reported + - Added an option to wait before retrying a step/deployment + - A release's channel is taken into account when checking if an existing release version can be used +13. Apr 21, 2017 - Paul Marston @paulmarsy (paul@marston.me) + - Complete step template rewrite + - Improved logging + * Logs only written when chained deployment changes + * Progress of deployment step states is reported + * Errors & warnings are reported without interpretation in parent deployment + * Manual intervention & guided failure events are reported + * Queue position reported before deployment starts + * Verbose logging of useful API urls + - Multi-tenancy support and handling multiple tenant deploys from one chain step + - Support for skipping steps + - Support for prompted form variables + - Create release functionality supports using the version from the incremented version template or donor package + - Ability to snapshot update variables of a release before deploying + - Automated handling of guided failure scenarios e.g. retry on step failure, then abort if it errors a second time + - Transient Octopus API request failures are handled (e.g. we saw many deployments failing because of a request timeout) + - Post-deploy script support with variable substitution performed using the manifest variable set of the chained deployment with appropriate scoping applied (though not advanced scope specificity) + - Defaulting channel to a blank value which looks for one with 'IsDefault' set true + - Create release performs a simplified package version lookup to populate the 'SelectedPackages' field +12. Mar 30, 2017 - Joe Waid @joewaid + - Pass the Environments \"Guided Failure\" setting + - Check status after deployment when Chain_WaitForDeployment is true +11. Nov 21, 2016 - Henrik Andersson @alfhenrik + - Add Wait for deployment option to chain deployment step template +10. May 2, 2016 - Damian Brady @Damovisa + - Add Chained Deployment step template +#> +#Requires -Version 5 +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +$DefaultUrl = $OctopusParameters['Octopus.Web.BaseUrl'] +$Chain_BaseApiUrl = \"/api\" + +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +function Test-String { + param([Parameter(Position = 0)]$InputObject, [switch]$ForAbsence) + + $hasNoValue = [System.String]::IsNullOrWhiteSpace($InputObject) + if ($ForAbsence) { $hasNoValue } + else { -not $hasNoValue } +} + +function Get-OctopusSetting { + param([Parameter(Position = 0, Mandatory)][string]$Name, [Parameter(Position = 1, Mandatory)]$DefaultValue) + $formattedName = 'Octopus.Action.{0}' -f $Name + if ($OctopusParameters.ContainsKey($formattedName)) { + $value = $OctopusParameters[$formattedName] + if ($DefaultValue -is [int]) { return ([int]::Parse($value)) } + if ($DefaultValue -is [bool]) { return ([System.Convert]::ToBoolean($value)) } + if ($DefaultValue -is [array] -or $DefaultValue -is [hashtable] -or $DefaultValue -is [pscustomobject]) { return (ConvertFrom-Json -InputObject $value) } + return $value + } + else { return $DefaultValue } +} + +# Write functions are re-defined using octopus service messages to preserve formatting of log messages received from the chained deployment and avoid errors being twice wrapped in an ErrorRecord +function Write-Fatal($message, $exitCode = -1) { + if (Test-Path Function:\\Fail-Step) { + Fail-Step $message + } + else { + Write-Host (\"##octopus[stdout-error]`n{0}\" -f $message) + Exit $exitCode + } +} +function Write-Error($message) { Write-Host (\"##octopus[stdout-error]`n{0}`n##octopus[stdout-default]\" -f $message) } +function Write-Warning($message) { Write-Host (\"##octopus[stdout-warning]`n{0}`n##octopus[stdout-default]\" -f $message) } +function Write-Verbose($message) { Write-Host (\"##octopus[stdout-verbose]`n{0}`n##octopus[stdout-default]\" -f $message) } + +# Use \"Octopus.Web.ServerUri\" if it is available +if ([string]::IsNullOrWhiteSpace($OctopusParameters['Octopus.Web.ServerUri']) -eq $False) { + $DefaultUrl = $OctopusParameters['Octopus.Web.ServerUri'] +} + +$Chain_BaseUrl = (Get-OctopusSetting OctopusServerUrl $DefaultUrl).Trim('/') +if (Test-String $Chain_ApiKey -ForAbsence) { + Write-Fatal \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\" +} +$DebugLogging = Get-OctopusSetting DebugLogging $false + +# Replace any \"virtual directory\" or route prefix e.g from the Links collection used +# with the api e.g. /api +function Format-LinksUri { + param( + [Parameter(Position = 0, Mandatory)] + $Uri + ) + $Uri = $Uri -replace '.*/api', '/api' + Return $Uri +} +# Replace any \"virtual directory\" or route prefix e.g from the Links collection used +# with the web app e.g. /app +function Format-WebLinksUri { + param( + [Parameter(Position = 0, Mandatory)] + $Uri + ) + $Uri = $Uri -replace '.*/app', '/app' + Return $Uri +} + +function Invoke-OctopusApi { + param( + [Parameter(Position = 0, Mandatory)]$Uri, + [ValidateSet('Get', 'Post', 'Put')]$Method = 'Get', + $Body, + [switch]$GetErrorResponse + ) + # Replace query string example parameters e.g. {?skip,take,partialName} + # Replace any \"virtual directory\" or route prefix e.g from the Links collection. + $Uri = $Uri -replace '{.*?}', '' -replace '.*/api', '/api' + $requestParameters = @{ + Uri = ('{0}/{1}' -f $Chain_BaseUrl, $Uri.TrimStart('/')) + Method = $Method + Headers = @{ 'X-Octopus-ApiKey' = $Chain_ApiKey } + UseBasicParsing = $true + } + if ($Method -ne 'Get' -or $DebugLogging) { + Write-Verbose ('{0} {1}' -f $Method.ToUpperInvariant(), $requestParameters.Uri) + } + if ($null -ne $Body) { + $requestParameters.Add('Body', (ConvertTo-Json -InputObject $Body -Depth 10)) + Write-Verbose $requestParameters.Body + } + + $wait = 0 + $webRequest = $null + while ($null -eq $webRequest) {\t + try { + $webRequest = Invoke-WebRequest @requestParameters + } + catch { + if ($_.Exception -is [System.Net.WebException] -and $null -ne $_.Exception.Response) { + $errorResponse = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()).ReadToEnd() + Write-Verbose (\"Error Response:`n{0}\" -f $errorResponse) + if ($GetErrorResponse) { + return ($errorResponse | ConvertFrom-Json) + } + if ($_.Exception.Response.StatusCode -in @([System.Net.HttpStatusCode]::NotFound, [System.Net.HttpStatusCode]::InternalServerError, [System.Net.HttpStatusCode]::BadRequest, [System.Net.HttpStatusCode]::Unauthorized)) { + Write-Fatal $_.Exception.Message + } + } + if ($wait -eq 120) { + Write-Fatal (\"Octopus web request ({0}: {1}) failed & the maximum number of retries has been exceeded:`n{2}\" -f $Method.ToUpperInvariant(), $requestParameters.Uri, $_.Exception.Message) -43 + } + $wait = switch ($wait) { + 0 { 30 } + 30 { 60 } + 60 { 120 } + } + Write-Warning (\"Octopus web request ({0}: {1}) failed & will be retried in $wait seconds:`n{2}\" -f $Method.ToUpperInvariant(), $requestParameters.Uri, $_.Exception.Message) + Start-Sleep -Seconds $wait + } + } + $webRequest.Content | ConvertFrom-Json | Write-Output +} + +function Get-FilteredOctopusItem { + param( + $itemList, + $itemName + ) + + if ($itemList.Items.Count -eq 0) { + Write-Fatal \"Unable to find $itemName. Exiting with an exit code of 1.\" + Exit 1 + } + + $item = $itemList.Items | Where-Object { $_.Name -eq $itemName } + + if ($null -eq $item) { + Write-Fatal \"Unable to find $itemName. Exiting with an exit code of 1.\" + exit 1 + } + + if ($item -is [array]) { + Write-Fatal \"More than one item exists with the name $itemName. Exiting with an exit code of 1.\" + exit 1 + } + + return $item +} + +function Test-SpacesApi { + Write-Verbose \"Checking API compatibility\"; + $rootDocument = Invoke-OctopusApi \"api/\"; + if ($null -ne $rootDocument.Links -and $null -ne $rootDocument.Links.Spaces) { + Write-Verbose \"Spaces API found\" + return $true; + } + Write-Verbose \"Pre-spaces API found\" + return $false; +} + +if (Test-SpacesApi) { + $spaceId = $OctopusParameters['Octopus.Space.Id']; + if ([string]::IsNullOrWhiteSpace($spaceId)) { + throw \"This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which isn't expected - please reach out to our support team at https://help.octopus.com if you encounter this error.\"; + } + $Chain_BaseApiUrl = \"/api/$spaceId\" ; +} + +enum GuidedFailure { + Default + Enabled + Disabled + RetryIgnore + RetryAbort + Ignore + RetryDeployment +} + +class DeploymentContext { + hidden $BaseUrl + hidden $BaseApiUrl + DeploymentContext($baseUrl, $baseApiUrl) { + $this.BaseUrl = $baseUrl + $this.BaseApiUrl = $baseApiUrl + } + + hidden $Project + hidden $Lifecycle + [void] SetProject($projectName) { + $this.Project = Invoke-OctopusApi \"$($this.BaseApiUrl)/projects/all\" | Where-Object Name -eq $projectName + if ($null -eq $this.Project) { + Write-Fatal \"Project $projectName not found\" + } + Write-Host \"Project: $($this.Project.Name)\" + Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Project.Links.Self)\" + + $this.Lifecycle = Invoke-OctopusApi (\"$($this.BaseApiUrl)/lifecycles/{0}\" -f $this.Project.LifecycleId) + Write-Host \"Project Lifecycle: $($this.Lifecycle.Name)\" + Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Lifecycle.Links.Self)\" + } + + hidden $Channel + [void] SetChannel($channelName) { + $useDefaultChannel = Test-String $channelName -ForAbsence + $this.Channel = Invoke-OctopusApi (Format-LinksUri -Uri $this.Project.Links.Channels) | ForEach-Object Items | Where-Object { $useDefaultChannel -and $_.IsDefault -or $_.Name -eq $channelName } + if ($null -eq $this.Channel) { + Write-Fatal \"$(if ($useDefaultChannel) { 'Default channel' } else { \"Channel $channelName\" }) not found\" + } + Write-Host \"Channel: $($this.Channel.Name)\" + Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Channel.Links.Self)\" + + if ($null -ne $this.Channel.LifecycleId) { + $this.Lifecycle = Invoke-OctopusApi (\"$($this.BaseApiUrl)/lifecycles/{0}\" -f $this.Channel.LifecycleId) + Write-Host \"Channel Lifecycle: $($this.Lifecycle.Name)\" + Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Lifecycle.Links.Self)\" + } + } + + hidden $Release + [void] SetRelease($releaseVersion) { + if (Test-String $releaseVersion) { + $this.Release = Invoke-OctopusApi (\"$($this.BaseApiUrl)/projects/{0}/releases/{1}\" -f $this.Project.Id, $releaseVersion) -GetErrorResponse + if ($null -ne $this.Release.ErrorMessage) { + Write-Fatal $this.Release.ErrorMessage + } + } + else { + $this.Release = Invoke-OctopusApi (Format-LinksUri -Uri $this.Channel.Links.Releases) | ForEach-Object Items | Select-Object -First 1 + if ($null -eq $this.Release) { + Write-Fatal \"There are no releases for channel $($this.Channel.Name)\" + } + } + Write-Host \"Release: $($this.Release.Version)\" + Write-Verbose \"`t$($this.BaseUrl)$($this.BaseApiUrl)/releases/$($this.Release.Id)\" + } + + [void] CreateRelease($releaseVersion) { + $template = Invoke-OctopusApi ('{0}/template?channel={1}' -f (Format-LinksUri -Uri $this.Project.Links.DeploymentProcess), $this.Channel.Id) + $selectedPackages = @() + Write-Host 'Resolving package versions...' + $template.Packages | ForEach-Object { + $preReleaseTag = $this.Channel.Rules | Where-Object Actions -contains $_.StepName | Where-Object { $null -ne $_ } | ForEach-Object { '&preReleaseTag={0}' -f $([System.Net.WebUtility]::UrlEncode($_.Tag)) } + $versionRange = $this.Channel.Rules | Where-Object Actions -contains $_.StepName | Where-Object { $null -ne $_ } | ForEach-Object { '&versionRange={0}' -f $([System.Net.WebUtility]::UrlEncode($_.VersionRange)) } + + $package = Invoke-OctopusApi (\"$($this.BaseApiUrl)/feeds/{0}/packages?packageId={1}&partialMatch=false&includeMultipleVersions=false&includeNotes=false&includePreRelease=true&take=1{2}{3}\" -f $_.FeedId, $_.PackageId, $preReleaseTag, $versionRange) + $packageDesc = \"$($package.Title) @ $($package.Version) for step $($_.StepName)\" + if ( $_.PackageReferenceName ) { + $packageDesc += \"/$($_.PackageReferenceName)\" + } + Write-Host \"Found $packageDesc\" + + $selectedPackages += @{ + StepName = $_.StepName + ActionName = $_.ActionName + PackageReferenceName = $_.PackageReferenceName + Version = $package.Version + } + + if ( (Test-String $releaseVersion -ForAbsence) -and ($_.StepName -eq $template.VersioningPackageStepName) ) { + Write-Host \"Release will be created using the version number from package step $($template.VersioningPackageStepName): $($package.Version)\" + $releaseVersion = $package.Version + } + } + if (Test-String $releaseVersion) { + $this.Release = Invoke-OctopusApi (\"$($this.BaseApiUrl)/projects/{0}/releases/{1}\" -f $this.Project.Id, $releaseVersion) -GetErrorResponse + if ( ($null -eq $this.Release.ErrorMessage) -and ($this.Release.Version -ieq $releaseVersion) -and ($this.Release.ChannelId -eq $this.Channel.Id) ) { + Write-Host \"Release version $($this.Release.Version) has already been created, selecting it for deployment\" + Write-Verbose \"`t$($this.BaseUrl)$($this.BaseApiUrl)/releases/$($this.Release.Id)\" + return + } + } + else { + Write-Host \"Release will be created using the incremented release version: $($template.NextVersionIncrement)\" + $releaseVersion = $template.NextVersionIncrement + } + + $this.Release = Invoke-OctopusApi \"$($this.BaseApiUrl)/releases?ignoreChannelRules=false\" -Method Post -Body @{ + ProjectId = $this.Project.Id + ChannelId = $this.Channel.Id + Version = $releaseVersion + SelectedPackages = $selectedPackages + } -GetErrorResponse + if ($null -ne $this.Release.ErrorMessage) { + Write-Fatal \"$($this.Release.ErrorMessage)`n$($this.Release.Errors -join \"`n\")\" + } + Write-Host \"Release $($this.Release.Version) has been successfully created\" + Write-Verbose \"`t$($this.BaseUrl)$($this.BaseApiUrl)/releases/$($this.Release.Id)\" + } + + [void] UpdateVariableSnapshot() { + $this.Release = Invoke-OctopusApi (Format-LinksUri -Uri $this.Release.Links.SnapshotVariables) -Method Post + Write-Host 'Variables snapshot update performed. The release now references the latest variables.' + } + + hidden $DeploymentTemplate + [void] GetDeploymentTemplate() { + Write-Host 'Getting deployment template for release...' + $this.DeploymentTemplate = Invoke-OctopusApi (Format-LinksUri -Uri $this.Release.Links.DeploymentTemplate) + } + + hidden [bool]$UseGuidedFailure + hidden [string[]]$GuidedFailureActions + hidden [string]$GuidedFailureMessage + hidden [int]$DeploymentRetryCount + [void] SetGuidedFailure([GuidedFailure]$guidedFailure, $guidedFailureMessage) { + $this.UseGuidedFailure = switch ($guidedFailure) { + ([GuidedFailure]::Default) { [System.Convert]::ToBoolean($global:OctopusUseGuidedFailure) } + ([GuidedFailure]::Enabled) { $true } + ([GuidedFailure]::Disabled) { $false } + ([GuidedFailure]::RetryIgnore) { $true } + ([GuidedFailure]::RetryAbort) { $true } + ([GuidedFailure]::Ignore) { $true } + ([GuidedFailure]::RetryDeployment) { $false } + } + Write-Host \"Setting Guided Failure: $($this.UseGuidedFailure)\" + + $retryActions = @(1..(Get-OctopusSetting StepRetryCount 1) | ForEach-Object { 'Retry' }) + $this.GuidedFailureActions = switch ($guidedFailure) { + ([GuidedFailure]::Default) { $null } + ([GuidedFailure]::Enabled) { $null } + ([GuidedFailure]::Disabled) { $null } + ([GuidedFailure]::RetryIgnore) { $retryActions + @('Ignore') } + ([GuidedFailure]::RetryAbort) { $retryActions + @('Abort') } + ([GuidedFailure]::Ignore) { @('Ignore') } + ([GuidedFailure]::RetryDeployment) { $null } + } + if ($null -ne $this.GuidedFailureActions) { + Write-Host \"Automated Failure Guidance: $($this.GuidedFailureActions -join '; ') \" + } + $this.GuidedFailureMessage = $guidedFailureMessage + + $defaultRetries = if ($guidedFailure -eq [GuidedFailure]::RetryDeployment) { 1 } else { 0 } + $this.DeploymentRetryCount = Get-OctopusSetting DeploymentRetryCount $defaultRetries + if ($this.DeploymentRetryCount -ne 0) { + Write-Host \"Failed Deployments will be retried #$($this.DeploymentRetryCount) times\" + } + } + + [bool]$ForcePackageDownload + [void] SetForcePackageDownload($forcePackageDownload) { + if ($forcePackageDownload -eq $true) { + $this.ForcePackageDownload = $true + Write-Host 'Deployment will Force Package Download...' + return + } + $this.ForcePackageDownload = $false + Write-Host 'Deployment will not Force Package Download.' + return + + } + + [bool]$WaitForDeployment + hidden [datetime]$QueueTime + hidden [datetime]$QueueTimeExpiry + [void] SetSchedule($deploySchedule) { + if (Test-String $deploySchedule -ForAbsence) { + Write-Fatal 'The deployment schedule step parameter was not found.' + } + if ($deploySchedule -eq 'WaitForDeployment') { + $this.WaitForDeployment = $true + Write-Host 'Deployment will be queued to start immediatley...' + return + } + $this.WaitForDeployment = $false + if ($deploySchedule -eq 'NoWait') { + Write-Host 'Deployment will be queued to start immediatley...' + return + } + <# + ^(?i) - Case-insensitive matching + (?: + (?MON|TUE|WED|THU|FRI|SAT|SUN)? - Capture an optional day + \\s*@\\s* - '@' indicates deploying at a specific time + (?(?:[01]?[0-9]|2[0-3]):[0-5][0-9]) - Captures the time of day, in 24 hour format + )? - Day & TimeOfDay are optional + \\s* + (?: + \\+\\s* - '+' indicates deploying after a length of tie + (? + \\d{1,3} - Match 1 to 3 digits + (?::[0-5][0-9])? - Optionally match a colon and 00 to 59, this denotes if the previous 1-3 digits are hours or minutes + ) + )?$ - TimeSpan is optional + #> + $parsedSchedule = [regex]::Match($deploySchedule, '^(?i)(?:(?MON|TUE|WED|THU|FRI|SAT|SUN)?\\s*@\\s*(?(?:[01]?[0-9]|2[0-3]):[0-5][0-9]))?\\s*(?:\\+\\s*(?\\d{1,3}(?::[0-5][0-9])?))?$') + if (!$parsedSchedule.Success) { + Write-Fatal \"The deployment schedule step parameter contains an invalid value. Valid values are 'WaitForDeployment', 'NoWait' or a schedule in the format '[[DayOfWeek] @ HH:mm] [+ ]'\" + } + $this.QueueTime = Get-Date + if ($parsedSchedule.Groups['Day'].Success) { + Write-Verbose \"Parsed Day: $($parsedSchedule.Groups['Day'].Value)\" + while (!$this.QueueTime.DayOfWeek.ToString().StartsWith($parsedSchedule.Groups['Day'].Value)) { + $this.QueueTime = $this.QueueTime.AddDays(1) + } + } + if ($parsedSchedule.Groups['TimeOfDay'].Success) { + Write-Verbose \"Parsed Time Of Day: $($parsedSchedule.Groups['TimeOfDay'].Value)\" + $timeOfDay = [datetime]::ParseExact($parsedSchedule.Groups['TimeOfDay'].Value, 'HH:mm', $null) + $this.QueueTime = $this.QueueTime.Date + $timeOfDay.TimeOfDay + } + if ($parsedSchedule.Groups['TimeSpan'].Success) { + Write-Verbose \"Parsed Time Span: $($parsedSchedule.Groups['TimeSpan'].Value)\" + $timeSpan = $parsedSchedule.Groups['TimeSpan'].Value.Split(':') + $hoursToAdd = if ($timeSpan.Length -eq 2) { $timeSpan[0] } else { 0 } + $minutesToAdd = if ($timeSpan.Length -eq 2) { $timeSpan[1] } else { $timeSpan[0] } + $this.QueueTime = $this.QueueTime.Add((New-TimeSpan -Hours $hoursToAdd -Minutes $minutesToAdd)) + } + Write-Host \"Deployment will be queued to start at: $($this.QueueTime.ToLongDateString()) $($this.QueueTime.ToLongTimeString())\" + Write-Verbose \"Local Time: $($this.QueueTime.ToLocalTime().ToString('r'))\" + Write-Verbose \"Universal Time: $($this.QueueTime.ToUniversalTime().ToString('o'))\" + $this.QueueTimeExpiry = $this.QueueTime.Add([timespan]::ParseExact((Get-OctopusSetting QueueTimeout '00:30'), \"hh\\:mm\", $null)) + Write-Verbose \"Queued deployment will expire on: $($this.QueueTimeExpiry.ToUniversalTime().ToString('o'))\" + } + + hidden $Environments + [void] SetEnvironment($environmentName) { + $lifecyclePhaseEnvironments = $this.Lifecycle.Phases | Where-Object Name -eq $environmentName | ForEach-Object { + $_.AutomaticDeploymentTargets + $_.OptionalDeploymentTargets + } + $this.Environments = $this.DeploymentTemplate.PromoteTo | Where-Object { $_.Id -in $lifecyclePhaseEnvironments -or $_.Name -ieq $environmentName } + if ($null -eq $this.Environments) { + Write-Fatal \"The specified environment ($environmentName) was not found or not eligible for deployment of the release ($($this.Release.Version)). Verify that the release has been deployed to all required environments before it can be promoted to this environment. Once you have corrected these problems you can try again.\" + } + Write-Host \"Environments: $(($this.Environments | ForEach-Object Name) -join ', ')\" + } + + [bool] $IsTenanted + hidden $Tenants + [void] SetTenants($tenantFilter) { + $this.IsTenanted = Test-String $tenantFilter + if (!$this.IsTenanted) { + return + } + $tenantPromotions = $this.DeploymentTemplate.TenantPromotions | ForEach-Object Id + $this.Tenants = $tenantFilter.Split(\"`n\") | ForEach-Object { [uri]::EscapeUriString($_.Trim()) } | ForEach-Object { + $criteria = if ($_ -like '*/*') { 'tags' } else { 'name' } + + $tenantResults = Invoke-OctopusApi (\"$($this.BaseApiUrl)/tenants/all?projectId={0}&{1}={2}\" -f $this.Project.Id, $criteria, $_) -GetErrorResponse + if ($tenantResults -isnot [array] -and $tenantResults.ErrorMessage) { + Write-Warning \"Full Exception: $($tenantResults.FullException)\" + Write-Fatal $tenantResults.ErrorMessage + } + $tenantResults + } | Where-Object Id -in $tenantPromotions + + if ($null -eq $this.Tenants) { + Write-Fatal \"No eligible tenants found for deployment of the release ($($this.Release.Version)). Verify that the tenants have been associated with the project.\" + } + Write-Host \"Tenants: $(($this.Tenants | ForEach-Object Name) -join ', ')\" + } + + [DeploymentController[]] GetDeploymentControllers() { + Write-Verbose 'Determining eligible environments & tenants. Retrieving deployment previews...' + $deploymentControllers = @() + foreach ($environment in $this.Environments) { + $envPrefix = if ($this.Environments.Count -gt 1) { $environment.Name } + if ($this.IsTenanted) { + foreach ($tenant in $this.Tenants) { + $tenantPrefix = if ($this.Tenants.Count -gt 1) { $tenant.Name } + if ($this.DeploymentTemplate.TenantPromotions | Where-Object Id -eq $tenant.Id | ForEach-Object PromoteTo | Where-Object Id -eq $environment.Id) { + $logPrefix = ($envPrefix, $tenantPrefix | Where-Object { $null -ne $_ }) -join '::' + $deploymentControllers += [DeploymentController]::new($this, $logPrefix, $environment, $tenant) + } + } + } + else { + $deploymentControllers += [DeploymentController]::new($this, $envPrefix, $environment, $null) + } + } + return $deploymentControllers + } +} + +class DeploymentController { + hidden [string]$BaseUrl + hidden [DeploymentContext]$DeploymentContext + hidden [string]$LogPrefix + hidden [object]$Environment + hidden [object]$Tenant + hidden [object]$DeploymentPreview + hidden [int]$DeploymentRetryCount + hidden [int]$DeploymentAttempt + + DeploymentController($deploymentContext, $logPrefix, $environment, $tenant) { + $this.BaseUrl = $deploymentContext.BaseUrl + $this.DeploymentContext = $deploymentContext + if (Test-String $logPrefix) { + $this.LogPrefix = \"[${logPrefix}] \" + } + $this.Environment = $environment + $this.Tenant = $tenant + if ($tenant) { + $this.DeploymentPreview = Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/releases/{0}/deployments/preview/{1}/{2}\" -f $this.DeploymentContext.Release.Id, $this.Environment.Id, $this.Tenant.Id) + } + else { + $this.DeploymentPreview = Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/releases/{0}/deployments/preview/{1}\" -f $this.DeploymentContext.Release.Id, $this.Environment.Id) + } + $this.DeploymentRetryCount = $deploymentContext.DeploymentRetryCount + $this.DeploymentAttempt = 0 + } + + hidden [string[]]$SkipActions = @() + [void] SetStepsToSkip($stepsToSkip) { + $comparisonArray = $stepsToSkip.Split(\"`n\") | ForEach-Object Trim + $this.SkipActions = $this.DeploymentPreview.StepsToExecute | Where-Object { + $_.CanBeSkipped -and ($_.ActionName -in $comparisonArray -or $_.ActionNumber -in $comparisonArray) + } | ForEach-Object { + $logMessage = \"Skipping Step $($_.ActionNumber): $($_.ActionName)\" + if ($this.LogPrefix) { Write-Verbose \"$($this.LogPrefix)$logMessage\" } + else { Write-Host $logMessage } + $_.ActionId + } + } + + + hidden [string[]]$SpecificMachineIds + [void] SetSpecificMachineIds($specificMachineNames) { + $this.SpecificMachineIds = @() + $specificMachineNames.Split(\"`n\") | ForEach-Object { + Write-Host \"Translating $_ to an Id. First checking to see if it is already an Id.\" + if ($_.Trim().StartsWith(\"Machines-\")) { + Write-Host \"$_ is already an Id, no need to look that up.\" + $this.SpecificMachineIds += $_.Trim() + continue + } + $itemNameToFind = $_.Trim() + Write-Host \"Attempting to find Deployment Target with the name of $itemNameToFind\" + $itemList = Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/machines/?partialName=$([uri]::EscapeDataString($itemNameToFind))&skip=0&take=100\" ) -GetErrorResponse + $machineObject = Get-FilteredOctopusItem -itemList $itemList -itemName $itemNameToFind + Write-Host \"Successfully found $itemNameToFind with id of $($machineObject.Id)\" + $this.SpecificMachineIds += $machineObject.Id + } + } + + hidden [hashtable]$FormValues + [void] SetFormValues($formValuesToSet) { + $this.FormValues = @{} + $this.DeploymentPreview.Form.Values | Get-Member -MemberType NoteProperty | ForEach-Object { + $this.FormValues.Add($_.Name, $this.DeploymentPreview.Form.Values.$($_.Name)) + } + + $formValuesToSet.Split(\"`n\") | ForEach-Object { + $entry = $_.Split('=') | ForEach-Object Trim + $entryName, $entryValues = $entry + $entry = @($entryName, $($entryValues -join \"=\")) + $this.DeploymentPreview.Form.Elements | Where-Object { $_.Control.Name -ieq $entry[0] } | ForEach-Object { + $logMessage = \"Setting Form Value '$($_.Control.Label)' to: $($entry[1])\" + if ($this.LogPrefix) { Write-Verbose \"$($this.LogPrefix)$logMessage\" } + else { Write-Host $logMessage } + $this.FormValues[$_.Name] = $entry[1] + } + } + } +\t + [ServerTask]$Task + [void] Start() { + $request = @{ + ReleaseId = $this.DeploymentContext.Release.Id + EnvironmentId = $this.Environment.Id + SkipActions = $this.SkipActions + FormValues = $this.FormValues + SpecificMachineIds = $this.SpecificMachineIds + ForcePackageDownload = $this.DeploymentContext.ForcePackageDownload + UseGuidedFailure = $this.DeploymentContext.UseGuidedFailure + } + if ($this.DeploymentContext.QueueTime -ne [datetime]::MinValue) { $request.Add('QueueTime', $this.DeploymentContext.QueueTime.ToUniversalTime().ToString('o')) } + if ($this.DeploymentContext.QueueTimeExpiry -ne [datetime]::MinValue) { $request.Add('QueueTimeExpiry', $this.DeploymentContext.QueueTimeExpiry.ToUniversalTime().ToString('o')) } + if ($this.Tenant) { $request.Add('TenantId', $this.Tenant.Id) } + + $deployment = Invoke-OctopusApi \"$($this.DeploymentContext.BaseApiUrl)/deployments\" -Method Post -Body $request -GetErrorResponse + if ($deployment.ErrorMessage) { Write-Fatal \"$($deployment.ErrorMessage)`n$($deployment.Errors -join \"`n\")\" } + Write-Host \"Queued $($deployment.Name)...\" + Write-Host \"`t$($this.BaseUrl)$(Format-WebLinksUri -Uri $deployment.Links.Web)\" + Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $deployment.Links.Self)\" + Write-Verbose \"`t$($this.BaseUrl)$($this.DeploymentContext.BaseApiUrl)/deploymentprocesses/$($deployment.DeploymentProcessId)\" + Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $deployment.Links.Variables)\" + Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $deployment.Links.Task)/details\" + + $this.Task = [ServerTask]::new($this.DeploymentContext, $deployment, $this.LogPrefix) + } + + [bool] PollCheck() { + $this.Task.Poll() + if ($this.Task.IsCompleted -and !$this.Task.FinishedSuccessfully -and $this.DeploymentAttempt -lt $this.DeploymentRetryCount) { + $retryWaitPeriod = New-TimeSpan -Seconds (Get-OctopusSetting RetryWaitPeriod 0) + $waitText = if ($retryWaitPeriod.TotalSeconds -gt 0) { + $minutesText = if ($retryWaitPeriod.Minutes -gt 1) { \" $($retryWaitPeriod.Minutes) minutes\" } elseif ($retryWaitPeriod.Minutes -eq 1) { \" $($retryWaitPeriod.Minutes) minute\" } + $secondsText = if ($retryWaitPeriod.Seconds -gt 1) { \" $($retryWaitPeriod.Seconds) seconds\" } elseif ($retryWaitPeriod.Seconds -eq 1) { \" $($retryWaitPeriod.Seconds) second\" } + \"Waiting${minutesText}${secondsText} before \" + } + $this.DeploymentAttempt++ + Write-Error \"$($this.LogPrefix)Deployment failed. ${waitText}Queuing retry #$($this.DeploymentAttempt) of $($this.DeploymentRetryCount)...\" + if ($retryWaitPeriod.TotalSeconds -gt 0) { + Start-Sleep -Seconds $retryWaitPeriod.TotalSeconds + } + $this.Start() + return $true + } + return !$this.Task.IsCompleted + } +} + +class ServerTask { + hidden [DeploymentContext]$DeploymentContext + hidden [object]$Deployment + hidden [string]$LogPrefix + + hidden [bool] $IsCompleted = $false + hidden [bool] $FinishedSuccessfully + hidden [string] $ErrorMessage + + hidden [int]$PollCount = 0 + hidden [bool]$HasInterruptions = $false + hidden [hashtable]$State = @{} + hidden [System.Collections.Generic.HashSet[string]]$Logs + + ServerTask($deploymentContext, $deployment, $logPrefix) { + $this.DeploymentContext = $deploymentContext + $this.Deployment = $deployment + $this.LogPrefix = $logPrefix + $this.Logs = [System.Collections.Generic.HashSet[string]]::new() + } + + [void] Poll() {\t + if ($this.IsCompleted) { return } + + $details = Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/tasks/{0}/details?verbose=false&tail=30\" -f $this.Deployment.TaskId) + $this.IsCompleted = $details.Task.IsCompleted + $this.FinishedSuccessfully = $details.Task.FinishedSuccessfully + $this.ErrorMessage = $details.Task.ErrorMessage + + $this.PollCount++ + if ($this.PollCount % 10 -eq 0) { + $this.Verbose(\"$($details.Task.State). $($details.Task.Duration), $($details.Progress.EstimatedTimeRemaining)\") + } + + if ($details.Task.HasPendingInterruptions) { $this.HasInterruptions = $true } + $this.LogQueuePosition($details.Task) + $activityLogs = $this.FlattenActivityLogs($details.ActivityLogs) + $this.WriteLogMessages($activityLogs) + } + + hidden [bool] IfNewState($firstKey, $secondKey, $value) { + $key = '{0}/{1}' -f $firstKey, $secondKey + $containsKey = $this.State.ContainsKey($key) + if ($containsKey) { return $false } + $this.State[$key] = $value + return $true + } + + hidden [bool] HasChangedState($firstKey, $secondKey, $value) { + $key = '{0}/{1}' -f $firstKey, $secondKey + $hasChanged = if (!$this.State.ContainsKey($key)) { $true } else { $this.State[$key] -ne $value } + if ($hasChanged) { + $this.State[$key] = $value + } + return $hasChanged + } + + hidden [object] GetState($firstKey, $secondKey) { return $this.State[('{0}/{1}' -f $firstKey, $secondKey)] } + + hidden [void] ResetState($firstKey, $secondKey) { $this.State.Remove(('{0}/{1}' -f $firstKey, $secondKey)) } + + hidden [void] Error($message) { Write-Error \"$($this.LogPrefix)${message}\" } + hidden [void] Warn($message) { Write-Warning \"$($this.LogPrefix)${message}\" } + hidden [void] Host($message) { Write-Host \"$($this.LogPrefix)${message}\" } + hidden [void] Verbose($message) { Write-Verbose \"$($this.LogPrefix)${message}\" } + + hidden [psobject[]] FlattenActivityLogs($ActivityLogs) { + $flattenedActivityLogs = { @() }.Invoke() + $this.FlattenActivityLogs($ActivityLogs, $null, $flattenedActivityLogs) + return $flattenedActivityLogs + } + + hidden [void] FlattenActivityLogs($ActivityLogs, $Parent, $flattenedActivityLogs) { + foreach ($log in $ActivityLogs) { + $log | Add-Member -MemberType NoteProperty -Name Parent -Value $Parent + $insertBefore = $null -eq $log.Parent -and $log.Status -eq 'Running'\t + if ($insertBefore) { $flattenedActivityLogs.Add($log) } + foreach ($childLog in $log.Children) { + $this.FlattenActivityLogs($childLog, $log, $flattenedActivityLogs) + } + if (!$insertBefore) { $flattenedActivityLogs.Add($log) } + } + } + + hidden [void] LogQueuePosition($Task) { + if ($Task.HasBeenPickedUpByProcessor) { + $this.ResetState($Task.Id, 'QueuePosition') + return + } +\t\t + $queuePosition = (Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/tasks/{0}/queued-behind\" -f $this.Deployment.TaskId)).Items.Count + if ($this.HasChangedState($Task.Id, 'QueuePosition', $queuePosition) -and $queuePosition -ne 0) { + $this.Host(\"Queued behind $queuePosition tasks...\") + } + } + + hidden [void] WriteLogMessages($ActivityLogs) { + $interrupts = if ($this.HasInterruptions) { + Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/interruptions?regarding={0}\" -f $this.Deployment.TaskId) | ForEach-Object Items + } + foreach ($activity in $ActivityLogs) { + $correlatedInterrupts = $interrupts | Where-Object CorrelationId -eq $activity.Id + $correlatedInterrupts | Where-Object IsPending -eq $false | ForEach-Object { $this.LogInterruptMessages($activity, $_) } + + $this.LogStepTransition($activity) + $this.LogErrorsAndWarnings($activity) + $correlatedInterrupts | Where-Object IsPending -eq $true | ForEach-Object { + $this.LogInterruptMessages($activity, $_) + $this.HandleInterrupt($_) + } + } + } + + hidden [void] LogStepTransition($ActivityLog) { + if ($ActivityLog.ShowAtSummaryLevel -and $ActivityLog.Status -ne 'Pending') { + $existingState = $this.GetState($ActivityLog.Id, 'Status') + if ($this.HasChangedState($ActivityLog.Id, 'Status', $ActivityLog.Status)) { + $existingStateText = if ($existingState) { \"$existingState -> \" } + $this.Host(\"$($ActivityLog.Name) ($existingStateText$($ActivityLog.Status))\") + } + } + } + + hidden [void] LogErrorsAndWarnings($ActivityLog) { + foreach ($logEntry in $ActivityLog.LogElements) { + if ($logEntry.Category -eq 'Info') { continue } + if ($this.Logs.Add(($ActivityLog.Id, $logEntry.OccurredAt, $logEntry.MessageText -join '/'))) { + switch ($logEntry.Category) { + 'Fatal' { + if ($ActivityLog.Parent) { + $this.Error(\"FATAL: During $($ActivityLog.Parent.Name)\") + $this.Error(\"FATAL: $($logEntry.MessageText)\") + } + } + 'Error' { $this.Error(\"[$($ActivityLog.Parent.Name)] $($logEntry.MessageText)\") } + 'Warning' { $this.Warn(\"[$($ActivityLog.Parent.Name)] $($logEntry.MessageText)\") } + } + } + } + } + + hidden [void] LogInterruptMessages($ActivityLog, $Interrupt) { + $message = $Interrupt.Form.Elements | Where-Object Name -eq Instructions | ForEach-Object Control | ForEach-Object Text + if ($Interrupt.IsPending -and $this.HasChangedState($Interrupt.Id, $ActivityLog.Parent.Name, $message)) { + $this.Warn(\"Deployment is paused at '$($ActivityLog.Parent.Name)' for manual intervention: $message\") + } + if ($null -ne $Interrupt.ResponsibleUserId -and $this.HasChangedState($Interrupt.Id, 'ResponsibleUserId', $Interrupt.ResponsibleUserId)) { + $user = Invoke-OctopusApi (Format-LinksUri -Uri $Interrupt.Links.User) + $emailText = if (Test-String $user.EmailAddress) { \" ($($user.EmailAddress))\" } + $this.Warn(\"$($user.DisplayName)$emailText has taken responsibility for the manual intervention\") + } + $manualAction = $Interrupt.Form.Values.Result + if ((Test-String $manualAction) -and $this.HasChangedState($Interrupt.Id, 'Action', $manualAction)) { + $this.Warn(\"Manual intervention action '$manualAction' submitted with notes: $($Interrupt.Form.Values.Notes)\") + } + $guidanceAction = $Interrupt.Form.Values.Guidance + if ((Test-String $guidanceAction) -and $this.HasChangedState($Interrupt.Id, 'Action', $guidanceAction)) { + $this.Warn(\"Failure guidance to '$guidanceAction' submitted with notes: $($Interrupt.Form.Values.Notes)\") + } + } + + hidden [void] HandleInterrupt($Interrupt) { + $isGuidedFailure = $null -ne ($Interrupt.Form.Elements | Where-Object Name -eq Guidance) + if (!$isGuidedFailure -or !$this.DeploymentContext.GuidedFailureActions -or !$Interrupt.IsPending) { + return + } + $this.IfNewState($Interrupt.CorrelationId, 'ActionIndex', 0) + if ($Interrupt.CanTakeResponsibility -and $null -eq $Interrupt.ResponsibleUserId) { + Invoke-OctopusApi (Format-LinksUri -Uri $Interrupt.Links.Responsible) -Method Put + } + if ($Interrupt.HasResponsibility) { + $guidanceIndex = $this.GetState($Interrupt.CorrelationId, 'ActionIndex') + $guidance = $this.DeploymentContext.GuidedFailureActions[$guidanceIndex] + $guidanceIndex++ + + $retryWaitPeriod = New-TimeSpan -Seconds (Get-OctopusSetting RetryWaitPeriod 0) + if ($guidance -eq 'Retry' -and $retryWaitPeriod.TotalSeconds -gt 0) { + $minutesText = if ($retryWaitPeriod.Minutes -gt 1) { \" $($retryWaitPeriod.Minutes) minutes\" } elseif ($retryWaitPeriod.Minutes -eq 1) { \" $($retryWaitPeriod.Minutes) minute\" } + $secondsText = if ($retryWaitPeriod.Seconds -gt 1) { \" $($retryWaitPeriod.Seconds) seconds\" } elseif ($retryWaitPeriod.Seconds -eq 1) { \" $($retryWaitPeriod.Seconds) second\" } + $this.Warn(\"Waiting${minutesText}${secondsText} before submitting retry failure guidance...\") + Start-Sleep -Seconds $retryWaitPeriod.TotalSeconds + } + Invoke-OctopusApi (Format-LinksUri -Uri $Interrupt.Links.Submit) -Body @{ + Notes = $this.DeploymentContext.GuidedFailureMessage.Replace('#{GuidedFailureActionIndex}', $guidanceIndex).Replace('#{GuidedFailureAction}', $guidance) + Guidance = $guidance + } -Method Post + + $this.HasChangedState($Interrupt.CorrelationId, 'ActionIndex', $guidanceIndex) + } + } +} + +function Show-Heading { + param($Text) + $padding = ' ' * ((80 - 2 - $Text.Length) / 2) + Write-Host \" `n\" + Write-Host (@(\"`t\", ([string][char]0x2554), (([string][char]0x2550) * 80), ([string][char]0x2557)) -join '') + Write-Host \"`t$(([string][char]0x2551))$padding $Text $padding$([string][char]0x2551)\" + Write-Host (@(\"`t\", ([string][char]0x255A), (([string][char]0x2550) * 80), ([string][char]0x255D)) -join '') + Write-Host \" `n\" +} + +if ($OctopusParameters['Octopus.Action.RunOnServer'] -ieq 'False') { + Write-Warning \"For optimal performance use 'Run On Server' for this action\" +} + +$deploymentContext = [DeploymentContext]::new($Chain_BaseUrl, $Chain_BaseApiUrl) + +if ($Chain_CreateOption -ieq 'True') { + Show-Heading 'Creating Release' +} +else { + Show-Heading 'Retrieving Release' +} +$deploymentContext.SetProject($Chain_ProjectName) +$deploymentContext.SetChannel($Chain_Channel) +Write-Host \"`t$Chain_BaseUrl$(Format-WebLinksUri -Uri $deploymentContext.Project.Links.Web)\" + +if ($Chain_CreateOption -ieq 'True') { + $deploymentContext.CreateRelease($Chain_ReleaseNum) +} +else { + $deploymentContext.SetRelease($Chain_ReleaseNum) +} +Write-Host \"`t$Chain_BaseUrl$(Format-WebLinksUri -Uri $deploymentContext.Release.Links.Web)\" +if ($Chain_SnapshotVariables -ieq 'True') { + $deploymentContext.UpdateVariableSnapshot() +} + + +Show-Heading 'Configuring Deployment' +$deploymentContext.GetDeploymentTemplate() +$email = if (Test-String $OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress']) { \"($($OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress']))\" } +$guidedFailureMessage = Get-OctopusSetting GuidedFailureMessage @\" +Automatic Failure Guidance will #{GuidedFailureAction} (Failure ###{GuidedFailureActionIndex}) +Initiated by $($OctopusParameters['Octopus.Deployment.Name']) of $($OctopusParameters['Octopus.Project.Name']) release $($OctopusParameters['Octopus.Release.Number']) +Created By: $($OctopusParameters['Octopus.Deployment.CreatedBy.DisplayName']) $email +${Chain_BaseUrl}$($OctopusParameters['Octopus.Web.DeploymentLink']) +\"@ +$deploymentContext.SetGuidedFailure($Chain_GuidedFailure, $guidedFailureMessage) +$deploymentContext.SetSchedule($Chain_DeploySchedule) + +$deploymentContext.SetEnvironment($Chain_DeployTo) +$deploymentContext.SetTenants($Chain_Tenants) +$deploymentContext.SetForcePackageDownload($Chain_ForcePackageDownload) +$deploymentControllers = $deploymentContext.GetDeploymentControllers() +if (Test-String $Chain_StepsToSkip) { + $deploymentControllers | ForEach-Object { $_.SetStepsToSkip($Chain_StepsToSkip) } +} +if (Test-String $Chain_FormValues) { + $deploymentControllers | ForEach-Object { $_.SetFormValues($Chain_FormValues) } +} + +if (Test-String $Chain_MachineList) { + $deploymentControllers | ForEach-Object { $_.SetSpecificMachineIds($Chain_MachineList) } +} + +Show-Heading 'Queue Deployment' +if ($deploymentContext.IsTenanted) { + Write-Host 'Queueing tenant deployments...' +} +else { + Write-Host 'Queueing untenanted deployment...' +} +$deploymentControllers | ForEach-Object Start + +if (!$deploymentContext.WaitForDeployment) { + Write-Host 'Deployments have been queued, proceeding to the next step...' + return +} + +Show-Heading 'Waiting For Deployment' +do { + Start-Sleep -Seconds 1 + $tasksStillRunning = $false + foreach ($deployment in $deploymentControllers) { + if ($deployment.PollCheck()) { + $tasksStillRunning = $true + } + } +} while ($tasksStillRunning) + +if ($deploymentControllers | ForEach-Object Task | Where-Object FinishedSuccessfully -eq $false) { + Show-Heading 'Deployment Failed!' + Write-Fatal (($deploymentControllers | ForEach-Object Task | ForEach-Object ErrorMessage) -join \"`n\") +} +else { + Show-Heading 'Deployment Successful!' +} + +if (Test-String $Chain_PostDeploy -ForAbsence) { + return +} + +Show-Heading 'Post-Deploy Script' +$rawPostDeployScript = Invoke-OctopusApi (\"$Chain_BaseApiUrl/releases/{0}\" -f $OctopusParameters['Octopus.Release.Id']) | +ForEach-Object { Invoke-OctopusApi (Format-LinksUri -Uri $_.Links.ProjectDeploymentProcessSnapshot) } | +ForEach-Object Steps | Where-Object Id -eq $OctopusParameters['Octopus.Step.Id'] | +ForEach-Object Actions | Where-Object Id -eq $OctopusParameters['Octopus.Action.Id'] | +ForEach-Object { $_.Properties.Chain_PostDeploy } +Write-Verbose \"Raw Post-Deploy Script:`n$rawPostDeployScript\" + +Add-Type -Path (Get-WmiObject Win32_Process | Where-Object ProcessId -eq $PID | ForEach-Object { Get-Process -Id $_.ParentProcessId } | ForEach-Object { Join-Path (Split-Path -Path $_.Path -Parent) 'Octostache.dll' }) + +$deploymentControllers | ForEach-Object { + $deployment = $_.Task.Deployment + $tenant = $_.Tenant + $variablesDictionary = [Octostache.VariableDictionary]::new() + Invoke-OctopusApi (\"$Chain_BaseApiUrl/variables/{0}\" -f $deployment.ManifestVariableSetId) | ForEach-Object Variables | Where-Object { + ($_.IsSensitive -eq $false) -and ` + ($_.Scope.Private -ne 'True') -and ` + ($null -eq $_.Scope.Action) -and ` + ($null -eq $_.Scope.Machine) -and ` + ($null -eq $_.Scope.TargetRole) -and ` + ($null -eq $_.Scope.Role) -and ` + ($null -eq $_.Scope.Tenant -or $_.Scope.Tenant -contains $tenant.Id) -and ` + ($null -eq $_.Scope.TenantTag -or (Compare-Object $_.Scope.TenantTag $tenant.TenantTags -ExcludeDifferent -IncludeEqual)) -and ` + ($null -eq $_.Scope.Environment -or $_.Scope.Environment -contains $deployment.EnvironmentId) -and ` + ($null -eq $_.Scope.Channel -or $_.Scope.Channel -contains $deployment.ChannelId) -and ` + ($null -eq $_.Scope.Project -or $_.Scope.Project -contains $deployment.ProjectId) + } | ForEach-Object { $variablesDictionary.Set($_.Name, $_.Value) } + $postDeployScript = $variablesDictionary.Evaluate($rawPostDeployScript) + Write-Host \"$($_.LogPrefix)Evaluated Post-Deploy Script:\" + Write-Host $postDeployScript + Write-Host 'Script output:' + [scriptblock]::Create($postDeployScript).Invoke() +}" + }, + "Parameters": [{ + "Id": "61bffab9-bb89-4107-a5e0-79d69eaf8f2a", + "Name": "Chain_ApiKey", + "Label": "API Key", + "HelpText": "An Octopus API Key with appropriate permissions to perform the deployment", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "a37cac4d-8fd3-4d58-bfda-45a436be8dd5", + "Name": "Chain_ProjectName", + "Label": "Project Name", + "HelpText": "Name of the Octopus project that should be deployed", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "4fd440af-70fe-41ca-bec3-074f05155e81", + "Name": "Chain_Channel", + "Label": "Channel Name", + "HelpText": "The project channel to use when finding or [creating](https://octopus.com/docs/releases/channels#Channels-CreatingReleases) the release to deploy + +_Leave blank to use the default channel_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "78739052-438d-4dc7-862a-d4567eafc5df", + "Name": "Chain_ReleaseNum", + "Label": "Release Number", + "HelpText": "Release number to use for the deployment + +_Leave blank to use the latest release in the channel_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "fd2c3474-7187-4356-aaec-96f4910bb9c5", + "Name": "Chain_CreateOption", + "Label": "Create new release?", + "HelpText": "If a release should be created as part of this deployment + + +The release is created using either the **Release Number** if specified, or from the project release version template / donor package step if not specified + +A release will not be created if it is found to already exist", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "f648fa0c-b271-4e4a-b4f2-7a88db6b605c", + "Name": "Chain_SnapshotVariables", + "Label": "Update Variable Snapshot?", + "HelpText": "Should variables in the release be updated before deploying? + +By updating the variables, the current snapshot will be discarded, and the latest variables (as seen on the Variables tab) will be imported", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "80634b3b-3171-4643-b164-a5077c6d387b", + "Name": "Chain_DeployTo", + "Label": "Environment Name", + "HelpText": "The name of an environment to deploy to + +Multiple environments can be deployed to by entering the name of a [lifecycle phase](https://octopus.com/docs/key-concepts/lifecycles#Lifecycles-LifecyclePhases)", + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1334093d-0be4-4115-bb93-d752171a19d8", + "Name": "Chain_Tenants", + "Label": "Tenants", + "HelpText": "_Leave blank to perform an untenanted deployment_ + +A list of [tenants & tenant tags](https://octopus.com/docs/tenants) to deploy. Tenant Tags are specified in [Canonical Name](https://octopus.com/docs/tenants/tenant-tags#referencing-tenant-tags) format: + + Tag Set Name/Tag Name + +Individual tenants can be listed in addition to tags + + Tenant Name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "83d9d973-7a72-4f71-a890-8f19d955bc37", + "Name": "Chain_FormValues", + "Label": "Form Values", + "HelpText": "Provide values for [prompted variables](https://octopus.com/docs/projects/variables/prompted-variables) to use in the deployment + +Variables should be listed one per line using the format + + VariableName = Value", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "6522ca29-898a-4da6-b0c3-da52991e6812", + "Name": "Chain_StepsToSkip", + "Label": "Steps To Skip", + "HelpText": "A list of steps which should be skipped in the deployment + +Steps should be listed one per line, and specified using either the step number (as per the deployment plan) or by the step name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "acb2cd0e-fc53-42a7-95da-089955ea1870", + "Name": "Chain_GuidedFailure", + "Label": "Failure Handling", + "HelpText": "Determines how deployment failures & [guided failure mode](https://octopus.com/docs/releases/guided-failures) should be handled for the deployment + +Automatic failure handling is performed by using [guided failure](https://octopus.com/docs/releases/guided-failures) and submitting an appropriate action + +The number of retry attempts can be customised by [setting the following variables in indexer notion](https://octopus.com/docs/deploying-applications/variables/system-variables#Systemvariables-Action) +- Octopus.Action.StepRetryCount +- Octopus.Action.DeploymentRetryCount", + "DefaultValue": "Default", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Default|Default - Guided Failure is inherited from this deployment +Enabled|Enable - Guided Failure is enabled +Disabled|Disable - Guided Failure is disabled +RetryIgnore|Retry & Ignore - Automatically retry a failing step, a second failure is ignored +RetryAbort|Retry & Abort - Automatically retry a failing step, and abort on a second failure +Ignore|Ignore - Automatically ignore any step failures +RetryDeployment|Retry Deployment - Automatically retry the entire deployment on failure" + }, + "Links": {} + }, + { + "Id": "73a80735-4ca0-4c12-9fa3-f0123db6349f", + "Name": "Chain_DeploySchedule", + "Label": "Scheduling", + "HelpText": "Defines how the deployment should be scheduled & run + +_Note: Automated failure handling & post-deploy script functionality is only available when the **Wait For Deployment** option is selected_ + +A user defined schedule can be set with the **Use a custom expression** option +For an exact date & time use the following format, the day is optional and the time is in 24-hour format + + [Mon/Tue/Wed/Thu/Fri/Sat/Sun] @ HH:MM + +To schedule a deployment a relative number of hours & minutes in the future use + + + MMM + + HHH:MM + +_Note: Reoccurring deployments & automatic retry of failed deployment are possible using a scheduled deployment and the [Always Run or On Failure run conditions](https://octopus.com/docs/deploying-applications#Deployingapplications-Conditions)_", + "DefaultValue": "WaitForDeployment", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "WaitForDeployment|Wait For Deployment +NoWait|Queue Immediately ++ 5|Deploy in 5 minutes ++ 15|Deploy in 15 minutes ++ 1:00|Deploy in 1 hour ++ 24:00|Deploy in 24 hours +@ 00:00|Deploy At Midnight +@ 00:00 + 12:00|Deploy At Noon Tomorrow +Mon @ 08:00|Deploy At 8am On Monday +Sat @ 00:00 + 168:00|Deploy the following Saturday at Midnight" + }, + "Links": {} + }, + { + "Id": "7e7f9ac5-8674-4a91-a94a-896a3ee1334d", + "Name": "Chain_PostDeploy", + "Label": "Post-Deploy Script", + "HelpText": "A PowerShell script which should be run after a successful deployment + +Variables are replaced in the script using the resultant **Manifest VariableSet** from the deployment in the [binding syntax](https://octopus.com/docs/projects/variables/variable-substitutions#binding-variables) format + +Variables are not available if they are: +- [Sensitive](https://octopus.com/docs/deploying-applications/variables/sensitive-variables) +- Action scoped +- Machine scoped +- Role scoped + + +When performing a tenanted deployment the script will be run once for each tenant using the specific variables from their deployment", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "5c3fddc3-69cb-4762-ac6a-4fdb05f43c6b", + "Name": "Chain_ForcePackageDownload", + "Label": "Force Package Download", + "HelpText": "Should we redownload the package for this release?", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "9ee151bb-78e5-4cb0-8780-6536ea319934", + "Name": "Chain_MachineList", + "Label": "Machine List", + "HelpText": "A list of Machine Names which should be Targeted in the deployment + +Machine Names should be listed one per line and specified using either the Machine Id or by the Machine name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } +], +"LastModifiedBy": "LeoDOD", +"$Meta": { + "ExportedAt": "2023-02-23T21:40:33.279Z", + "OctopusVersion": "2022.3.10847", + "Type": "ActionTemplate" +}, +"Category": "octopus" +} diff --git a/step-templates/octopus-consolidate-releasenotes.json.human b/step-templates/octopus-consolidate-releasenotes.json.human new file mode 100644 index 000000000..30809d10c --- /dev/null +++ b/step-templates/octopus-consolidate-releasenotes.json.human @@ -0,0 +1,184 @@ +{ + "Id": "fa570d27-1405-4030-87b2-c0abf12bb833", + "Name": "Consolidate Release Notes", + "Description": "Consolidates all Release Notes between the last successful release in the current Environment and this one by merging or concatenating them.", + "ActionType": "Octopus.Script", + "Version": 13, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptBody": "$baseUri = $OctopusParameters['#{if Octopus.Web.ServerUri}Octopus.Web.ServerUri#{else}Octopus.Web.BaseUrl#{/if}'] +$reqheaders = @{\"X-Octopus-ApiKey\" = $Consolidate_ApiKey } +$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $Consolidate_ApiKey } + +$remWhiteSpace = [bool]::Parse($Consolidate_RemoveWhitespace) +$deDupe = [bool]::Parse($Consolidate_Dedupe) +$reverse = ($Consolidate_Order -eq \"Oldest\") + +# Get details we'll need +$projectId = $OctopusParameters['Octopus.Project.Id'] +$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number'] +$lastSuccessfulReleaseId = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Id'] +$lastSuccessfulReleaseNumber = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Number'] + +function Test-SpacesApi { +\tWrite-Verbose \"Checking API compatibility\"; +\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqHeaders -UseBasicParsing | ConvertFrom-Json; + if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) { + \tWrite-Verbose \"Spaces API found\" + \treturn $true; + } + Write-Verbose \"Pre-spaces API found\" + return $false; +} + +if(Test-SpacesApi) { +\t$spaceId = $OctopusParameters['Octopus.Space.Id']; + if([string]::IsNullOrWhiteSpace($spaceId)) { + throw \"This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which isn't expected - please reach out to our support team at https://help.octopus.com if you encounter this error.\"; + } +\t$baseApiUrl = \"/api/$spaceId\" ; +} else { +\t$baseApiUrl = \"/api\" ; +} + +# Get all previous releases to this environment +$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases\" +try { + $allReleases = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json +} catch { + if ($_.Exception.Response.StatusCode.Value__ -ne 404) { + $result = $_.Exception.Response.GetResponseStream() + $reader = New-Object System.Io.StreamReader($result); + $responseBody = $reader.ReadToEnd(); + throw \"Error occurred: $responseBody\" + } +} + +# Find and aggregate release notes +$aggregateNotes = @() + +Write-Host \"Finding all release notes between the last successful release: $lastSuccessfulReleaseNumber and this release: $thisReleaseNumber\" +foreach ($rel in $allReleases.Items) { + if ($rel.Id -ne $lastSuccessfulReleaseId) { + Write-Host \"Found release notes for $($rel.Version)\" + $theseNotes = @() + #split into lines + $lines = $rel.ReleaseNotes -split \"`n\" + foreach ($line in $lines) { + if (-not $remWhitespace -or -not [string]::IsNullOrWhiteSpace($line)) { + if (-not $deDupe -or -not $aggregateNotes.Contains($line)) { + $theseNotes = $theseNotes + $line + } + } + } + if ($reverse) { + $aggregateNotes = $theseNotes + $aggregateNotes + } else { + $aggregateNotes = $aggregateNotes + $theseNotes + } + } else { + break + } +} +$aggregateNotesText = $aggregateNotes -join \"`n`n\" + +# Get the current release +$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases/$thisReleaseNumber\" +try { + $currentRelease = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json +} catch { + if ($_.Exception.Response.StatusCode.Value__ -ne 404) { + $result = $_.Exception.Response.GetResponseStream() + $reader = New-Object System.Io.StreamReader($result); + $responseBody = $reader.ReadToEnd(); + throw \"Error occurred: $responseBody\" + } +} + +# Update the release notes for the current release +$currentRelease.ReleaseNotes = $aggregateNotesText +Write-Host \"Updating release notes for $thisReleaseNumber`:`n`n\" +Write-Host $aggregateNotesText +try { + $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\" + $currentReleaseBody = $currentRelease | ConvertTo-Json -Depth 10 + $result = Invoke-WebRequest $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing | ConvertFrom-Json +} catch { + $result = $_.Exception.Response.GetResponseStream() + $reader = New-Object System.Io.StreamReader($result); + $responseBody = $reader.ReadToEnd(); + Write-Host $responseBody + throw \"Error occurred: $responseBody\" +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "fa5cfdb4-b006-4f92-90ee-affc1791fc79", + "Name": "Consolidate_ApiKey", + "Type": "String", + "Label": "Api Key", + "HelpText": "The API Key to use for authentication", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "2fe221e5-1f47-4cf9-bde7-ed3b77028bf4", + "Name": "Consolidate_Dedupe", + "Type": "String", + "Label": "Remove Duplicates", + "HelpText": "Whether to remove **duplicate** lines when constructing release notes", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox", + "Octopus.SelectOptions": "Yes|Yes +No|No" + }, + "Links": {} + }, + { + "Id": "301aa2a8-f06b-4904-9636-99189074f224", + "Name": "Consolidate_RemoveWhitespace", + "Type": "String", + "Label": "Remove Blank Lines", + "HelpText": "Whether to remove **blank** lines when constructing release notes", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox", + "Octopus.SelectOptions": "Yes|Yes +No|No" + }, + "Links": {} + }, + { + "Id": "517d4aac-e6ae-451c-b18f-be31c6c153b8", + "Name": "Consolidate_Order", + "Type": "String", + "Label": "Concatenation Order", + "HelpText": "The order in which to append release notes", + "DefaultValue": "Newest", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Newest|Newest to Oldest +Oldest|Oldest to Newest" + }, + "Links": {} + } + ], + "LastModifiedBy": "FinnianDempsey", + "$Meta": { + "ExportedAt": "2023-03-09T13:12:44.043Z", + "OctopusVersion": "2022.3.10936", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} + + diff --git a/step-templates/octopus-create-octoterra-space-azure.json.human b/step-templates/octopus-create-octoterra-space-azure.json.human new file mode 100644 index 000000000..90070e6a4 --- /dev/null +++ b/step-templates/octopus-create-octoterra-space-azure.json.human @@ -0,0 +1,188 @@ +{ + "Id": "c9c5a6a2-0ce7-4d7a-8eb5-111ac44df24e", + "Name": "Octopus - Create Octoterra Space (Azure Backend)", + "Description": "This step exposes the fields required to create the initial blank space serialized with [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport) using Terraform. The new space is then populated using the `Octopus - Populate Octoterra Space (Azure Backend)` step. + +This step configures a Terraform Azure backend. + +It is recommended that this step be run with the `octopuslabs/terraform-workertools` worker image.", + "ActionType": "Octopus.TerraformApply", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "093b1515-15a9-4446-8dc2-6297018a77e7", + "Name": "", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "SelectionMode": "deferred", + "PackageParameterName": "OctoterraApply.Terraform.Package.Id" + } + } + ], + "GitDependencies": [], + "Properties": { + "Octopus.Action.GoogleCloud.UseVMServiceAccount": "False", + "Octopus.Action.GoogleCloud.ImpersonateServiceAccount": "False", + "Octopus.Action.Terraform.GoogleCloudAccount": "False", + "Octopus.Action.Terraform.AzureAccount": "True", + "Octopus.Action.Terraform.ManagedAccount": "None", + "Octopus.Action.Terraform.AllowPluginDownloads": "True", + "Octopus.Action.Script.ScriptSource": "Package", + "Octopus.Action.Terraform.RunAutomaticFileSubstitution": "False", + "Octopus.Action.Terraform.PlanJsonOutput": "False", + "Octopus.Action.Terraform.Workspace": "#{OctoterraApply.Terraform.Workspace.Name}", + "Octopus.Action.Terraform.AdditionalInitParams": "-backend-config=\"resource_group_name=#{OctoterraApply.Azure.Storage.ResourceGroup}\" -backend-config=\"storage_account_name=#{OctoterraApply.Azure.Storage.AccountName}\" -backend-config=\"container_name=#{OctoterraApply.Azure.Storage.Container}\" -backend-config=\"key=#{OctoterraApply.Azure.Storage.Key}\" #{if OctoterraApply.Terraform.AdditionalInitParams}#{OctoterraApply.Terraform.AdditionalInitParams}#{/if}", + "Octopus.Action.Terraform.AdditionalActionParams": "-var=octopus_server=#{OctoterraApply.Octopus.ServerUrl} -var=octopus_apikey=#{OctoterraApply.Octopus.ApiKey} \"-var=octopus_space_name=#{OctoterraApply.Octopus.Space.NewName}\" -var=octopus_space_managers=#{OctoterraApply.Octopus.Space.Managers} #{if OctoterraApply.Terraform.AdditionalApplyParams}#{OctoterraApply.Terraform.AdditionalApplyParams}#{/if}", + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.Terraform.TemplateDirectory": "space_creation", + "Octopus.Action.AzureAccount.Variable": "OctoterraApply.Azure.Account" + }, + "Parameters": [ + { + "Id": "281f1167-aa9b-4061-9550-2c4a08d20256", + "Name": "OctoterraApply.Octopus.Space.NewName", + "Label": "Octopus Space Name", + "HelpText": "The name of the new space to create. The default value assumes that the step is run against a tenant, and the tenant name matches the space.", + "DefaultValue": "#{Octopus.Deployment.Tenant.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ec7890fd-6a73-49a1-b4c4-f53e5a76e0dd", + "Name": "OctoterraApply.Octopus.Space.Managers", + "Label": "Octopus Space Managers", + "HelpText": "The name of a team to assign as the space manager.", + "DefaultValue": "teams-administrators", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ea73512f-aab0-4fdc-8381-ad37a3f31220", + "Name": "OctoterraApply.Terraform.Workspace.Name", + "Label": "Terraform Workspace", + "HelpText": "The name of the Terraform workspace. This must be unique for every project this module is deployed to. The default value is based on the space ID that the module is applied to: `#{OctoterraApply.Octopus.SpaceID}`. Leave this as the default value unless you have a specific reason to change it.", + "DefaultValue": "#{OctoterraApply.Octopus.Space.NewName | Replace \"[^A-Za-z0-9]\" \"_\"}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d675cd1e-4306-4b8c-91fc-7090377dc34c", + "Name": "OctoterraApply.Terraform.Package.Id", + "Label": "Terraform Module Package", + "HelpText": "The package created by [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport). It must include the `space_creation` and `space_population` directories.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "1d039630-0602-4491-b906-e29b45876411", + "Name": "OctoterraApply.Octopus.ServerUrl", + "Label": "Octopus Server URL", + "HelpText": "The Octopus server URL.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0eab90d6-c7a0-482c-a386-e254a477c291", + "Name": "OctoterraApply.Octopus.ApiKey", + "Label": "Octopus API key", + "HelpText": "The Octopus API key. See the [documentation](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) for details on creating an API key.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "dd0fe15d-c377-45d2-86b8-61d326e7e411", + "Name": "OctoterraApply.Azure.Account", + "Label": "Azure Account Variable", + "HelpText": "The Azure account variable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "9442e8fb-056b-4a24-a129-adfe9726ea8d", + "Name": "OctoterraApply.Azure.Storage.ResourceGroup", + "Label": "Azure Backend Resource Group", + "HelpText": "The name of the resource group holding the Azure storage account. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure storage accounts as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d3098333-99ac-463f-83eb-f66aca3d1055", + "Name": "OctoterraApply.Azure.Storage.AccountName", + "Label": "Azure Storage Account Name", + "HelpText": "The name of the Azure storage account used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure storage accounts as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "514db3ac-89a7-4537-abfc-cf7bf7c6ac8c", + "Name": "OctoterraApply.Azure.Storage.Container", + "Label": "Azure Storage Container", + "HelpText": "The name of the Azure storage account container used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure storage accounts as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e34b942b-f99b-48d1-9989-6810a2d0a71b", + "Name": "OctoterraApply.Azure.Storage.Key", + "Label": "Azure Storage Key", + "HelpText": "The file used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) for details on using Azure as a backend. The combination of the workspace name and this key must be unique. + +The default value is the name of the space and a prefix to indicate the type of resource: `Space_#{OctoterraApply.Octopus.Space.NewName | Replace \"[^A-Za-z0-9]\" \"_\"}`.", + "DefaultValue": "Space_#{OctoterraApply.Octopus.Space.NewName | Replace \"[^A-Za-z0-9]\" \"_\"}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "05715061-fda9-48b1-8bec-960b9fa56e89", + "Name": "OctoterraApply.Terraform.AdditionalApplyParams", + "Label": "Terraform Additional Apply Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform apply` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/apply) for details on the `apply` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e6ffdabd-5009-4e55-aab8-eeafc2291e47", + "Name": "OctoterraApply.Terraform.AdditionalInitParams", + "Label": "Terraform Additional Init Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform init` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/init) for details on the `init` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.TerraformApply", + "$Meta": { + "ExportedAt": "2023-12-20T22:22:32.372Z", + "OctopusVersion": "2024.1.5406", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-create-octoterra-space-s3.json.human b/step-templates/octopus-create-octoterra-space-s3.json.human new file mode 100644 index 000000000..9e6bcc2dd --- /dev/null +++ b/step-templates/octopus-create-octoterra-space-s3.json.human @@ -0,0 +1,178 @@ +{ + "Id": "90a8dd76-6456-49f9-9c03-baf85442aa57", + "Name": "Octopus - Create Octoterra Space (S3 Backend)", + "Description": "This step exposes the fields required to create the initial blank space serialized with [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport) using Terraform. The new space is then populated using the `Octopus - Populate Octoterra Space (S3 Backend)` step. + +This step configures a Terraform S3 backend. + +It is recommended that this step be run with the `octopuslabs/terraform-workertools` worker image.", + "ActionType": "Octopus.TerraformApply", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "093b1515-15a9-4446-8dc2-6297018a77e7", + "Name": "", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "SelectionMode": "deferred", + "PackageParameterName": "OctoterraApply.Terraform.Package.Id" + } + } + ], + "Properties": { + "Octopus.Action.GoogleCloud.UseVMServiceAccount": "False", + "Octopus.Action.GoogleCloud.ImpersonateServiceAccount": "False", + "Octopus.Action.Terraform.GoogleCloudAccount": "False", + "Octopus.Action.Terraform.AzureAccount": "False", + "Octopus.Action.Terraform.ManagedAccount": "AWS", + "Octopus.Action.Terraform.AllowPluginDownloads": "True", + "Octopus.Action.Script.ScriptSource": "Package", + "Octopus.Action.Terraform.RunAutomaticFileSubstitution": "False", + "Octopus.Action.Terraform.PlanJsonOutput": "False", + "Octopus.Action.Terraform.Workspace": "#{OctoterraApply.Terraform.Workspace.Name}", + "Octopus.Action.Terraform.AdditionalInitParams": "-backend-config=\"bucket=#{OctoterraApply.AWS.S3.BucketName}\" -backend-config=\"region=#{OctoterraApply.AWS.S3.BucketRegion}\" -backend-config=\"key=#{OctoterraApply.AWS.S3.BucketKey}\" #{if OctoterraApply.Terraform.AdditionalInitParams}#{OctoterraApply.Terraform.AdditionalInitParams}#{/if}", + "Octopus.Action.Terraform.AdditionalActionParams": "-var=octopus_server=#{OctoterraApply.Octopus.ServerUrl} -var=octopus_apikey=#{OctoterraApply.Octopus.ApiKey} \"-var=octopus_space_name=#{OctoterraApply.Octopus.Space.NewName}\" -var=octopus_space_managers=#{OctoterraApply.Octopus.Space.Managers} #{if OctoterraApply.Terraform.AdditionalApplyParams}#{OctoterraApply.Terraform.AdditionalApplyParams}#{/if}", + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "Octopus.Action.AwsAccount.Variable": "OctoterraApply.AWS.Account", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.Aws.Region": "#{OctoterraApply.AWS.S3.BucketRegion}", + "Octopus.Action.Terraform.TemplateDirectory": "space_creation" + }, + "Parameters": [ + { + "Id": "737a32e1-802f-4e15-a67d-cd271aef2c27", + "Name": "OctoterraApply.Octopus.Space.NewName", + "Label": "Octopus Space Name", + "HelpText": "The name of the new space to create. The default value assumes that the step is run against a tenant, and the tenant name matches the space.", + "DefaultValue": "#{Octopus.Deployment.Tenant.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "229cc8d4-5200-4f4e-b1c4-8d6464e246a0", + "Name": "OctoterraApply.Octopus.Space.Managers", + "Label": "Octopus Space Managers", + "HelpText": "The name of a team to assign as the space manager.", + "DefaultValue": "teams-administrators", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4d9ad5a5-7058-4f2c-8212-b55f0c7533fd", + "Name": "OctoterraApply.Terraform.Workspace.Name", + "Label": "Terraform Workspace", + "HelpText": "The name of the Terraform workspace. This must be unique for every project this module is deployed to. The default value is based on the space ID that the module is applied to: `#{OctoterraApply.Octopus.SpaceID}`. Leave this as the default value unless you have a specific reason to change it.", + "DefaultValue": "#{OctoterraApply.Octopus.Space.NewName | Replace \"[^A-Za-z0-9]\" \"_\"}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1e6fc5c3-ab5d-42ec-83e1-0ac7a4b01d31", + "Name": "OctoterraApply.Terraform.Package.Id", + "Label": "Terraform Module Package", + "HelpText": "The package created by [octoterra](https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport). It must include the `space_creation` and `space_population` directories.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "0239982b-f0d6-49ff-a59f-5fa5045f25d5", + "Name": "OctoterraApply.Octopus.ServerUrl", + "Label": "Octopus Server URL", + "HelpText": "The Octopus server URL.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "61758b61-5fbf-418c-8bce-a153c0403bb4", + "Name": "OctoterraApply.Octopus.ApiKey", + "Label": "Octopus API key", + "HelpText": "The Octopus API key. See the [documentation](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) for details on creating an API key.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "b9312b36-328a-497d-9af8-ab7007e5315a", + "Name": "OctoterraApply.AWS.Account", + "Label": "AWS Account Variable", + "HelpText": "The AWS account variable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + }, + { + "Id": "124b8e4c-abf5-4ebd-ab0e-4eeea76a8bff", + "Name": "OctoterraApply.AWS.S3.BucketName", + "Label": "AWS S3 Bucket Name", + "HelpText": "The name of the S3 bucket used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/s3) for details on using S3 as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "04739ba2-45da-4ac7-aad8-b04cff4df015", + "Name": "OctoterraApply.AWS.S3.BucketRegion", + "Label": "AWS S3 Bucket Region", + "HelpText": "The AWS region hosting the S3 bucket. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/s3) for details on using S3 as a backend.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "88e07b45-c2d0-4e73-abb9-294143669ba6", + "Name": "OctoterraApply.AWS.S3.BucketKey", + "Label": "AWS S3 Bucket Key", + "HelpText": "The S3 file used to hold the Terraform state. See the [Terraform documentation](https://developer.hashicorp.com/terraform/language/settings/backends/s3) for details on using S3 as a backend. The combination of the workspace name and this key must be unique. + +The default value is the name of the project and a prefix to indicate the type of resource: `Project_#{Octopus.Project.Name | Replace \"[^A-Za-z0-9]\" \"_\"}`.", + "DefaultValue": "Space_#{OctoterraApply.Octopus.Space.NewName | Replace \"[^A-Za-z0-9]\" \"_\"}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e5628bba-7022-403e-9165-893da6d682ab", + "Name": "OctoterraApply.Terraform.AdditionalApplyParams", + "Label": "Terraform Additional Apply Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform apply` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/apply) for details on the `apply` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5600a0cb-cf66-43aa-bf76-6add3e5d52a2", + "Name": "OctoterraApply.Terraform.AdditionalInitParams", + "Label": "Terraform Additional Init Params", + "HelpText": "This field can be used to define additional parameters passed to the `terraform init` command. This field can be left blank. See the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands/init) for details on the `init` command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.TerraformApply", + "$Meta": { + "ExportedAt": "2023-10-24T03:21:15.975Z", + "OctopusVersion": "2023.4.6612", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-delete-machine-registration.json.human b/step-templates/octopus-delete-machine-registration.json.human new file mode 100644 index 000000000..2fdac6368 --- /dev/null +++ b/step-templates/octopus-delete-machine-registration.json.human @@ -0,0 +1,130 @@ +{ + "Id": "e4255fcb-fe7d-4d5b-8ec0-0243e5f48a9c", + "Name": "Delete Target or Worker Registration From Octopus", + "Description": "Step that will attempt to delete a target or worker from Octopus Deploy using the API. If it cannot delete the target or worker it will disable the target and rename it.", + "ActionType": "Octopus.Script", + "Version": 1, + "Author": "octobob", + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$OctopusAPIKey = $OctopusParameters[\"DeleteTarget.Octopus.Api.Key\"] +$TargetName = $OctopusParameters[\"DeleteTarget.Target.Name\"] +$OctopusUrl = $OctopusParameters[\"DeleteTarget.Octopus.Base.Url\"] +$SpaceId = $OctopusParameters[\"Octopus.Space.Id\"] +$TargetType = $OctopusParameters[\"DeleteTarget.Target.TargetType\"] + +Write-Host \"Target Name: $TargetName\" +Write-Host \"Octopus URL: $OctopusUrl\" +Write-Host \"Space Id: $SpaceId\" +Write-Host \"Target Type: $TargetType\" + +$header = New-Object \"System.Collections.Generic.Dictionary[[String],[String]]\" +$header.Add(\"X-Octopus-ApiKey\", $OctopusAPIKey) + +$baseApiUrl = \"$OctopusUrl/api\" +$baseApiInformation = Invoke-RestMethod $baseApiUrl -Headers $header +if ((Get-Member -InputObject $baseApiInformation.Links -Name \"Spaces\" -MemberType Properties) -ne $null) +{ +\t$baseApiUrl = \"$baseApiUrl/$SpaceId\" +} + +$baseTargetUrl = \"$baseApiUrl/machines\" + +if ($TargetType -eq \"Worker\") +{ +\t$baseTargetUrl = \"$baseApiUrl/workers\" + Write-Host \"Worker was selected, switching over to use the URL $baseTargetUrl\" +} + +$targetListUrl = \"$($baseTargetUrl)?skip=0&take=1000&partialName=$TargetName\" +Write-Host \"Get a list of all machine using the URL $targetListUrl\" + +$targetList = (Invoke-RestMethod $targetListUrl -Headers $header) + +foreach($target in $targetList.Items) +{ + if ($target.Name -eq $TargetName) + { + $targetId = $target.Id + $itemToDeleteEndPoint = \"$baseTargetUrl/$targetId\" + try + { \t + \tWrite-Highlight \"Deleting the machine $targetId because the name $($target.Name) matches the $TargetName $itemToDeleteEndPoint\" + \t$deleteResponse = (Invoke-RestMethod $itemToDeleteEndPoint -Headers $header -Method Delete) + Write-Highlight \"Successfully deleted machine $TargetName\" + Write-Host \"Delete Response $deleteResponse\" + } + catch + { \t + \t$currentDate = Get-Date -Format \"_MMddyyyy_HHmm\" + \t$target.Name = \"$($target.Name)-old$currentdate\" + $target.IsDisabled = $True + + $jsonRequest = $target | ConvertTo-Json + + Write-Highlight \"There was an error deleting the machine, renaming it to $($target.name) and disabling it\" + \tWrite-Host $_ + $machineResponse = Invoke-RestMethod $itemToDeleteEndPoint -Headers $header -Method PUT -Body $jsonRequest + } + + break + } +}" + }, + "Parameters": [ + { + "Id": "f287ffbb-9b6b-48b8-bea6-2ba399ae570c", + "Name": "DeleteTarget.Octopus.Base.Url", + "Label": "Octopus Base Url", + "HelpText": "The Base URL of the Octopus Deploy Server. Example: https://samples.octopus.app", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cabd24c3-3eb3-4913-b73a-0df2df02e82d", + "Name": "DeleteTarget.Octopus.Api.Key", + "Label": "Octopus API Key", + "HelpText": "The API key of a user who has permissions to delete a target from Octopus Deploy", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "396591f9-8bd0-4acb-9de6-2dacf0ca2c50", + "Name": "DeleteTarget.Target.Name", + "Label": "Target Name", + "HelpText": "The Name of the Target to delete from Octopus Deploy", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e01b6695-1cef-423b-b53f-b1185cc3b894", + "Name": "DeleteTarget.Target.TargetType", + "Label": "Target Type", + "HelpText": "What kind of registration is it, a worker or a deployment target", + "DefaultValue": "Target", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Target|Deployment Target +Worker|Worker" + } + } + ], + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2020-04-13T15:37:29.866Z", + "OctopusVersion": "2020.1.10", + "Type": "ActionTemplate" + }, + "Category": "octopus" + } diff --git a/step-templates/octopus-find-cac-updates.json.human b/step-templates/octopus-find-cac-updates.json.human new file mode 100644 index 000000000..343360e14 --- /dev/null +++ b/step-templates/octopus-find-cac-updates.json.human @@ -0,0 +1,504 @@ +{ + "Id": "05210515-1a52-45d9-8be3-16caef808326", + "Name": "Octopus - Find CaC Updates (S3 Backend)", + "Description": "This step queries each workspace in the Terraform state for downstream Octopus CaC enabled projects, extracts the Git repo associated with the CaC project, and determines if there are any changes to merge into the downstream project from the upstream project. + +This indicates if changes to an upstream project are available to be merged into a downstream project, either automatically, or after resolving merge conflicts.", + "ActionType": "Octopus.AwsRunScript", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Script.ScriptBody": "# Check to see if $IsWindows is available +if ($null -eq $IsWindows) { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +Function Get-GitExecutable +{ + # Define parameters + param ( + $WorkingDirectory + ) + + # Define variables + $gitExe = \"PortableGit-2.41.0.3-64-bit.7z.exe\" + $gitDownloadUrl = \"https://github.com/git-for-windows/git/releases/download/v2.41.0.windows.3/$gitExe\" + $gitDownloadArguments = @{} + $gitDownloadArguments.Add(\"Uri\", $gitDownloadUrl) + $gitDownloadArguments.Add(\"OutFile\", \"$WorkingDirectory/git/$gitExe\") + + # This makes downloading faster + $ProgressPreference = 'SilentlyContinue' + + # Check to see if git subfolder exists + if ((Test-Path -Path \"$WorkingDirectory/git\") -eq $false) + { + # Create subfolder + New-Item -Path \"$WorkingDirectory/git\" -ItemType Directory | Out-Null + } + + # Check PowerShell version + if ($PSVersionTable.PSVersion.Major -lt 6) + { + # Use basic parsing is required + $gitDownloadArguments.Add(\"UseBasicParsing\", $true) + } + + # Download Git + Write-Host \"Downloading Git ...\" + Invoke-WebRequest @gitDownloadArguments + + # Extract Git + $gitExtractArguments = @() + $gitExtractArguments += \"-o\" + $gitExtractArguments += \"$WorkingDirectory\\git\" + $gitExtractArguments += \"-y\" + $gitExtractArguments += \"-bd\" + + Write-Host \"Extracting Git download ...\" + & \"$WorkingDirectory\\git\\$gitExe\" $gitExtractArguments + + # Wait until unzip action is complete + while ($null -ne (Get-Process | Where-Object {$_.ProcessName -eq ($gitExe.Substring(0, $gitExe.LastIndexOf(\".\")))})) + { + Start-Sleep 5 + } + + # Add bin folder to path + $env:PATH = \"$WorkingDirectory\\git\\bin$([IO.Path]::PathSeparator)\" + $env:PATH + + # Disable promopt for credential helper + Invoke-CustomCommand \"git\" @(\"config\", \"--system\", \"--unset\", \"credential.helper\") | Write-Results +} + +Function Invoke-CustomCommand +{ + Param ( + $commandPath, + $commandArguments, + $workingDir = (Get-Location), + $path = @() + ) + + $path += $env:PATH + $newPath = $path -join [IO.Path]::PathSeparator + + $pinfo = New-Object System.Diagnostics.ProcessStartInfo + $pinfo.FileName = $commandPath + $pinfo.WorkingDirectory = $workingDir + $pinfo.RedirectStandardError = $true + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + $pinfo.Arguments = $commandArguments + $pinfo.EnvironmentVariables[\"PATH\"] = $newPath + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $pinfo + $p.Start() | Out-Null + + # Capture output during process execution so we don't hang + # if there is too much output. + # Microsoft documents a C# solution here: + # https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.redirectstandardoutput?view=net-7.0&redirectedfrom=MSDN#remarks + # This code is based on https://stackoverflow.com/a/74748844 + $stdOut = [System.Text.StringBuilder]::new() + $stdErr = [System.Text.StringBuilder]::new() + do + { + if (!$p.StandardOutput.EndOfStream) + { + $stdOut.AppendLine($p.StandardOutput.ReadLine()) + } + if (!$p.StandardError.EndOfStream) + { + $stdErr.AppendLine($p.StandardError.ReadLine()) + } + + Start-Sleep -Milliseconds 10 + } + while (-not $p.HasExited) + + # Capture any standard output generated between our last poll and process end. + while (!$p.StandardOutput.EndOfStream) + { + $stdOut.AppendLine($p.StandardOutput.ReadLine()) + } + + # Capture any error output generated between our last poll and process end. + while (!$p.StandardError.EndOfStream) + { + $stdErr.AppendLine($p.StandardError.ReadLine()) + } + + $p.WaitForExit() + + $executionResults = [pscustomobject]@{ + StdOut = $stdOut.ToString() + StdErr = $stdErr.ToString() + ExitCode = $p.ExitCode + } + + return $executionResults + +} + +function Write-Results +{ + [cmdletbinding()] + param ( + [Parameter(Mandatory=$True,ValuefromPipeline=$True)] + $results + ) + + if (![String]::IsNullOrWhiteSpace($results.StdOut)) + { + Write-Verbose $results.StdOut + } + + if (![String]::IsNullOrWhiteSpace($results.StdErr)) + { + Write-Verbose $results.StdErr + } +} + +function Write-TerraformBackend { + Set-Content -Path 'backend.tf' -Value @\" +terraform { + backend \"s3\" {} + required_providers { + octopusdeploy = { source = \"OctopusDeployLabs/octopusdeploy\", version = \"0.14.9\" } + } + } +\"@ +} + +function Format-StringAsNullOrTrimmed { + [cmdletbinding()] + param ( + [Parameter(ValuefromPipeline=$True)] + $input + ) + + if ([string]::IsNullOrWhitespace($input)) { + return $null + } + + return $input.Trim() +} + +$username = $OctopusParameters[\"FindConflicts.Git.Credentials.Username\"] +$password = $OctopusParameters[\"FindConflicts.Git.Credentials.Password\"] +$protocol = $OctopusParameters[\"FindConflicts.Git.Url.Protocol\"] +$gitHost = $OctopusParameters[\"FindConflicts.Git.Url.Host\"] +$org = $OctopusParameters[\"FindConflicts.Git.Url.Organization\"] +$repo = $OctopusParameters[\"FindConflicts.Git.Url.Template\"] +$region = $OctopusParameters[\"FindConflicts.Terraform.Backend.S3Region\"] +$key = $OctopusParameters[\"FindConflicts.Terraform.Backend.S3Key\"] +$bucket = $OctopusParameters[\"FindConflicts.Terraform.Backend.S3Bucket\"] + +# Validate the inputs +if ([string]::IsNullOrWhitespace($username)) { + Write-Error \"The FindConflicts.Git.Credentials.Username variable must be provided\" +} + +if ([string]::IsNullOrWhitespace($password)) { + Write-Error \"The FindConflicts.Git.Credentials.Password variable must be provided\" +} + +if ([string]::IsNullOrWhitespace($protocol)) { + Write-Error \"The FindConflicts.Git.Url.Protocol variable must be provided\" +} + +if ([string]::IsNullOrWhitespace($gitHost)) { + Write-Error \"The FindConflicts.Git.Url.Host variable must be provided\" +} + +if ([string]::IsNullOrWhitespace($org)) { + Write-Error \"The FindConflicts.Git.Url.Organization variable must be provided\" +} + +if ([string]::IsNullOrWhitespace($repo)) { + Write-Error \"The FindConflicts.Git.Url.Template variable must be provided\" +} + +if ([string]::IsNullOrWhitespace($region)) { + Write-Error \"The FindConflicts.Terraform.Backend.S3Region variable must be provided\" +} + +if ([string]::IsNullOrWhitespace($key)) { + Write-Error \"The FindConflicts.Terraform.Backend.S3Key variable must be provided\" +} + +if ([string]::IsNullOrWhitespace($bucket)) { + Write-Error \"The FindConflicts.Terraform.Backend.S3Bucket variable must be provided\" +} + +$templateRepoUrl = $protocol + \"://\" + $gitHost + \"/\" + $org + \"/\" + $repo + \".git\" +$templateRepo = $protocol + \"://\" + $username + \":\" + $password + \"@\" + $gitHost + \"/\" + $org + \"/\" + $repo + \".git\" +$branch = \"main\" + +# Check to see if it's Windows +if ($IsWindows -and $OctopusParameters['Octopus.Workerpool.Name'] -eq \"Hosted Windows\") +{ + # Dynamic worker don't have git, download portable version and add to path for execution + Write-Host \"Detected usage of Windows Dynamic Worker ...\" + Get-GitExecutable -WorkingDirectory $PWD +} + +Write-TerraformBackend + +Invoke-CustomCommand \"git\" @(\"config\", \"--global\", \"user.email\", \"octopus@octopus.com\") | Write-Results +Invoke-CustomCommand \"git\" @(\"config\", \"--global\", \"user.name\", \"Octopus Server\") | Write-Results + +Invoke-CustomCommand \"terraform\" @(\"init\", \"-no-color\", \"-backend-config=`\"bucket=$bucket`\"\", \"-backend-config=`\"region=$region`\"\", \"-backend-config=`\"key=$key`\"\") | Write-Results + +Write-Host \"- Up to date\" +Write-Host \"> Can automatically merge\" +Write-Host \"× Merge conflict\" +Write-Host \"Verbose logs contain instructions for resolving merge conflicts.\" + +$workspaces = Invoke-CustomCommand \"terraform\" @(\"workspace\", \"list\") + +Write-Results $workspaces + +$parsedWorkspaces = $workspaces.StdOut.Replace(\"*\", \"\").Split(\"`n\") + +$downstreamCount = 0 +foreach ($workspace in $parsedWorkspaces) +{ + $trimmedWorkspace = $workspace | Format-StringAsNullOrTrimmed + + if ($trimmedWorkspace -eq \"default\" -or [string]::IsNullOrWhitespace($trimmedWorkspace)) + { + continue + } + + Write-Verbose \"Processing workspace $trimmedWorkspace\" + + Invoke-CustomCommand \"terraform\" @(\"workspace\", \"select\", $trimmedWorkspace) | Write-Results + + $state = Invoke-CustomCommand \"terraform\" @(\"show\", \"-json\") + + # state might include sensitive values, so don't print it unless there was an error + + if (-not $state.ExitCode -eq 0) + { + Write-Results $state + continue + } + + $parsedState = $state.StdOut | ConvertFrom-Json + + $resources = $parsedState.values.root_module.resources | Where-Object { + $_.type -eq \"octopusdeploy_project\" + } + + # The outputs allow us to contact the downstream instance) + $spaceId = Invoke-CustomCommand \"terraform\" @(\"output\", \"-raw\", \"octopus_space_id\") + $spaceName = Invoke-CustomCommand \"terraform\" @(\"output\", \"-raw\", \"octopus_space_name\") + $space = if ([string]::IsNullOrWhitespace($spaceName.StdOut)) + { + $spaceId.StdOut | Format-StringAsNullOrTrimmed + } + else + { + $spaceName.StdOut | Format-StringAsNullOrTrimmed + } + + foreach ($resource in $resources) + { + $url = $resource.values.git_library_persistence_settings.url | Format-StringAsNullOrTrimmed + $spaceId = $resource.values.space_id | Format-StringAsNullOrTrimmed + $name = $resource.values.name | Format-StringAsNullOrTrimmed + + if (-not [string]::IsNullOrWhitespace($url)) + { + $downstreamCount++ + + mkdir $trimmedWorkspace | Out-Null + + Invoke-CustomCommand \"git\" @(\"clone\", $url, $trimmedWorkspace) | Write-Results + Invoke-CustomCommand \"git\" @(\"remote\", 'add', 'upstream', $templateRepo) $trimmedWorkspace | Write-Results + Invoke-CustomCommand \"git\" @(\"fetch\", \"--all\") $trimmedWorkspace | Write-Results + Invoke-CustomCommand \"git\" @(\"checkout\", \"-b\", \"upstream-$branch\", \"upstream/$branch\") $trimmedWorkspace | Write-Results + + if (-not($branch -eq \"master\" -or $branch -eq \"main\")) + { + Invoke-CustomCommand \"git\" @(\"checkout\", \"-b\", $branch, \"origin/$branch\") $trimmedWorkspace | Write-Results + } + else + { + Invoke-CustomCommand \"git\" @(\"checkout\", $branch) $trimmedWorkspace | Write-Results + } + + $mergeBase = Invoke-CustomCommand \"git\" @(\"merge-base\", $branch, \"upstream-$branch\") $trimmedWorkspace + + Write-Results $mergeBase + + $mergeSourceCurrentCommit = Invoke-CustomCommand \"git\" @(\"rev-parse\", \"upstream-$branch\") $trimmedWorkspace + + Write-Results $mergeSourceCurrentCommit + + $mergeResult = Invoke-CustomCommand \"git\" @(\"merge\", \"--no-commit\", \"--no-ff\", \"upstream-$branch\") $trimmedWorkspace + + Write-Results $mergeResult + + if ($mergeBase.StdOut -eq $mergeSourceCurrentCommit.StdOut) + { + Write-Host \"$space `\"$name`\" $url -\" + } + elseif (-not $mergeResult.ExitCode -eq 0) + { + Write-Host \"$space `\"$name`\" $url ×\" + Write-Verbose \"To resolve the conflicts, run the following commands:\" + Write-Verbose \"mkdir cac\" + Write-Verbose \"cd cac\" + Write-Verbose \"git clone $url .\" + Write-Verbose \"git remote add upstream $templateRepoUrl\" + Write-Verbose \"git fetch --all\" + Write-Verbose \"git checkout -b upstream-$branch upstream/$branch\" + if (-not($branch -eq \"master\" -or $branch -eq \"main\")) + { + Write-Verbose \"git checkout -b $branch origin/$branch\" + } + else + { + Write-Verbose \"git checkout $branch\" + Write-Verbose \"git merge-base $branch upstream-$branch\" + Write-Verbose \"git merge --no-commit --no-ff upstream-$branch\" + } + } + else + { + Write-Host \"$space `\"$name`\" $url >\" + } + } + else { + Write-Verbose \"`\"$name`\" is not a CaC project\" + } + } +}", + "Octopus.Action.Aws.Region": "#{FindConflicts.Terraform.Backend.S3Region}", + "Octopus.Action.AwsAccount.Variable": "#{FindConflicts.Terraform.Aws.Account}" + }, + "Parameters": [ + { + "Id": "1eea75a0-74c7-4af9-8569-a9e9ece1bd55", + "Name": "FindConflicts.Git.Credentials.Username", + "Label": "Git Username", + "HelpText": "The git repo username. When using GitHub with an access token, the value is `x-access-token`.", + "DefaultValue": "x-access-token", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1cf36824-079a-4cf1-b67c-36f07933f642", + "Name": "FindConflicts.Git.Credentials.Password", + "Label": "Git Password", + "HelpText": "The git repo password or access token.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e6a7414d-8d84-4fb8-9149-2a87f20502bf", + "Name": "FindConflicts.Git.Url.Protocol", + "Label": "Git Protocol", + "HelpText": "The git repo protocol.", + "DefaultValue": "https", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "https|HTTPS +http|HTTP" + } + }, + { + "Id": "f5d0d829-ef6c-4e06-b115-7b2845339544", + "Name": "FindConflicts.Git.Url.Host", + "Label": "Git Hostname", + "HelpText": "The git repo host name.", + "DefaultValue": "github.com", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8b6e78e2-07fa-4783-9f9a-c23f5295f146", + "Name": "FindConflicts.Git.Url.Organization", + "Label": "Git Organization", + "HelpText": "The git repo owner or organization i.e. `owner` in the url `https://github.com/owner/repo`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1e99c543-9e80-41b2-9384-8cbefd5c3ee6", + "Name": "FindConflicts.Git.Url.Template", + "Label": "Git Template Repo", + "HelpText": "The repo holding the upstream, or template, CaC project i.e. `repo` in the url `https://github.com/owner/repo`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b0131561-4928-4dfb-85a9-a1282ac4a1be", + "Name": "FindConflicts.Terraform.Backend.S3Region", + "Label": "AWS Region", + "HelpText": "The AWS region hosting the S3 bucket persisting the Terraform state.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "652fa73d-99fa-4c81-9ef8-2e79d985222d", + "Name": "FindConflicts.Terraform.Backend.S3Key", + "Label": "S3 Key", + "HelpText": "The name of the file in the S3 bucket hosting the Terraform state.", + "DefaultValue": "Project_#{Octopus.Project.Name | Replace \"[^A-Za-z0-9]\" \"_\"}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7102a4ab-0f05-4b97-be26-8361b23df361", + "Name": "FindConflicts.Terraform.Backend.S3Bucket", + "Label": "S3 Bucket", + "HelpText": "The name of the S3 bucket hosting the Terraform state.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7015ee07-f265-41d4-863a-d1f13fcfbc68", + "Name": "FindConflicts.Terraform.Aws.Account", + "Label": "AWS Account", + "HelpText": "The AWS account used to access the S3 bucket.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + } + ], + "StepPackageId": "Octopus.AwsRunScript", + "$Meta": { + "ExportedAt": "2023-11-16T20:09:13.659Z", + "OctopusVersion": "2024.1.1838", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-get-octopus-usage.json.human b/step-templates/octopus-get-octopus-usage.json.human new file mode 100644 index 000000000..cfa063d82 --- /dev/null +++ b/step-templates/octopus-get-octopus-usage.json.human @@ -0,0 +1,645 @@ +{ + "Id": "f70f16a9-7bf3-448c-9532-e5a69364581d", + "Name": "Get Octopus Usage", + "Description": "Step template to gather Octopus usage details across spaces. The results will be output to the log and also as a downloadable artifact. + +To avoid slowing down your instance, this script will pull back 50 items at a time and count them. It is designed to run on instances as old as 3.4. + +**Required:** An API Key for a user or service account that has read access in every space on the instance.", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$OctopusDeployUrl = $OctopusParameters[\"GetUsage.Octopus.ServerUri\"] +$OctopusDeployApiKey = $OctopusParameters[\"GetUsage.Octopus.ApiKey\"] + +function Get-OctopusUrl +{ + param ( + $EndPoint, + $SpaceId, + $OctopusUrl + ) + + $octopusUrlToUse = $OctopusUrl + if ($OctopusUrl.EndsWith(\"/\")) + { + $octopusUrlToUse = $OctopusUrl.Substring(0, $OctopusUrl.Length - 1) + } + + if ($EndPoint -match \"/api\") + { + if (!$EndPoint.StartsWith(\"/api\")) + { + $EndPoint = $EndPoint.Substring($EndPoint.IndexOf(\"/api\")) + } + + return \"$octopusUrlToUse$EndPoint\" + } + + if ([string]::IsNullOrWhiteSpace($SpaceId)) + { + return \"$octopusUrlToUse/api/$EndPoint\" + } + + return \"$octopusUrlToUse/api/$spaceId/$EndPoint\" +} + +function Invoke-OctopusApi +{ + param + ( + $endPoint, + $spaceId, + $octopusUrl, + $apiKey + ) + + try + { + $url = Get-OctopusUrl -EndPoint $endPoint -SpaceId $spaceId -OctopusUrl $octopusUrl + + Write-Host \"Invoking $url\" + return Invoke-RestMethod -Method Get -Uri $url -Headers @{\"X-Octopus-ApiKey\" = \"$apiKey\" } -ContentType 'application/json; charset=utf-8' -TimeoutSec 60 + } + catch + { + Write-Host \"There was an error making a Get call to the $url. Please check that for more information.\" -ForegroundColor Red + + if ($null -ne $_.Exception.Response) + { + if ($_.Exception.Response.StatusCode -eq 401) + { + Write-Host \"Unauthorized error returned from $url, please verify API key and try again\" -ForegroundColor Red + } + elseif ($_.ErrorDetails.Message) + { + Write-Host -Message \"Error calling $url StatusCode: $($_.Exception.Response) $($_.ErrorDetails.Message)\" -ForegroundColor Red + Write-Host $_.Exception -ForegroundColor Red + } + else + { + Write-Host $_.Exception -ForegroundColor Red + } + } + else + { + Write-Host $_.Exception -ForegroundColor Red + } + + Write-Host \"Stopping the script from proceeding\" -ForegroundColor Red + exit 1 + } +} + +function Get-OctopusObjectCount +{ + param + ( + $endPoint, + $spaceId, + $octopusUrl, + $apiKey + ) + + $itemCount = 0 + $currentPage = 1 + $pageSize = 50 + $skipValue = 0 + $haveReachedEndOfList = $false + + while ($haveReachedEndOfList -eq $false) + { + $currentEndPoint = \"$($endPoint)?skip=$skipValue&take=$pageSize\" + + $itemList = Invoke-OctopusApi -endPoint $currentEndPoint -spaceId $spaceId -octopusUrl $octopusUrl -apiKey $apiKey + + foreach ($item in $itemList.Items) + { + if ($null -ne (Get-Member -InputObject $item -Name \"IsDisabled\" -MemberType Properties)) + { + if ($item.IsDisabled -eq $false) + { + $itemCount += 1 + } + } + else + { + $itemCount += 1 + } + } + + if ($currentPage -lt $itemList.NumberOfPages) + { + $skipValue = $currentPage * $pageSize + $currentPage += 1 + + Write-Host \"The endpoint $endpoint has reported there are $($itemList.NumberOfPages) pages. Setting the skip value to $skipValue and re-querying\" + } + else + { + $haveReachedEndOfList = $true + } + } + + return $itemCount +} + +function Get-OctopusDeploymentTargetsCount +{ + param + ( + $spaceId, + $octopusUrl, + $apiKey + ) + + $targetCount = @{ + TargetCount = 0 + ActiveTargetCount = 0 + UnavailableTargetCount = 0 + DisabledTargets = 0 + ActiveListeningTentacleTargets = 0 + ActivePollingTentacleTargets = 0 + ActiveSshTargets = 0 + ActiveKubernetesCount = 0 + ActiveAzureWebAppCount = 0 + ActiveAzureServiceFabricCount = 0 + ActiveAzureCloudServiceCount = 0 + ActiveOfflineDropCount = 0 + ActiveECSClusterCount = 0 + ActiveCloudRegions = 0 + ActiveFtpTargets = 0 + DisabledListeningTentacleTargets = 0 + DisabledPollingTentacleTargets = 0 + DisabledSshTargets = 0 + DisabledKubernetesCount = 0 + DisabledAzureWebAppCount = 0 + DisabledAzureServiceFabricCount = 0 + DisabledAzureCloudServiceCount = 0 + DisabledOfflineDropCount = 0 + DisabledECSClusterCount = 0 + DisabledCloudRegions = 0 + DisabledFtpTargets = 0 + } + + $currentPage = 1 + $pageSize = 50 + $skipValue = 0 + $haveReachedEndOfList = $false + + while ($haveReachedEndOfList -eq $false) + { + $currentEndPoint = \"machines?skip=$skipValue&take=$pageSize\" + + $itemList = Invoke-OctopusApi -endPoint $currentEndPoint -spaceId $spaceId -octopusUrl $octopusUrl -apiKey $apiKey + + foreach ($item in $itemList.Items) + { + $targetCount.TargetCount += 1 + + if ($item.IsDisabled -eq $true) + { + $targetCount.DisabledTargets += 1 + + if ($item.EndPoint.CommunicationStyle -eq \"None\") + { + $targetCount.DisabledCloudRegions += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"TentacleActive\") + { + $targetCount.DisabledPollingTentacleTargets += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"TentaclePassive\") + { + $targetCount.DisabledListeningTentacleTargets += 1 + } + # Cover newer k8s agent and traditional worker-API approach + elseif ($item.EndPoint.CommunicationStyle -ilike \"Kubernetes*\") + { + $targetCount.DisabledKubernetesCount += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"AzureWebApp\") + { + $targetCount.DisabledAzureWebAppCount += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"Ssh\") + { + $targetCount.DisabledSshTargets += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"Ftp\") + { + $targetCount.DisabledFtpTargets += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"AzureCloudService\") + { + $targetCount.DisabledAzureCloudServiceCount += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"AzureServiceFabricCluster\") + { + $targetCount.DisabledAzureServiceFabricCount += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"OfflineDrop\") + { + $targetCount.DisabledOfflineDropCount += 1 + } + else + { + $targetCount.DisabledECSClusterCount += 1 + } + } + else + { + if ($item.HealthStatus -eq \"Healthy\" -or $item.HealthStatus -eq \"HealthyWithWarnings\") + { + $targetCount.ActiveTargetCount += 1 + } + else + { + $targetCount.UnavailableTargetCount += 1 + } + + if ($item.EndPoint.CommunicationStyle -eq \"None\") + { + $targetCount.ActiveCloudRegions += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"TentacleActive\") + { + $targetCount.ActivePollingTentacleTargets += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"TentaclePassive\") + { + $targetCount.ActiveListeningTentacleTargets += 1 + } + # Cover newer k8s agent and traditional worker-API approach + elseif ($item.EndPoint.CommunicationStyle -ilike \"Kubernetes*\") + { + $targetCount.ActiveKubernetesCount += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"AzureWebApp\") + { + $targetCount.ActiveAzureWebAppCount += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"Ssh\") + { + $targetCount.ActiveSshTargets += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"Ftp\") + { + $targetCount.ActiveFtpTargets += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"AzureCloudService\") + { + $targetCount.ActiveAzureCloudServiceCount += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"AzureServiceFabricCluster\") + { + $targetCount.ActiveAzureServiceFabricCount += 1 + } + elseif ($item.EndPoint.CommunicationStyle -eq \"OfflineDrop\") + { + $targetCount.ActiveOfflineDropCount += 1 + } + else + { + $targetCount.ActiveECSClusterCount += 1 + } + } + } + + if ($currentPage -lt $itemList.NumberOfPages) + { + $skipValue = $currentPage * $pageSize + $currentPage += 1 + + Write-Host \"The endpoint $endpoint has reported there are $($itemList.NumberOfPages) pages. Setting the skip value to $skipValue and re-querying\" + } + else + { + $haveReachedEndOfList = $true + } + } + + return $targetCount +} + +$ObjectCounts = @{ + ProjectCount = 0 + TenantCount = 0 + TargetCount = 0 + DisabledTargets = 0 + ActiveTargetCount = 0 + UnavailableTargetCount = 0 + ActiveListeningTentacleTargets = 0 + ActivePollingTentacleTargets = 0 + ActiveSshTargets = 0 + ActiveKubernetesCount = 0 + ActiveAzureWebAppCount = 0 + ActiveAzureServiceFabricCount = 0 + ActiveAzureCloudServiceCount = 0 + ActiveOfflineDropCount = 0 + ActiveECSClusterCount = 0 + ActiveCloudRegions = 0 + ActiveFtpTargets = 0 + DisabledListeningTentacleTargets = 0 + DisabledPollingTentacleTargets = 0 + DisabledSshTargets = 0 + DisabledKubernetesCount = 0 + DisabledAzureWebAppCount = 0 + DisabledAzureServiceFabricCount = 0 + DisabledAzureCloudServiceCount = 0 + DisabledOfflineDropCount = 0 + DisabledECSClusterCount = 0 + DisabledCloudRegions = 0 + DisabledFtpTargets = 0 + WorkerCount = 0 + ListeningTentacleWorkers = 0 + PollingTentacleWorkers = 0 + SshWorkers = 0 + ActiveWorkerCount = 0 + UnavailableWorkerCount = 0 + WindowsLinuxAgentCount = 0 + LicensedTargetCount = 0 + LicensedWorkerCount = 0 +} + +Write-Host \"Getting Octopus Deploy Version Information\" +$apiInformation = Invoke-OctopusApi -endPoint \"/api\" -spaceId $null -octopusUrl $OctopusDeployUrl -apiKey $OctopusDeployApiKey +$splitVersion = $apiInformation.Version -split \"\\.\" +$OctopusMajorVersion = [int]$splitVersion[0] +$OctopusMinorVersion = [int]$splitVersion[1] + +$hasLicenseSummary = $OctopusMajorVersion -ge 4 +$hasSpaces = $OctopusMajorVersion -ge 2019 +$hasWorkers = ($OctopusMajorVersion -eq 2018 -and $OctopusMinorVersion -ge 7) -or $OctopusMajorVersion -ge 2019 + +$spaceIdList = @() +if ($hasSpaces -eq $true) +{ + $OctopusSpaceList = Invoke-OctopusApi -endPoint \"spaces?skip=0&take=10000\" -octopusUrl $OctopusDeployUrl -spaceId $null -apiKey $OctopusDeployApiKey + foreach ($space in $OctopusSpaceList.Items) + { + $spaceIdList += $space.Id + } +} +else +{ + $spaceIdList += $null +} + +if ($hasLicenseSummary -eq $true) +{ + Write-Host \"Checking the license summary for this instance\" + $licenseSummary = Invoke-OctopusApi -endPoint \"licenses/licenses-current-status\" -octopusUrl $OctopusDeployUrl -spaceId $null -apiKey $OctopusDeployApiKey + + if ($null -ne (Get-Member -InputObject $licenseSummary -Name \"NumberOfMachines\" -MemberType Properties)) + { + $ObjectCounts.LicensedTargetCount = $licenseSummary.NumberOfMachines + } + else + { + foreach ($limit in $licenseSummary.Limits) + { + if ($limit.Name -eq \"Targets\") + { + Write-Host \"Your instance is currently using $($limit.CurrentUsage) Targets\" + $ObjectCounts.LicensedTargetCount = $limit.CurrentUsage + } + + if ($limit.Name -eq \"Workers\") + { + Write-Host \"Your instance is currently using $($limit.CurrentUsage) Workers\" + $ObjectCounts.LicensedWorkerCount = $limit.CurrentUsage + } + } + } +} + + +foreach ($spaceId in $spaceIdList) +{ + Write-Host \"Getting project counts for $spaceId\" + $activeProjectCount = Get-OctopusObjectCount -endPoint \"projects\" -spaceId $spaceId -octopusUrl $OctopusDeployUrl -apiKey $OctopusDeployApiKey + + Write-Host \"$spaceId has $activeProjectCount active projects.\" + $ObjectCounts.ProjectCount += $activeProjectCount + + Write-Host \"Getting tenant counts for $spaceId\" + $activeTenantCount = Get-OctopusObjectCount -endPoint \"tenants\" -spaceId $spaceId -octopusUrl $OctopusDeployUrl -apiKey $OctopusDeployApiKey + + Write-Host \"$spaceId has $activeTenantCount tenants.\" + $ObjectCounts.TenantCount += $activeTenantCount + + Write-Host \"Getting Infrastructure Summary for $spaceId\" + $infrastructureSummary = Get-OctopusDeploymentTargetsCount -spaceId $spaceId -octopusUrl $OctopusDeployUrl -apiKey $OctopusDeployApiKey + + Write-host \"$spaceId has $($infrastructureSummary.TargetCount) targets\" + $ObjectCounts.TargetCount += $infrastructureSummary.TargetCount + + Write-Host \"$spaceId has $($infrastructureSummary.ActiveTargetCount) Healthy Targets\" + $ObjectCounts.ActiveTargetCount += $infrastructureSummary.ActiveTargetCount + + Write-Host \"$spaceId has $($infrastructureSummary.DisabledTargets) Disabled Targets\" + $ObjectCounts.DisabledTargets += $infrastructureSummary.DisabledTargets + + Write-Host \"$spaceId has $($infrastructureSummary.UnavailableTargetCount) Unhealthy Targets\" + $ObjectCounts.UnavailableTargetCount += $infrastructureSummary.UnavailableTargetCount + + Write-host \"$spaceId has $($infrastructureSummary.ActiveListeningTentacleTargets) Active Listening Tentacles Targets\" + $ObjectCounts.ActiveListeningTentacleTargets += $infrastructureSummary.ActiveListeningTentacleTargets + + Write-host \"$spaceId has $($infrastructureSummary.ActivePollingTentacleTargets) Active Polling Tentacles Targets\" + $ObjectCounts.ActivePollingTentacleTargets += $infrastructureSummary.ActivePollingTentacleTargets + + Write-host \"$spaceId has $($infrastructureSummary.ActiveCloudRegions) Active Cloud Region Targets\" + $ObjectCounts.ActiveCloudRegions += $infrastructureSummary.ActiveCloudRegions + + Write-host \"$spaceId has $($infrastructureSummary.ActiveOfflineDropCount) Active Offline Packages\" + $ObjectCounts.ActiveOfflineDropCount += $infrastructureSummary.ActiveOfflineDropCount + + Write-host \"$spaceId has $($infrastructureSummary.ActiveSshTargets) Active SSH Targets\" + $ObjectCounts.ActiveSshTargets += $infrastructureSummary.ActiveSshTargets + + Write-host \"$spaceId has $($infrastructureSummary.ActiveSshTargets) Active Kubernetes Targets\" + $ObjectCounts.ActiveKubernetesCount += $infrastructureSummary.ActiveKubernetesCount + + Write-host \"$spaceId has $($infrastructureSummary.ActiveAzureWebAppCount) Active Azure Web App Targets\" + $ObjectCounts.ActiveAzureWebAppCount += $infrastructureSummary.ActiveAzureWebAppCount + + Write-host \"$spaceId has $($infrastructureSummary.ActiveAzureServiceFabricCount) Active Azure Service Fabric Cluster Targets\" + $ObjectCounts.ActiveAzureServiceFabricCount += $infrastructureSummary.ActiveAzureServiceFabricCount + + Write-host \"$spaceId has $($infrastructureSummary.ActiveAzureCloudServiceCount) Active (Legacy) Azure Cloud Service Targets\" + $ObjectCounts.ActiveAzureCloudServiceCount += $infrastructureSummary.ActiveAzureCloudServiceCount + + Write-host \"$spaceId has $($infrastructureSummary.ActiveECSClusterCount) Active ECS Cluster Targets\" + $ObjectCounts.ActiveECSClusterCount += $infrastructureSummary.ActiveECSClusterCount + + Write-host \"$spaceId has $($infrastructureSummary.ActiveFtpTargets) Active FTP Targets\" + $ObjectCounts.ActiveFtpTargets += $infrastructureSummary.ActiveFtpTargets + + Write-host \"$spaceId has $($infrastructureSummary.DisabledListeningTentacleTargets) Disabled Listening Tentacles Targets\" + $ObjectCounts.DisabledListeningTentacleTargets += $infrastructureSummary.DisabledListeningTentacleTargets + + Write-host \"$spaceId has $($infrastructureSummary.DisabledPollingTentacleTargets) Disabled Polling Tentacles Targets\" + $ObjectCounts.DisabledPollingTentacleTargets += $infrastructureSummary.DisabledPollingTentacleTargets + + Write-host \"$spaceId has $($infrastructureSummary.DisabledCloudRegions) Disabled Cloud Region Targets\" + $ObjectCounts.DisabledCloudRegions += $infrastructureSummary.DisabledCloudRegions + + Write-host \"$spaceId has $($infrastructureSummary.DisabledOfflineDropCount) Disabled Offline Packages\" + $ObjectCounts.DisabledOfflineDropCount += $infrastructureSummary.DisabledOfflineDropCount + + Write-host \"$spaceId has $($infrastructureSummary.DisabledSshTargets) Disabled SSH Targets\" + $ObjectCounts.DisabledSshTargets += $infrastructureSummary.DisabledSshTargets + + Write-host \"$spaceId has $($infrastructureSummary.ActiveSshTargets) Disabled Kubernetes Targets\" + $ObjectCounts.DisabledKubernetesCount += $infrastructureSummary.DisabledKubernetesCount + + Write-host \"$spaceId has $($infrastructureSummary.DisabledAzureWebAppCount) Disabled Azure Web App Targets\" + $ObjectCounts.DisabledAzureWebAppCount += $infrastructureSummary.DisabledAzureWebAppCount + + Write-host \"$spaceId has $($infrastructureSummary.DisabledAzureServiceFabricCount) Disabled Azure Service Fabric Cluster Targets\" + $ObjectCounts.DisabledAzureServiceFabricCount += $infrastructureSummary.DisabledAzureServiceFabricCount + + Write-host \"$spaceId has $($infrastructureSummary.DisabledAzureCloudServiceCount) Disabled (Legacy) Azure Cloud Service Targets\" + $ObjectCounts.DisabledAzureCloudServiceCount += $infrastructureSummary.DisabledAzureCloudServiceCount + + Write-host \"$spaceId has $($infrastructureSummary.DisabledECSClusterCount) Disabled ECS Cluster Targets\" + $ObjectCounts.DisabledECSClusterCount += $infrastructureSummary.DisabledECSClusterCount + + Write-host \"$spaceId has $($infrastructureSummary.DisabledFtpTargets) Disabled FTP Targets\" + $ObjectCounts.DisabledFtpTargets += $infrastructureSummary.DisabledFtpTargets + + if ($hasWorkers -eq $true) + { + Write-Host \"Getting worker information for $spaceId\" + $workerPoolSummary = Invoke-OctopusApi -endPoint \"workerpools/summary\" -spaceId $spaceId -octopusUrl $OctopusDeployUrl -apiKey $OctopusDeployApiKey + + Write-host \"$spaceId has $($workerPoolSummary.TotalMachines) Workers\" + $ObjectCounts.WorkerCount += $workerPoolSummary.TotalMachines + + Write-Host \"$spaceId has $($workerPoolSummary.MachineHealthStatusSummaries.Healthy) Healthy Workers\" + $ObjectCounts.ActiveWorkerCount += $workerPoolSummary.MachineHealthStatusSummaries.Healthy + + Write-Host \"$spaceId has $($workerPoolSummary.MachineHealthStatusSummaries.HasWarnings) Healthy with Warning Workers\" + $ObjectCounts.ActiveWorkerCount += $workerPoolSummary.MachineHealthStatusSummaries.HasWarnings + + Write-Host \"$spaceId has $($workerPoolSummary.MachineHealthStatusSummaries.Unhealthy) Unhealthy Workers\" + $ObjectCounts.UnavailableWorkerCount += $workerPoolSummary.MachineHealthStatusSummaries.Unhealthy + + Write-Host \"$spaceId has $($workerPoolSummary.MachineHealthStatusSummaries.Unknown) Workers with a Status of Unknown\" + $ObjectCounts.UnavailableWorkerCount += $workerPoolSummary.MachineHealthStatusSummaries.Unknown + + Write-host \"$spaceId has $($workerPoolSummary.MachineEndpointSummaries.TentaclePassive) Listening Tentacles Workers\" + $ObjectCounts.ListeningTentacleWorkers += $workerPoolSummary.MachineEndpointSummaries.TentaclePassive + + Write-host \"$spaceId has $($workerPoolSummary.MachineEndpointSummaries.TentacleActive) Polling Tentacles Workers\" + $ObjectCounts.PollingTentacleWorkers += $workerPoolSummary.MachineEndpointSummaries.TentacleActive + + if ($null -ne (Get-Member -InputObject $workerPoolSummary.MachineEndpointSummaries -Name \"Ssh\" -MemberType Properties)) + { + Write-host \"$spaceId has $($workerPoolSummary.MachineEndpointSummaries.TentacleActive) SSH Targets Workers\" + $ObjectCounts.SshWorkers += $workerPoolSummary.MachineEndpointSummaries.Ssh + } + } +} + +Write-Host \"Calculating Windows and Linux Agent Count\" +$ObjectCounts.WindowsLinuxAgentCount = $ObjectCounts.ActivePollingTentacleTargets + $ObjectCounts.ActiveListeningTentacleTargets + $ObjectCounts.ActiveSshTargets + +if ($hasLicenseSummary -eq $false) +{ + $ObjectCounts.LicensedTargetCount = $ObjectCounts.TargetCount - $ObjectCounts.ActiveCloudRegions - $ObjectCounts.DisabledTargets +} + + +# Get node information +$nodeInfo = Invoke-OctopusApi -endPoint \"octopusservernodes\" -octopusUrl $OctopusDeployUrl -spaceId $null -apiKey $OctopusDeployApiKey + +$text = @\" +The item counts are as follows: +`tInstance ID: $($apiInformation.InstallationId) +`tServer Version: $($apiInformation.Version) +`tNumber of Server Nodes: $($nodeInfo.TotalResults) +`tLicensed Target Count: $($ObjectCounts.LicensedTargetCount) (these are active targets de-duped across the instance if running a modern version of Octopus) +`tProject Count: $($ObjectCounts.ProjectCount) +`tTenant Count: $($ObjectCounts.TenantCount) +`tAgent Counts: $($ObjectCounts.WindowsLinuxAgentCount) +`tDeployment Target Count: $($ObjectCounts.TargetCount) +`t`tActive and Available Targets: $($ObjectCounts.ActiveTargetCount) +`t`tActive but Unavailable Targets: $($ObjectCounts.UnavailableTargetCount) +`t`tActive Target Breakdown +`t`t`tListening Tentacle Target Count: $($ObjectCounts.ActiveListeningTentacleTargets) +`t`t`tPolling Tentacle Target Count: $($ObjectCounts.ActivePollingTentacleTargets) +`t`t`tSSH Target Count: $($ObjectCounts.ActiveSshTargets) +`t`t`tKubernetes Target Count: $($ObjectCounts.ActiveKubernetesCount) +`t`t`tAzure Web App Target Count: $($ObjectCounts.ActiveAzureWebAppCount) +`t`t`tAzure Service Fabric Cluster Target Count: $($ObjectCounts.ActiveAzureServiceFabricCount) +`t`t`tAzure (Legacy) Cloud Service Target Count: $($ObjectCounts.ActiveAzureCloudServiceCount) +`t`t`tAWS ECS Cluster Target Count: $($ObjectCounts.ActiveECSClusterCount) +`t`t`tOffline Target Count: $($ObjectCounts.ActiveOfflineDropCount) +`t`t`tCloud Region Target Count: $($ObjectCounts.ActiveCloudRegions) +`t`t`tFtp Target Count: $($ObjectCounts.ActiveFtpTargets) +`t`tDisabled Targets Targets: $($ObjectCounts.DisabledTargets) +`t`tDisabled Target Breakdown +`t`t`tListening Tentacle Target Count: $($ObjectCounts.DisabledListeningTentacleTargets) +`t`t`tPolling Tentacle Target Count: $($ObjectCounts.DisabledPollingTentacleTargets) +`t`t`tSSH Target Count: $($ObjectCounts.DisabledSshTargets) +`t`t`tKubernetes Target Count: $($ObjectCounts.DisabledKubernetesCount) +`t`t`tAzure Web App Target Count: $($ObjectCounts.DisabledAzureWebAppCount) +`t`t`tAzure Service Fabric Cluster Target Count: $($ObjectCounts.DisabledAzureServiceFabricCount) +`t`t`tAzure (Legacy) Cloud Service Target Count: $($ObjectCounts.DisabledAzureCloudServiceCount) +`t`t`tAWS ECS Cluster Target Count: $($ObjectCounts.DisabledECSClusterCount) +`t`t`tOffline Target Count: $($ObjectCounts.DisabledOfflineDropCount) +`t`t`tCloud Region Target Count: $($ObjectCounts.DisabledCloudRegions) +`t`t`tFtp Target Count: $($ObjectCounts.DisabledFtpTargets) +`tWorker Count: $($ObjectCounts.WorkerCount) +`t`tActive Workers: $($ObjectCounts.ActiveWorkerCount) +`t`tUnavailable Workers: $($ObjectCounts.UnavailableWorkerCount) +`t`tWorker Breakdown +`t`t`tListening Tentacle Target Count: $($ObjectCounts.ListeningTentacleWorkers) +`t`t`tPolling Tentacle Target Count: $($ObjectCounts.PollingTentacleWorkers) +`t`t`tSSH Target Count: $($ObjectCounts.SshWorkers) +\"@ + +Write-Host $text +$text > octopus-usage.txt +New-OctopusArtifact \"$($PWD)/octopus-usage.txt\"" + }, + "Parameters": [ + { + "Id": "afe5d565-c87a-481d-a9d1-005a32763b9e", + "Name": "GetUsage.Octopus.ServerUri", + "Label": "Octopus Server URI", + "HelpText": "The URI of the Octopus Server. For use on the same server, #{Octopus.Web.ServerUri} will work.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "51bfc97d-d174-452d-a852-0e120cc4cba8", + "Name": "GetUsage.Octopus.ApiKey", + "Label": "Octopus Server API Key", + "HelpText": "The API key with read permissions to all spaces in the instance being inspected.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2024-02-07T15:56:59.084Z", + "OctopusVersion": "2024.1.10309", + "Type": "ActionTemplate" + }, + "Author": "ryanrousseau", + "LastModifiedBy": "harrisonmeister", + "Category": "Octopus" +} diff --git a/step-templates/octopus-import-certificate.json.human b/step-templates/octopus-import-certificate.json.human new file mode 100644 index 000000000..7c5daf6aa --- /dev/null +++ b/step-templates/octopus-import-certificate.json.human @@ -0,0 +1,359 @@ +{ + "Id": "3b1f6c62-c2cb-480b-9b14-435686b9f2cc", + "Name": "Octopus - Import Certificate", + "Description": "Create or replace an [Octopus Certificate](https://octopus.com/docs/deploying-applications/certificates) from a certificate file", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": " + +<# + ----- Octopus - Import Certificate ----- + Paul Marston @paulmarsy (paul@marston.me) +Links + https://github.com/OctopusDeploy/Library/commits/master/step-templates/octopus-import-certificate.json +#> + +$securityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 +[Net.ServicePointManager]::SecurityProtocol = $securityProtocol + +$ErrorActionPreference = 'Stop' + +$StepTemplate_BaseUrl = $StepTemplate_OctopusUrl.Trim('/') + +if ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) { + throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\" +} +filter Out-Verbose { + Write-Verbose ($_ | Out-String) +} +filter Out-Indented { + $_ | Out-String | % Trim | % Split \"`n\" | % { \"`t$_\" } +} +function Invoke-OctopusApi { + param( + [Parameter(Position = 0, Mandatory)]$Uri, + [ValidateSet(\"Get\", \"Post\")]$Method = 'Get', + $Body + ) + $requestParameters = @{ + Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/')) + Method = $Method + Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey } + UseBasicParsing = $true + } + Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" + if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) } + try { + Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output + } + catch [System.Net.WebException] { + if ($_.Exception.Response) { + $errorResponse = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()).ReadToEnd() + throw (\"$($_.Exception.Message)`n{0}\" -f $errorResponse) + } + + if ($_.Exception.Message) { + \t$message = $_.Exception.Message + \tWrite-Highlight $message + throw \"$message\" + } + } +} + +function Test-SpacesApi { +\tWrite-Verbose \"Checking API compatibility\"; +\t$rootDocument = Invoke-OctopusApi 'api/'; + if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) { + \tWrite-Verbose \"Spaces API found\" + \treturn $true; + } + Write-Verbose \"Pre-spaces API found\" + return $false; +} + +function Get-OctopusItems +{ +\t# Define parameters + param( + \t$OctopusUri, + $ApiKey, + $SkipCount = 0 + ) + + # Define working variables + $items = @() + $skipQueryString = \"\" + $headers = @{\"X-Octopus-ApiKey\"=\"$ApiKey\"} + + # Check to see if there there is already a querystring + if ($octopusUri.Contains(\"?\")) + { + $skipQueryString = \"&skip=\" + } + else + { + $skipQueryString = \"?skip=\" + } + + $skipQueryString += $SkipCount + + # Get intial set + $resultSet = Invoke-RestMethod -Uri \"$($OctopusUri)$skipQueryString\" -Method GET -Headers $headers + + # Check to see if it returned an item collection + if ($resultSet.Items) + { + # Store call results + $items += $resultSet.Items + + # Check to see if resultset is bigger than page amount + if (($resultSet.Items.Count -gt 0) -and ($resultSet.Items.Count -eq $resultSet.ItemsPerPage)) + { + # Increment skip count + $SkipCount += $resultSet.ItemsPerPage + + # Recurse + $items += Get-OctopusItems -OctopusUri $OctopusUri -ApiKey $ApiKey -SkipCount $SkipCount + } + } + else + { + return $resultSet + } + + + # Return results + return $items +} + +function Get-OctopusIds +{ +\t# Define parameters + param ( + \t$OctopusCollection, + $NamesArray + ) + + $returnList = @() + + foreach ($item in $NamesArray) + { + \t# Trim item + $item = $item.Trim() + + # Compare + $octopusItem = $OctopusCollection | Where-Object {$_.Name -eq $item} + + if ($null -ne $octopusItem) + { + \t# Add to array + $returnList += $item.Id + } + } + + # Return list + return $returnList +} + +if(Test-SpacesApi) { +\t$spaceId = $OctopusParameters['Octopus.Space.Id']; + if([string]::IsNullOrWhiteSpace($spaceId)) { + throw \"This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which isn't expected - please reach out to our support team at https://help.octopus.com if you encounter this error.\"; + } +\t$baseApiUrl = \"/api/$spaceId\" ; +} else { +\t$baseApiUrl = \"/api\" ; +} + +# Get all environments +Write-Host \"Getting list of Environments ...$($StepTemplate_BaseUrl)$($baseApiUrl)/environments\" +$environmentList = Get-OctopusItems -OctopusUri \"$($StepTemplate_BaseUrl)$($baseApiUrl)/environments\" -ApiKey $StepTemplate_ApiKey +$environmentIds = Get-OctopusIds -OctopusCollection $environmentList -NamesArray $StepTemplate_Environments.Split(\",\") + +# Get tenants +Write-Host \"Getting list of Tenants ...\" +$tenantList = Get-OctopusItems -OctopusUri \"$($StepTemplate_BaseUrl)$($baseApiUrl)/tenants\" -ApiKey $StepTemplate_ApiKey +$tenantIds = Get-OctopusIds -OctopusCollection $tenantList -NamesArray $StepTemplate_Tenants.Split(\",\") + +# Get tenant tags +Write-Host \"Getting list of Tenant Tags ...\" +$tenantTagList = Get-OctopusItems -OctopusUri \"$($StepTemplate_BaseUrl)$($baseApiUrl)/tagsets\" -ApiKey $StepTemplate_ApiKey +$tenantTagIds = Get-OctopusIds -OctopusCollection $tenantTagList -NamesArray $StepTemplate_TenantTags.Split(\",\") + +$certificate = switch ($StepTemplate_CertEncoding) { + 'file' { + if (!(Test-Path $StepTemplate_Certificate)) { + throw \"Certificate file $StepTemplate_Certificate does not exist\" + } + $certificateBytes = Get-Content -Path $StepTemplate_Certificate -Encoding Byte + [System.Convert]::ToBase64String($certificateBytes) + } + 'base64' { + $StepTemplate_Certificate + } +} + +$existingCert = Invoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName +if ($existingCert) { + Write-Host 'Existing certificate will be archived & replaced...' + Invoke-OctopusApi (\"$baseApiUrl/certificates/{0}/replace\" -f $existingCert.Id) -Method Post -Body @{ + certificateData = $certificate + password = $StepTemplate_Password + } | % { + $_.CertificateData = $null + $_.Password = $null + $_ + } | Out-Verbose +} else { + Write-Host 'Creating & importing new certificate...' + Invoke-OctopusApi \"$baseApiUrl/certificates\" -Method Post -Body @{ + Name = $StepTemplate_CertificateName + CertificateData = @{ + HasValue = $true + NewValue = $certificate + } + Password = @{ + HasValue = $true + NewValue = $StepTemplate_Password + } + TenantedDeploymentParticipation = $StepTemplate_TenantParticipation + EnvironmentIds = $environmentIds + TenantIds = $tenantIds + TenantTags = $tenantTagIds + } | Out-Verbose +} +Write-Host 'Certificate has been imported:' +Invoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName | Out-Indented", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "6a723531-1272-4c7f-ae04-9576051396ad", + "Name": "StepTemplate_OctopusUrl", + "Label": "Octopus Url", + "HelpText": "Provide the URL of your Octopus Server. The default is `#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}`. Cloud instances should use `Octopus.Web.ServerUri`. See [System Variables - Server](https://octopus.com/docs/projects/variables/system-variables#Systemvariables-Server) for more info.", + "DefaultValue": "#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9a84f62c-99f7-4349-bf6d-f42397f4de73", + "Name": "StepTemplate_ApiKey", + "Label": "API Key", + "HelpText": "Provide an Octopus API Key with appropriate permissions to save the certificate.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "4fcb5ddf-14a9-42b1-8e77-d8d68e69b2fe", + "Name": "StepTemplate_CertificateName", + "Label": "Certificate Name", + "HelpText": "A short, memorable, unique name for this certificate. + +If the certificate already exists it [will be replaced](https://octopus.com/docs/deployments/certificates/replace-certificate).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "0664b204-c11e-47d1-b388-58ef0b0a7b1a", + "Name": "StepTemplate_CertEncoding", + "Label": "Certificate Encoding", + "HelpText": "Defines the format of the **Certificate** parameter.", + "DefaultValue": "file", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "file|File Path +base64|Base64 Encoded String" + }, + "Links": {} + }, + { + "Id": "df336e72-328a-4bad-92b1-374155ec3fb4", + "Name": "StepTemplate_Certificate", + "Label": "Certificate", + "HelpText": "The certificate to import into Octopus, either as a **File Path** to the certificate, or as a **Base64 Encoded String** representation depending on the _Certificate Encoding_ chosen. + +Supported formats: [PFX (PKCS #12), DER, PEM](https://octopus.com/docs/deployments/certificates)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "b875e962-5edc-44e8-be03-51f8a87eca5d", + "Name": "StepTemplate_Password", + "Label": "Password", + "HelpText": "The password protecting the certificate (if required).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "9d87e43f-d17a-40ea-affc-a3755f1cc16a", + "Name": "StepTemplate_Environments", + "Label": "Environments", + "HelpText": "Comma-delimited list of environments to restrict certificate to. A blank value will not restrict the certificate.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "014474f8-d7a2-4d67-b57f-364ba723ece2", + "Name": "StepTemplate_TenantParticipation", + "Label": "Tenant Participation", + "HelpText": "Select the tenant participation level.", + "DefaultValue": "untenanted", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "untenanted|Untenanted +tenanted|Tenanted +tenantedoruntenanted|Tenanted or untenanted" + } + }, + { + "Id": "bf9a1f70-0c67-4070-b4ff-e38f159ff701", + "Name": "StepTemplate_Tenants", + "Label": "Tenants", + "HelpText": "Comma-delimited list of tenants that can use this certificate. Used with `Tenant Participation` values of `Tenanted` or `Tenanted or untenanted`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e041fbd8-66da-4b6a-ae3e-90d20f110a48", + "Name": "StepTemplate_TenantTags", + "Label": "Tenant Tags", + "HelpText": "Comma-delimited list of tenant tags to apply to the certificate.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2022-11-14T23:22:38.482Z", + "OctopusVersion": "2022.4.8111", + "Type": "ActionTemplate" + }, + "Category": "octopus", + "Author": "paulmarsy" +} diff --git a/step-templates/octopus-lookup-space-id.json.human b/step-templates/octopus-lookup-space-id.json.human new file mode 100644 index 000000000..5c76bacc1 --- /dev/null +++ b/step-templates/octopus-lookup-space-id.json.human @@ -0,0 +1,161 @@ +{ + "Id": "324f747e-e2cd-439d-a660-774baf4991f2", + "Name": "Octopus - Lookup Space ID", + "Description": "This step queries an Octopus server to return the ID of a named space. The ID is captured in an output variables called `SpaceID`.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptBody": "# This script exists for those scenarios where the tenant space is created as part of the same process that +# attempts to populate the space. The tenant space ID variable, saved when the space is created, won't be refreshed +# in the middle of the deployment, so we query it directly. + +import argparse +import json +import urllib.request +import urllib.parse +import os +import sys +import time + +# If this script is not being run as part of an Octopus step, return variables from environment variables. +# Periods are replaced with underscores, and the variable name is converted to uppercase +if \"get_octopusvariable\" not in globals(): + def get_octopusvariable(variable): + return os.environ[re.sub('\\\\.', '_', variable.upper())] + +# If this script is not being run as part of an Octopus step, just print any set variable to std out. +if \"set_octopusvariable\" not in globals(): + def set_octopusvariable(name, variable): + print(variable) + + +def get_octopusvariable_quiet(variable): + \"\"\" + Gets an octopus variable, or an empty string if it does not exist. + :param variable: The variable name + :return: The variable value, or an empty string if the variable does not exist + \"\"\" + try: + return get_octopusvariable(variable) + except: + return '' + + +def init_argparse(): + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTION] [FILE]...', + description='Lookup a space ID from the name' + ) + parser.add_argument('--server-url', + action='store', + default=get_octopusvariable_quiet( + 'SpaceLookup.ThisInstance.Server.Url') or get_octopusvariable_quiet( + 'ThisInstance.Server.Url'), + help='Sets the Octopus server URL.') + parser.add_argument('--api-key', + action='store', + default=get_octopusvariable_quiet( + 'SpaceLookup.ThisInstance.Api.Key') or get_octopusvariable_quiet('ThisInstance.Api.Key'), + help='Sets the Octopus API key.') + parser.add_argument('--space-name', + action='store', + default=get_octopusvariable_quiet('SpaceLookup.Lookup.Space.Name') or get_octopusvariable_quiet( + 'Lookup.Space.Name') or get_octopusvariable_quiet('Octopus.Deployment.Tenant.Name'), + help='The name of the space to lookup.') + + return parser.parse_known_args() + + +parser, _ = init_argparse() + +# Variable precondition checks +if len(parser.server_url) == 0: + print(\"--server-url, ThisInstance.Server.Url, or SerializeProject.ThisInstance.Server.Url must be defined\") + sys.exit(1) + +if len(parser.api_key) == 0: + print(\"--api-key, ThisInstance.Api.Key, or ThisInstance.Api.Key must be defined\") + sys.exit(1) + +if len(parser.space_name) == 0: + print(\"--space-name, Lookup.Space.Name, SpaceLookup.Lookup.Space.Name, or Octopus.Deployment.Tenant.Name must be defined\") + sys.exit(1) + +url = parser.server_url + '/api/Spaces?partialName=' + urllib.parse.quote(parser.space_name) + \"&take=1000\" +headers = { + 'X-Octopus-ApiKey': parser.api_key, + 'Accept': 'application/json' +} +request = urllib.request.Request(url, headers=headers) + +# Retry the request for up to a minute. +response = None +for x in range(12): + response = urllib.request.urlopen(request) + if response.getcode() == 200: + break + time.sleep(5) + +if not response or not response.getcode() == 200: + print('The API query failed') + sys.exit(1) + +data = json.loads(response.read().decode(\"utf-8\")) + +space = [x for x in data['Items'] if x['Name'] == parser.space_name] + +if len(space) != 0: + print('Space ' + parser.space_name + ' has ID ' + space[0]['Id']) + print('The space ID is available as the output variable #{Octopus.Action[' + get_octopusvariable_quiet('Octopus.Action.Name') + '].Output.SpaceID}') + set_octopusvariable(\"SpaceID\", space[0]['Id']) +else: + print('Failed to find space called ' + parser.space_name) + sys.exit(1) +", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Python" + }, + "Parameters": [ + { + "Id": "69b98409-b4ba-401f-8986-042b6d58584f", + "Name": "SpaceLookup.ThisInstance.Server.Url", + "Label": "Server URL", + "HelpText": "The URL of the Octopus Server hosting the space to resolved.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9089cd55-75d6-4e91-a390-d7a3359e3c59", + "Name": "SpaceLookup.ThisInstance.Api.Key", + "Label": "Server API Key", + "HelpText": "The Octopus API Key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "4f5d46a5-ea76-478f-a5fa-0bacad6afa5b", + "Name": "SpaceLookup.Lookup.Space.Name", + "Label": "Space Name", + "HelpText": "The name of the space return the ID of", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-09-11T21:15:03.881Z", + "OctopusVersion": "2023.4.2151", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-merge-cac-updates.json.human b/step-templates/octopus-merge-cac-updates.json.human new file mode 100644 index 000000000..ac855d9d1 --- /dev/null +++ b/step-templates/octopus-merge-cac-updates.json.human @@ -0,0 +1,640 @@ +{ + "Id": "c2536053-024f-499e-bf14-7e55c5a675d0", + "Name": "Octopus - Merge CaC Updates (S3 Backend)", + "Description": "This step queries each workspace in the Terraform state for downstream Octopus CaC enabled projects, extracts the Git repo associated with the CaC project, and merges any changes so long as there are no merge conflicts. + +If there is a merge conflict between the upstream and downstream repos, instructions for manually resolving the conflict are provided.", + "ActionType": "Octopus.AwsRunScript", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Aws.AssumeRole": "False", + "Octopus.Action.AwsAccount.UseInstanceRole": "False", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Script.ScriptBody": "# Check to see if $IsWindows is available +if ($null -eq $IsWindows) +{ + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") +} + +Function Get-GitExecutable +{ + # Define parameters + param ( + $WorkingDirectory + ) + + # Define variables + $gitExe = \"PortableGit-2.41.0.3-64-bit.7z.exe\" + $gitDownloadUrl = \"https://github.com/git-for-windows/git/releases/download/v2.41.0.windows.3/$gitExe\" + $gitDownloadArguments = @{ } + $gitDownloadArguments.Add(\"Uri\", $gitDownloadUrl) + $gitDownloadArguments.Add(\"OutFile\", \"$WorkingDirectory/git/$gitExe\") + + # This makes downloading faster + $ProgressPreference = 'SilentlyContinue' + + # Check to see if git subfolder exists + if ((Test-Path -Path \"$WorkingDirectory/git\") -eq $false) + { + # Create subfolder + New-Item -Path \"$WorkingDirectory/git\" -ItemType Directory | Out-Null + } + + # Check PowerShell version + if ($PSVersionTable.PSVersion.Major -lt 6) + { + # Use basic parsing is required + $gitDownloadArguments.Add(\"UseBasicParsing\", $true) + } + + # Download Git + Write-Host \"Downloading Git ...\" + Invoke-WebRequest @gitDownloadArguments + + # Extract Git + $gitExtractArguments = @() + $gitExtractArguments += \"-o\" + $gitExtractArguments += \"$WorkingDirectory\\git\" + $gitExtractArguments += \"-y\" + $gitExtractArguments += \"-bd\" + + Write-Host \"Extracting Git download ...\" + & \"$WorkingDirectory\\git\\$gitExe\" $gitExtractArguments + + # Wait until unzip action is complete + while ($null -ne (Get-Process | Where-Object { $_.ProcessName -eq ($gitExe.Substring(0,$gitExe.LastIndexOf(\".\"))) })) + { + Start-Sleep 5 + } + + # Add bin folder to path + $env:PATH = \"$WorkingDirectory\\git\\bin$( [IO.Path]::PathSeparator )\" + $env:PATH + + # Disable promopt for credential helper + Invoke-CustomCommand \"git\" @(\"config\", \"--system\", \"--unset\", \"credential.helper\") | Write-Results +} + +Function Invoke-CustomCommand +{ + Param ( + $commandPath, + $commandArguments, + $workingDir = (Get-Location), + $path = @(), + $envVars = @{ } + ) + + $path += $env:PATH + $newPath = $path -join [IO.Path]::PathSeparator + + $pinfo = New-Object System.Diagnostics.ProcessStartInfo + $pinfo.FileName = $commandPath + $pinfo.WorkingDirectory = $workingDir + $pinfo.RedirectStandardError = $true + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + $pinfo.Arguments = $commandArguments + $pinfo.EnvironmentVariables[\"PATH\"] = $newPath + + foreach ($env in $envVars.Keys) + { + Write-Verbose \"Setting $env to $( $envVars.$env )\" + $pinfo.EnvironmentVariables.Add($env,$envVars.$env.ToString()) + } + + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $pinfo + $p.Start() | Out-Null + + # Capture output during process execution so we don't hang + # if there is too much output. + # Microsoft documents a C# solution here: + # https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.redirectstandardoutput?view=net-7.0&redirectedfrom=MSDN#remarks + # This code is based on https://stackoverflow.com/a/74748844 + $stdOut = [System.Text.StringBuilder]::new() + $stdErr = [System.Text.StringBuilder]::new() + do + { + if (!$p.StandardOutput.EndOfStream) + { + $stdOut.AppendLine($p.StandardOutput.ReadLine()) + } + if (!$p.StandardError.EndOfStream) + { + $stdErr.AppendLine($p.StandardError.ReadLine()) + } + + Start-Sleep -Milliseconds 10 + } + while (-not $p.HasExited) + + # Capture any standard output generated between our last poll and process end. + while (!$p.StandardOutput.EndOfStream) + { + $stdOut.AppendLine($p.StandardOutput.ReadLine()) + } + + # Capture any error output generated between our last poll and process end. + while (!$p.StandardError.EndOfStream) + { + $stdErr.AppendLine($p.StandardError.ReadLine()) + } + + $p.WaitForExit() + + $executionResults = [pscustomobject]@{ + StdOut = $stdOut.ToString() + StdErr = $stdErr.ToString() + ExitCode = $p.ExitCode + } + + return $executionResults + +} + +function Write-Results +{ + [cmdletbinding()] + param ( + [Parameter(Mandatory = $True, ValuefromPipeline = $True)] + $results + ) + + if (![String]::IsNullOrWhiteSpace($results.StdOut)) + { + Write-Verbose $results.StdOut + } + + if (![String]::IsNullOrWhiteSpace($results.StdErr)) + { + Write-Verbose $results.StdErr + } +} + +function Format-StringAsNullOrTrimmed { + [cmdletbinding()] + param ( + [Parameter(ValuefromPipeline=$True)] + $input + ) + + if ([string]::IsNullOrWhitespace($input)) { + return $null + } + + return $input.Trim() +} + +function Write-TerraformBackend +{ + Set-Content -Path 'backend.tf' -Value @\" +terraform { + backend \"s3\" {} + required_providers { + octopusdeploy = { source = \"OctopusDeployLabs/octopusdeploy\", version = \">= 0.21.1\" } + } + } +\"@ +} + +function Set-GitContactDetails +{ + $gitEmail = Invoke-CustomCommand \"git\" @(\"config\", \"--global\", \"user.email\", \"octopus@octopus.com\") + Write-Results $gitEmail + if (-not $gitEmail.ExitCode -eq 0) + { + Write-Error \"Failed to set the git email address (exit code was $( $gitEmail.ExitCode )).\" + } + + $gitUser = Invoke-CustomCommand \"git\" @(\"config\", \"--global\", \"user.name\", \"Octopus Server\") + Write-Results $gitUser + if (-not $gitUser.ExitCode -eq 0) + { + Write-Error \"Failed to set the git name (exit code was $( $gitUser.ExitCode )).\" + } +} + +$spaceFilter = if (-not [string]::IsNullOrWhitespace($OctopusParameters[\"FindConflicts.Octopus.Spaces\"])) +{ + $OctopusParameters[\"FindConflicts.Octopus.Spaces\"].Split(\"`n\") +} +else +{ + @() +} +$projectFilter = if (-not [string]::IsNullOrWhitespace($OctopusParameters[\"FindConflicts.Octopus.Projects\"])) +{ + $OctopusParameters[\"FindConflicts.Octopus.Projects\"].Split(\"`n\") +} +else +{ + @() +} +$username = $OctopusParameters[\"FindConflicts.Git.Credentials.Username\"] +$password = $OctopusParameters[\"FindConflicts.Git.Credentials.Password\"] +$protocol = $OctopusParameters[\"FindConflicts.Git.Url.Protocol\"] +$gitHost = $OctopusParameters[\"FindConflicts.Git.Url.Host\"] +$org = $OctopusParameters[\"FindConflicts.Git.Url.Organization\"] +$repo = $OctopusParameters[\"FindConflicts.Git.Url.Template\"] +$region = $OctopusParameters[\"FindConflicts.Terraform.Backend.S3Region\"] +$key = $OctopusParameters[\"FindConflicts.Terraform.Backend.S3Key\"] +$bucket = $OctopusParameters[\"FindConflicts.Terraform.Backend.S3Bucket\"] + +if ([string]::IsNullOrWhitespace($username)) +{ + Write-Error \"The FindConflicts.Git.Credentials.Username variable must be provided\" +} + +if ([string]::IsNullOrWhitespace($password)) +{ + Write-Error \"The FindConflicts.Git.Credentials.Password variable must be provided\" +} + +if ( [string]::IsNullOrWhitespace($protocol)) +{ + Write-Error \"The FindConflicts.Git.Url.Protocol variable must be defined.\" +} + +if ( [string]::IsNullOrWhitespace($gitHost)) +{ + Write-Error \"The FindConflicts.Git.Url.Host variable must be defined.\" +} + +if ( [string]::IsNullOrWhitespace($repo)) +{ + Write-Error \"The FindConflicts.Git.Url.Template variable must be defined.\" +} + +if ( [string]::IsNullOrWhitespace($region)) +{ + Write-Error \"The FindConflicts.Terraform.Backend.S3Region variable must be defined.\" +} + +if ( [string]::IsNullOrWhitespace($key)) +{ + Write-Error \"The FindConflicts.Terraform.Backend.S3Key variable must be defined.\" +} + +if ( [string]::IsNullOrWhitespace($bucket)) +{ + Write-Error \"The FindConflicts.Terraform.Backend.S3Bucket variable must be defined.\" +} + +$templateRepoUrl = $protocol + \"://\" + $gitHost + \"/\" + $org + \"/\" + $repo + \".git\" +$templateRepo = $protocol + \"://\" + $username + \":\" + $password + \"@\" + $gitHost + \"/\" + $org + \"/\" + $repo + \".git\" +$branch = \"main\" + +# Check to see if it's Windows +if ($IsWindows -and $OctopusParameters['Octopus.Workerpool.Name'] -eq \"Hosted Windows\") +{ + # Dynamic worker don't have git, download portable version and add to path for execution + Write-Host \"Detected usage of Windows Dynamic Worker ...\" + Get-GitExecutable -WorkingDirectory $PWD +} + +Write-TerraformBackend +Set-GitContactDetails + +Invoke-CustomCommand \"terraform\" @(\"init\", \"-no-color\", \"-backend-config=`\"bucket=$bucket`\"\", \"-backend-config=`\"region=$region`\"\", \"-backend-config=`\"key=$key`\"\") | Write-Results + +Write-Host \"Verbose logs contain instructions for resolving merge conflicts.\" + +$workspaces = Invoke-CustomCommand \"terraform\" @(\"workspace\", \"list\") + +Write-Results $workspaces + +$parsedWorkspaces = $workspaces.StdOut.Replace(\"*\", \"\").Split(\"`n\") + +foreach ($workspace in $parsedWorkspaces) +{ + $trimmedWorkspace = $workspace | Format-StringAsNullOrTrimmed + + if ($trimmedWorkspace -eq \"default\" -or [string]::IsNullOrWhitespace($trimmedWorkspace)) + { + continue + } + + Write-Verbose \"Processing workspace $trimmedWorkspace\" + + Invoke-CustomCommand \"terraform\" @(\"workspace\", \"select\", $trimmedWorkspace) | Write-Results + + $state = Invoke-CustomCommand \"terraform\" @(\"show\", \"-json\") + + # state might include sensitive values, so don't print it unless there was an error + + if (-not $state.ExitCode -eq 0) + { + Write-Results $state + continue + } + + $parsedState = $state.StdOut | ConvertFrom-Json + + $resources = $parsedState.values.root_module.resources | Where-Object { + $_.type -eq \"octopusdeploy_project\" + } + + # The outputs allow us to contact the downstream instance) + $spaceName = (Invoke-CustomCommand \"terraform\" @(\"output\", \"-raw\", \"octopus_space_name\")).StdOut | Format-StringAsNullOrTrimmed + + foreach ($resource in $resources) + { + $url = $resource.values.git_library_persistence_settings.url | Format-StringAsNullOrTrimmed + $name = $resource.values.name | Format-StringAsNullOrTrimmed + + # Optional filtering + if (-not($spaceFilter.Count -eq 0 -or $spaceFilter.Contains($spaceName))) + { + continue + } + + if (-not($projectFilter.Count -eq 0 -or $projectFilter.Contains($name))) + { + continue + } + + if (-not [string]::IsNullOrWhitespace($url)) + { + mkdir $trimmedWorkspace | Out-Null + + $parsedUrl = [System.Uri]$url + $urlWithCreds = $parsedUrl.Scheme + \"://\" + $username + \":\" + $password + \"@\" + $parsedUrl.Host + \":\" + $parsedUrl.Port + $parsedUrl.AbsolutePath + + Write-Verbose \"Cloning repo\" + $cloneRepo = Invoke-CustomCommand \"git\" @(\"clone\", $urlWithCreds, $trimmedWorkspace) + Write-Results $cloneRepo + if (-not $cloneRepo.ExitCode -eq 0) + { + Write-Error \"Failed to clone repo (exit code was $( $cloneRepo.ExitCode )).\" + } + + Write-Verbose \"Cloning upstream remote\" + $addRemote = Invoke-CustomCommand \"git\" @(\"remote\", 'add', 'upstream', $templateRepo) $trimmedWorkspace + Write-Results $addRemote + if (-not $addRemote.ExitCode -eq 0) + { + Write-Error \"Failed to clone repo (exit code was $( $addRemote.ExitCode )).\" + } + + Write-Verbose \"Fetching all\" + $fetchAll = Invoke-CustomCommand \"git\" @(\"fetch\", \"--all\") $trimmedWorkspace + Write-Results $fetchAll + if (-not $fetchAll.ExitCode -eq 0) + { + Write-Error \"Failed to fetch all (exit code was $( $fetchAll.ExitCode )).\" + } + + Write-Verbose \"Checking out upstream-$branch upstream/$branch\" + $checkoutUpstream = Invoke-CustomCommand \"git\" @(\"checkout\", \"-b\", \"upstream-$branch\", \"upstream/$branch\") $trimmedWorkspace + Write-Results $checkoutUpstream + if (-not $checkoutUpstream.ExitCode -eq 0) + { + Write-Error \"Failed to checkout upstream (exit code was $( $checkoutUpstream.ExitCode )).\" + } + + if (-not($branch -eq \"master\" -or $branch -eq \"main\")) + { + Write-Verbose \"Checking out $branch origin/$branch\" + $checkoutDownstream = Invoke-CustomCommand \"git\" @(\"checkout\", \"-b\", $branch, \"origin/$branch\") $trimmedWorkspace + } + else + { + Write-Verbose \"Checking out $branch\" + $checkoutDownstream = Invoke-CustomCommand \"git\" @(\"checkout\", $branch) $trimmedWorkspace + } + + Write-Results $checkoutDownstream + if (-not $checkoutDownstream.ExitCode -eq 0) + { + Write-Error \"Failed to checkout downstream (exit code was $( $checkoutDownstream.ExitCode )).\" + } + + Write-Verbose \"Merge base\" + $mergeBase = Invoke-CustomCommand \"git\" @(\"merge-base\", $branch, \"upstream-$branch\") $trimmedWorkspace + Write-Results $mergeBase + if (-not $mergeBase.ExitCode -eq 0) + { + Write-Error \"Failed to merge base (exit code was $( $mergeBase.ExitCode )).\" + } + + Write-Verbose \"Rev parse\" + $mergeSourceCurrentCommit = Invoke-CustomCommand \"git\" @(\"rev-parse\", \"upstream-$branch\") $trimmedWorkspace + Write-Results $mergeSourceCurrentCommit + if (-not $mergeSourceCurrentCommit.ExitCode -eq 0) + { + Write-Error \"Failed to rev parse (exit code was $( $mergeSourceCurrentCommit.ExitCode )).\" + } + + Write-Verbose \"Merge (no commit)\" + $mergeResult = Invoke-CustomCommand \"git\" @(\"merge\", \"--no-commit\", \"--no-ff\", \"upstream-$branch\") $trimmedWorkspace + Write-Results $mergeResult + + if ($mergeBase.StdOut -eq $mergeSourceCurrentCommit.StdOut) + { + Write-Host \"No changes found in the upstream repo $templateRepoUrl that do not exist in the downstream repo $url for project `\"$name`\" in space $spaceName\" + } + elseif (-not $mergeResult.ExitCode -eq 0) + { + Write-Warning \"Changes between upstream repo $templateRepoUrl conflict with changes in downstream repo $url for project `\"$name`\" in space $spaceName.\" + Write-Verbose \"To resolve the conflicts, run the following commands:\" + Write-Verbose \"mkdir cac\" + Write-Verbose \"cd cac\" + Write-Verbose \"git clone $url .\" + Write-Verbose \"git remote add upstream $templateRepoUrl\" + Write-Verbose \"git fetch --all\" + Write-Verbose \"git checkout -b upstream-$branch upstream/$branch\" + if (-not($branch -eq \"master\" -or $branch -eq \"main\")) + { + Write-Verbose \"git checkout -b $branch origin/$branch\" + } + else + { + Write-Verbose \"git checkout $branch\" + Write-Verbose \"git merge-base $branch upstream-$branch\" + Write-Verbose \"git merge --no-commit --no-ff upstream-$branch\" + } + } + else + { + # https://stackoverflow.com/a/76272919 + # How to commit a merge non-interactively + Write-Verbose \"Git commit\" + $mergeContinue = Invoke-CustomCommand \"git\" @(\"commit\", \"--no-edit\") $trimmedWorkspace + Write-Results $mergeContinue + if (-not $mergeContinue.ExitCode -eq 0) + { + Write-Error \"Failed to merge continue (exit code was $( $mergeContinue.ExitCode )).\" + } + + $diffResult = Invoke-CustomCommand \"git\" @(\"diff\", \"--quiet\", \"--exit-code\", \"@{upstream}\") $trimmedWorkspace + Write-Results $diffResult + + if (-not $diffResult.ExitCode -eq 0) + { + $pushResult = Invoke-CustomCommand \"git\" @(\"push\", \"origin\") $trimmedWorkspace + Write-Results $pushResult + + if ($pushResult.ExitCode -eq 0) + { + Write-Host \"Changes were merged between upstream repo $templateRepoUrl and downstream repo $url for project `\"$name`\" in space $spaceName.\" + } + else + { + Write-Warning \"Failed to push changes to downstream repo $url for project `\"$name`\" in space $spaceName (exit code $( $pushResult.ExitCode )).\" + } + } + else + { + Write-Host \"No changes found in the upstream repo $templateRepoUrl that do not exist in the downstream repo $url for project `\"$name`\" in space $spaceName\" + } + } + } + else + { + Write-Verbose \"`\"$name`\" is not a CaC project\" + } + } +}", + "Octopus.Action.Aws.Region": "#{FindConflicts.Terraform.Backend.S3Region}", + "Octopus.Action.AwsAccount.Variable": "#{FindConflicts.Terraform.Aws.Account}" + }, + "Parameters": [ + { + "Id": "5abfbf98-22f0-47a2-8dd8-f16e2227d4ba", + "Name": "FindConflicts.Octopus.Spaces", + "Label": "Octopus Spaces", + "HelpText": "An optional newline-separated list of space names with projects to merge changes into. Leave this field blank to merge changes to projects in all spaces.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "56b85050-7c06-4a84-9565-88730a4859dc", + "Name": "FindConflicts.Octopus.Projects", + "Label": "Octopus Projects", + "HelpText": "A newline-separated list of projects to merge changes into. Leave this field blank to merge changes to all projects.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "539dc165-d880-4bf2-99c2-7024f36f7593", + "Name": "FindConflicts.Git.Credentials.Username", + "Label": "Git Username", + "HelpText": "The git repo username. When using GitHub with an access token, the value is `x-access-token`.", + "DefaultValue": "x-access-token", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7cf2352e-7381-485d-994c-ff128ee0fe8b", + "Name": "FindConflicts.Git.Credentials.Password", + "Label": "Git Password", + "HelpText": "The git repo password or access token.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "3887173c-a7f9-4311-848f-6f82736df2ce", + "Name": "FindConflicts.Git.Url.Protocol", + "Label": "Git Protocol", + "HelpText": "The git repo protocol.", + "DefaultValue": "https", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "https|HTTPS +http|HTTP" + } + }, + { + "Id": "0781245e-3f02-4d5b-a2ad-68c5f6270cb5", + "Name": "FindConflicts.Git.Url.Host", + "Label": "Git Hostname", + "HelpText": "The git repo host name.", + "DefaultValue": "github.com", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7891f539-752e-4933-abcc-68db8ba9d114", + "Name": "FindConflicts.Git.Url.Organization", + "Label": "Git Organization", + "HelpText": "The git repo owner or organization i.e. `owner` in the url `https://github.com/owner/repo`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e5a6f5a3-fec6-47dd-ad46-82ed7df409f7", + "Name": "FindConflicts.Git.Url.Template", + "Label": "Git Template Repo", + "HelpText": "The repo holding the upstream, or template, CaC project i.e. `repo` in the url `https://github.com/owner/repo`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d8abeacf-b392-4a9e-b95f-50d0d033819f", + "Name": "FindConflicts.Terraform.Backend.S3Region", + "Label": "AWS Region", + "HelpText": "The AWS region hosting the S3 bucket persisting the Terraform state.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c9f76f2e-9c8b-4af2-9768-6449526d52ca", + "Name": "FindConflicts.Terraform.Backend.S3Key", + "Label": "S3 Key", + "HelpText": "The name of the file in the S3 bucket hosting the Terraform state.", + "DefaultValue": "Project_#{Octopus.Project.Name | Replace \"[^A-Za-z0-9]\" \"_\"}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fa261df0-01dc-4310-ad1e-1b07c3145e41", + "Name": "FindConflicts.Terraform.Backend.S3Bucket", + "Label": "S3 Bucket", + "HelpText": "The name of the S3 bucket hosting the Terraform state.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "00d184f8-287f-46e4-8848-282138484cfa", + "Name": "FindConflicts.Terraform.Aws.Account", + "Label": "AWS Account", + "HelpText": "The AWS account used to access the S3 bucket.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AmazonWebServicesAccount" + } + } + ], + "StepPackageId": "Octopus.AwsRunScript", + "$Meta": { + "ExportedAt": "2023-11-17T01:19:30.474Z", + "OctopusVersion": "2024.1.1838", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-octolint-a-space.json.human b/step-templates/octopus-octolint-a-space.json.human new file mode 100644 index 000000000..878715913 --- /dev/null +++ b/step-templates/octopus-octolint-a-space.json.human @@ -0,0 +1,64 @@ +{ + "Id": "48e2b213-324a-43be-8fa1-f8e08a2bb547", + "Name": "Octolint a Space", + "Description": "Run the [octolint tool](https://github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine) against a Space to get a usage recommendation report. + +This step requires a worker or execution container that has docker installed.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$server = $OctopusParameters[\"Octolint.Octopus.ServerUri\"] +$apiKey = $OctopusParameters[\"Octolint.Octopus.ApiKey\"] +$spaceName = $OctopusParameters[\"Octolint.Octopus.SpaceName\"] + +docker pull octopussamples/octolint +docker run --rm octopussamples/octolint -url \"$server\" -apiKey \"$apiKey\" -space \"$spaceName\" -verboseErrors" + }, + "Parameters": [ + { + "Id": "5d03c192-1126-4912-986e-799098fcf4a7", + "Name": "Octolint.Octopus.ServerUri", + "Label": "Octopus Server URI", + "HelpText": "The URI of the Octopus Server. For use on the same server, #{Octopus.Web.ServerUri} will work.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "58392cba-5d54-4ba1-9447-388b0bc439fb", + "Name": "Octolint.Octopus.ApiKey", + "Label": "Octopus Server API Key", + "HelpText": "The API key with [read permissions](https://github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine#permissions) to the space being linted.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "b51e59f4-86ca-423d-87a0-f29faa1cd159", + "Name": "Octolint.Octopus.SpaceName", + "Label": "Octopus Space Name", + "HelpText": "The name of the Space being linted.", + "DefaultValue": "#{Octopus.Space.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2024-01-12T16:25:50.307Z", + "OctopusVersion": "2024.1.6809", + "Type": "ActionTemplate" + }, + "Author": "ryanrousseau", + "LastModifiedBy": "ryanrousseau", + "Category": "octopus" + } diff --git a/step-templates/octopus-redeploy-previous-release.json.human b/step-templates/octopus-redeploy-previous-release.json.human new file mode 100644 index 000000000..7370a2df1 --- /dev/null +++ b/step-templates/octopus-redeploy-previous-release.json.human @@ -0,0 +1,306 @@ +{ + "Id": "829b2777-144c-498b-b6aa-2b387da76ead", + "Name": "Octopus - Re-deploy previous version", + "Description": "Re-deploy the previous version of a project.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Get-OctopusItems +{ +\t# Define parameters + param( + \t$OctopusUri, + $ApiKey, + $SkipCount = 0 + ) + + # Define working variables + $items = @() + $skipQueryString = \"\" + $headers = @{\"X-Octopus-ApiKey\"=\"$ApiKey\"} + + # Check to see if there there is already a querystring + if ($octopusUri.Contains(\"?\")) + { + $skipQueryString = \"&skip=\" + } + else + { + $skipQueryString = \"?skip=\" + } + + $skipQueryString += $SkipCount + + # Get intial set + Write-Host \"Calling $OctopusUri$skipQueryString\" + $resultSet = Invoke-RestMethod -Uri \"$($OctopusUri)$skipQueryString\" -Method GET -Headers $headers + + # Check to see if it returned an item collection + if ($null -ne $resultSet.Items) + { + # Store call results + $items += $resultSet.Items + + # Check to see if resultset is bigger than page amount + if (($resultSet.Items.Count -gt 0) -and ($resultSet.Items.Count -eq $resultSet.ItemsPerPage)) + { + # Increment skip count + $SkipCount += $resultSet.ItemsPerPage + + # Recurse + $items += Get-OctopusItems -OctopusUri $OctopusUri -ApiKey $ApiKey -SkipCount $SkipCount + } + } + else + { + return $resultSet + } + + # Return results + return $items +} + +# Define variables +$octopusApiKey = $OctopusParameters['redeploy.api.key'] +$octopusUri = $OctopusParameters['redeploy.octopus.server.uri'] +$octopusReleaseNumber = $OctopusParameters['redeploy.release.number'] +$octopusReleaseId = $null +$header = @{ \"X-Octopus-ApiKey\" = $octopusApiKey } +$spaceId = $OctopusParameters['Octopus.Space.Id'] +$environmentId = $OctopusParameters['Octopus.Environment.Id'] +$projectId = $OctopusParameters['Octopus.Project.Id'] +$promptedVariables = $OctopusParameters['redeploy.prompted.variables'] +$usePreviousPromptedVariables = [System.Convert]::ToBoolean($OctopusParameters['redeploy.prompted.useexisting']) +$deploymentFormValues = @{} + +if ($octopusUri.EndsWith(\"/\") -eq $true) +{ + # Add trailing slash + $octopusUri = $octopusUri.Substring(0, ($octopusUri.Length - 1)) +} + +# Check to see if a release number was provided +if ([string]::IsNullOrWhitespace($octopusReleaseNumber)) +{ + # Get the previous release number + $octopusReleaseId = $OctopusParameters['Octopus.Release.PreviousForEnvironment.Id'] + + $release = Get-OctopusItems -OctopusUri \"$octopusUri/api/$spaceId/releases/$octopusReleaseId\" -ApiKey $octopusApiKey +} +else +{ + # Get the specific release + $release = Get-OctopusItems -OctopusUri \"$octopusUri/api/$spaceId/projects/$projectId/releases?searchByVersion=$octopusReleaseNumber\" -ApiKey $octopusApiKey + + # Record the id + $octopusReleaseId = $release.Id +} + +# Verify result +if ($null -ne $release) +{ + # Get deployments + $deployments = Get-OctopusItems -OctopusUri \"$octopusUri/api/$spaceId/releases/$($release.Id)/deployments\" -ApiKey $octopusApiKey + + # Ensure this release has been deployed to this environment + $deployment = ($deployments | Where-Object {$_.EnvironmentId -eq $environmentId}) + + if ($null -eq $deployment) + { + Write-Error \"Error: $octopusReleaseNumber has not been deployed to $($OctopusParameters['Octopus.Environment.Name'])!\" + } + + # Get the task + if ($deployment.Links.Task -is [array]) + { + # Get the last attempt + $taskLink = $deployment.Links.Task[-1] + } + else + { + $taskLink = $deployment.Links.Task + } + + $serverTask = Invoke-RestMethod -Method Get -Uri \"$octopusUri$($taskLink)\" -Headers $header + + # Ensure it was successful before continuing + if ($serverTask.State -eq \"Failed\") + { + Write-Error \"The previous deployment of $($release.Version) to $($OctopusParameters['Octopus.Environment.Name']) was not successful, unable to re-deploy.\" + } + + try + { + $deploymentVariables = Invoke-RestMethod -Method Get -Uri \"$octopusUri/api/$spaceId/variables/variableset-$($serverTask.Arguments.DeploymentId)\" -Headers $header + } + catch + { + if ($_.Exception.Response.StatusCode -eq \"NotFound\") + { + $deploymentVariables = $null + } + else + { + throw + } + } + + # Get only prompted variables + $deploymentVariables = ($deploymentVariables.Variables | Where-Object {$null -ne $_.Prompt}) +} +else +{ + Write-Error \"Unable to find release version $octopusReleaseNumber!\" +} + +# Check to see if there prompted variables that need to be included +if ($usePreviousPromptedVariables -or ![string]::IsNullOrWhitespace($promptedVariables)) +{ + # Ensure the previous deployment variables were retrieved + if ($null -eq $deploymentVariables) + { + throw \"Error: Unable to retrieve previous deployment variables!\" + } + + if ($usePreviousPromptedVariables) + { + # Create list + $promptedValueList = @() + foreach ($variable in $deploymentVariables) + { + $promptedValueList += \"$($variable.Name)=$($variable.Value)\" + } + } + else + { + $promptedValueList = @(($promptedVariables -Split \"`n\").Trim()) + } + + # Get deployment preview for prompted variables + $deploymentPreview = Invoke-RestMethod \"$OctopusUri/api/$spaceId/releases/$octopusReleaseId/deployments/preview/$($environmentId)?includeDisabledSteps=true\" -Headers $header + + foreach($element in $deploymentPreview.Form.Elements) + { + $nameToSearchFor = $element.Control.Name + $uniqueName = $element.Name + $isRequired = $element.Control.Required + + $promptedVariablefound = $false + + Write-Host \"Looking for the prompted variable value for $nameToSearchFor\" + foreach ($promptedValue in $promptedValueList) + { + $splitValue = $promptedValue -Split \"=\" + Write-Host \"Comparing $nameToSearchFor with provided prompted variable $($promptedValue[$nameToSearchFor])\" + if ($splitValue.Length -gt 1) + { + if ($nameToSearchFor -eq $splitValue[0].Trim()) + { + Write-Host \"Found the prompted variable value $nameToSearchFor\" + $deploymentFormValues[$uniqueName] = $splitValue[1].Trim() + $promptedVariableFound = $true + break + } + } + } + + if ($promptedVariableFound -eq $false -and $isRequired -eq $true) + { + Write-Highlight \"Unable to find a value for the required prompted variable $nameToSearchFor, exiting\" + Exit 1 + } + } + +} + +# Create json object to deploy the release +$deploymentBody = @{ + ReleaseId = $octopusReleaseId + EnvironmentId = $environmentId +} + +# Check to see if there were any Prompted Variables +if ($deploymentFormValues.Count -gt 0) +{ + $deploymentBody.Add(\"FormValues\", $deploymentFormValues) +} + +# Submit deployment +Write-Host \"Submitting release $($release.Version) to $($OctopusParameters['Octopus.Environment.Name'])\" +$submittedRelease = (Invoke-RestMethod -Uri \"$octopusUri/api/$spaceId/deployments\" -Method POST -Headers $header -Body ($deploymentBody | ConvertTo-Json -Depth 10)) + +Write-Host \"[View the re-deployment]($octopusUri$($submittedRelease.Links.Web))\"" + }, + "Parameters": [ + { + "Id": "9f8d7b00-417c-406e-8788-326fd67d22b3", + "Name": "redeploy.octopus.server.uri", + "Label": "Octopus URI", + "HelpText": "Provide the URI to your Octopus server.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7b2ed439-b215-42fe-8971-3839626988fd", + "Name": "redeploy.api.key", + "Label": "API Key", + "HelpText": "Provide an API key with permission to re-deploy the previous version.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "92461617-cab6-4dc7-8fdd-0fcfc08c5d66", + "Name": "redeploy.release.number", + "Label": "Release number", + "HelpText": "(Optional) +Provide a release number to re-deploy. Leave blank to use the immediate previous release number.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "47eaaeaf-e414-4b34-ba7d-7d84715fd2ea", + "Name": "redeploy.prompted.variables", + "Label": "Prompted variables", + "HelpText": "Enter any prompted variables and their values in the format `VariableName=VariableValue`. Enter one entry per line. + +Example: `MyVar=MyValue`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "c0137294-b4e3-406e-a36d-fd7aca2731e7", + "Name": "redeploy.prompted.useexisting", + "Label": "Use previous prompted variables", + "HelpText": "Tick this box to use the same prompted variables and values from the previous deployment. + +Note: This setting will override anything in the `Prompted variables` input with the values from the previous deployment. In addition, it will use the values from the latest deployment of the Release. + +Note: Please be aware that this template uses the API to retrieve variable which will not work sensitive values.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2024-06-18T23:04:33.802Z", + "OctopusVersion": "2024.2.9220", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "octopus" +} diff --git a/step-templates/octopus-reference-architecture-eks.json.human b/step-templates/octopus-reference-architecture-eks.json.human new file mode 100644 index 000000000..0eca9f9d4 --- /dev/null +++ b/step-templates/octopus-reference-architecture-eks.json.human @@ -0,0 +1,11424 @@ +{ + "Id": "87b2154a-5c8d-4c31-9680-575bb6df9789", + "Name": "Octopus - EKS Reference Architecture", + "Description": "This step populates an Octopus space with the environments, feeds, accounts, lifecycles, projects, and runbooks required to deploy a sample application to an AWS EKS Kubernetes cluster. These resources combine to form a reference architecture teams can use to bootstrap an Octopus space with best practices and example projects. It is recommended that you run this step with the `octopuslabs/terraform-workertools` [container image](https://octopus.com/docs/projects/steps/execution-containers-for-workers). + +That this step assumes it is run on a cloud Octopus instance, or the default worker runs Linux, has Docker installed, and has PowerShell Core installed. + +The step will not update existing projects, environments etc. If you wish to recreate these resource with the latest configuration, for example if this step is updated and you wish to see the latest settings, you must manually delete or rename the resources to be recreated.", + "ActionType": "Octopus.TerraformApply", + "Version": 16, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.GoogleCloud.UseVMServiceAccount": "True", + "Octopus.Action.GoogleCloud.ImpersonateServiceAccount": "False", + "Octopus.Action.Terraform.GoogleCloudAccount": "False", + "Octopus.Action.Terraform.AzureAccount": "False", + "Octopus.Action.Terraform.ManagedAccount": "None", + "Octopus.Action.Terraform.AllowPluginDownloads": "True", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Terraform.RunAutomaticFileSubstitution": "True", + "Octopus.Action.Terraform.PlanJsonOutput": "False", + "Octopus.Action.Terraform.Template": "terraform { + required_providers { + octopusdeploy = { source = \"OctopusDeployLabs/octopusdeploy\", version = \"0.21.1\" } + } +} + +#region Locals +locals { + # These local variables define the name of the projects created by this module. + infrastructure_project_name = \"_ AWS EKS Infrastructure\" + project_template_project_name = \"Docker Project Templates\" + frontend_project_name = \"EKS Octopub Frontend\" + products_project_name = \"EKS Octopub Products\" + audits_project_name = \"EKS Octopub Audits\" + + development_environment_id = length(data.octopusdeploy_environments.environment_development.environments) == 0 ? octopusdeploy_environment.environment_development[0].id : data.octopusdeploy_environments.environment_development.environments[0].id + test_environment_id = length(data.octopusdeploy_environments.environment_test.environments) == 0 ? octopusdeploy_environment.environment_test[0].id : data.octopusdeploy_environments.environment_test.environments[0].id + production_environment_id = length(data.octopusdeploy_environments.environment_production.environments) == 0 ? octopusdeploy_environment.environment_production[0].id : data.octopusdeploy_environments.environment_production.environments[0].id + sync_environment_id = length(data.octopusdeploy_environments.environment_sync.environments) == 0 ? octopusdeploy_environment.environment_sync[0].id : data.octopusdeploy_environments.environment_sync.environments[0].id + security_environment_id = length(data.octopusdeploy_environments.environment_security.environments) == 0 ? octopusdeploy_environment.environment_security[0].id : data.octopusdeploy_environments.environment_security.environments[0].id + featurebranch_environment_id = length(data.octopusdeploy_environments.environment_featurebranch.environments) == 0 ? octopusdeploy_environment.environment_featurebranch[0].id : data.octopusdeploy_environments.environment_featurebranch.environments[0].id + this_instance_library_variable_set = length(data.octopusdeploy_library_variable_sets.this_instance.library_variable_sets) == 0 ? octopusdeploy_library_variable_set.this_instance[0].id : data.octopusdeploy_library_variable_sets.this_instance.library_variable_sets[0].id + github_library_variable_set = length(data.octopusdeploy_library_variable_sets.github.library_variable_sets) == 0 ? octopusdeploy_library_variable_set.github[0].id : data.octopusdeploy_library_variable_sets.github.library_variable_sets[0].id + docker_library_variable_set = length(data.octopusdeploy_library_variable_sets.docker.library_variable_sets) == 0 ? octopusdeploy_library_variable_set.docker[0].id : data.octopusdeploy_library_variable_sets.docker.library_variable_sets[0].id + docker_hub_feed_id = length(data.octopusdeploy_feeds.dockerhub.feeds) == 0 ? octopusdeploy_docker_container_registry.docker_hub[0].id : data.octopusdeploy_feeds.dockerhub.feeds[0].id + github_feed_id = length(data.octopusdeploy_feeds.github_feed.feeds) == 0 ? octopusdeploy_github_repository_feed.github_feed[0].id : data.octopusdeploy_feeds.github_feed.feeds[0].id + worker_pool_id = length(data.octopusdeploy_worker_pools.workerpool_hosted_ubuntu.worker_pools) == 0 ? \"\" : data.octopusdeploy_worker_pools.workerpool_hosted_ubuntu.worker_pools[0].id + aws_account = length(data.octopusdeploy_accounts.aws_account.accounts) == 0 ? octopusdeploy_aws_account.aws_account[0].id : data.octopusdeploy_accounts.aws_account.accounts[0].id + devops_lifecycle_id = length(data.octopusdeploy_lifecycles.devsecops.lifecycles) == 0 ? octopusdeploy_lifecycle.lifecycle_devsecops[0].id : data.octopusdeploy_lifecycles.devsecops.lifecycles[0].id + featurebranch_lifecycle_id = length(data.octopusdeploy_lifecycles.featurebranch.lifecycles) == 0 ? octopusdeploy_lifecycle.lifecycle_featurebranch[0].id : data.octopusdeploy_lifecycles.featurebranch.lifecycles[0].id + eks_project_group_id = length(data.octopusdeploy_project_groups.eks.project_groups) == 0 ? octopusdeploy_project_group.project_group_eks[0].id : data.octopusdeploy_project_groups.eks.project_groups[0].id + project_templates_project_group_id = length(data.octopusdeploy_project_groups.project_templates.project_groups) == 0 ? octopusdeploy_project_group.project_group_project_templates[0].id : data.octopusdeploy_project_groups.project_templates.project_groups[0].id + application_lifecycle_id = length(data.octopusdeploy_lifecycles.application.lifecycles) == 0 ? octopusdeploy_lifecycle.lifecycle_application[0].id : data.octopusdeploy_lifecycles.application.lifecycles[0].id + create_cluster_script = <<-EOT + # Check to see if $IsWindows is available + if ($null -eq $IsWindows) + { + Write-Host \"Determining Operating System...\" + $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\") + $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\") + } + + Function Invoke-CustomCommand + { + Param ( + $commandPath, + $commandArguments, + $workingDir = (Get-Location), + $path = @() + ) + + $path += $env:PATH + $newPath = $path -join [IO.Path]::PathSeparator + + $pinfo = New-Object System.Diagnostics.ProcessStartInfo + $pinfo.FileName = $commandPath + $pinfo.WorkingDirectory = $workingDir + $pinfo.RedirectStandardError = $true + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + $pinfo.Arguments = $commandArguments + $pinfo.EnvironmentVariables[\"PATH\"] = $newPath + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $pinfo + $p.Start() | Out-Null + $p.WaitForExit() + $executionResults = [pscustomobject]@{ + StdOut = $p.StandardOutput.ReadToEnd() + StdErr = $p.StandardError.ReadToEnd() + ExitCode = $p.ExitCode + } + + return $executionResults + + } + + function Write-Results + { + param ( + $results + ) + + if (![String]::IsNullOrWhiteSpace($results.StdOut)) + { + Write-Verbose $results.StdOut + } + if (![String]::IsNullOrWhiteSpace($results.StdErr)) + { + Write-Verbose $results.StdErr + } + } + + function Install-App + { + param ( + $app, + $versionArgument, + $download, + $downloadFileName, + $downloadBinary + ) + + Try + { + if ($versionArgument -is [array]) + { + $results = Invoke-CustomCommand $app $versionArgument + } else { + $results = Invoke-CustomCommand $app @($versionArgument) + } + if ($results.ExitCode -ne 0) + { + throw \"Exit code was \" + $results.ExitCode + } + return $app + } + Catch + { + # Ignore the error, we assume the app does not exist + } + + $fileWithoutExtenion = [System.IO.Path]::GetFileNameWithoutExtension($downloadFileName) + $extension = [System.IO.Path]::GetExtension($downloadFileName) + + Write-Host \"Downloading $download\" + Invoke-WebRequest -Uri $download -OutFile $downloadFileName + + if ($extension -eq \".zip\") + { + Write-Host \"Extracting $downloadFileName\" + New-Item -ItemType Directory -Path $fileWithoutExtenion | Out-Null + Expand-Archive -Path $downloadFileName -DestinationPath $fileWithoutExtenion + + $binary = Join-Path -Path $fileWithoutExtenion -ChildPath $downloadBinary + $results = Invoke-CustomCommand $binary @($versionArgument) + Write-Results $results + if ($results.ExitCode -ne 0) + { + throw \"Installed app failed to execute \" + $binary + \". Returned \" + $results.ExitCode + } + return $binary + } + elseif ($extension -eq \".gz\") + { + Write-Host \"Extracting $downloadFileName\" + $extractedFile = [System.IO.Path]::GetFileNameWithoutExtension($downloadFileName) + $extractedDir = [System.IO.Path]::GetFileNameWithoutExtension($extractedFile) + New-Item -ItemType Directory -Path $extractedDir | Out-Null + + $results = Invoke-CustomCommand \"tar\" @(\"xzf\", $downloadFileName, \"-C\", $extractedDir) + Write-Results $results + if ($results.ExitCode -ne 0) + { + throw \"Failed to extract file \" + $results.ExitCode + } + + $binary = Join-Path -Path $extractedDir -ChildPath $downloadBinary + $results = Invoke-CustomCommand $binary @($versionArgument) + Write-Results $results + if ($results.ExitCode -ne 0) + { + throw \"Installed app failed to execute \" + $binary + \". Returned \" + $results.ExitCode + } + return $binary + } + else + { + # We likely have to make a downloaded binary executable + if ($IsLinux) + { + $results = Invoke-CustomCommand \"chmod\" @(\"+x\", $downloadFileName) + Write-Results $results + if ($results.ExitCode -ne 0) + { + throw \"Failed to make download executable\" + } + } + + $results = Invoke-CustomCommand $downloadFileName @($versionArgument) + Write-Results $results + if ($results.ExitCode -ne 0) + { + throw \"Installed app failed to execute \" + $downloadFileName + \". Returned \" + $results.ExitCode + } + return $downloadFileName + } + } + + function Install-CustomModule + { + param ( + $module + ) + + if (!(Get-Module -ListAvailable -Name $module)) + { + Install-Module -Scope CurrentUser -Force $module + } + } + + function Install-Eksctl + { + if ($IsWindows) + { + return Install-App \"eksctl.exe\" \"version\" \"https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_Windows_amd64.zip\" \"eksctl.zip\" \"eksctl.exe\" + } + elseif ($IsLinux) + { + return Install-App \"eksctl\" \"version\" \"https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_Linux_amd64.tar.gz\" \"eksctl.tar.gz\" \"eksctl\" + } + + throw \"Unexpected operation system\" + } + + function Install-IamAuthenticator + { + if ($IsWindows) + { + return Install-App \"aws-iam-authenticator.exe\" \"version\" \"https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/v0.5.9/aws-iam-authenticator_0.5.9_windows_amd64.exe\" \"aws-iam-authenticator.exe\" \"aws-iam-authenticator.exe\" + } + elseif ($IsLinux) + { + return Install-App \"aws-iam-authenticator\" \"version\" \"https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/v0.5.9/aws-iam-authenticator_0.5.9_linux_amd64\" \"aws-iam-authenticator\" \"aws-iam-authenticator\" + } + + throw \"Unexpected operation system\" + } + + function Install-Helm + { + if ($IsWindows) + { + return Install-App \"helm\" \"version\" \"https://get.helm.sh/helm-v3.12.3-windows-amd64.zip\" \"helm.zip\" \"windows-amd64/helm.exe\" + } + elseif ($IsLinux) + { + return Install-App \"helm\" \"version\" \"https://get.helm.sh/helm-v3.12.3-linux-amd64.tar.gz\" \"helm.tar.gz\" \"linux-amd64/helm\" + } + + throw \"Unexpected operation system\" + } + + function Install-Kubectl + { + if ($IsWindows) + { + return Install-App \"kubectl\" @(\"version\", \"--client=true\") \"https://dl.k8s.io/release/v1.28.2/bin/windows/amd64/kubectl.exe\" \"kubectl.exe\" \"kubectl.exe\" + } + elseif ($IsLinux) + { + return Install-App \"kubectl\" @(\"version\", \"--client=true\") \"https://dl.k8s.io/release/v1.28.2/bin/linux/amd64/kubectl\" \"kubectl\" \"kubectl\" + } + + throw \"Unexpected operation system\" + } + + function Write-EksConfig + { + param ( + $clusterName, + $clusterRegion + ) + + Set-Content -Path \"cluster.yaml\" -Value @\" + apiVersion: eksctl.io/v1alpha5 + kind: ClusterConfig + + metadata: + name: $clusterName + region: $clusterRegion + + # A regular node group is required for NGINX + nodeGroups: + - name: ng-1 + instanceType: t3a.small + desiredCapacity: 1 + volumeSize: 80 + + fargateProfiles: + - name: fp-default + selectors: + # All workloads in the \"default\" Kubernetes namespace will be + # scheduled onto Fargate: + - namespace: default + # All workloads in the \"kube-system\" Kubernetes namespace will be + # scheduled onto Fargate: + - namespace: kube-system + - name: fp-development + selectors: + - namespace: development + - name: fp-test + selectors: + - namespace: test + - name: fp-production + selectors: + - namespace: production + \"@ + } + + Install-CustomModule powershell-yaml + + $clusterName = $OctopusParameters[\"AWS.EKS.Name\"] + $clusterRegion = $OctopusParameters[\"AWS.EKS.Region\"] + $awsAccountVariable = $OctopusParameters[\"Octopus.Action.AwsAccount.Variable\"] + $awsAccount = $OctopusParameters[$awsAccountVariable] + $environment = $OctopusParameters[\"Octopus.Environment.Name\"] + $sortOrder = $OctopusParameters[\"Octopus.Environment.SortOrder\"] + + # When multiple environments share a cluster, we want to make sure eksctl doesn't attempt to + # create the same cluster at the same time. + $sleep = ([int]$sortOrder - 10) * 5 + + # Sanity check in the case of existing environments + if ($sleep -lt 0) { + $sleep = 0 + } + Write-Host \"Sleeping for $sleep seconds\" + Start-Sleep -Seconds $sleep + + Install-IamAuthenticator | Out-Null + $helm = Install-Helm + $kubectl = Install-Kubectl + $eksctl = Install-Eksctl + Write-EksConfig $clusterName $clusterRegion + $results = Invoke-CustomCommand $eksctl @(\"get\", \"cluster\", \"--name\", $clusterName, \"--region\", $clusterRegion) -Path @((Get-Location)) + if ($results.ExitCode -eq 0) + { + Write-Host \"Getting cluster details\" + $result = Invoke-CustomCommand $eksctl @(\"utils\", \"write-kubeconfig\", \"--cluster\", $clusterName, \"--region\", $clusterRegion, \"--kubeconfig\", \"eks-config.yaml\") -Path @((Get-Location)) + Write-Results $result + } + else + { + Write-Host \"Creating cluster - this can take a while\" + $result = Invoke-CustomCommand $eksctl @(\"create\", \"cluster\", \"-f\", \"cluster.yaml\", \"--kubeconfig\", 'eks-config.yaml') -Path @((Get-Location)) + Write-Results $result + } + + # https://kubernetes.github.io/ingress-nginx/deploy/#aws + $result = Invoke-CustomCommand $kubectl @(\"apply\", \"-f\", \"https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/aws/deploy.yaml\", \"--kubeconfig\", \"eks-config.yaml\") -Path @((Get-Location)) + + Write-Results $result + + $kubeConfig = ConvertFrom-Yaml (Get-Content eks-config.yaml -Raw) + + New-OctopusKubernetesTarget ` + -name \"$clusterName $($environment.ToLower())\" ` + -octopusRoles \"EKS_Reference_Cluster,Kubernetes\" ` + -clusterUrl $kubeConfig.clusters[0].cluster.server ` + -octopusAccountIdOrName $awsAccount ` + -clusterName $clusterName ` + -namespace $($environment -replace '[^A-Za-z0-9]', '_').ToLower() ` + -updateIfExisting ` + -skipTlsVerification True + EOT + variable_script = <<-EOT + Set-OctopusVariable -name \"OctopusEnvironmentName\" -value $OctopusParameters[\"Octopus.Environment.Name\"] + EOT + orchestration_project_script = <<-EOT + # If we are deploying to the feature branch environment, the \"Kubernetes.Namespace\" variable is defined, + # and it is passed down as a prompted variable. Otherwise, pass down a placeholder that is not used. + $value = if ([string]::IsNullOrWhitespace($OctopusParameters[\"Kubernetes.Namespace\"])) {$($OctopusParameters[\"Octopus.Environment.Name\"] -replace '[^A-Za-z0-9]', '_').ToLower()} else {$OctopusParameters[\"Kubernetes.Namespace\"]} + Set-OctopusVariable -name \"KubernetesNamespaceValue\" -value $value + + # Define the deployment condition used by the \"Deploy a release\" steps. + # Mainline deployments ignore existing deployments. Feature branch deployments are always redeployed. + $deploymentCondition = if ([string]::IsNullOrWhitespace($OctopusParameters[\"Kubernetes.Namespace\"])) {\"IfNotCurrentVersion\"} else {\"Always\"} + Set-OctopusVariable -name \"DeploymentCondition\" -value $deploymentCondition + EOT + smoke_test = <<-EOT + for i in {1..30} + do + HOSTNAME=$(kubectl get ingress #{Kubernetes.Ingress.Name} -o json -n #{Kubernetes.Namespace} | jq -r '.status.loadBalancer.ingress[0].hostname') + if [[ -n \"$${HOSTNAME}\" && \"$${HOSTNAME}\" != \"null\" ]] + then + break + fi + echo \"Waiting for ingress hostname\" + sleep 10 + done + + # Load balancers can take a minute or so before their DNS is propagated. + # A status code of 000 means curl could not resolve the DNS name, so we wait for a bit until DNS is updated. + write_highlight \"Testing [http://$${HOSTNAME}#{Kubernetes.App.HealthCheck}](http://$${HOSTNAME}#{Kubernetes.App.HealthCheck})\" + echo \"Waiting for DNS to propagate. This can take a while for a new load balancer.\" + for i in {1..30} + do + CODE=$(curl -o /dev/null -s -w \"%%{http_code}\ +\" http://$${HOSTNAME}#{Kubernetes.App.HealthCheck}) + if [[ \"$${CODE}\" == \"200\" ]] + then + break + fi + echo \"Waiting for DNS name to be resolvable and for service to respond\" + sleep 10 + done + + echo \"response code: $${CODE}\" + if [[ \"$${CODE}\" == \"200\" ]] + then + echo \"success\" + exit 0 + else + echo \"error\" + exit 1 + fi + EOT + security_scan_script = <<-EOT + echo \"Pulling Trivy Docker Image\" + echo \"##octopus[stdout-verbose]\" + docker pull aquasec/trivy + echo \"##octopus[stdout-default]\" + + echo \"Installing umoci\" + echo \"##octopus[stdout-verbose]\" + # Install umoci + if ! which umoci + then + curl -o umoci -L https://github.com/opencontainers/umoci/releases/latest/download/umoci.amd64 2>&1 + chmod +x umoci + fi + echo \"##octopus[stdout-default]\" + + echo \"Extracting Application Docker Image\" + echo \"##octopus[stdout-verbose]\" + # Download and extract the docker image + # https://manpages.ubuntu.com/manpages/jammy/man1/umoci-raw-unpack.1.html + docker pull quay.io/skopeo/stable:latest 2>&1 + docker run -v $(pwd):/output quay.io/skopeo/stable:latest copy docker://#{Octopus.Action[Deploy Container].Package[web].PackageId}:#{Octopus.Action[Deploy Container].Package[web].PackageVersion} oci:/output/image:latest 2>&1 + ./umoci unpack --image image --rootless bundle 2>&1 + echo \"##octopus[stdout-default]\" + + TIMESTAMP=$(date +%s%3N) + SUCCESS=0 + for x in $(find . -name bom.json -type f -print); do + echo \"Scanning $${x}\" + + # Delete any existing report file + if [[ -f \"$PWD/depscan-bom.json\" ]]; then + rm \"$PWD/depscan-bom.json\" + fi + + # Generate the report, capturing the output, and ensuring $? is set to the exit code + OUTPUT=$(bash -c \"docker run --rm -v \\\"$PWD/$${x}:/app/$${x}\\\" aquasec/trivy sbom \\\"/app/$${x}\\\"; exit \\$?\" 2>&1) + + # Success is set to 1 if the exit code is not zero + if [[ $? -ne 0 ]]; then + SUCCESS=1 + fi + + # Print the output stripped of ANSI colour codes + echo -e \"$${OUTPUT}\" | sed 's/\\x1b\\[[0-9;]*m//g' + done + + # Cleanup + for i in {1..10} + do + chmod -R +rw bundle &> /dev/null + rm -rf bundle &> /dev/null + if [[ $? == 0 ]]; then break; fi + echo \"Attempting to clean up files\" + sleep 1 + done + + set_octopusvariable \"VerificationResult\" $SUCCESS + + if [[ $SUCCESS -ne 0 ]]; then + >&2 echo \"Critical vulnerabilities were detected\" + fi + + exit 0 + EOT +} +#endregion + +#region Provider +variable \"octopus_server\" { + type = string + nullable = false + sensitive = false + description = \"The URL of the Octopus server e.g. https://myinstance.octopus.app.\" + default = \"#{Octopus.Web.ServerUri}\" +} + +variable \"octopus_apikey\" { + type = string + nullable = false + sensitive = true + description = \"The API key used to access the Octopus server. See https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key for details on creating an API key.\" +} + +variable \"octopus_space_id\" { + type = string + nullable = false + sensitive = false + description = \"The ID of the Octopus space to populate.\" + default = \"#{Octopus.Space.Id}\" +} + +provider \"octopusdeploy\" { + address = var.octopus_server + api_key = var.octopus_apikey + space_id = var.octopus_space_id +} +#endregion + +#region Environments +data \"octopusdeploy_environments\" \"environment_development\" { + ids = null + partial_name = \"Development\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_environment\" \"environment_development\" { + count = length(data.octopusdeploy_environments.environment_development.environments) == 0 ? 1 : 0 + name = \"Development\" + description = \"\" + allow_dynamic_infrastructure = true + use_guided_failure = true + sort_order = 10 + + jira_extension_settings { + environment_type = \"development\" + } + + jira_service_management_extension_settings { + is_enabled = false + } + + servicenow_extension_settings { + is_enabled = false + } +} + +data \"octopusdeploy_environments\" \"environment_test\" { + ids = null + partial_name = \"Test\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_environment\" \"environment_test\" { + count = length(data.octopusdeploy_environments.environment_test.environments) == 0 ? 1 : 0 + name = \"Test\" + description = \"\" + allow_dynamic_infrastructure = true + use_guided_failure = true + sort_order = 12 + + jira_extension_settings { + environment_type = \"testing\" + } + + jira_service_management_extension_settings { + is_enabled = false + } + + servicenow_extension_settings { + is_enabled = false + } +} + +data \"octopusdeploy_environments\" \"environment_production\" { + ids = null + partial_name = \"Production\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_environment\" \"environment_production\" { + count = length(data.octopusdeploy_environments.environment_production.environments) == 0 ? 1 : 0 + name = \"Production\" + description = \"\" + allow_dynamic_infrastructure = true + use_guided_failure = true + sort_order = 13 + + jira_extension_settings { + environment_type = \"production\" + } + + jira_service_management_extension_settings { + is_enabled = false + } + + servicenow_extension_settings { + is_enabled = false + } +} + +data \"octopusdeploy_environments\" \"environment_security\" { + ids = null + partial_name = \"Security\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_environment\" \"environment_security\" { + count = length(data.octopusdeploy_environments.environment_security.environments) == 0 ? 1 : 0 + name = \"Security\" + description = \"\" + allow_dynamic_infrastructure = true + use_guided_failure = false + sort_order = 14 + + jira_extension_settings { + environment_type = \"production\" + } + + jira_service_management_extension_settings { + is_enabled = false + } + + servicenow_extension_settings { + is_enabled = false + } +} + +data \"octopusdeploy_environments\" \"environment_sync\" { + ids = null + partial_name = \"Sync\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_environment\" \"environment_sync\" { + count = length(data.octopusdeploy_environments.environment_sync.environments) == 0 ? 1 : 0 + name = \"Sync\" + description = \"\" + allow_dynamic_infrastructure = true + use_guided_failure = false + sort_order = 15 + + jira_extension_settings { + environment_type = \"development\" + } + + jira_service_management_extension_settings { + is_enabled = false + } + + servicenow_extension_settings { + is_enabled = false + } +} + +data \"octopusdeploy_environments\" \"environment_featurebranch\" { + ids = null + partial_name = \"Feature Branch\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_environment\" \"environment_featurebranch\" { + count = length(data.octopusdeploy_environments.environment_featurebranch.environments) == 0 ? 1 : 0 + name = \"Feature Branch\" + description = \"\" + allow_dynamic_infrastructure = true + use_guided_failure = false + sort_order = 11 + + jira_extension_settings { + environment_type = \"development\" + } + + jira_service_management_extension_settings { + is_enabled = false + } + + servicenow_extension_settings { + is_enabled = false + } +} + +data \"octopusdeploy_lifecycles\" \"featurebranch\" { + ids = [] + partial_name = \"Feature Branch\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_lifecycle\" \"lifecycle_featurebranch\" { + count = length(data.octopusdeploy_lifecycles.featurebranch.lifecycles) == 0 ? 1 : 0 + name = \"Feature Branch\" + description = \"\" + + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.featurebranch_environment_id + ] + name = \"Feature Branch\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + + release_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } + + tentacle_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } +} + +data \"octopusdeploy_lifecycles\" \"sync\" { + ids = [] + partial_name = \"Sync\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_lifecycle\" \"sync\" { + count = length(data.octopusdeploy_lifecycles.sync.lifecycles) == 0 ? 1 : 0 + name = \"Sync\" + description = \"\" + + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.sync_environment_id + ] + name = \"Sync\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + + release_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } + + tentacle_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } +} + +data \"octopusdeploy_lifecycles\" \"devsecops\" { + ids = [] + partial_name = \"DevSecOps\" + skip = 0 + take = 1 +} + +data \"octopusdeploy_lifecycles\" \"application\" { + ids = [] + partial_name = \"Application\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_lifecycle\" \"lifecycle_devsecops\" { + count = length(data.octopusdeploy_lifecycles.devsecops.lifecycles) == 0 ? 1 : 0 + name = \"DevSecOps\" + description = \"\" + + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.development_environment_id + ] + name = \"Development\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.featurebranch_environment_id + ] + name = \"Feature Branch\" + is_optional_phase = true + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.test_environment_id + ] + name = \"Test\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.production_environment_id + ] + name = \"Production\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [ + local.security_environment_id + ] + optional_deployment_targets = [] + name = \"Security\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + + release_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } + + tentacle_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } +} + +resource \"octopusdeploy_lifecycle\" \"lifecycle_application\" { + count = length(data.octopusdeploy_lifecycles.application.lifecycles) == 0 ? 1 : 0 + name = \"Application\" + description = \"\" + + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.development_environment_id + ] + name = \"Development\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.featurebranch_environment_id + ] + name = \"Feature Branch\" + is_optional_phase = true + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.test_environment_id + ] + name = \"Test\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.production_environment_id + ] + name = \"Production\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + + release_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } + + tentacle_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } +} +#endregion + +#region Feeds + +data \"octopusdeploy_feeds\" \"project\" { + feed_type = \"OctopusProject\" + ids = [] + skip = 0 + take = 1 +} + +data \"octopusdeploy_feeds\" \"bitnami\" { + feed_type = \"Helm\" + ids = [] + partial_name = \"Bitnami\" + skip = 0 + take = 1 +} + + +resource \"octopusdeploy_helm_feed\" \"feed_helm\" { + count = length(data.octopusdeploy_feeds.bitnami.feeds) == 0 ? 1 : 0 + name = \"Bitnami\" + feed_uri = \"https://repo.vmware.com/bitnami-files/\" + package_acquisition_location_options = [\"ExecutionTarget\", \"NotAcquired\"] +} + +data \"octopusdeploy_feeds\" \"dockerhub\" { + feed_type = \"Docker\" + ids = [] + partial_name = \"Docker Hub\" + skip = 0 + take = 1 +} + +variable \"feed_docker_hub_username\" { + type = string + nullable = false + sensitive = true + description = \"The username used by the feed Docker Hub\" +} + +variable \"feed_docker_hub_password\" { + type = string + nullable = false + sensitive = true + description = \"The password used by the feed Docker Hub\" +} + +resource \"octopusdeploy_docker_container_registry\" \"docker_hub\" { + count = length(data.octopusdeploy_feeds.dockerhub.feeds) == 0 ? 1 : 0 + name = \"Docker Hub\" + password = var.feed_docker_hub_password + username = var.feed_docker_hub_username + api_version = \"v1\" + feed_uri = \"https://index.docker.io\" + package_acquisition_location_options = [\"ExecutionTarget\", \"NotAcquired\"] +} + +data \"octopusdeploy_feeds\" \"sales_maven_feed\" { + feed_type = \"Maven\" + ids = [] + partial_name = \"Sales Maven Feed\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_maven_feed\" \"feed_sales_maven_feed\" { + count = length(data.octopusdeploy_feeds.sales_maven_feed.feeds) == 0 ? 1 : 0 + name = \"Sales Maven Feed\" + feed_uri = \"https://octopus-sales-public-maven-repo.s3.ap-southeast-2.amazonaws.com/snapshot\" + package_acquisition_location_options = [\"Server\", \"ExecutionTarget\"] + download_attempts = 3 + download_retry_backoff_seconds = 20 +} + +data \"octopusdeploy_feeds\" \"github_feed\" { + feed_type = \"GitHub\" + ids = [] + partial_name = \"Github Releases\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_github_repository_feed\" \"github_feed\" { + count = length(data.octopusdeploy_feeds.github_feed.feeds) == 0 ? 1 : 0 + download_attempts = 1 + download_retry_backoff_seconds = 30 + feed_uri = \"https://api.github.com\" + name = \"Github Releases\" +} +#endregion + +#region Library Variable Sets +data \"octopusdeploy_library_variable_sets\" \"this_instance\" { + partial_name = \"This Instance\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_library_variable_set\" \"this_instance\" { + count = length(data.octopusdeploy_library_variable_sets.this_instance.library_variable_sets) == 0 ? 1 : 0 + name = \"This Instance\" + description = \"Credentials used to interact with this Octopus instance\" +} + +resource \"octopusdeploy_variable\" \"octopus_admin_api_key\" { + count = length(data.octopusdeploy_library_variable_sets.this_instance.library_variable_sets) == 0 ? 1 : 0 + name = \"Octopus.ApiKey\" + type = \"Sensitive\" + description = \"Octopus API Key\" + is_sensitive = true + is_editable = true + owner_id = octopusdeploy_library_variable_set.this_instance[0].id + sensitive_value = var.octopus_apikey +} + +data \"octopusdeploy_library_variable_sets\" \"github\" { + partial_name = \"GitHub\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_library_variable_set\" \"github\" { + count = length(data.octopusdeploy_library_variable_sets.github.library_variable_sets) == 0 ? 1 : 0 + name = \"GitHub\" + description = \"Credentials used to interact with GitHub\" +} + +variable \"github_access_token\" { + type = string + nullable = false + sensitive = true + description = \"The GitHub access token\" +} + +resource \"octopusdeploy_variable\" \"github_access_token\" { + count = length(data.octopusdeploy_library_variable_sets.github.library_variable_sets) == 0 ? 1 : 0 + name = \"Git.Credentials.Password\" + type = \"Sensitive\" + description = \"The GitHub access token\" + is_sensitive = true + is_editable = true + owner_id = octopusdeploy_library_variable_set.github[0].id + sensitive_value = var.github_access_token +} + +data \"octopusdeploy_library_variable_sets\" \"docker\" { + partial_name = \"Docker\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_library_variable_set\" \"docker\" { + count = length(data.octopusdeploy_library_variable_sets.docker.library_variable_sets) == 0 ? 1 : 0 + name = \"Docker\" + description = \"Credentials used to interact with Docker\" +} + +resource \"octopusdeploy_variable\" \"docker_username\" { + count = length(data.octopusdeploy_library_variable_sets.docker.library_variable_sets) == 0 ? 1 : 0 + name = \"Docker.Credentials.Username\" + type = \"String\" + description = \"The docker username\" + is_sensitive = false + is_editable = true + owner_id = octopusdeploy_library_variable_set.docker[0].id + value = var.feed_docker_hub_username +} + +resource \"octopusdeploy_variable\" \"docker_password\" { + count = length(data.octopusdeploy_library_variable_sets.docker.library_variable_sets) == 0 ? 1 : 0 + name = \"Docker.Credentials.Password\" + type = \"Sensitive\" + description = \"The docker password\" + is_sensitive = true + is_editable = true + owner_id = octopusdeploy_library_variable_set.docker[0].id + sensitive_value = var.feed_docker_hub_password +} + +data \"octopusdeploy_library_variable_sets\" \"aws\" { + partial_name = \"AWS\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_library_variable_set\" \"aws\" { + count = length(data.octopusdeploy_library_variable_sets.aws.library_variable_sets) == 0 ? 1 : 0 + name = \"AWS\" + description = \"Credentials used to interact with AWS\" +} + +resource \"octopusdeploy_variable\" \"library_var_set_aws_access_key\" { + count = length(data.octopusdeploy_library_variable_sets.aws.library_variable_sets) == 0 ? 1 : 0 + name = \"AWS.Credentials.AccessKey\" + type = \"String\" + description = \"AWS Access Key\" + is_sensitive = false + is_editable = true + owner_id = octopusdeploy_library_variable_set.aws[0].id + value = var.account_aws_access_key +} + +resource \"octopusdeploy_variable\" \"library_var_set_aws_secret_key\" { + count = length(data.octopusdeploy_library_variable_sets.aws.library_variable_sets) == 0 ? 1 : 0 + name = \"AWS.Credentials.SecretKey\" + type = \"Sensitive\" + description = \"AWS Secret Key\" + is_sensitive = true + is_editable = true + owner_id = octopusdeploy_library_variable_set.aws[0].id + sensitive_value = var.account_aws_secret_key +} +#endregion + +#region Accounts + +data \"octopusdeploy_accounts\" \"aws_account\" { + account_type = \"AmazonWebServicesAccount\" + ids = [] + partial_name = \"AWS Account\" + skip = 0 + take = 1 +} + +variable \"account_aws_access_key\" { + type = string + nullable = false + sensitive = false + description = \"The AWS access key associated with the account AWS Account\" +} + +variable \"account_aws_secret_key\" { + type = string + nullable = false + sensitive = true + description = \"The AWS secret key associated with the account AWS Account\" +} + +resource \"octopusdeploy_aws_account\" \"aws_account\" { + count = length(data.octopusdeploy_accounts.aws_account.accounts) == 0 ? 1 : 0 + name = \"AWS Account\" + description = \"\" + environments = [] + tenant_tags = [] + tenants = [] + tenanted_deployment_participation = \"Untenanted\" + access_key = var.account_aws_access_key + secret_key = var.account_aws_secret_key +} + +#endregion + +#region Project Groups +data \"octopusdeploy_project_groups\" \"eks\" { + ids = [] + partial_name = \"EKS\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_project_group\" \"project_group_eks\" { + count = length(data.octopusdeploy_project_groups.eks.project_groups) == 0 ? 1 : 0 + name = \"EKS\" + description = \"EKS projects.\" +} + +data \"octopusdeploy_project_groups\" \"project_templates\" { + ids = [] + partial_name = \"Project Templates\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_project_group\" \"project_group_project_templates\" { + count = length(data.octopusdeploy_project_groups.project_templates.project_groups) == 0 ? 1 : 0 + name = \"Project Templates\" + description = \"Sample code project generators\" +} +#endregion + +#region Worker Pools + +data \"octopusdeploy_worker_pools\" \"workerpool_hosted_ubuntu\" { + partial_name = \"Hosted Ubuntu\" + ids = null + skip = 0 + take = 1 +} +#endregion + +#region Projects + +#region AWS Infrastructure +variable \"infrastructure_project_name\" { + type = string + default = \"\" +} + +data \"octopusdeploy_projects\" \"aws_infrastructure\" { + cloned_from_project_id = \"\" + ids = [] + is_clone = true + partial_name = var.infrastructure_project_name == \"\" ? local.infrastructure_project_name : var.infrastructure_project_name + skip = 0 + take = 1 +} + +resource \"octopusdeploy_project\" \"aws_infrastructure\" { + count = length(data.octopusdeploy_projects.aws_infrastructure.projects) == 0 ? 1 : 0 + name = var.infrastructure_project_name == \"\" ? local.infrastructure_project_name : var.infrastructure_project_name + auto_create_release = false + default_guided_failure_mode = \"EnvironmentDefault\" + default_to_skip_if_already_installed = false + discrete_channel_release = false + is_disabled = false + is_version_controlled = true + lifecycle_id = local.devops_lifecycle_id + project_group_id = local.eks_project_group_id + included_library_variable_sets = [] + tenanted_deployment_participation = \"Untenanted\" + + connectivity_policy { + allow_deployments_to_no_targets = false + exclude_unhealthy_targets = false + skip_machine_behavior = \"None\" + } + + versioning_strategy { + template = \"\" + } + + lifecycle { + ignore_changes = [\"connectivity_policy\"] + } + description = \"AWS infrastrucutre runbooks\" +} + +resource \"octopusdeploy_variable\" \"library_variable_set_variables_octopub_aws_account_1\" { + count = length(data.octopusdeploy_projects.aws_infrastructure.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.aws_infrastructure[0].id + value = local.aws_account + name = \"AWS.Account\" + type = \"AmazonWebServicesAccount\" + description = \"\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"aws_infrastructure_aws_eks_name_1\" { + count = length(data.octopusdeploy_projects.aws_infrastructure.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.aws_infrastructure[0].id + value = \"octopus\" + name = \"AWS.EKS.Name\" + type = \"String\" + is_sensitive = false + + prompt { + description = \"EKS Cluster Name\" + label = \"EKS Cluster Name\" + is_required = true + display_settings { + control_type = \"SingleLineText\" + } + } +} + +resource \"octopusdeploy_variable\" \"aws_infrastructure_aws_eks_region_1\" { + count = length(data.octopusdeploy_projects.aws_infrastructure.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.aws_infrastructure[0].id + value = \"ap-southeast-2\" + name = \"AWS.EKS.Region\" + type = \"String\" + is_sensitive = false + + prompt { + description = \"EKS Cluster Region\" + label = \"EKS Cluster Region\" + is_required = true + display_settings { + control_type = \"SingleLineText\" + } + } +} + +resource \"octopusdeploy_runbook\" \"runbook_create_eks_cluster\" { + count = length(data.octopusdeploy_projects.aws_infrastructure.projects) == 0 ? 1 : 0 + name = \"🛠️ Create EKS Cluster\" + project_id = octopusdeploy_project.aws_infrastructure[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + local.featurebranch_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"EnvironmentDefault\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"node\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_delete_eks_cluster\" { + count = length(data.octopusdeploy_projects.aws_infrastructure.projects) == 0 ? 1 : 0 + name = \"🗑️ Delete EKS Cluster\" + project_id = octopusdeploy_project.aws_infrastructure[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"EnvironmentDefault\" + description = < str:\ + \\\"\\\"\\\"Encrypt a Unicode string using the public key.\\\"\\\"\\\"\ + sealed_box = public.SealedBox(public.PublicKey(public_key_for_repo.encode(\\\"utf-8\\\"), encoding.Base64Encoder()))\ + encrypted = sealed_box.encrypt(secret_value_input.encode(\\\"utf-8\\\"))\ + return b64encode(encrypted).decode(\\\"utf-8\\\")\ +\ +\ +def get_public_key(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str) -> (str, str):\ + public_key_endpoint: str = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/public-key\\\"\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\"}\ + response = requests.get(url=public_key_endpoint, headers=headers)\ + if response.status_code != 200:\ + raise IOError(\ + f\\\"Could not get public key for repository {gh_owner}/{gh_repo}. The Response code was {response.status_code}\\\")\ +\ + public_key_json = response.json()\ + return public_key_json['key_id'], public_key_json['key']\ +\ +\ +def set_secret(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str, public_key_id: str, secret_key: str,\ + encrypted_secret_value: str):\ + secret_creation_url = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/{secret_key}\\\"\ + secret_creation_body = {\\\"key_id\\\": public_key_id, \\\"encrypted_value\\\": encrypted_secret_value}\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\", \\\"Content-Type\\\": \\\"application/json\\\"}\ +\ + secret_creation_response = requests.put(url=secret_creation_url, json=secret_creation_body, headers=headers)\ + if secret_creation_response.status_code == 201 or secret_creation_response.status_code == 204:\ + print(\\\"--Secret Created / Updated!--\\\")\ + else:\ + print(f\\\"-- Error creating / updating github secret, the reason was : {secret_creation_response.reason}\\\")\ +\ +\ +parser, _ = init_argparse()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +token = generate_github_token(parser.github_app_id, parser.github_app_private_key,\ + parser.github_app_installation_id) if len(\ + parser.git_password.strip()) == 0 else parser.git_password.strip()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +if not parser.secret_name.strip():\ + print(\\\"You must define the secret name\\\")\ + sys.exit(1)\ + \ +if not verify_new_repo(token, parser.git_organization, parser.repo):\ + print(\\\"Could not find the repo\\\")\ + sys.exit(1)\ +\ +key_id, public_key = get_public_key('https://api.github.com/repos', parser.git_organization, parser.repo,\ + token)\ +encrypted_secret: str = encrypt(public_key_for_repo=public_key, secret_value_input=parser.secret_value)\ +set_secret(gh_base_url='https://api.github.com/repos', gh_owner=parser.git_organization, gh_repo=parser.repo,\ + gh_auth_token=token, public_key_id=key_id, secret_key=parser.secret_name,\ + encrypted_secret_value=encrypted_secret)\ +\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + step { + condition = \"Success\" + name = \"Create Docker Hub Password Secret\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Create Docker Hub Password Secret\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"CreateGitHubSecret.Git.Credentials.Password\" = \"#{Git.Credentials.Password}\" + \"CreateGitHubSecret.GitHub.Secret.Name\" = \"DOCKERHUB_TOKEN\" + \"Octopus.Action.Script.Syntax\" = \"Python\" + \"CreateGitHubSecret.GitHub.Secret.Value\" = \"#{Docker.Credentials.Password}\" + \"CreateGitHubSecret.Git.Url.Organization\" = \"#{Git.Url.Organization}\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.RunOnServer\" = \"true\" + \"CreateGitHubSecret.Git.Url.Repo\" = \"#{Octopus.Action[Create Repo].Output.NewRepo}\" + \"Octopus.Action.Script.ScriptBody\" = \"# https://gist.github.com/comdotlinux/9a53bb00767a16d6646464c4b8249094\ +\ +# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid\ +# having to use a regular user account.\ +import subprocess\ +import sys\ +\ +# Install our own dependencies\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt', '--disable-pip-version-check'])\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pynacl', '--disable-pip-version-check'])\ +\ +import requests\ +import json\ +import subprocess\ +import sys\ +import os\ +import urllib.request\ +import base64\ +import re\ +import jwt\ +import time\ +import argparse\ +import urllib3\ +from base64 import b64encode\ +from typing import TypedDict\ +from nacl import public, encoding\ +\ +# Disable insecure http request warnings\ +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\ +\ +# If this script is not being run as part of an Octopus step, setting variables is a noop\ +if 'set_octopusvariable' not in globals():\ + def set_octopusvariable(variable, value):\ + pass\ +\ +# If this script is not being run as part of an Octopus step, return variables from environment variables.\ +# Periods are replaced with underscores, and the variable name is converted to uppercase\ +if \\\"get_octopusvariable\\\" not in globals():\ + def get_octopusvariable(variable):\ + return os.environ[re.sub('\\\\\\\\.', '_', variable.upper())]\ +\ +# If this script is not being run as part of an Octopus step, print directly to std out.\ +if 'printverbose' not in globals():\ + def printverbose(msg):\ + print(msg)\ +\ +\ +def printverbose_noansi(output):\ + \\\"\\\"\\\"\ + Strip ANSI color codes and print the output as verbose\ + :param output: The output to print\ + \\\"\\\"\\\"\ + output_no_ansi = re.sub(r'\\\\x1b\\\\[[0-9;]*m', '', output)\ + printverbose(output_no_ansi)\ +\ +\ +def get_octopusvariable_quiet(variable):\ + \\\"\\\"\\\"\ + Gets an octopus variable, or an empty string if it does not exist.\ + :param variable: The variable name\ + :return: The variable value, or an empty string if the variable does not exist\ + \\\"\\\"\\\"\ + try:\ + return get_octopusvariable(variable)\ + except:\ + return ''\ +\ +\ +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi, raise_on_non_zero=False,\ + append_to_path=None):\ + \\\"\\\"\\\"\ + The execute method provides the ability to execute external processes while capturing and returning the\ + output to std err and std out and exit code.\ + \\\"\\\"\\\"\ +\ + my_env = os.environ.copy() if env is None else env\ +\ + if append_to_path is not None:\ + my_env[\\\"PATH\\\"] = append_to_path + os.pathsep + my_env['PATH']\ +\ + process = subprocess.Popen(args,\ + stdout=subprocess.PIPE,\ + stderr=subprocess.PIPE,\ + stdin=open(os.devnull),\ + text=True,\ + cwd=cwd,\ + env=my_env)\ + stdout, stderr = process.communicate()\ + retcode = process.returncode\ +\ + if not retcode == 0 and raise_on_non_zero:\ + raise Exception('command returned exit code ' + retcode)\ +\ + if print_args is not None:\ + print_output(' '.join(args))\ +\ + if print_output is not None:\ + print_output(stdout)\ + print_output(stderr)\ +\ + return stdout, stderr, retcode\ +\ +\ +def init_argparse():\ + parser = argparse.ArgumentParser(\ + usage='%(prog)s [OPTION]',\ + description='Fork a GitHub repo'\ + )\ +\ + parser.add_argument('--secret-name', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.Secret.Name') or get_octopusvariable_quiet(\ + 'GitHub.Secret.Name'))\ + parser.add_argument('--secret-value', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.Secret.Value') or get_octopusvariable_quiet(\ + 'GitHub.Secret.Value'))\ +\ + parser.add_argument('--repo', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Url.Repo') or get_octopusvariable_quiet(\ + 'Git.Url.Repo') or get_octopusvariable_quiet('Octopus.Project.Name'))\ + parser.add_argument('--git-organization', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Url.Organization') or get_octopusvariable_quiet(\ + 'Git.Url.Organization'))\ + parser.add_argument('--github-app-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id'))\ + parser.add_argument('--github-app-installation-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.InstallationId') or get_octopusvariable_quiet(\ + 'GitHub.App.InstallationId'))\ + parser.add_argument('--github-app-private-key', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.PrivateKey') or get_octopusvariable_quiet(\ + 'GitHub.App.PrivateKey'))\ + parser.add_argument('--git-password', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Credentials.Password') or get_octopusvariable_quiet(\ + 'Git.Credentials.Password'),\ + help='The git password. This takes precedence over the --github-app-id, --github-app-installation-id, and --github-app-private-key')\ +\ + return parser.parse_known_args()\ +\ +\ +def generate_github_token(github_app_id, github_app_private_key, github_app_installation_id):\ + # Generate the tokens used by git and the GitHub API\ + app_id = github_app_id\ + signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8'))\ +\ + payload = {\ + # Issued at time\ + 'iat': int(time.time()),\ + # JWT expiration time (10 minutes maximum)\ + 'exp': int(time.time()) + 600,\ + # GitHub App's identifier\ + 'iss': app_id\ + }\ +\ + # Create JWT\ + jwt_instance = jwt.JWT()\ + encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256')\ +\ + # Create access token\ + url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens'\ + headers = {\ + 'Authorization': 'Bearer ' + encoded_jwt,\ + 'Accept': 'application/vnd.github+json',\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers, method='POST')\ + response = urllib.request.urlopen(request)\ + response_json = json.loads(response.read().decode())\ + return response_json['token']\ +\ +\ +def generate_auth_header(token):\ + auth = base64.b64encode(('x-access-token:' + token).encode('ascii'))\ + return 'Basic ' + auth.decode('ascii')\ +\ +\ +def verify_new_repo(token, cac_org, new_repo):\ + # Attempt to view the new repo\ + try:\ + url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo\ + headers = {\ + 'Accept': 'application/vnd.github+json',\ + 'Authorization': 'Bearer ' + token,\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers)\ + urllib.request.urlopen(request)\ + return True\ + except:\ + return False\ +\ +\ +def encrypt(public_key_for_repo: str, secret_value_input: str) -> str:\ + \\\"\\\"\\\"Encrypt a Unicode string using the public key.\\\"\\\"\\\"\ + sealed_box = public.SealedBox(public.PublicKey(public_key_for_repo.encode(\\\"utf-8\\\"), encoding.Base64Encoder()))\ + encrypted = sealed_box.encrypt(secret_value_input.encode(\\\"utf-8\\\"))\ + return b64encode(encrypted).decode(\\\"utf-8\\\")\ +\ +\ +def get_public_key(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str) -> (str, str):\ + public_key_endpoint: str = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/public-key\\\"\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\"}\ + response = requests.get(url=public_key_endpoint, headers=headers)\ + if response.status_code != 200:\ + raise IOError(\ + f\\\"Could not get public key for repository {gh_owner}/{gh_repo}. The Response code was {response.status_code}\\\")\ +\ + public_key_json = response.json()\ + return public_key_json['key_id'], public_key_json['key']\ +\ +\ +def set_secret(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str, public_key_id: str, secret_key: str,\ + encrypted_secret_value: str):\ + secret_creation_url = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/{secret_key}\\\"\ + secret_creation_body = {\\\"key_id\\\": public_key_id, \\\"encrypted_value\\\": encrypted_secret_value}\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\", \\\"Content-Type\\\": \\\"application/json\\\"}\ +\ + secret_creation_response = requests.put(url=secret_creation_url, json=secret_creation_body, headers=headers)\ + if secret_creation_response.status_code == 201 or secret_creation_response.status_code == 204:\ + print(\\\"--Secret Created / Updated!--\\\")\ + else:\ + print(f\\\"-- Error creating / updating github secret, the reason was : {secret_creation_response.reason}\\\")\ +\ +\ +parser, _ = init_argparse()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +token = generate_github_token(parser.github_app_id, parser.github_app_private_key,\ + parser.github_app_installation_id) if len(\ + parser.git_password.strip()) == 0 else parser.git_password.strip()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +if not parser.secret_name.strip():\ + print(\\\"You must define the secret name\\\")\ + sys.exit(1)\ + \ +if not verify_new_repo(token, parser.git_organization, parser.repo):\ + print(\\\"Could not find the repo\\\")\ + sys.exit(1)\ +\ +key_id, public_key = get_public_key('https://api.github.com/repos', parser.git_organization, parser.repo,\ + token)\ +encrypted_secret: str = encrypt(public_key_for_repo=public_key, secret_value_input=parser.secret_value)\ +set_secret(gh_base_url='https://api.github.com/repos', gh_owner=parser.git_organization, gh_repo=parser.repo,\ + gh_auth_token=token, public_key_id=key_id, secret_key=parser.secret_name,\ + encrypted_secret_value=encrypted_secret)\ +\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + step { + condition = \"Success\" + name = \"Create Docker Hub Password Username\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Create Docker Hub Password Username\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"CreateGitHubSecret.Git.Credentials.Password\" = \"#{Git.Credentials.Password}\" + \"Octopus.Action.RunOnServer\" = \"true\" + \"CreateGitHubSecret.GitHub.Secret.Value\" = \"#{Docker.Credentials.Username}\" + \"CreateGitHubSecret.Git.Url.Repo\" = \"#{Octopus.Action[Create Repo].Output.NewRepo}\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.ScriptBody\" = \"# https://gist.github.com/comdotlinux/9a53bb00767a16d6646464c4b8249094\ +\ +# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid\ +# having to use a regular user account.\ +import subprocess\ +import sys\ +\ +# Install our own dependencies\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt', '--disable-pip-version-check'])\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pynacl', '--disable-pip-version-check'])\ +\ +import requests\ +import json\ +import subprocess\ +import sys\ +import os\ +import urllib.request\ +import base64\ +import re\ +import jwt\ +import time\ +import argparse\ +import urllib3\ +from base64 import b64encode\ +from typing import TypedDict\ +from nacl import public, encoding\ +\ +# Disable insecure http request warnings\ +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\ +\ +# If this script is not being run as part of an Octopus step, setting variables is a noop\ +if 'set_octopusvariable' not in globals():\ + def set_octopusvariable(variable, value):\ + pass\ +\ +# If this script is not being run as part of an Octopus step, return variables from environment variables.\ +# Periods are replaced with underscores, and the variable name is converted to uppercase\ +if \\\"get_octopusvariable\\\" not in globals():\ + def get_octopusvariable(variable):\ + return os.environ[re.sub('\\\\\\\\.', '_', variable.upper())]\ +\ +# If this script is not being run as part of an Octopus step, print directly to std out.\ +if 'printverbose' not in globals():\ + def printverbose(msg):\ + print(msg)\ +\ +\ +def printverbose_noansi(output):\ + \\\"\\\"\\\"\ + Strip ANSI color codes and print the output as verbose\ + :param output: The output to print\ + \\\"\\\"\\\"\ + output_no_ansi = re.sub(r'\\\\x1b\\\\[[0-9;]*m', '', output)\ + printverbose(output_no_ansi)\ +\ +\ +def get_octopusvariable_quiet(variable):\ + \\\"\\\"\\\"\ + Gets an octopus variable, or an empty string if it does not exist.\ + :param variable: The variable name\ + :return: The variable value, or an empty string if the variable does not exist\ + \\\"\\\"\\\"\ + try:\ + return get_octopusvariable(variable)\ + except:\ + return ''\ +\ +\ +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi, raise_on_non_zero=False,\ + append_to_path=None):\ + \\\"\\\"\\\"\ + The execute method provides the ability to execute external processes while capturing and returning the\ + output to std err and std out and exit code.\ + \\\"\\\"\\\"\ +\ + my_env = os.environ.copy() if env is None else env\ +\ + if append_to_path is not None:\ + my_env[\\\"PATH\\\"] = append_to_path + os.pathsep + my_env['PATH']\ +\ + process = subprocess.Popen(args,\ + stdout=subprocess.PIPE,\ + stderr=subprocess.PIPE,\ + stdin=open(os.devnull),\ + text=True,\ + cwd=cwd,\ + env=my_env)\ + stdout, stderr = process.communicate()\ + retcode = process.returncode\ +\ + if not retcode == 0 and raise_on_non_zero:\ + raise Exception('command returned exit code ' + retcode)\ +\ + if print_args is not None:\ + print_output(' '.join(args))\ +\ + if print_output is not None:\ + print_output(stdout)\ + print_output(stderr)\ +\ + return stdout, stderr, retcode\ +\ +\ +def init_argparse():\ + parser = argparse.ArgumentParser(\ + usage='%(prog)s [OPTION]',\ + description='Fork a GitHub repo'\ + )\ +\ + parser.add_argument('--secret-name', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.Secret.Name') or get_octopusvariable_quiet(\ + 'GitHub.Secret.Name'))\ + parser.add_argument('--secret-value', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.Secret.Value') or get_octopusvariable_quiet(\ + 'GitHub.Secret.Value'))\ +\ + parser.add_argument('--repo', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Url.Repo') or get_octopusvariable_quiet(\ + 'Git.Url.Repo') or get_octopusvariable_quiet('Octopus.Project.Name'))\ + parser.add_argument('--git-organization', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Url.Organization') or get_octopusvariable_quiet(\ + 'Git.Url.Organization'))\ + parser.add_argument('--github-app-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id'))\ + parser.add_argument('--github-app-installation-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.InstallationId') or get_octopusvariable_quiet(\ + 'GitHub.App.InstallationId'))\ + parser.add_argument('--github-app-private-key', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.PrivateKey') or get_octopusvariable_quiet(\ + 'GitHub.App.PrivateKey'))\ + parser.add_argument('--git-password', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Credentials.Password') or get_octopusvariable_quiet(\ + 'Git.Credentials.Password'),\ + help='The git password. This takes precedence over the --github-app-id, --github-app-installation-id, and --github-app-private-key')\ +\ + return parser.parse_known_args()\ +\ +\ +def generate_github_token(github_app_id, github_app_private_key, github_app_installation_id):\ + # Generate the tokens used by git and the GitHub API\ + app_id = github_app_id\ + signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8'))\ +\ + payload = {\ + # Issued at time\ + 'iat': int(time.time()),\ + # JWT expiration time (10 minutes maximum)\ + 'exp': int(time.time()) + 600,\ + # GitHub App's identifier\ + 'iss': app_id\ + }\ +\ + # Create JWT\ + jwt_instance = jwt.JWT()\ + encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256')\ +\ + # Create access token\ + url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens'\ + headers = {\ + 'Authorization': 'Bearer ' + encoded_jwt,\ + 'Accept': 'application/vnd.github+json',\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers, method='POST')\ + response = urllib.request.urlopen(request)\ + response_json = json.loads(response.read().decode())\ + return response_json['token']\ +\ +\ +def generate_auth_header(token):\ + auth = base64.b64encode(('x-access-token:' + token).encode('ascii'))\ + return 'Basic ' + auth.decode('ascii')\ +\ +\ +def verify_new_repo(token, cac_org, new_repo):\ + # Attempt to view the new repo\ + try:\ + url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo\ + headers = {\ + 'Accept': 'application/vnd.github+json',\ + 'Authorization': 'Bearer ' + token,\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers)\ + urllib.request.urlopen(request)\ + return True\ + except:\ + return False\ +\ +\ +def encrypt(public_key_for_repo: str, secret_value_input: str) -> str:\ + \\\"\\\"\\\"Encrypt a Unicode string using the public key.\\\"\\\"\\\"\ + sealed_box = public.SealedBox(public.PublicKey(public_key_for_repo.encode(\\\"utf-8\\\"), encoding.Base64Encoder()))\ + encrypted = sealed_box.encrypt(secret_value_input.encode(\\\"utf-8\\\"))\ + return b64encode(encrypted).decode(\\\"utf-8\\\")\ +\ +\ +def get_public_key(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str) -> (str, str):\ + public_key_endpoint: str = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/public-key\\\"\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\"}\ + response = requests.get(url=public_key_endpoint, headers=headers)\ + if response.status_code != 200:\ + raise IOError(\ + f\\\"Could not get public key for repository {gh_owner}/{gh_repo}. The Response code was {response.status_code}\\\")\ +\ + public_key_json = response.json()\ + return public_key_json['key_id'], public_key_json['key']\ +\ +\ +def set_secret(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str, public_key_id: str, secret_key: str,\ + encrypted_secret_value: str):\ + secret_creation_url = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/{secret_key}\\\"\ + secret_creation_body = {\\\"key_id\\\": public_key_id, \\\"encrypted_value\\\": encrypted_secret_value}\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\", \\\"Content-Type\\\": \\\"application/json\\\"}\ +\ + secret_creation_response = requests.put(url=secret_creation_url, json=secret_creation_body, headers=headers)\ + if secret_creation_response.status_code == 201 or secret_creation_response.status_code == 204:\ + print(\\\"--Secret Created / Updated!--\\\")\ + else:\ + print(f\\\"-- Error creating / updating github secret, the reason was : {secret_creation_response.reason}\\\")\ +\ +\ +parser, _ = init_argparse()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +token = generate_github_token(parser.github_app_id, parser.github_app_private_key,\ + parser.github_app_installation_id) if len(\ + parser.git_password.strip()) == 0 else parser.git_password.strip()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +if not parser.secret_name.strip():\ + print(\\\"You must define the secret name\\\")\ + sys.exit(1)\ + \ +if not verify_new_repo(token, parser.git_organization, parser.repo):\ + print(\\\"Could not find the repo\\\")\ + sys.exit(1)\ +\ +key_id, public_key = get_public_key('https://api.github.com/repos', parser.git_organization, parser.repo,\ + token)\ +encrypted_secret: str = encrypt(public_key_for_repo=public_key, secret_value_input=parser.secret_value)\ +set_secret(gh_base_url='https://api.github.com/repos', gh_owner=parser.git_organization, gh_repo=parser.repo,\ + gh_auth_token=token, public_key_id=key_id, secret_key=parser.secret_name,\ + encrypted_secret_value=encrypted_secret)\ +\" + \"CreateGitHubSecret.Git.Url.Organization\" = \"#{Git.Url.Organization}\" + \"CreateGitHubSecret.GitHub.Secret.Name\" = \"DOCKERHUB_USERNAME\" + \"Octopus.Action.Script.Syntax\" = \"Python\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + step { + condition = \"Success\" + name = \"Generate and Push\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Generate and Push\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"PopulateGithubRepo.Yeoman.Generator.SubGenerator\" = \"nodejs-docker-webapp\" + \"PopulateGithubRepo.Yeoman.Generator.Arguments\" = \"--octopusUrl #{Octopus.Action[Get Variables].Output.Web.ServerUri} --octopusSpace \\\"#{Octopus.Action[Get Variables].Output.Space.Name}\\\" --octopusApi #{Octopus.ApiKey} --octopusProject \\\"#{Application.Octopus.Project}\\\" --dockerImage #{Application.Docker.Image}\" + \"PopulateGithubRepo.Yeoman.Generator.Name\" = \"octopus-reference-architecture-apps\" + \"Octopus.Action.Script.Syntax\" = \"Python\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.ScriptBody\" = \"# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid\ +# having to use a regular user account.\ +import subprocess\ +import sys\ +\ +# Install our own dependencies\ +env_vars = os.environ.copy()\ +env_vars['PIP_ROOT_USER_ACTION'] = 'ignore'\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt', '--disable-pip-version-check'], env=env_vars)\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'requests', '--disable-pip-version-check'], env=env_vars)\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'anyascii', '--disable-pip-version-check'], env=env_vars)\ +\ +import requests\ +import json\ +import subprocess\ +import sys\ +import os\ +import urllib.request\ +import base64\ +import re\ +import jwt\ +import time\ +import argparse\ +import platform\ +import zipfile\ +import lzma\ +import tarfile\ +import shutil\ +import urllib3\ +from shlex import split\ +from anyascii import anyascii\ +\ +# Disable insecure http request warnings\ +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\ +\ +# If this script is not being run as part of an Octopus step, setting variables is a noop\ +if 'set_octopusvariable' not in globals():\ + def set_octopusvariable(variable, value):\ + pass\ +\ +# If this script is not being run as part of an Octopus step, return variables from environment variables.\ +# Periods are replaced with underscores, and the variable name is converted to uppercase\ +if \\\"get_octopusvariable\\\" not in globals():\ + def get_octopusvariable(variable):\ + return os.environ[re.sub('\\\\\\\\.', '_', variable.upper())]\ +\ +# If this script is not being run as part of an Octopus step, print directly to std out.\ +if 'printverbose' not in globals():\ + def printverbose(msg):\ + print(msg)\ +\ +\ +def printverbose_noansi(output):\ + \\\"\\\"\\\"\ + Strip ANSI color codes and print the output as verbose\ + :param output: The output to print\ + \\\"\\\"\\\"\ + output_no_ansi = re.sub(r'\\\\x1b\\\\[[0-9;]*m', '', output)\ + printverbose(output_no_ansi)\ +\ +\ +def get_octopusvariable_quiet(variable):\ + \\\"\\\"\\\"\ + Gets an octopus variable, or an empty string if it does not exist.\ + :param variable: The variable name\ + :return: The variable value, or an empty string if the variable does not exist\ + \\\"\\\"\\\"\ + try:\ + return get_octopusvariable(variable)\ + except Exception as inst:\ + return ''\ +\ +\ +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi, raise_on_non_zero=False,\ + append_to_path=None):\ + \\\"\\\"\\\"\ + The execute method provides the ability to execute external processes while capturing and returning the\ + output to std err and std out and exit code.\ + \\\"\\\"\\\"\ +\ + my_env = os.environ.copy() if env is None else env\ +\ + if append_to_path is not None:\ + my_env[\\\"PATH\\\"] = append_to_path + os.pathsep + my_env['PATH']\ +\ + process = subprocess.Popen(args,\ + stdout=subprocess.PIPE,\ + stderr=subprocess.PIPE,\ + stdin=open(os.devnull),\ + text=True,\ + cwd=cwd,\ + env=my_env)\ + stdout, stderr = process.communicate()\ + retcode = process.returncode\ +\ + if not retcode == 0 and raise_on_non_zero:\ + raise Exception('command returned exit code ' + retcode)\ +\ + if print_args is not None:\ + print_output(' '.join(args))\ +\ + if print_output is not None:\ + print_output(stdout)\ + print_output(stderr)\ +\ + return stdout, stderr, retcode\ +\ +\ +def init_argparse():\ + parser = argparse.ArgumentParser(\ + usage='%(prog)s [OPTION]',\ + description='Fork a GitHub repo'\ + )\ + parser.add_argument('--generator', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Yeoman.Generator.Name') or get_octopusvariable_quiet(\ + 'Yeoman.Generator.Name'))\ + parser.add_argument('--sub-generator', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Yeoman.Generator.SubGenerator') or get_octopusvariable_quiet(\ + 'Yeoman.Generator.SubGenerator'))\ + parser.add_argument('--generator-arguments', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Yeoman.Generator.Arguments') or get_octopusvariable_quiet(\ + 'Yeoman.Generator.Arguments'),\ + help='The arguments to pas to yo. Pass all arguments as a single string. This string is then parsed as if it were yo arguments.')\ + parser.add_argument('--repo', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Git.Url.Repo') or get_octopusvariable_quiet(\ + 'Git.Url.Repo'))\ + parser.add_argument('--git-organization', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Git.Url.Organization') or get_octopusvariable_quiet(\ + 'Git.Url.Organization'))\ + parser.add_argument('--github-app-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id'))\ + parser.add_argument('--github-app-installation-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.GitHub.App.InstallationId') or get_octopusvariable_quiet(\ + 'GitHub.App.InstallationId'))\ + parser.add_argument('--github-app-private-key', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.GitHub.App.PrivateKey') or get_octopusvariable_quiet(\ + 'GitHub.App.PrivateKey'))\ + parser.add_argument('--git-password', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Git.Credentials.Password') or get_octopusvariable_quiet(\ + 'Git.Credentials.Password'),\ + help='The git password. This takes precedence over the --github-app-id, --github-app-installation-id, and --github-app-private-key')\ + parser.add_argument('--git-username', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Git.Credentials.Username') or get_octopusvariable_quiet(\ + 'Git.Credentials.Username'),\ + help='The git username. This will be used for both the git authentication and the username associated with any commits.')\ +\ + return parser.parse_known_args()\ +\ +\ +def generate_github_token(github_app_id, github_app_private_key, github_app_installation_id):\ + # Generate the tokens used by git and the GitHub API\ + app_id = github_app_id\ + signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8'))\ +\ + payload = {\ + # Issued at time\ + 'iat': int(time.time()),\ + # JWT expiration time (10 minutes maximum)\ + 'exp': int(time.time()) + 600,\ + # GitHub App's identifier\ + 'iss': app_id\ + }\ +\ + # Create JWT\ + jwt_instance = jwt.JWT()\ + encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256')\ +\ + # Create access token\ + url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens'\ + headers = {\ + 'Authorization': 'Bearer ' + encoded_jwt,\ + 'Accept': 'application/vnd.github+json',\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers, method='POST')\ + response = urllib.request.urlopen(request)\ + response_json = json.loads(response.read().decode())\ + return response_json['token']\ +\ +\ +def generate_auth_header(token):\ + auth = base64.b64encode(('x-access-token:' + token).encode('ascii'))\ + return 'Basic ' + auth.decode('ascii')\ +\ +\ +def verify_new_repo(token, cac_org, new_repo):\ + # Attempt to view the new repo\ + try:\ + url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo\ + headers = {\ + 'Accept': 'application/vnd.github+json',\ + 'Authorization': 'Bearer ' + token,\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers)\ + urllib.request.urlopen(request)\ + return True\ + except Exception as inst:\ + return False\ +\ +\ +def is_windows():\ + return platform.system() == 'Windows'\ +\ +\ +def download_file(url, filename, verify_ssl=True):\ + r = requests.get(url, verify=verify_ssl)\ + with open(filename, 'wb') as file:\ + file.write(r.content)\ +\ +\ +def ensure_git_exists():\ + if is_windows():\ + print(\\\"Checking git is installed\\\")\ + try:\ + stdout, _, exit_code = execute(['git', 'version'])\ + printverbose(stdout)\ + if not exit_code == 0:\ + raise \\\"git not found\\\"\ + except:\ + print(\\\"Downloading git\\\")\ + download_file('https://www.7-zip.org/a/7zr.exe', '7zr.exe')\ + download_file(\ + 'https://github.com/git-for-windows/git/releases/download/v2.42.0.windows.2/PortableGit-2.42.0.2-64-bit.7z.exe',\ + 'PortableGit.7z.exe')\ + print(\\\"Installing git\\\")\ + print(\\\"Consider installing git on the worker or using a standard worker-tools image\\\")\ + execute(['7zr.exe', 'x', 'PortableGit.7z.exe', '-o' + os.path.join(os.getcwd(), 'git'), '-y'])\ + return os.path.join(os.getcwd(), 'git', 'bin', 'git')\ +\ + return 'git'\ +\ +\ +def install_npm_linux():\ + print(\\\"Downloading node\\\")\ + download_file(\ + 'https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz',\ + 'node.tar.xz')\ + print(\\\"Installing node on Linux\\\")\ + with lzma.open(\\\"node.tar.xz\\\", \\\"r\\\") as lzma_ref:\ + with open(\\\"node.tar\\\", \\\"wb\\\") as fdst:\ + shutil.copyfileobj(lzma_ref, fdst)\ + with tarfile.open(\\\"node.tar\\\", \\\"r\\\") as tar_ref:\ + tar_ref.extractall(os.getcwd())\ +\ + try:\ + _, _, exit_code = execute([os.getcwd() + '/node-v18.18.2-linux-x64/bin/npm', '--version'],\ + append_to_path=os.getcwd() + '/node-v18.18.2-linux-x64/bin')\ + if not exit_code == 0:\ + raise Exception(\\\"Failed to run npm\\\")\ + except Exception as ex:\ + print('Failed to install npm ' + str(ex))\ + sys.exit(1)\ + return os.getcwd() + '/node-v18.18.2-linux-x64/bin/npm', os.getcwd() + '/node-v18.18.2-linux-x64/bin'\ +\ +\ +def install_npm_windows():\ + print(\\\"Downloading node\\\")\ + download_file('https://nodejs.org/dist/v18.18.2/node-v18.18.2-win-x64.zip', 'node.zip', False)\ + print(\\\"Installing node on Windows\\\")\ + with zipfile.ZipFile(\\\"node.zip\\\", \\\"r\\\") as zip_ref:\ + zip_ref.extractall(os.getcwd())\ + try:\ + _, _, exit_code = execute([os.path.join(os.getcwd(), 'node-v18.18.2-win-x64', 'npm.cmd'), '--version'],\ + append_to_path=os.path.join(os.getcwd(), 'node-v18.18.2-win-x64'))\ + if not exit_code == 0:\ + raise Exception(\\\"Failed to run npm\\\")\ + except Exception as ex:\ + print('Failed to install npm ' + str(ex))\ + sys.exit(1)\ +\ + return (os.path.join(os.getcwd(), 'node-v18.18.2-win-x64', 'npm.cmd'),\ + os.path.join(os.getcwd(), 'node-v18.18.2-win-x64'))\ +\ +\ +def ensure_node_exists():\ + try:\ + print(\\\"Checking node is installed\\\")\ + _, _, exit_code = execute(['npm', '--version'])\ + if not exit_code == 0:\ + raise Exception(\\\"npm not found\\\")\ + except:\ + if is_windows():\ + return install_npm_windows()\ + else:\ + return install_npm_linux()\ +\ + return 'npm', None\ +\ +\ +def ensure_yo_exists(npm_executable, npm_path):\ + try:\ + print(\\\"Checking Yeoman is installed\\\")\ + _, _, exit_code = execute(['yo', '--version'])\ + if not exit_code == 0:\ + raise Exception(\\\"yo not found\\\")\ + except:\ + print('Installing Yeoman')\ +\ + _, _, retcode = execute([npm_executable, 'install', '-g', 'yo'], append_to_path=npm_path)\ +\ + if not retcode == 0:\ + print(\\\"Failed to set install Yeoman. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ + npm_bin, _, retcode = execute([npm_executable, 'config', 'get', 'prefix'], append_to_path=npm_path)\ +\ + if not retcode == 0:\ + print(\\\"Failed to set get the npm prefix directory. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ + try:\ + if is_windows():\ + _, _, exit_code = execute([os.path.join(npm_bin.strip(), 'yo.cmd'), '--version'],\ + append_to_path=npm_path)\ + else:\ + _, _, exit_code = execute([os.path.join(npm_bin.strip(), 'bin', 'yo'), '--version'],\ + append_to_path=npm_path)\ +\ + if not exit_code == 0:\ + raise Exception(\\\"Failed to run yo\\\")\ + except Exception as ex:\ + print('Failed to install yo ' + str(ex))\ + sys.exit(1)\ +\ + # Windows and Linux save NPM binaries in different directories\ + if is_windows():\ + return os.path.join(npm_bin.strip(), 'yo.cmd')\ +\ + return os.path.join(npm_bin.strip(), 'bin', 'yo')\ +\ + return 'yo'\ +\ +\ +git_executable = ensure_git_exists()\ +npm_executable, npm_path = ensure_node_exists()\ +yo_executable = ensure_yo_exists(npm_executable, npm_path)\ +parser, _ = init_argparse()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +if not parser.generator.strip():\ + print(\\\"You must define the Yeoman generator\\\")\ + sys.exit(1)\ +\ +# Create a dir for the git clone\ +if os.path.exists('downstream'):\ + shutil.rmtree('downstream')\ +\ +os.mkdir('downstream')\ +\ +# Create a dir for yeoman to use\ +if os.path.exists('downstream-yeoman'):\ + shutil.rmtree('downstream-yeoman')\ +\ +os.mkdir('downstream-yeoman')\ +# Yeoman will use a less privileged user to write to this directory, so grant full access\ +if not is_windows():\ + os.chmod('downstream-yeoman', 0o777)\ +\ +downstream_dir = os.path.join(os.getcwd(), 'downstream')\ +downstream_yeoman_dir = os.path.join(os.getcwd(), 'downstream-yeoman')\ +\ +# The access token is generated from a github app or supplied directly as an access token\ +token = generate_github_token(parser.github_app_id, parser.github_app_private_key,\ + parser.github_app_installation_id) if len(\ + parser.git_password.strip()) == 0 else parser.git_password.strip()\ +\ +if not verify_new_repo(token, parser.git_organization, parser.repo):\ + print('Repo at https://github.com/' + parser.git_organization + '/' + parser.repo + ' could not be accessed')\ + sys.exit(1)\ +\ +# We need to disable the credentials helper prompt, which will pop open a GUI prompt that we can never close\ +if is_windows():\ + _, _, retcode = execute([git_executable, 'config', '--system', 'credential.helper', 'manager'])\ +\ + if not retcode == 0:\ + print(\\\"Failed to set the credential.helper setting. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ + _, _, retcode = execute([git_executable, 'config', '--system', 'credential.modalprompt', 'false'])\ +\ + if not retcode == 0:\ + print(\\\"Failed to srt the credential.modalprompt setting. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ + # We need to disable the credentials helper prompt, which will pop open a GUI prompt that we can never close\ + _, _, retcode = execute(\ + [git_executable, 'config', '--system', 'credential.microsoft.visualstudio.com.interactive', 'never'])\ +\ + if not retcode == 0:\ + print(\ + \\\"Failed to set the credential.microsoft.visualstudio.com.interactive setting. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +_, _, retcode = execute([git_executable, 'config', '--global', 'user.email', 'octopus@octopus.com'])\ +\ +if not retcode == 0:\ + print(\\\"Failed to set the user.email setting. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +_, _, retcode = execute([git_executable, 'config', '--global', 'core.autocrlf', 'input'])\ +\ +if not retcode == 0:\ + print(\\\"Failed to set the core.autocrlf setting. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +username = parser.git_username if len(parser.git_username) != 0 else 'Octopus'\ +_, _, retcode = execute([git_executable, 'config', '--global', 'user.name', username])\ +\ +if not retcode == 0:\ + print(\\\"Failed to set the git username. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +_, _, retcode = execute([git_executable, 'config', '--global', 'credential.helper', 'cache'])\ +\ +if not retcode == 0:\ + print(\\\"Failed to set the git credential helper. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +print('Cloning repo')\ +\ +_, _, retcode = execute(\ + [git_executable, 'clone',\ + 'https://' + username + ':' + token + '@github.com/' + parser.git_organization + '/' + parser.repo + '.git',\ + 'downstream'])\ +\ +if not retcode == 0:\ + print(\\\"Failed to clone the git repo. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +print('Configuring Yeoman Generator')\ +\ +_, _, retcode = execute([npm_executable, 'install'], cwd=os.path.join(os.getcwd(), 'YeomanGenerator'), append_to_path=npm_path)\ +\ +if not retcode == 0:\ + print(\\\"Failed to install the generator dependencies. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +_, _, retcode = execute([npm_executable, 'link'], cwd=os.path.join(os.getcwd(), 'YeomanGenerator'), append_to_path=npm_path)\ +\ +if not retcode == 0:\ + print(\\\"Failed to link the npm module. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +print('Running Yeoman Generator')\ +\ +# Treat the string of yo arguments as a raw input and parse it again. The resulting list of unknown arguments\ +# is then passed to yo. We have to convert the incoming values from utf to ascii when parsing a second time.\ +yo_args = split(anyascii(parser.generator_arguments))\ +\ +generator_name = parser.generator + ':' + parser.sub_generator if len(parser.sub_generator) != 0 else parser.generator\ +\ +yo_arguments = [yo_executable, generator_name, '--force', '--skip-install']\ +\ +# Yeoman has issues running as root, which it will often do in a container.\ +# So we run Yeoman in its own directory, and then copy the changes to the git directory.\ +_, _, retcode = execute(yo_arguments + yo_args, cwd=downstream_yeoman_dir, append_to_path=npm_path)\ +\ +if not retcode == 0:\ + print(\\\"Failed to run Yeoman. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +shutil.copytree(downstream_yeoman_dir, downstream_dir, dirs_exist_ok=True)\ +\ +print('Adding changes to git')\ +\ +_, _, retcode = execute([git_executable, 'add', '.'], cwd=downstream_dir)\ +\ +if not retcode == 0:\ + print(\\\"Failed to add the git changes. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +# Check for pending changes\ +_, _, retcode = execute([git_executable, 'diff-index', '--quiet', 'HEAD'], cwd=downstream_dir)\ +\ +if not retcode == 0:\ + print('Committing changes to git')\ + _, _, retcode = execute([git_executable, 'commit', '-m',\ + 'Added files from Yeoman generator ' + parser.generator + ':' + parser.sub_generator],\ + cwd=downstream_dir)\ +\ + if not retcode == 0:\ + print(\\\"Failed to set commit the git changes. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ + print('Pushing changes to git')\ +\ + _, _, retcode = execute([git_executable, 'push', 'origin', 'main'], cwd=downstream_dir)\ +\ + if not retcode == 0:\ + print(\\\"Failed to push the git changes. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\" + \"PopulateGithubRepo.Yeoman.Generator.Package\" = jsonencode({ + \"PackageId\" = \"OctopusSolutionsEngineering/ReferenceArchitectureAppGenerators\" + \"FeedId\" = local.github_feed_id + }) + \"Octopus.Action.RunOnServer\" = \"true\" + \"PopulateGithubRepo.Git.Url.Organization\" = \"#{Git.Url.Organization}\" + \"PopulateGithubRepo.Git.Url.Repo\" = \"#{Octopus.Action[Create Repo].Output.NewRepo}\" + \"PopulateGithubRepo.Git.Credentials.Password\" = \"#{Git.Credentials.Password}\" + } + + container { + feed_id = local.docker_hub_feed_id + image = \"octopussamples/node-workertools\" + } + + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + + package { + name = \"YeomanGenerator\" + package_id = \"OctopusSolutionsEngineering/ReferenceArchitectureAppGenerators\" + acquisition_location = \"Server\" + extract_during_deployment = false + feed_id = local.github_feed_id + properties = { + Extract = \"True\", PackageParameterName = \"PopulateGithubRepo.Yeoman.Generator.Package\", Purpose = \"\", + SelectionMode = \"deferred\" + } + } + features = [] + } + + properties = {} + target_roles = [] + } +} +#endregion + +#region Frontend +variable \"frontend_project_name\" { + type = string + default = \"\" +} + +data \"octopusdeploy_projects\" \"octopub_frontend\" { + partial_name = var.frontend_project_name == \"\" ? local.frontend_project_name : var.frontend_project_name + skip = 0 + take = 1 +} + +resource \"octopusdeploy_variable\" \"frontend_deployment_feed\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_frontend[0].id + value = local.docker_hub_feed_id + name = \"Kubernetes.Deployment.Feed\" + type = \"String\" + description = \"The feed ID hosting the image\" + is_sensitive = false +} + +resource \"octopusdeploy_variable\" \"frontend_deployment_image\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_frontend[0].id + value = \"octopussamples/octopub-frontend\" + name = \"Kubernetes.Deployment.Image\" + type = \"String\" + description = \"The image to deploy\" + is_sensitive = false +} + +resource \"octopusdeploy_variable\" \"frontend_deployment_port\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_frontend[0].id + value = \"8080\" + name = \"Kubernetes.Deployment.Port\" + type = \"String\" + description = \"The port exposed by the web app\" + is_sensitive = false +} + +resource \"octopusdeploy_variable\" \"frontend_microservice_name\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_frontend[0].id + value = \"frontend\" + name = \"Microservice.Name\" + type = \"String\" + description = \"The microservice name, which is used as the basis for K8s resources and networking paths\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"frontend_deployment_name\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_frontend[0].id + value = \"#{Microservice.Name}\" + name = \"Kubernetes.Deployment.Name\" + type = \"String\" + description = \"The name of the Kubernetes deployment resource\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"frontend_service_name\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_frontend[0].id + value = \"#{Microservice.Name}\" + name = \"Kubernetes.Service.Name\" + type = \"String\" + description = \"The name of the Kubernetes service resource\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"frontend_ingress_path\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_frontend[0].id + value = \"/#{Kubernetes.Namespace}(/.*)?\" + name = \"Kubernetes.Ingress.Path\" + type = \"String\" + description = \"The name of the Kubernetes ingress resource\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"frontend_app_path\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_frontend[0].id + value = \"/#{Kubernetes.Namespace}/index.html\" + name = \"Kubernetes.App.HealthCheck\" + type = \"String\" + description = \"The path to perform a health check on.\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"frontend_ingress_name\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_frontend[0].id + value = \"#{Microservice.Name}\" + name = \"Kubernetes.Ingress.Name\" + type = \"String\" + description = \"The name of the Kubernetes ingress resource\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"frontend_namespace_default\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_frontend[0].id + value = \"#{Octopus.Action.Kubernetes.Namespace}\" + name = \"Kubernetes.Namespace\" + type = \"String\" + description = \"The namespace to perform the deployments in.\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"frontend_namespace_featurebranch\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_frontend[0].id + value = \"\" + name = \"Kubernetes.Namespace\" + type = \"String\" + description = \"The custom namespace to use when deploying a feature branch\" + is_sensitive = false + + scope { + actions = [] + channels = [] + environments = [local.featurebranch_environment_id] + machines = [] + roles = null + tenant_tags = null + } + + prompt { + description = \"Feature branch namespace\" + label = \"Namespace\" + is_required = true + display_settings { + control_type = \"SingleLineText\" + } + } +} + +resource \"octopusdeploy_channel\" \"frontend_featurebranch\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + name = \"Feature Branch\" + project_id = octopusdeploy_project.project_octopub_frontend[0].id + description = \"Deploy feature branch builds\" + is_default = false + lifecycle_id = local.featurebranch_lifecycle_id +} + +resource \"octopusdeploy_channel\" \"frontend_mainline\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + name = \"Mainline\" + project_id = octopusdeploy_project.project_octopub_frontend[0].id + description = \"Deploy mainline builds\" + is_default = true + lifecycle_id = local.devops_lifecycle_id + rule { + tag = \"^$\" + action_package { + deployment_action = \"Deploy Container\" + package_reference = \"web\" + } + } + + depends_on = [octopusdeploy_deployment_process.deployment_process_octopub_frontend] +} + +resource \"octopusdeploy_project\" \"project_octopub_frontend\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + name = var.frontend_project_name == \"\" ? local.frontend_project_name : var.frontend_project_name + auto_create_release = false + default_guided_failure_mode = \"Off\" + default_to_skip_if_already_installed = false + discrete_channel_release = false + is_disabled = false + is_version_controlled = false + lifecycle_id = local.devops_lifecycle_id + project_group_id = local.eks_project_group_id + included_library_variable_sets = [local.this_instance_library_variable_set, local.github_library_variable_set] + tenanted_deployment_participation = \"Untenanted\" + + connectivity_policy { + allow_deployments_to_no_targets = true + exclude_unhealthy_targets = false + skip_machine_behavior = \"None\" + } + + versioning_strategy { + template = \"#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.NextPatch}\" + } + + lifecycle { + ignore_changes = [] + } + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"pod\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"logs\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_frontend_get_pods\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Pods\" + project_id = octopusdeploy_project.project_octopub_frontend[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"pod\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_frontend_describe_pods\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + name = \"🛠️ Describe Pods\" + project_id = octopusdeploy_project.project_octopub_frontend[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"pod\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"describe\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_frontend_get_ingress\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Ingress\" + project_id = octopusdeploy_project.project_octopub_frontend[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"ingress\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Ingress.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_frontend_get_service\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Service\" + project_id = octopusdeploy_project.project_octopub_frontend[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"service\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Service.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_frontend_get_deployment\" { + count = length(data.octopusdeploy_projects.octopub_frontend.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Deployment\" + project_id = octopusdeploy_project.project_octopub_frontend[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"deployment\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} +#endregion + +#region Products +variable \"products_project_name\" { + type = string + default = \"\" +} + +data \"octopusdeploy_projects\" \"octopub_products\" { + partial_name = var.products_project_name == \"\" ? local.products_project_name : var.products_project_name + skip = 0 + take = 1 +} + +resource \"octopusdeploy_variable\" \"products_deployment_feed\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_products[0].id + value = local.docker_hub_feed_id + name = \"Kubernetes.Deployment.Feed\" + type = \"String\" + description = \"The feed ID hosting the image\" + is_sensitive = false +} + +resource \"octopusdeploy_variable\" \"products_deployment_image\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_products[0].id + value = \"octopussamples/octopub-products-microservice\" + name = \"Kubernetes.Deployment.Image\" + type = \"String\" + description = \"The image to deploy\" + is_sensitive = false +} + +resource \"octopusdeploy_variable\" \"products_deployment_port\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_products[0].id + value = \"8083\" + name = \"Kubernetes.Deployment.Port\" + type = \"String\" + description = \"The port exposed by the web app\" + is_sensitive = false +} + +resource \"octopusdeploy_variable\" \"products_microservice_name\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_products[0].id + value = \"products\" + name = \"Microservice.Name\" + type = \"String\" + description = \"The microservice name, which is used as the basis for K8s resources and networking paths\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"products_deployment_name\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_products[0].id + value = \"#{Microservice.Name}\" + name = \"Kubernetes.Deployment.Name\" + type = \"String\" + description = \"The name of the Kubernetes deployment resource\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"products_service_name\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_products[0].id + value = \"#{Microservice.Name}\" + name = \"Kubernetes.Service.Name\" + type = \"String\" + description = \"The name of the Kubernetes service resource\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"products_ingress_name\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_products[0].id + value = \"#{Microservice.Name}\" + name = \"Kubernetes.Ingress.Name\" + type = \"String\" + description = \"The name of the Kubernetes ingress resource\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"products_ingress_path\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_products[0].id + value = \"/#{Kubernetes.Namespace}(/api/#{Microservice.Name})(/.*)?\" + name = \"Kubernetes.Ingress.Path\" + type = \"String\" + description = \"The path of the Kubernetes ingress resource\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"products_app_path\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_products[0].id + value = \"/#{Kubernetes.Namespace}/api/#{Microservice.Name}\" + name = \"Kubernetes.App.HealthCheck\" + type = \"String\" + description = \"The path to perform a health check on.\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"products_namespace_default\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_products[0].id + value = \"#{Octopus.Action.Kubernetes.Namespace}\" + name = \"Kubernetes.Namespace\" + type = \"String\" + description = \"The namespace to perform the deployments in.\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"products_namespace_featurebranch\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_products[0].id + value = \"\" + name = \"Kubernetes.Namespace\" + type = \"String\" + description = \"The custom namespace to use when deploying a feature branch\" + is_sensitive = false + + scope { + actions = [] + channels = [] + environments = [local.featurebranch_environment_id] + machines = [] + roles = null + tenant_tags = null + } + + prompt { + description = \"Feature branch namespace\" + label = \"Namespace\" + is_required = true + display_settings { + control_type = \"SingleLineText\" + } + } +} + +resource \"octopusdeploy_channel\" \"products_featurebranch\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + name = \"Feature Branch\" + project_id = octopusdeploy_project.project_octopub_products[0].id + description = \"Deploy feature branch builds\" + is_default = false + lifecycle_id = local.featurebranch_lifecycle_id +} + +resource \"octopusdeploy_channel\" \"products_mainline\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + name = \"Mainline\" + project_id = octopusdeploy_project.project_octopub_products[0].id + description = \"Deploy mainline builds\" + is_default = true + lifecycle_id = local.devops_lifecycle_id + rule { + tag = \"^$\" + action_package { + deployment_action = \"Deploy Container\" + package_reference = \"web\" + } + } + + depends_on = [octopusdeploy_deployment_process.deployment_process_octopub_products] +} + +resource \"octopusdeploy_project\" \"project_octopub_products\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + name = var.products_project_name == \"\" ? local.products_project_name : var.products_project_name + auto_create_release = false + default_guided_failure_mode = \"Off\" + default_to_skip_if_already_installed = false + discrete_channel_release = false + is_disabled = false + is_version_controlled = false + lifecycle_id = local.devops_lifecycle_id + project_group_id = local.eks_project_group_id + included_library_variable_sets = [local.this_instance_library_variable_set, local.github_library_variable_set] + tenanted_deployment_participation = \"Untenanted\" + + connectivity_policy { + allow_deployments_to_no_targets = true + exclude_unhealthy_targets = false + skip_machine_behavior = \"None\" + } + + versioning_strategy { + template = \"#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.NextPatch}\" + } + + lifecycle { + ignore_changes = [] + } + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"pod\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"logs\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_products_get_pods\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Pods\" + project_id = octopusdeploy_project.project_octopub_products[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"pod\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_products_describe_pods\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + name = \"🛠️ Describe Pods\" + project_id = octopusdeploy_project.project_octopub_products[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"pod\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"describe\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_products_get_ingress\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Ingress\" + project_id = octopusdeploy_project.project_octopub_products[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"ingress\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Ingress.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_products_get_service\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Service\" + project_id = octopusdeploy_project.project_octopub_products[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"service\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Service.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_products_get_deployment\" { + count = length(data.octopusdeploy_projects.octopub_products.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Deployment\" + project_id = octopusdeploy_project.project_octopub_products[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"deployment\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} +#endregion + +#region Audits +variable \"audits_project_name\" { + type = string + default = \"\" +} + +data \"octopusdeploy_projects\" \"octopub_audits\" { + partial_name = var.audits_project_name == \"\" ? local.audits_project_name : var.audits_project_name + skip = 0 + take = 1 +} + +resource \"octopusdeploy_variable\" \"audits_deployment_feed\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_audits[0].id + value = local.docker_hub_feed_id + name = \"Kubernetes.Deployment.Feed\" + type = \"String\" + description = \"The feed ID hosting the image\" + is_sensitive = false +} + +resource \"octopusdeploy_variable\" \"audits_deployment_image\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_audits[0].id + value = \"octopussamples/octopub-audit-microservice\" + name = \"Kubernetes.Deployment.Image\" + type = \"String\" + description = \"The image to deploy\" + is_sensitive = false +} + +resource \"octopusdeploy_variable\" \"audits_deployment_port\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_audits[0].id + value = \"10000\" + name = \"Kubernetes.Deployment.Port\" + type = \"String\" + description = \"The port exposed by the web app\" + is_sensitive = false +} + +resource \"octopusdeploy_variable\" \"auditss_microservice_name\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_audits[0].id + value = \"audits\" + name = \"Microservice.Name\" + type = \"String\" + description = \"The microservice name, which is used as the basis for K8s resources and networking paths\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"audits_deployment_name\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_audits[0].id + value = \"#{Microservice.Name}\" + name = \"Kubernetes.Deployment.Name\" + type = \"String\" + description = \"\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"audits_service_name\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_audits[0].id + value = \"#{Microservice.Name}\" + name = \"Kubernetes.Service.Name\" + type = \"String\" + description = \"\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"audits_ingress_name\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_audits[0].id + value = \"#{Microservice.Name}\" + name = \"Kubernetes.Ingress.Name\" + type = \"String\" + description = \"\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"audits_ingress_path\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_audits[0].id + value = \"/#{Kubernetes.Namespace}(/api/#{Microservice.Name})(/.*)?\" + name = \"Kubernetes.Ingress.Path\" + type = \"String\" + description = \"The path of the Kubernetes ingress resource\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"audits_app_path\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_audits[0].id + value = \"/#{Kubernetes.Namespace}/api/#{Microservice.Name}\" + name = \"Kubernetes.App.HealthCheck\" + type = \"String\" + description = \"The path to perform a health check on.\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"audits_namespace_default\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_audits[0].id + value = \"#{Octopus.Action.Kubernetes.Namespace}\" + name = \"Kubernetes.Namespace\" + type = \"String\" + description = \"The namespace to perform the deployments in.\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"audits_namespace_featurebranch\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.project_octopub_audits[0].id + value = \"\" + name = \"Kubernetes.Namespace\" + type = \"String\" + description = \"The custom namespace to use when deploying a feature branch\" + is_sensitive = false + + scope { + actions = [] + channels = [] + environments = [local.featurebranch_environment_id] + machines = [] + roles = null + tenant_tags = null + } + + prompt { + description = \"Feature branch namespace\" + label = \"Namespace\" + is_required = true + display_settings { + control_type = \"SingleLineText\" + } + } +} + +resource \"octopusdeploy_channel\" \"audits_featurebranch\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + name = \"Feature Branch\" + project_id = octopusdeploy_project.project_octopub_audits[0].id + description = \"Deploy feature branch builds\" + is_default = false + lifecycle_id = local.featurebranch_lifecycle_id +} + +resource \"octopusdeploy_channel\" \"audits_mainline\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + name = \"Mainline\" + project_id = octopusdeploy_project.project_octopub_audits[0].id + description = \"Deploy mainline builds\" + is_default = true + lifecycle_id = local.devops_lifecycle_id + rule { + tag = \"^$\" + action_package { + deployment_action = \"Deploy Container\" + package_reference = \"web\" + } + } + + depends_on = [octopusdeploy_deployment_process.deployment_process_octopub_audits] +} + +resource \"octopusdeploy_project\" \"project_octopub_audits\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + name = var.audits_project_name == \"\" ? local.audits_project_name : var.audits_project_name + auto_create_release = false + default_guided_failure_mode = \"Off\" + default_to_skip_if_already_installed = false + discrete_channel_release = false + is_disabled = false + is_version_controlled = false + lifecycle_id = local.devops_lifecycle_id + project_group_id = local.eks_project_group_id + included_library_variable_sets = [local.this_instance_library_variable_set, local.github_library_variable_set] + tenanted_deployment_participation = \"Untenanted\" + + connectivity_policy { + allow_deployments_to_no_targets = true + exclude_unhealthy_targets = false + skip_machine_behavior = \"None\" + } + + versioning_strategy { + template = \"#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.NextPatch}\" + } + + lifecycle { + ignore_changes = [] + } + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"pod\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"logs\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_audits_get_pods\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Pods\" + project_id = octopusdeploy_project.project_octopub_audits[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"pod\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_audits_describe_pods\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + name = \"🛠️ Describe Pods\" + project_id = octopusdeploy_project.project_octopub_audits[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"pod\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"describe\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_audits_get_ingress\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Ingress\" + project_id = octopusdeploy_project.project_octopub_audits[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"ingress\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Ingress.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_audits_get_service\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Service\" + project_id = octopusdeploy_project.project_octopub_audits[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"service\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Service.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_audits_get_deployment\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + name = \"🛠️ Get Deployment\" + project_id = octopusdeploy_project.project_octopub_audits[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = <\ +\ +<#\ +.Description\ +Execute an application, capturing the output. Based on https://stackoverflow.com/a/33652732/157605\ +#>\ +Function Execute-Command ($commandPath, $commandArguments)\ +{\ + Write-Host \\\"Executing: $commandPath $($commandArguments -join \\\" \\\")\\\"\ + \ + Try {\ + $pinfo = New-Object System.Diagnostics.ProcessStartInfo\ + $pinfo.FileName = $commandPath\ + $pinfo.RedirectStandardError = $true\ + $pinfo.RedirectStandardOutput = $true\ + $pinfo.UseShellExecute = $false\ + $pinfo.Arguments = $commandArguments\ + $p = New-Object System.Diagnostics.Process\ + $p.StartInfo = $pinfo\ + $p.Start() | Out-Null\ + [pscustomobject]@{\ + stdout = $p.StandardOutput.ReadToEnd()\ + stderr = $p.StandardError.ReadToEnd()\ + ExitCode = $p.ExitCode\ + }\ + $p.WaitForExit()\ + }\ + Catch {\ + exit\ + }\ +}\ +\ +<#\ +.Description\ +Find any resource names that match a wildcard input if one was specified\ +#>\ +function Get-Resources() \ +{\ + $names = $OctopusParameters[\\\"K8SInspectNames\\\"] -Split \\\"`n\\\" | % {$_.Trim()}\ + \ + if ($OctopusParameters[\\\"K8SInspectNames\\\"] -match '\\\\*' )\ + {\ + return Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", $OctopusParameters[\\\"K8SInspectResource\\\"])) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Extract the name\ + % {$_.metadata.name} |\ + # Find any matching resources\ + ? {$k8sName = $_; ($names | ? {$k8sName -like $_}).Count -ne 0}\ + }\ + else\ + {\ + return $names\ + }\ +}\ +\ +<#\ +.Description\ +Get the kubectl arguments for a given action\ +#>\ +function Get-KubectlVerb() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {return ,@(\\\"-o\\\", \\\"json\\\", \\\"get\\\")}\ + \\\"get yaml\\\" {return ,@(\\\"-o\\\", \\\"yaml\\\", \\\"get\\\")}\ + \\\"describe\\\" {return ,@(\\\"describe\\\")}\ + \\\"logs\\\" {return ,@(\\\"logs\\\")}\ + \\\"logs tail\\\" {return ,@(\\\"logs\\\", \\\"--tail\\\", \\\"100\\\")}\ + \\\"previous logs\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\")}\ + \\\"previous logs tail\\\" {return ,@(\\\"logs\\\", \\\"--previous\\\", \\\"--tail\\\", \\\"100\\\")}\ + default {return ,@(\\\"get\\\")}\ + }\ +}\ +\ +<#\ +.Description\ +Get an appropiate file extension based on the selected action\ +#>\ +function Get-ArtifactExtension() \ +{\ + switch($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"])\ + {\ + \\\"get json\\\" {\\\"json\\\"}\ + \\\"get yaml\\\" {\\\"yaml\\\"}\ + default {\\\"txt\\\"}\ + }\ +}\ +\ +if ($OctopusParameters[\\\"K8SInspectKubectlVerb\\\"] -like \\\"*logs*\\\") \ +{\ + if ( -not @($OctopusParameters[\\\"K8SInspectResource\\\"]) -like \\\"pod*\\\")\ + {\ + Write-Error \\\"Logs can only be returned for pods, not $($OctopusParameters[\\\"K8SInspectResource\\\"])\\\"\ + }\ + else\ + {\ + Execute-Command kubectl (@(\\\"-o\\\", \\\"json\\\", \\\"get\\\", \\\"pods\\\") + (Get-Resources)) |\ + # Select the stdout property from the execution\ + Select-Object -ExpandProperty stdout |\ + # Convert the output from JSON\ + ConvertFrom-JSON | \ + # Get the items object from the kubectl response\ + % {if ((Get-Member -InputObject $_ -Name items).Count -ne 0) {Select-Object -InputObject $_ -ExpandProperty items} else {$_}} |\ + # Get the pod logs for each container\ + % {\ + $podDetails = $_\ + @{\ + logs=$podDetails.spec.containers | % {$logs=\\\"\\\"} {$logs += (Select-Object -InputObject (Execute-Command kubectl ((Get-KubectlVerb) + @($podDetails.metadata.name, \\\"-c\\\", $_.name))) -ExpandProperty stdout)} {$logs}; \ + name=$podDetails.metadata.name\ + } \ + } |\ + # Write the output\ + % {Write-Host $_.logs; $_} |\ + # Optionally capture the artifact\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"$($_.name).$(Get-ArtifactExtension)\\\" -Value $_.logs\ + New-OctopusArtifact \\\"$($_.name).$(Get-ArtifactExtension)\\\"\ + }\ + }\ + } \ +}\ +else\ +{\ + Execute-Command kubectl ((Get-KubectlVerb) + @($OctopusParameters[\\\"K8SInspectResource\\\"]) + (Get-Resources)) |\ + % {Select-Object -InputObject $_ -ExpandProperty stdout} |\ + % {Write-Host $_; $_} |\ + % {\ + if ($OctopusParameters[\\\"K8SInspectCreateArtifact\\\"] -ieq \\\"true\\\") \ + {\ + Set-Content -Path \\\"output.$(Get-ArtifactExtension)\\\" -Value $_\ + New-OctopusArtifact \\\"output.$(Get-ArtifactExtension)\\\"\ + }\ + }\ +}\ +\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"K8SInspectResource\" = \"deployment\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.KubernetesContainers.Namespace\" = \"#{if K8SInspectNamespace}#{K8SInspectNamespace}#{/if}#{unless K8SInspectNamespace}#{Octopus.Action.Kubernetes.Namespace}#{/unless}\" + \"K8SInspectNames\" = \"#{Kubernetes.Deployment.Name}*\" + \"K8SInspectKubectlVerb\" = \"get\" + } + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/k8s-workertools\" + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"EKS_Reference_Cluster\"] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Write-Highlight \\\"Please share your feedback on this step in our GitHub discussion at [https://oc.to/CfiezA](https://oc.to/CfiezA).\\\"\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +resource \"octopusdeploy_runbook\" \"runbook_octopub_audits_scale_to_zero\" { + count = length(data.octopusdeploy_projects.octopub_audits.projects) == 0 ? 1 : 0 + name = \"🌃 Scale Pods to Zero\" + project_id = octopusdeploy_project.project_octopub_audits[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"Off\" + description = < Project` runbooks.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "03759c5c-fce1-4770-8bcc-0ed3004a0d81", + "Name": "ReferenceArchitecture.Eks.Octopus.ApiKey", + "Label": "Octopus API Key", + "HelpText": "The Octopus API key. See the [Octopus docs](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) for more details on creating an API Key.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "003255fd-8bdb-4e5c-9646-5588eef5c524", + "Name": "ReferenceArchitecture.Eks.Octopus.SpaceId", + "Label": "Octopus Space ID", + "HelpText": "The Octopus space ID.", + "DefaultValue": "#{Octopus.Space.Id}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cf35bbb0-eb2f-4dec-84bd-1cef24361d0d", + "Name": "ReferenceArchitecture.Eks.Octopus.ServerUrl", + "Label": "Octopus Server URL", + "HelpText": "The Octopus server URL.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e45078c1-d344-4315-a6f5-1295f6057d77", + "Name": "ReferenceArchitecture.Terraform.ApplyArgs", + "Label": "Optional Terraform Apply Args", + "HelpText": "Optional arguments passed to the `terraform apply` command. See the [documentation](https://oc.to/wRvMoP) for details on any optional variables that can be defined here. Leave this field blank unless you have a specific reason to change it.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "d68e37c6-23d1-4224-a973-1edcfa55fa2f", + "Name": "ReferenceArchitecture.Terraform.InitArgs", + "Label": "Optional Terraform Init Args", + "HelpText": "Optional arguments passed to the `terraform init` command. See the [documentation](https://oc.to/wRvMoP) for details on any optional variables that can be defined here. Leave this field blank unless you have a specific reason to change it.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.TerraformApply", + "$Meta": { + "ExportedAt": "2023-10-18T22:44:44.053Z", + "OctopusVersion": "2023.4.6357", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-reference-architecture-webapp.json.human b/step-templates/octopus-reference-architecture-webapp.json.human new file mode 100644 index 000000000..409d9a4a1 --- /dev/null +++ b/step-templates/octopus-reference-architecture-webapp.json.human @@ -0,0 +1,4674 @@ +{ + "Id": "614ec627-22a1-442a-b6a1-d51ae7009999", + "Name": "Octopus - Web App Reference Architecture", + "Description": "This step populates an Octopus space with the environments, feeds, accounts, lifecycles, projects, and runbooks required to deploy a sample application to an Azure Web App. These resources combine to form a reference architecture teams can use to bootstrap an Octopus space with best practices and example projects. It is recommended that you run this step with the octopuslabs/terraform-workertools container image. + +That this step assumes it is run on a cloud Octopus instance, or the default worker runs Linux, has Docker installed, and has PowerShell Core installed. + +The step will not update existing projects, environments etc. If you wish to recreate these resource with the latest configuration, for example if this step is updated and you wish to see the latest settings, you must manually delete or rename the resources to be recreated.", + "ActionType": "Octopus.TerraformApply", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.GoogleCloud.UseVMServiceAccount": "True", + "Octopus.Action.GoogleCloud.ImpersonateServiceAccount": "False", + "Octopus.Action.Terraform.GoogleCloudAccount": "False", + "Octopus.Action.Terraform.AzureAccount": "False", + "Octopus.Action.Terraform.ManagedAccount": "None", + "Octopus.Action.Terraform.AllowPluginDownloads": "True", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Terraform.RunAutomaticFileSubstitution": "True", + "Octopus.Action.Terraform.PlanJsonOutput": "False", + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Terraform.Template": "#region Provider +terraform { + required_providers { + octopusdeploy = { + source = \"OctopusDeployLabs/octopusdeploy\" + version = \"0.21.1\" + } + } +} + +variable \"octopus_server\" { + type = string + nullable = false + sensitive = false + description = \"The URL of the Octopus server e.g. https://myinstance.octopus.app.\" + default = \"#{Octopus.Web.ServerUri}\" +} + +variable \"octopus_apikey\" { + type = string + nullable = false + sensitive = true + description = \"The API key used to access the Octopus server. See https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key for details on creating an API key.\" +} + +variable \"octopus_space_id\" { + type = string + nullable = false + sensitive = false + description = \"The ID of the Octopus space to populate.\" + default = \"#{Octopus.Space.Id}\" +} + +provider \"octopusdeploy\" { + address = var.octopus_server + api_key = var.octopus_apikey + space_id = var.octopus_space_id +} +#endregion +#region Locals +locals { + /* + You will need to edit: + * smoke_test_properties - depending on the step type, custom properties will need to be defined + * smoke_test_script - replace REPLACE THIS WITH CODE TO RETURN THE PUBLIC HOSTNAME with the appropriate logic + * cloud_account - this will need to be created in most cases + * security_scan_docker_script - the reference to the docker image deployed in earlier steps needs to be updated from #{Octopus.Action[Deploy Container].Package[web].PackageId} + * octopusdeploy_deployment_process - each of the sample application deployment processes (there are 3 defined in this template) needs to have the actual deployment step added + */ + + project_template_project_name = \"Docker Project Templates\" + project_group_name = \"Azure Web App\" + infrastructure_project_name = \"_ Azure Web App Infrastructure\" + infrastructure_project_description = \"Runbooks to create and manage Azure Web Apps\" + infrastructure_runbook_name = \"Create Web App\" + infrastructure_runbook_description = \"Creates an Azure web app and the associated Octopus target\" + feedback_link = \"https://oc.to/CfiezA\" + octopub_frontend_project_name = \"Azure WebApp Octopub Frontend\" + octopub_frontend_project_description = \"Deploys the Octopub Frontend app to an Azure Web App target\" + octopub_products_project_name = \"Azure WebApp Octopub Products\" + octopub_products_project_description = \"Deploys the Octopub Products app to an Azure Web App target\" + octopub_audits_project_name = \"Azure WebApp Octopub Audits\" + octopub_audits_project_description = \"Deploys the Octopub Audits app to an Azure Web App target\" + octopub_orchestration_project_name = \"_ Deploy Azure Web App Octopub Stack\" + octopub_orchestration_project_description = \"Deploys the full Octopub application stack\" + frontend_health_check = \"/index.html\" + products_health_check = \"/health/products\" + audits_health_check = \"/health/audits\" + target_role = \"Web App Reference Architecture\" + unique_prefix = \"#{Octopus.Web.ServerUri | Replace \\\"^https?://\\\" \\\"\\\" | Replace \\\"[^A-Za-z0-9]\\\" \\\"-\\\"}\" + + // Use the example below for any accounts that might be included in this reference architecture + cloud_account = length(data.octopusdeploy_accounts.account.accounts) == 0 ? octopusdeploy_azure_service_principal.account[0].id : data.octopusdeploy_accounts.account.accounts[0].id + + development_environment_id = length(data.octopusdeploy_environments.environment_development.environments) == 0 ? octopusdeploy_environment.environment_development[0].id : data.octopusdeploy_environments.environment_development.environments[0].id + test_environment_id = length(data.octopusdeploy_environments.environment_test.environments) == 0 ? octopusdeploy_environment.environment_test[0].id : data.octopusdeploy_environments.environment_test.environments[0].id + production_environment_id = length(data.octopusdeploy_environments.environment_production.environments) == 0 ? octopusdeploy_environment.environment_production[0].id : data.octopusdeploy_environments.environment_production.environments[0].id + sync_environment_id = length(data.octopusdeploy_environments.environment_sync.environments) == 0 ? octopusdeploy_environment.environment_sync[0].id : data.octopusdeploy_environments.environment_sync.environments[0].id + security_environment_id = length(data.octopusdeploy_environments.environment_security.environments) == 0 ? octopusdeploy_environment.environment_security[0].id : data.octopusdeploy_environments.environment_security.environments[0].id + this_instance_library_variable_set = length(data.octopusdeploy_library_variable_sets.this_instance.library_variable_sets) == 0 ? octopusdeploy_library_variable_set.this_instance[0].id : data.octopusdeploy_library_variable_sets.this_instance.library_variable_sets[0].id + github_library_variable_set = length(data.octopusdeploy_library_variable_sets.github.library_variable_sets) == 0 ? octopusdeploy_library_variable_set.github[0].id : data.octopusdeploy_library_variable_sets.github.library_variable_sets[0].id + docker_library_variable_set = length(data.octopusdeploy_library_variable_sets.docker.library_variable_sets) == 0 ? octopusdeploy_library_variable_set.docker[0].id : data.octopusdeploy_library_variable_sets.docker.library_variable_sets[0].id + docker_hub_feed_id = length(data.octopusdeploy_feeds.dockerhub.feeds) == 0 ? octopusdeploy_docker_container_registry.docker_hub[0].id : data.octopusdeploy_feeds.dockerhub.feeds[0].id + github_feed_id = length(data.octopusdeploy_feeds.github_feed.feeds) == 0 ? octopusdeploy_github_repository_feed.github_feed[0].id : data.octopusdeploy_feeds.github_feed.feeds[0].id + worker_pool_id = length(data.octopusdeploy_worker_pools.workerpool_hosted_ubuntu.worker_pools) == 0 ? \"\" : data.octopusdeploy_worker_pools.workerpool_hosted_ubuntu.worker_pools[0].id + devops_lifecycle_id = length(data.octopusdeploy_lifecycles.devsecops.lifecycles) == 0 ? octopusdeploy_lifecycle.lifecycle_devsecops[0].id : data.octopusdeploy_lifecycles.devsecops.lifecycles[0].id + project_group_id = length(data.octopusdeploy_project_groups.ra.project_groups) == 0 ? octopusdeploy_project_group.project_group_ra[0].id : data.octopusdeploy_project_groups.ra.project_groups[0].id + project_templates_project_group_id = length(data.octopusdeploy_project_groups.project_templates.project_groups) == 0 ? octopusdeploy_project_group.project_group_project_templates[0].id : data.octopusdeploy_project_groups.project_templates.project_groups[0].id + application_lifecycle_id = length(data.octopusdeploy_lifecycles.application.lifecycles) == 0 ? octopusdeploy_lifecycle.lifecycle_application[0].id : data.octopusdeploy_lifecycles.application.lifecycles[0].id + smoke_test_container_image = \"octopuslabs/azure-workertools\" + smoke_test_action_type = \"Octopus.AzurePowerShell\" + smoke_test_properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"Bash\" + \"Octopus.Action.Script.ScriptBody\" = local.smoke_test_script + \"OctopusUseBundledTooling\" : \"False\" + \"Octopus.Action.Azure.AccountId\" : local.cloud_account + } + link_highlight_container_image = \"octopuslabs/azure-workertools\" + link_highlight_action_type = \"Octopus.AzurePowerShell\" + link_highlight_properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"Bash\" + \"Octopus.Action.Script.ScriptBody\" = local.link_highlight_script + \"OctopusUseBundledTooling\" : \"False\" + \"Octopus.Action.Azure.AccountId\" : local.cloud_account + } + + link_highlight_script = <<-EOT + write_highlight \"[https://#{Octopus.Action.Azure.WebAppName}.azurewebsites.net#{App.HealthCheck}](https://#{Octopus.Action.Azure.WebAppName}.azurewebsites.net#{App.HealthCheck})\" + EOT + + feedback_script = <<-EOT + Write-Highlight \"Please share your feedback on this step in our [GitHub discussion](${local.feedback_link}).\" + EOT + smoke_test_script = <<-EOT + for i in {1..30} + do + HOSTNAME=#{Octopus.Action.Azure.WebAppName}.azurewebsites.net + if [[ -n \"$${HOSTNAME}\" ]] + then + break + fi + echo \"Waiting for ingress hostname\" + sleep 10 + done + + + # Load balancers can take a minute or so before their DNS is propagated. + # A status code of 000 means curl could not resolve the DNS name, so we wait for a bit until DNS is updated. + echo \"Testing https://$${HOSTNAME}#{App.HealthCheck}\" + echo \"Waiting for DNS to propagate. This can take a while for a new load balancer.\" + for i in {1..30} + do + CODE=$(curl -o /dev/null -s -w \"%%{http_code}\ +\" https://$${HOSTNAME}#{App.HealthCheck}) + if [[ \"$${CODE}\" == \"200\" ]] + then + break + fi + echo \"Waiting for DNS name to be resolvable and for service to respond\" + sleep 10 + done + + echo \"response code: $${CODE}\" + if [[ \"$${CODE}\" == \"200\" ]] + then + echo \"success\" + exit 0 + else + echo \"error\" + exit 1 + fi + EOT + security_scan_docker_script = <<-EOT + echo \"Pulling Trivy Docker Image\" + echo \"##octopus[stdout-verbose]\" + docker pull aquasec/trivy + echo \"##octopus[stdout-default]\" + + echo \"Installing umoci\" + echo \"##octopus[stdout-verbose]\" + # Install umoci + if ! which umoci + then + curl -o umoci -L https://github.com/opencontainers/umoci/releases/latest/download/umoci.amd64 2>&1 + chmod +x umoci + fi + echo \"##octopus[stdout-default]\" + + DOCKERIMAGE=#{Octopus.Action[Deploy Container].Package.PackageId} + DOCKERTAG=#{Octopus.Action[Deploy Container].Package.PackageVersion} + + echo \"Extracting Application Docker Image\" + echo \"##octopus[stdout-verbose]\" + # Download and extract the docker image + # https://manpages.ubuntu.com/manpages/jammy/man1/umoci-raw-unpack.1.html + docker pull quay.io/skopeo/stable:latest 2>&1 + docker run -v $(pwd):/output quay.io/skopeo/stable:latest copy docker://$${DOCKERIMAGE}:$${DOCKERTAG} oci:/output/image:latest 2>&1 + ./umoci unpack --image image --rootless bundle 2>&1 + echo \"##octopus[stdout-default]\" + + TIMESTAMP=$(date +%s%3N) + SUCCESS=0 + for x in $(find . -name bom.json -type f -print); do + echo \"Scanning $${x}\" + + # Delete any existing report file + if [[ -f \"$PWD/depscan-bom.json\" ]]; then + rm \"$PWD/depscan-bom.json\" + fi + + # Generate the report, capturing the output, and ensuring $? is set to the exit code + OUTPUT=$(bash -c \"docker run --rm -v \\\"$PWD:/app\\\" aquasec/trivy sbom \\\"/app/$${x}\\\"; exit \\$?\" 2>&1) + + # Success is set to 1 if the exit code is not zero + if [[ $? -ne 0 ]]; then + SUCCESS=1 + fi + + # Print the output stripped of ANSI colour codes + echo -e \"$${OUTPUT}\" | sed 's/\\x1b\\[[0-9;]*m//g' + done + + set_octopusvariable \"VerificationResult\" $SUCCESS + + if [[ $SUCCESS -ne 0 ]]; then + >&2 echo \"Critical vulnerabilities were detected\" + fi + + exit 0 + EOT +} +#endregion +#region Environments +data \"octopusdeploy_environments\" \"environment_development\" { + ids = null + partial_name = \"Development\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_environment\" \"environment_development\" { + count = length(data.octopusdeploy_environments.environment_development.environments) == 0 ? 1 : 0 + name = \"Development\" + description = \"\" + allow_dynamic_infrastructure = true + use_guided_failure = true + sort_order = 10 + + jira_extension_settings { + environment_type = \"development\" + } + + jira_service_management_extension_settings { + is_enabled = false + } + + servicenow_extension_settings { + is_enabled = false + } +} + +data \"octopusdeploy_environments\" \"environment_test\" { + ids = null + partial_name = \"Test\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_environment\" \"environment_test\" { + count = length(data.octopusdeploy_environments.environment_test.environments) == 0 ? 1 : 0 + name = \"Test\" + description = \"\" + allow_dynamic_infrastructure = true + use_guided_failure = true + sort_order = 11 + + jira_extension_settings { + environment_type = \"testing\" + } + + jira_service_management_extension_settings { + is_enabled = false + } + + servicenow_extension_settings { + is_enabled = false + } +} + +data \"octopusdeploy_environments\" \"environment_production\" { + ids = null + partial_name = \"Production\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_environment\" \"environment_production\" { + count = length(data.octopusdeploy_environments.environment_production.environments) == 0 ? 1 : 0 + name = \"Production\" + description = \"\" + allow_dynamic_infrastructure = true + use_guided_failure = true + sort_order = 12 + + jira_extension_settings { + environment_type = \"production\" + } + + jira_service_management_extension_settings { + is_enabled = false + } + + servicenow_extension_settings { + is_enabled = false + } +} + +data \"octopusdeploy_environments\" \"environment_security\" { + ids = null + partial_name = \"Security\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_environment\" \"environment_security\" { + count = length(data.octopusdeploy_environments.environment_security.environments) == 0 ? 1 : 0 + name = \"Security\" + description = \"\" + allow_dynamic_infrastructure = true + use_guided_failure = false + sort_order = 14 + + jira_extension_settings { + environment_type = \"production\" + } + + jira_service_management_extension_settings { + is_enabled = false + } + + servicenow_extension_settings { + is_enabled = false + } +} + +data \"octopusdeploy_environments\" \"environment_sync\" { + ids = null + partial_name = \"Sync\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_environment\" \"environment_sync\" { + count = length(data.octopusdeploy_environments.environment_sync.environments) == 0 ? 1 : 0 + name = \"Sync\" + description = \"\" + allow_dynamic_infrastructure = true + use_guided_failure = false + sort_order = 15 + + jira_extension_settings { + environment_type = \"development\" + } + + jira_service_management_extension_settings { + is_enabled = false + } + + servicenow_extension_settings { + is_enabled = false + } +} + +data \"octopusdeploy_lifecycles\" \"devsecops\" { + ids = [] + partial_name = \"DevSecOps\" + skip = 0 + take = 1 +} + +data \"octopusdeploy_lifecycles\" \"application\" { + ids = [] + partial_name = \"Application\" + skip = 0 + take = 1 +} + +data \"octopusdeploy_lifecycles\" \"sync\" { + ids = [] + partial_name = \"Sync\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_lifecycle\" \"sync\" { + count = length(data.octopusdeploy_lifecycles.sync.lifecycles) == 0 ? 1 : 0 + name = \"Sync\" + description = \"\" + + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.sync_environment_id + ] + name = \"Sync\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + + release_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } + + tentacle_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } +} + +resource \"octopusdeploy_lifecycle\" \"lifecycle_devsecops\" { + count = length(data.octopusdeploy_lifecycles.devsecops.lifecycles) == 0 ? 1 : 0 + name = \"DevSecOps\" + description = \"\" + + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.development_environment_id + ] + name = \"Development\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.test_environment_id + ] + name = \"Test\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.production_environment_id + ] + name = \"Production\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [ + local.security_environment_id + ] + optional_deployment_targets = [] + name = \"Security\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + + release_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } + + tentacle_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } +} + +resource \"octopusdeploy_lifecycle\" \"lifecycle_application\" { + count = length(data.octopusdeploy_lifecycles.application.lifecycles) == 0 ? 1 : 0 + name = \"Application\" + description = \"\" + + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.development_environment_id + ] + name = \"Development\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.test_environment_id + ] + name = \"Test\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + phase { + automatic_deployment_targets = [] + optional_deployment_targets = [ + local.production_environment_id + ] + name = \"Production\" + is_optional_phase = false + minimum_environments_before_promotion = 0 + } + + release_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } + + tentacle_retention_policy { + quantity_to_keep = 3 + should_keep_forever = false + unit = \"Days\" + } +} +#endregion +#region Feeds + +data \"octopusdeploy_feeds\" \"project\" { + feed_type = \"OctopusProject\" + ids = [] + skip = 0 + take = 1 +} + +data \"octopusdeploy_feeds\" \"bitnami\" { + feed_type = \"Helm\" + ids = [] + partial_name = \"Bitnami\" + skip = 0 + take = 1 +} + + +resource \"octopusdeploy_helm_feed\" \"feed_helm\" { + count = length(data.octopusdeploy_feeds.bitnami.feeds) == 0 ? 1 : 0 + name = \"Bitnami\" + feed_uri = \"https://repo.vmware.com/bitnami-files/\" + package_acquisition_location_options = [\"ExecutionTarget\", \"NotAcquired\"] +} + +data \"octopusdeploy_feeds\" \"dockerhub\" { + feed_type = \"Docker\" + ids = [] + partial_name = \"Docker Hub\" + skip = 0 + take = 1 +} + +variable \"feed_docker_hub_username\" { + type = string + nullable = false + sensitive = true + description = \"The username used by the feed Docker Hub\" +} + +variable \"feed_docker_hub_password\" { + type = string + nullable = false + sensitive = true + description = \"The password used by the feed Docker Hub\" +} + +resource \"octopusdeploy_docker_container_registry\" \"docker_hub\" { + count = length(data.octopusdeploy_feeds.dockerhub.feeds) == 0 ? 1 : 0 + name = \"Docker Hub\" + password = var.feed_docker_hub_password + username = var.feed_docker_hub_username + api_version = \"v1\" + feed_uri = \"https://index.docker.io\" + package_acquisition_location_options = [\"ExecutionTarget\", \"NotAcquired\"] +} + +data \"octopusdeploy_feeds\" \"sales_maven_feed\" { + feed_type = \"Maven\" + ids = [] + partial_name = \"Sales Maven Feed\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_maven_feed\" \"feed_sales_maven_feed\" { + count = length(data.octopusdeploy_feeds.sales_maven_feed.feeds) == 0 ? 1 : 0 + name = \"Sales Maven Feed\" + feed_uri = \"https://octopus-sales-public-maven-repo.s3.ap-southeast-2.amazonaws.com/snapshot\" + package_acquisition_location_options = [\"Server\", \"ExecutionTarget\"] + download_attempts = 3 + download_retry_backoff_seconds = 20 +} + +data \"octopusdeploy_feeds\" \"github_feed\" { + feed_type = \"GitHub\" + ids = [] + partial_name = \"Github Releases\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_github_repository_feed\" \"github_feed\" { + count = length(data.octopusdeploy_feeds.github_feed.feeds) == 0 ? 1 : 0 + download_attempts = 1 + download_retry_backoff_seconds = 30 + feed_uri = \"https://api.github.com\" + name = \"Github Releases\" +} +#endregion +#region Library Variable Sets +data \"octopusdeploy_library_variable_sets\" \"this_instance\" { + partial_name = \"This Instance\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_library_variable_set\" \"this_instance\" { + count = length(data.octopusdeploy_library_variable_sets.this_instance.library_variable_sets) == 0 ? 1 : 0 + name = \"This Instance\" + description = \"Credentials used to interact with this Octopus instance\" +} + +resource \"octopusdeploy_variable\" \"octopus_admin_api_key\" { + count = length(data.octopusdeploy_library_variable_sets.this_instance.library_variable_sets) == 0 ? 1 : 0 + name = \"Octopus.ApiKey\" + type = \"Sensitive\" + description = \"Octopus API Key\" + is_sensitive = true + is_editable = true + owner_id = octopusdeploy_library_variable_set.this_instance[0].id + sensitive_value = var.octopus_apikey +} + +data \"octopusdeploy_library_variable_sets\" \"github\" { + partial_name = \"GitHub\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_library_variable_set\" \"github\" { + count = length(data.octopusdeploy_library_variable_sets.github.library_variable_sets) == 0 ? 1 : 0 + name = \"GitHub\" + description = \"Credentials used to interact with GitHub\" +} + +variable \"github_access_token\" { + type = string + nullable = false + sensitive = true + description = \"The GitHub access token\" +} + +resource \"octopusdeploy_variable\" \"github_access_token\" { + count = length(data.octopusdeploy_library_variable_sets.github.library_variable_sets) == 0 ? 1 : 0 + name = \"Git.Credentials.Password\" + type = \"Sensitive\" + description = \"The GitHub access token\" + is_sensitive = true + is_editable = true + owner_id = octopusdeploy_library_variable_set.github[0].id + sensitive_value = var.github_access_token +} + +data \"octopusdeploy_library_variable_sets\" \"docker\" { + partial_name = \"Docker\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_library_variable_set\" \"docker\" { + count = length(data.octopusdeploy_library_variable_sets.docker.library_variable_sets) == 0 ? 1 : 0 + name = \"Docker\" + description = \"Credentials used to interact with Docker\" +} + +resource \"octopusdeploy_variable\" \"docker_username\" { + count = length(data.octopusdeploy_library_variable_sets.docker.library_variable_sets) == 0 ? 1 : 0 + name = \"Docker.Credentials.Username\" + type = \"String\" + description = \"The docker username\" + is_sensitive = false + is_editable = true + owner_id = octopusdeploy_library_variable_set.docker[0].id + value = var.feed_docker_hub_username +} + +resource \"octopusdeploy_variable\" \"docker_password\" { + count = length(data.octopusdeploy_library_variable_sets.docker.library_variable_sets) == 0 ? 1 : 0 + name = \"Docker.Credentials.Password\" + type = \"Sensitive\" + description = \"The docker password\" + is_sensitive = true + is_editable = true + owner_id = octopusdeploy_library_variable_set.docker[0].id + sensitive_value = var.feed_docker_hub_password +} +#endregion +#region Accounts + +data \"octopusdeploy_accounts\" \"account\" { + account_type = \"AzureServicePrincipal\" + partial_name = \"Azure\" + skip = 0 + take = 1 +} + +variable \"azure_account_application_id\" { + type = string + nullable = false + sensitive = false + description = \"The Azure account application ID\" +} + +variable \"azure_account_subscription_id\" { + type = string + nullable = false + sensitive = false + description = \"The Azure account subscription ID\" +} + +variable \"azure_account_tenant_id\" { + type = string + nullable = false + sensitive = false + description = \"The Azure account tenant ID\" +} + +variable \"azure_account_password\" { + type = string + nullable = false + sensitive = true + description = \"The Azure account password\" +} + +resource \"octopusdeploy_azure_service_principal\" \"account\" { + count = length(data.octopusdeploy_accounts.account.accounts) == 0 ? 1 : 0 + description = \"Azure Account\" + name = \"Azure\" + environments = [] + tenants = [] + tenanted_deployment_participation = \"Untenanted\" + application_id = var.azure_account_application_id + password = var.azure_account_password + subscription_id = var.azure_account_subscription_id + tenant_id = var.azure_account_tenant_id +} + +#endregion +#region Worker Pools + +data \"octopusdeploy_worker_pools\" \"workerpool_hosted_ubuntu\" { + partial_name = \"Hosted Ubuntu\" + ids = null + skip = 0 + take = 1 +} +#endregion +#region Project Groups +data \"octopusdeploy_project_groups\" \"ra\" { + ids = [] + partial_name = local.project_group_name + skip = 0 + take = 1 +} + +resource \"octopusdeploy_project_group\" \"project_group_ra\" { + count = length(data.octopusdeploy_project_groups.ra.project_groups) == 0 ? 1 : 0 + name = local.project_group_name + description = \"${local.project_group_name} projects.\" +} + +data \"octopusdeploy_project_groups\" \"project_templates\" { + ids = [] + partial_name = \"Project Templates\" + skip = 0 + take = 1 +} + +resource \"octopusdeploy_project_group\" \"project_group_project_templates\" { + count = length(data.octopusdeploy_project_groups.project_templates.project_groups) == 0 ? 1 : 0 + name = \"Project Templates\" + description = \"Sample code project generators\" +} +#endregion + +#region Projects + +#region Infrastructure Project +variable \"infrastructure_project_name\" { + type = string + default = \"\" +} + + +data \"octopusdeploy_projects\" \"infrastructure\" { + partial_name = var.infrastructure_project_name == \"\" ? local.infrastructure_project_name : var.infrastructure_project_name + skip = 0 + take = 1 +} + +resource \"octopusdeploy_variable\" \"azure_account\" { + count = length(data.octopusdeploy_projects.infrastructure.projects) == 0 ? 1 : 0 + name = \"Azure.Account\" + type = \"AzureAccount\" + description = \"The Azure account used to create the web app infrastructure\" + value = local.cloud_account + owner_id = octopusdeploy_project.infrastructure[0].id +} + +resource \"octopusdeploy_project\" \"infrastructure\" { + count = length(data.octopusdeploy_projects.infrastructure.projects) == 0 ? 1 : 0 + name = var.infrastructure_project_name == \"\" ? local.infrastructure_project_name : var.infrastructure_project_name + auto_create_release = false + default_guided_failure_mode = \"EnvironmentDefault\" + default_to_skip_if_already_installed = false + discrete_channel_release = false + is_disabled = false + is_version_controlled = true + lifecycle_id = local.devops_lifecycle_id + project_group_id = local.project_group_id + included_library_variable_sets = [] + tenanted_deployment_participation = \"Untenanted\" + + connectivity_policy { + allow_deployments_to_no_targets = true + exclude_unhealthy_targets = false + skip_machine_behavior = \"None\" + } + + versioning_strategy { + template = \"#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.NextPatch}\" + } + + lifecycle { + ignore_changes = [connectivity_policy] + } + description = local.infrastructure_project_description +} + +resource \"octopusdeploy_runbook\" \"runbook_create_infrastructure\" { + count = length(data.octopusdeploy_projects.infrastructure.projects) == 0 ? 1 : 0 + name = local.infrastructure_runbook_name + project_id = octopusdeploy_project.infrastructure[0].id + environment_scope = \"Specified\" + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + force_package_download = false + default_guided_failure_mode = \"EnvironmentDefault\" + description = local.infrastructure_runbook_description + multi_tenancy_mode = \"Untenanted\" + + retention_policy { + quantity_to_keep = 100 + should_keep_forever = false + } + + connectivity_policy { + allow_deployments_to_no_targets = true + exclude_unhealthy_targets = false + skip_machine_behavior = \"None\" + } +} + +resource \"octopusdeploy_runbook_process\" \"runbook_process_runbook_create_infrastructure\" { + count = length(data.octopusdeploy_projects.infrastructure.projects) == 0 ? 1 : 0 + runbook_id = octopusdeploy_runbook.runbook_create_infrastructure[0].id + + step { + condition = \"Success\" + name = \"Generate Variables\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Generate Variables\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = \"${data.octopusdeploy_worker_pools.workerpool_hosted_ubuntu.worker_pools[0].id}\" + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Set-OctopusVariable -name \\\"OctopusEnvironmentName\\\" -value $OctopusParameters[\\\"Octopus.Environment.Name\\\"]\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + + step { + condition = \"Success\" + name = \"Create Resource Group If Not Exists (AZ Module)\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.AzurePowerShell\" + name = \"Create Resource Group If Not Exists (AZ Module)\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"CreateResourceGroup.ResourceGroup.Location.Abbr\" = \"australiasoutheast\" + \"Octopus.Action.Script.ScriptBody\" = \"$resourceGroupName = $OctopusParameters[\\\"CreateResourceGroup.ResourceGroup.Name\\\"]\ +$resourceGroupLocationAbbr = $OctopusParameters[\\\"CreateResourceGroup.ResourceGroup.Location.Abbr\\\"]\ +\ +$existingResourceGroups = (az group list --query \\\"[?location=='$resourceGroupLocationAbbr']\\\") | ConvertFrom-JSON\ +\ +$createResourceGroup = $true\ +foreach ($resourceGroupFound in $existingResourceGroups)\ +{\\t\ +\\tWrite-Host \\\"Checking if current resource group $($resourceGroupFound.name) matches $resourceGroupName\\\"\ + if ($resourceGroupFound.name -eq $resourceGroupName)\ + {\ + \\t$createResourceGroup = $false\ + \\tWrite-Highlight \\\"Resource group already exists, skipping creation\\\"\ + \\tbreak\ + }\ +}\ +\ +if ($createResourceGroup)\ +{\ +\\tWrite-Host \\\"Creating the $resourceGroupName because it was not found in $resourceGroupLocationAbbr\\\"\ +\\taz group create -l $resourceGroupLocationAbbr -n $resourceGroupName\ +}\" + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Azure.AccountId\" = \"#{CreateResourceGroup.Azure.Account}\" + \"CreateResourceGroup.Azure.Account\" = \"Azure.Account\" + \"CreateResourceGroup.ResourceGroup.Name\" = \"octopub-#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}\" + \"OctopusUseBundledTooling\" = \"False\" + } + + container { + feed_id = local.docker_hub_feed_id + image = \"octopuslabs/azure-workertools\" + } + + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + step { + condition = \"Success\" + name = \"Create App Service Plan\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.AzureResourceGroup\" + name = \"Create App Service Plan\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.Azure.ResourceGroupDeploymentMode\" = \"Incremental\" + \"Octopus.Action.Azure.TemplateSource\" = \"Inline\" + \"Octopus.Action.Azure.ResourceGroupTemplateParameters\" = jsonencode({ + \"environment\" = { + \"value\" = \"#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}\" + } + \"projectName\" = { + \"value\" = \"octopub\" + } + \"appServiceSku\" = { + \"value\" = \"B2\" + } + }) + \"Octopus.Action.Azure.ResourceGroupTemplate\" = jsonencode({ + \"parameters\" = { + \"environment\" = { + \"type\" = \"string\" + } + \"projectName\" = { + \"type\" = \"string\" + } + \"appServiceSku\" = { + \"type\" = \"string\" + \"defaultValue\" = \"B2\" + \"metadata\" = { + \"description\" = \"The SKU of App Service Plan\" + } + } + } + \"variables\" = { + \"appServicePlanName\" = \"[concat(parameters('projectName'), '-', parameters('environment'),'-hosting')]\" + } + \"resources\" = [ + { + \"kind\" = \"linux\" + \"location\" = \"[resourceGroup().location]\" + \"properties\" = { + \"reserved\" = \"true\" + } + \"dependsOn\" = [] + \"sku\" = { + \"name\" = \"[parameters('appServiceSku')]\" + } + \"apiVersion\" = \"2017-08-01\" + \"type\" = \"Microsoft.Web/serverfarms\" + \"name\" = \"[variables('appServicePlanName')]\" + \"comments\" = \"This app service plan is used for the web app and slots.\" + }, + ] + \"outputs\" = { + \"appServicePlan\" = { + \"type\" = \"string\" + \"value\" = \"[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]\" + } + } + \"$schema\" = \"https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#\" + \"contentVersion\" = \"1.0.0.0\" + }) + \"Octopus.Action.Azure.AccountId\" = local.cloud_account + \"Octopus.Action.Azure.ResourceGroupName\" = \"octopub-#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}\" + \"Octopus.Action.RunOnServer\" = \"true\" + \"OctopusUseBundledTooling\" = \"False\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + step { + condition = \"Success\" + name = \"Create Frontend Web App Service\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.AzureResourceGroup\" + name = \"Create Frontend Web App Service\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.Azure.ResourceGroupTemplateParameters\" = jsonencode({ + \"environment\" = { + \"value\" = \"#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}\" + } + \"projectName\" = { + \"value\" = \"octopubfrontend\" + } + \"uniquePrefix\" = { + \"value\" = local.unique_prefix + } + \"appServicePlanId\" = { + \"value\" = \"#{Octopus.Action[Create App Service Plan].Output.AzureRmOutputs[appServicePlan]}\" + } + \"dockerImageName\" = { + \"value\" = \"nginx:latest\" + } + \"resourceTags\" = { + \"value\" = { + \"octopus-environment\" = \"#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName}\" + \"octopus-role\" = \"${local.target_role} Frontend\" + \"octopus-space\" = \"#{Octopus.Space.Name}\" + \"octopus-project\" = \"Azure WebApp Octopub Frontend\" + } + } + }) + \"Octopus.Action.Azure.ResourceGroupTemplate\" = jsonencode({ + \"resources\" = [ + { + \"apiVersion\" = \"2016-08-01\" + \"name\" = \"[variables('webAppPortalName')]\" + \"kind\" = \"app,linux,container\" + \"location\" = \"[resourceGroup().location]\" + \"tags\" = \"[parameters('resourceTags')]\" + \"dependsOn\" = [] + \"properties\" = { + \"name\" = \"[variables('webAppPortalName')]\" + \"siteConfig\" = { + \"linuxFxVersion\" = \"[concat('DOCKER|', parameters('dockerImageName'))]\" + \"alwaysOn\" = \"true\" + } + \"serverFarmId\" = \"[parameters('appServicePlanId')]\" + } + \"type\" = \"Microsoft.Web/sites\" + }, + ] + \"$schema\" = \"https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#\" + \"contentVersion\" = \"1.0.0.0\" + \"parameters\" = { + \"environment\" = { + \"type\" = \"string\" + } + \"projectName\" = { + \"type\" = \"string\" + } + \"uniquePrefix\" = { + \"defaultValue\" = local.unique_prefix + \"type\" = \"string\" + } + \"appServicePlanId\" = { + \"type\" = \"string\" + \"defaultValue\" = \"#{Octopus.Action[Create App Service Plan].Output.AzureRmOutputs[appServicePlan]}\" + } + \"dockerImageName\" = { + \"type\" = \"string\" + \"defaultValue\" = \"nginx:latest\" + } + \"resourceTags\" = { + \"type\" = \"object\" + \"defaultValue\" = { + \"octopus-environment\" = \"#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName}\" + \"octopus-role\" = \"${local.target_role} Frontend\" + \"octopus-space\" = \"#{Octopus.Space.Name}\" + \"octopus-project\" = \"\" + } + } + } + \"variables\" = { + \"webAppPortalName\" = \"[concat(parameters('uniquePrefix'), '-', parameters('projectName'), '-', parameters('environment'), '-webapp')]\" + } + }) + \"Octopus.Action.Azure.AccountId\" = local.cloud_account + \"Octopus.Action.Azure.ResourceGroupName\" = \"octopub-#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}\" + \"Octopus.Action.RunOnServer\" = \"true\" + \"OctopusUseBundledTooling\" = \"False\" + \"Octopus.Action.Azure.ResourceGroupDeploymentMode\" = \"Incremental\" + \"Octopus.Action.Azure.TemplateSource\" = \"Inline\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + + step { + condition = \"Success\" + name = \"Create Products Microservice App Service\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.AzureResourceGroup\" + name = \"Create Products Microservice App Service\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.Azure.ResourceGroupTemplateParameters\" = jsonencode({ + \"environment\" = { + \"value\" = \"#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}\" + } + \"projectName\" = { + \"value\" = \"octopubproducts\" + } + \"uniquePrefix\" = { + \"value\" = local.unique_prefix + } + \"appServicePlanId\" = { + \"value\" = \"#{Octopus.Action[Create App Service Plan].Output.AzureRmOutputs[appServicePlan]}\" + } + \"dockerImageName\" = { + \"value\" = \"nginx:latest\" + } + \"resourceTags\" = { + \"value\" = { + \"octopus-environment\" = \"#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName}\" + \"octopus-role\" = \"${local.target_role} Products\" + \"octopus-space\" = \"#{Octopus.Space.Name}\" + \"octopus-project\" = \"Azure WebApp Octopub Products\" + } + } + }) + \"Octopus.Action.Azure.ResourceGroupTemplate\" = jsonencode({ + \"resources\" = [ + { + \"apiVersion\" = \"2016-08-01\" + \"name\" = \"[variables('webAppPortalName')]\" + \"kind\" = \"app,linux,container\" + \"location\" = \"[resourceGroup().location]\" + \"tags\" = \"[parameters('resourceTags')]\" + \"dependsOn\" = [] + \"properties\" = { + \"name\" = \"[variables('webAppPortalName')]\" + \"siteConfig\" = { + \"linuxFxVersion\" = \"[concat('DOCKER|', parameters('dockerImageName'))]\" + \"alwaysOn\" = \"true\" + } + \"serverFarmId\" = \"[parameters('appServicePlanId')]\" + } + \"type\" = \"Microsoft.Web/sites\" + }, + ] + \"$schema\" = \"https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#\" + \"contentVersion\" = \"1.0.0.0\" + \"parameters\" = { + \"environment\" = { + \"type\" = \"string\" + } + \"projectName\" = { + \"type\" = \"string\" + } + \"uniquePrefix\" = { + \"defaultValue\" = local.unique_prefix + \"type\" = \"string\" + } + \"appServicePlanId\" = { + \"type\" = \"string\" + \"defaultValue\" = \"#{Octopus.Action[Create App Service Plan].Output.AzureRmOutputs[appServicePlan]}\" + } + \"dockerImageName\" = { + \"type\" = \"string\" + \"defaultValue\" = \"nginx:latest\" + } + \"resourceTags\" = { + \"type\" = \"object\" + \"defaultValue\" = { + \"octopus-environment\" = \"#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName}\" + \"octopus-role\" = \"${local.target_role} Products\" + \"octopus-space\" = \"#{Octopus.Space.Name}\" + \"octopus-project\" = \"\" + } + } + } + \"variables\" = { + \"webAppPortalName\" = \"[concat(parameters('uniquePrefix'), '-', parameters('projectName'), '-', parameters('environment'), '-webapp')]\" + } + }) + \"Octopus.Action.Azure.AccountId\" = local.cloud_account + \"Octopus.Action.Azure.ResourceGroupName\" = \"octopub-#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}\" + \"Octopus.Action.RunOnServer\" = \"true\" + \"OctopusUseBundledTooling\" = \"False\" + \"Octopus.Action.Azure.ResourceGroupDeploymentMode\" = \"Incremental\" + \"Octopus.Action.Azure.TemplateSource\" = \"Inline\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + + step { + condition = \"Success\" + name = \"Create Audits Microservice App Service\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.AzureResourceGroup\" + name = \"Create Audits Microservice App Service\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.Azure.ResourceGroupTemplateParameters\" = jsonencode({ + \"environment\" = { + \"value\" = \"#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}\" + } + \"projectName\" = { + \"value\" = \"octopubaudits\" + } + \"uniquePrefix\" = { + \"value\" = local.unique_prefix + } + \"appServicePlanId\" = { + \"value\" = \"#{Octopus.Action[Create App Service Plan].Output.AzureRmOutputs[appServicePlan]}\" + } + \"dockerImageName\" = { + \"value\" = \"nginx:latest\" + } + \"resourceTags\" = { + \"value\" = { + \"octopus-environment\" = \"#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName}\" + \"octopus-role\" = \"${local.target_role} Audits\" + \"octopus-space\" = \"#{Octopus.Space.Name}\" + \"octopus-project\" = \"Azure WebApp Octopub Audits\" + } + } + }) + \"Octopus.Action.Azure.ResourceGroupTemplate\" = jsonencode({ + \"resources\" = [ + { + \"apiVersion\" = \"2016-08-01\" + \"name\" = \"[variables('webAppPortalName')]\" + \"kind\" = \"app,linux,container\" + \"location\" = \"[resourceGroup().location]\" + \"tags\" = \"[parameters('resourceTags')]\" + \"dependsOn\" = [] + \"properties\" = { + \"name\" = \"[variables('webAppPortalName')]\" + \"siteConfig\" = { + \"linuxFxVersion\" = \"[concat('DOCKER|', parameters('dockerImageName'))]\" + \"alwaysOn\" = \"true\" + } + \"serverFarmId\" = \"[parameters('appServicePlanId')]\" + } + \"type\" = \"Microsoft.Web/sites\" + }, + ] + \"$schema\" = \"https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#\" + \"contentVersion\" = \"1.0.0.0\" + \"parameters\" = { + \"environment\" = { + \"type\" = \"string\" + } + \"projectName\" = { + \"type\" = \"string\" + } + \"uniquePrefix\" = { + \"defaultValue\" = local.unique_prefix + \"type\" = \"string\" + } + \"appServicePlanId\" = { + \"type\" = \"string\" + \"defaultValue\" = \"#{Octopus.Action[Create App Service Plan].Output.AzureRmOutputs[appServicePlan]}\" + } + \"dockerImageName\" = { + \"type\" = \"string\" + \"defaultValue\" = \"nginx:latest\" + } + \"resourceTags\" = { + \"type\" = \"object\" + \"defaultValue\" = { + \"octopus-environment\" = \"#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName}\" + \"octopus-role\" = \"${local.target_role} Audits\" + \"octopus-space\" = \"#{Octopus.Space.Name}\" + \"octopus-project\" = \"\" + } + } + } + \"variables\" = { + \"webAppPortalName\" = \"[concat(parameters('uniquePrefix'), '-', parameters('projectName'), '-', parameters('environment'), '-webapp')]\" + } + }) + \"Octopus.Action.Azure.AccountId\" = local.cloud_account + \"Octopus.Action.Azure.ResourceGroupName\" = \"octopub-#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}\" + \"Octopus.Action.RunOnServer\" = \"true\" + \"OctopusUseBundledTooling\" = \"False\" + \"Octopus.Action.Azure.ResourceGroupDeploymentMode\" = \"Incremental\" + \"Octopus.Action.Azure.TemplateSource\" = \"Inline\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = local.feedback_script + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} +#endregion + +#region Octopub Frontend Project +variable \"frontend_project_name\" { + type = string + default = \"\" +} + +data \"octopusdeploy_projects\" \"frontend\" { + partial_name = var.frontend_project_name == \"\" ? local.octopub_frontend_project_name : var.frontend_project_name + skip = 0 + take = 1 +} + +resource \"octopusdeploy_variable\" \"frontend_health_check\" { + count = length(data.octopusdeploy_projects.frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.frontend[0].id + value = local.frontend_health_check + name = \"App.HealthCheck\" + type = \"String\" + description = \"The path to perform a health check on.\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_variable\" \"azure_webapp_octopub_frontend_octopus_azure_account\" { + count = length(data.octopusdeploy_projects.frontend.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.frontend[0].id + value = local.cloud_account + name = \"Octopus.Azure.Account\" + type = \"AzureAccount\" + description = \"\" + is_sensitive = false +} + +resource \"octopusdeploy_project\" \"frontend\" { + count = length(data.octopusdeploy_projects.frontend.projects) == 0 ? 1 : 0 + name = var.frontend_project_name == \"\" ? local.octopub_frontend_project_name : var.frontend_project_name + auto_create_release = false + default_guided_failure_mode = \"EnvironmentDefault\" + default_to_skip_if_already_installed = false + discrete_channel_release = false + is_disabled = false + is_version_controlled = true + lifecycle_id = local.devops_lifecycle_id + project_group_id = local.project_group_id + included_library_variable_sets = [] + tenanted_deployment_participation = \"Untenanted\" + + connectivity_policy { + allow_deployments_to_no_targets = true + exclude_unhealthy_targets = false + skip_machine_behavior = \"None\" + } + + versioning_strategy { + template = \"#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.NextPatch}\" + } + + lifecycle { + ignore_changes = [connectivity_policy] + } + description = local.octopub_frontend_project_description +} + +resource \"octopusdeploy_deployment_process\" \"frontend\" { + count = length(data.octopusdeploy_projects.frontend.projects) == 0 ? 1 : 0 + project_id = octopusdeploy_project.frontend[0].id + + step { + condition = \"Success\" + name = \"Generate Variables\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Generate Variables\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = \"${data.octopusdeploy_worker_pools.workerpool_hosted_ubuntu.worker_pools[0].id}\" + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Set-OctopusVariable -name \\\"OctopusEnvironmentName\\\" -value $OctopusParameters[\\\"Octopus.Environment.Name\\\"]\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + + // The deployment step goes here. + // Call the step \"Deploy Container\" + + step { + condition = \"Success\" + name = \"Deploy Container\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.AzureAppService\" + name = \"Deploy Container\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"OctopusUseBundledTooling\" = \"False\" + \"Octopus.Action.Azure.DeploymentType\" = \"Container\" + \"Octopus.Action.Package.DownloadOnTentacle\" = \"False\" + \"Octopus.Action.Azure.AppSettings\" = jsonencode([ + { + \"name\" = \"UDL_SETVALUE_1\" + \"value\" = \"[/usr/share/nginx/html/config.json][productEndpoint]https://${local.unique_prefix}-octopubproducts-#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}-webapp.azurewebsites.net/api/products\" + \"slotSetting\" = \"false\" + }, + { + \"name\" = \"UDL_SETVALUE_2\" + \"value\" = \"[/usr/share/nginx/html/config.json][productHealthEndpoint]https://${local.unique_prefix}-octopubproducts-#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}-webapp.azurewebsites.net/health/products\" + \"slotSetting\" = \"false\" + }, + { + \"name\" = \"UDL_SETVALUE_3\" + \"value\" = \"[/usr/share/nginx/html/config.json][auditEndpoint]https://${local.unique_prefix}-octopubaudits-#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}-webapp.azurewebsites.net/api/audits\" + \"slotSetting\" = \"false\" + }, + { + \"name\" = \"UDL_SETVALUE_4\" + \"value\" = \"[/usr/share/nginx/html/config.json][auditHealthEndpoint]https://${local.unique_prefix}-octopubaudits-#{Octopus.Action[Generate Variables].Output.OctopusEnvironmentName | ToLower}-webapp.azurewebsites.net/health/audits\" + \"slotSetting\" = \"false\" + } + ]) + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + + primary_package { + package_id = \"octopussamples/octopub-frontend\" + acquisition_location = \"NotAcquired\" + feed_id = local.docker_hub_feed_id + properties = { SelectionMode = \"immediate\" } + } + + features = [] + } + + properties = {} + target_roles = [\"${local.target_role} Frontend\"] + } + + step { + condition = \"Success\" + name = \"Service Link\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = local.link_highlight_action_type + properties = local.link_highlight_properties + container { + feed_id = local.docker_hub_feed_id + image = local.link_highlight_container_image + } + name = \"Service Link\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"${local.target_role} Frontend\"] + } + + step { + condition = \"Success\" + name = \"Smoke Test\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = local.smoke_test_action_type + name = \"Smoke Test\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = local.smoke_test_properties + container { + feed_id = local.docker_hub_feed_id + image = local.smoke_test_container_image + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"${local.target_role} Frontend\"] + } + + step { + condition = \"Success\" + name = \"Security Scan\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Security Scan\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"Bash\" + \"Octopus.Action.Script.ScriptBody\" = local.security_scan_docker_script + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = local.feedback_script + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} + +#endregion + +#region Octopub Products Project +variable \"products_project_name\" { + type = string + default = \"\" +} + + +data \"octopusdeploy_projects\" \"products\" { + partial_name = var.products_project_name == \"\" ? local.octopub_products_project_name : var.products_project_name + skip = 0 + take = 1 +} + +resource \"octopusdeploy_variable\" \"azure_webapp_octopub_products_octopus_azure_account\" { + count = length(data.octopusdeploy_projects.products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.products[0].id + value = local.cloud_account + name = \"Octopus.Azure.Account\" + type = \"AzureAccount\" + description = \"\" + is_sensitive = false +} + +resource \"octopusdeploy_variable\" \"products_health_check\" { + count = length(data.octopusdeploy_projects.products.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.products[0].id + value = local.products_health_check + name = \"App.HealthCheck\" + type = \"String\" + description = \"The path to perform a health check on.\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_project\" \"products\" { + count = length(data.octopusdeploy_projects.products.projects) == 0 ? 1 : 0 + name = var.products_project_name == \"\" ? local.octopub_products_project_name : var.products_project_name + auto_create_release = false + default_guided_failure_mode = \"EnvironmentDefault\" + default_to_skip_if_already_installed = false + discrete_channel_release = false + is_disabled = false + is_version_controlled = true + lifecycle_id = local.devops_lifecycle_id + project_group_id = local.project_group_id + included_library_variable_sets = [] + tenanted_deployment_participation = \"Untenanted\" + + connectivity_policy { + allow_deployments_to_no_targets = true + exclude_unhealthy_targets = false + skip_machine_behavior = \"None\" + } + + versioning_strategy { + template = \"#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.NextPatch}\" + } + + lifecycle { + ignore_changes = [connectivity_policy] + } + description = local.octopub_products_project_description +} + +resource \"octopusdeploy_deployment_process\" \"products\" { + count = length(data.octopusdeploy_projects.products.projects) == 0 ? 1 : 0 + project_id = octopusdeploy_project.products[0].id + + step { + condition = \"Success\" + name = \"Generate Variables\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Generate Variables\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = \"${data.octopusdeploy_worker_pools.workerpool_hosted_ubuntu.worker_pools[0].id}\" + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Set-OctopusVariable -name \\\"OctopusEnvironmentName\\\" -value $OctopusParameters[\\\"Octopus.Environment.Name\\\"]\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + + // The deployment step goes here. + // Call the step \"Deploy Container\" + + step { + condition = \"Success\" + name = \"Deploy Container\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.AzureAppService\" + name = \"Deploy Container\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"OctopusUseBundledTooling\" = \"False\" + \"Octopus.Action.Azure.DeploymentType\" = \"Container\" + \"Octopus.Action.Package.DownloadOnTentacle\" = \"False\" + \"Octopus.Action.Azure.AppSettings\" = jsonencode([ + { + \"name\" = \"WEBSITES_PORT\" + \"value\" = \"8083\" + \"slotSetting\" = \"false\" + } + ]) + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + + primary_package { + package_id = \"octopussamples/octopub-products-microservice\" + acquisition_location = \"NotAcquired\" + feed_id = local.docker_hub_feed_id + properties = { SelectionMode = \"immediate\" } + } + + features = [] + } + + properties = {} + target_roles = [\"${local.target_role} Products\"] + } + + step { + condition = \"Success\" + name = \"Service Link\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = local.link_highlight_action_type + properties = local.link_highlight_properties + container { + feed_id = local.docker_hub_feed_id + image = local.link_highlight_container_image + } + name = \"Service Link\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"${local.target_role} Products\"] + } + + step { + condition = \"Success\" + name = \"Smoke Test\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = local.smoke_test_action_type + name = \"Smoke Test\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = local.smoke_test_properties + container { + feed_id = local.docker_hub_feed_id + image = local.smoke_test_container_image + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"${local.target_role} Products\"] + } + + step { + condition = \"Success\" + name = \"Security Scan\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Security Scan\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"Bash\" + \"Octopus.Action.Script.ScriptBody\" = local.security_scan_docker_script + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = local.feedback_script + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} +#endregion + +#region Octopub Audits Project +variable \"audits_project_name\" { + type = string + default = \"\" +} + +data \"octopusdeploy_projects\" \"audits\" { + partial_name = var.audits_project_name == \"\" ? local.octopub_audits_project_name : var.audits_project_name + skip = 0 + take = 1 +} + +resource \"octopusdeploy_variable\" \"azure_webapp_octopub_audits_octopus_azure_account\" { + count = length(data.octopusdeploy_projects.audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.audits[0].id + value = local.cloud_account + name = \"Octopus.Azure.Account\" + type = \"AzureAccount\" + description = \"\" + is_sensitive = false +} + +resource \"octopusdeploy_variable\" \"audits_healthcheck\" { + count = length(data.octopusdeploy_projects.audits.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.audits[0].id + value = local.audits_health_check + name = \"App.HealthCheck\" + type = \"String\" + description = \"The path to perform a health check on.\" + is_sensitive = false + depends_on = [] +} + +resource \"octopusdeploy_project\" \"audits\" { + count = length(data.octopusdeploy_projects.audits.projects) == 0 ? 1 : 0 + name = var.audits_project_name == \"\" ? local.octopub_audits_project_name : var.audits_project_name + auto_create_release = false + default_guided_failure_mode = \"EnvironmentDefault\" + default_to_skip_if_already_installed = false + discrete_channel_release = false + is_disabled = false + is_version_controlled = true + lifecycle_id = local.devops_lifecycle_id + project_group_id = local.project_group_id + included_library_variable_sets = [] + tenanted_deployment_participation = \"Untenanted\" + + connectivity_policy { + allow_deployments_to_no_targets = true + exclude_unhealthy_targets = false + skip_machine_behavior = \"None\" + } + + versioning_strategy { + template = \"#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.NextPatch}\" + } + + lifecycle { + ignore_changes = [connectivity_policy] + } + description = local.octopub_audits_project_description +} + +resource \"octopusdeploy_deployment_process\" \"audits\" { + count = length(data.octopusdeploy_projects.audits.projects) == 0 ? 1 : 0 + project_id = octopusdeploy_project.audits[0].id + + + step { + condition = \"Success\" + name = \"Generate Variables\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Generate Variables\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = \"${data.octopusdeploy_worker_pools.workerpool_hosted_ubuntu.worker_pools[0].id}\" + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = \"Set-OctopusVariable -name \\\"OctopusEnvironmentName\\\" -value $OctopusParameters[\\\"Octopus.Environment.Name\\\"]\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + + // The deployment step goes here. + // Call the step \"Deploy Container\" + + step { + condition = \"Success\" + name = \"Deploy Container\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.AzureAppService\" + name = \"Deploy Container\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"OctopusUseBundledTooling\" = \"False\" + \"Octopus.Action.Azure.DeploymentType\" = \"Container\" + \"Octopus.Action.Package.DownloadOnTentacle\" = \"False\" + \"Octopus.Action.Azure.AppSettings\" = jsonencode([ + { + \"name\" = \"WEBSITES_PORT\" + \"value\" = \"10000\" + \"slotSetting\" = \"false\" + }, + { + \"name\" = \"MIGRATE_AT_START\" + \"value\" = \"true\" + \"slotSetting\" = \"false\" + }, + ]) + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + + primary_package { + package_id = \"octopussamples/octopub-audit-microservice\" + acquisition_location = \"NotAcquired\" + feed_id = local.docker_hub_feed_id + properties = { SelectionMode = \"immediate\" } + } + + features = [] + } + + properties = {} + target_roles = [\"${local.target_role} Audits\"] + } + + step { + condition = \"Success\" + name = \"Service Link\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = local.link_highlight_action_type + properties = local.link_highlight_properties + container { + feed_id = local.docker_hub_feed_id + image = local.link_highlight_container_image + } + name = \"Service Link\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"${local.target_role} Audits\"] + } + + step { + condition = \"Success\" + name = \"Smoke Test\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = local.smoke_test_action_type + properties = local.smoke_test_properties + name = \"Smoke Test\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + container { + feed_id = local.docker_hub_feed_id + image = local.smoke_test_container_image + } + environments = [ + local.development_environment_id, + local.test_environment_id, + local.production_environment_id, + ] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [\"${local.target_role} Audits\"] + } + + step { + condition = \"Success\" + name = \"Security Scan\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Security Scan\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"Bash\" + \"Octopus.Action.Script.ScriptBody\" = local.security_scan_docker_script + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.RunOnServer\" = \"true\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.Script.ScriptBody\" = local.feedback_script + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} +#endregion + +#region Octopub Orchestration Project +data \"octopusdeploy_projects\" \"orchestration\" { + partial_name = local.octopub_orchestration_project_name + skip = 0 + take = 1 +} + +resource \"octopusdeploy_project\" \"orchestration\" { + count = length(data.octopusdeploy_projects.orchestration.projects) == 0 ? 1 : 0 + name = local.octopub_orchestration_project_name + auto_create_release = false + default_guided_failure_mode = \"EnvironmentDefault\" + default_to_skip_if_already_installed = false + discrete_channel_release = false + is_disabled = false + is_version_controlled = false + lifecycle_id = local.devops_lifecycle_id + project_group_id = local.project_group_id + included_library_variable_sets = [] + tenanted_deployment_participation = \"Untenanted\" + + connectivity_policy { + allow_deployments_to_no_targets = true + exclude_unhealthy_targets = false + skip_machine_behavior = \"None\" + } + + versioning_strategy { + template = \"#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.NextPatch}\" + } + + lifecycle { + ignore_changes = [] + } + description = local.octopub_products_project_description +} + +resource \"octopusdeploy_deployment_process\" \"orchestration\" { + project_id = octopusdeploy_project.orchestration[0].id + count = length(data.octopusdeploy_projects.orchestration.projects) == 0 ? 1 : 0 + + step { + condition = \"Success\" + name = \"Deploy Frontend\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.DeployRelease\" + name = \"Deploy Frontend\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = \"\" + properties = { + \"Octopus.Action.Package.DownloadOnTentacle\" = \"NotAcquired\" + \"Octopus.Action.RunOnServer\" = \"True\" + \"Octopus.Action.DeployRelease.DeploymentCondition\" = \"Always\" + \"Octopus.Action.DeployRelease.ProjectId\" = length(data.octopusdeploy_projects.frontend.projects) == 0 ? octopusdeploy_project.frontend[0].id : data.octopusdeploy_projects.frontend.projects[0].id + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + + primary_package { + package_id = length(data.octopusdeploy_projects.frontend.projects) == 0 ? octopusdeploy_project.frontend[0].id : data.octopusdeploy_projects.frontend.projects[0].id + acquisition_location = \"NotAcquired\" + feed_id = data.octopusdeploy_feeds.project.feeds[0].id + properties = {} + } + + features = [] + } + + properties = {} + target_roles = [] + } + step { + condition = \"Success\" + name = \"Deploy Products\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.DeployRelease\" + name = \"Deploy Products\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = \"\" + properties = { + \"Octopus.Action.DeployRelease.ProjectId\" = length(data.octopusdeploy_projects.products.projects) == 0 ? octopusdeploy_project.products[0].id : data.octopusdeploy_projects.products.projects[0].id + \"Octopus.Action.Package.DownloadOnTentacle\" = \"NotAcquired\" + \"Octopus.Action.RunOnServer\" = \"True\" + \"Octopus.Action.DeployRelease.DeploymentCondition\" = \"Always\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + + primary_package { + package_id = length(data.octopusdeploy_projects.products.projects) == 0 ? octopusdeploy_project.products[0].id : data.octopusdeploy_projects.products.projects[0].id + acquisition_location = \"NotAcquired\" + feed_id = data.octopusdeploy_feeds.project.feeds[0].id + properties = {} + } + + features = [] + } + + properties = {} + target_roles = [] + } + step { + condition = \"Success\" + name = \"Deploy Audits\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.DeployRelease\" + name = \"Deploy Audits\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = \"\" + properties = { + \"Octopus.Action.RunOnServer\" = \"True\" + \"Octopus.Action.DeployRelease.DeploymentCondition\" = \"Always\" + \"Octopus.Action.DeployRelease.ProjectId\" = length(data.octopusdeploy_projects.audits.projects) == 0 ? octopusdeploy_project.audits[0].id : data.octopusdeploy_projects.audits.projects[0].id + \"Octopus.Action.Package.DownloadOnTentacle\" = \"NotAcquired\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + + primary_package { + package_id = length(data.octopusdeploy_projects.audits.projects) == 0 ? octopusdeploy_project.audits[0].id : data.octopusdeploy_projects.audits.projects[0].id + acquisition_location = \"NotAcquired\" + feed_id = data.octopusdeploy_feeds.project.feeds[0].id + properties = {} + } + + features = [] + } + + properties = {} + target_roles = [] + } + step { + condition = \"Always\" + name = \"Feedback\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Feedback\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"Octopus.Action.Script.ScriptBody\" = local.feedback_script + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.Syntax\" = \"PowerShell\" + \"Octopus.Action.RunOnServer\" = \"true\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } +} +#endregion + +#region Project Templates +variable \"project_template_project_name\" { + type = string + default = \"\" +} + +data \"octopusdeploy_projects\" \"docker_project_template\" { + partial_name = var.project_template_project_name == \"\" ? local.project_template_project_name : var.project_template_project_name + skip = 0 + take = 1 +} + +resource \"octopusdeploy_variable\" \"docker_project_template_git_organization\" { + count = length(data.octopusdeploy_projects.docker_project_template.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.docker_project_template[0].id + value = \"\" + name = \"Git.Url.Organization\" + type = \"String\" + description = \"The GitHub organization to create the repo in.\" + is_sensitive = false + + prompt { + description = \"The Github organization where the repo will be created. This is the `owner` part of the URL `https://github.com/owner/myrepo`.\" + label = \"Github Organization\" + is_required = true + display_settings { + control_type = \"SingleLineText\" + } + } + + scope { + actions = [] + channels = [] + environments = [local.sync_environment_id] + machines = [] + roles = null + tenant_tags = null + } +} + +resource \"octopusdeploy_variable\" \"docker_project_template_git_repo\" { + count = length(data.octopusdeploy_projects.docker_project_template.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.docker_project_template[0].id + value = \"\" + name = \"Git.Url.Repo\" + type = \"String\" + description = \"The GitHub repo to create.\" + is_sensitive = false + + prompt { + description = \"The Github repo to be created. This is the `myrepo` part of the URL `https://github.com/owner/myrepo`.\" + label = \"Github Repo\" + is_required = true + display_settings { + control_type = \"SingleLineText\" + } + } + + scope { + actions = [] + channels = [] + environments = [local.sync_environment_id] + machines = [] + roles = null + tenant_tags = null + } +} + +resource \"octopusdeploy_variable\" \"docker_project_template_image_name\" { + count = length(data.octopusdeploy_projects.docker_project_template.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.docker_project_template[0].id + value = \"\" + name = \"Application.Docker.Image\" + type = \"String\" + description = \"The Docker image to create containing the new application.\" + is_sensitive = false + + prompt { + description = \"The Docker image to create containing the new application.\" + label = \"Docker Image\" + is_required = true + display_settings { + control_type = \"SingleLineText\" + } + } + + scope { + actions = [] + channels = [] + environments = [local.sync_environment_id] + machines = [] + roles = null + tenant_tags = null + } +} + +resource \"octopusdeploy_variable\" \"docker_project_template_octopus_project\" { + count = length(data.octopusdeploy_projects.docker_project_template.projects) == 0 ? 1 : 0 + owner_id = octopusdeploy_project.docker_project_template[0].id + value = \"\" + name = \"Application.Octopus.Project\" + type = \"String\" + description = \"The Octopus project to associate with the new application.\" + is_sensitive = false + + prompt { + description = \"The Octopus project to associate with the new application. A release is created in this project when the image is successfully built.\" + label = \"Octopus Project\" + is_required = true + display_settings { + control_type = \"SingleLineText\" + } + } + + scope { + actions = [] + channels = [] + environments = [local.sync_environment_id] + machines = [] + roles = null + tenant_tags = null + } +} + +resource \"octopusdeploy_project\" \"docker_project_template\" { + count = length(data.octopusdeploy_projects.docker_project_template.projects) == 0 ? 1 : 0 + name = var.project_template_project_name == \"\" ? local.project_template_project_name : var.project_template_project_name + auto_create_release = false + default_guided_failure_mode = \"Off\" + default_to_skip_if_already_installed = false + discrete_channel_release = false + is_disabled = false + is_version_controlled = false + lifecycle_id = local.application_lifecycle_id + project_group_id = local.project_templates_project_group_id + included_library_variable_sets = [ + local.this_instance_library_variable_set, local.github_library_variable_set, local.docker_library_variable_set + ] + tenanted_deployment_participation = \"Untenanted\" + + connectivity_policy { + allow_deployments_to_no_targets = true + exclude_unhealthy_targets = false + skip_machine_behavior = \"None\" + } + + versioning_strategy { + template = \"#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.NextPatch}\" + } + + lifecycle { + ignore_changes = [] + } + description = < str:\ + \\\"\\\"\\\"Encrypt a Unicode string using the public key.\\\"\\\"\\\"\ + sealed_box = public.SealedBox(public.PublicKey(public_key_for_repo.encode(\\\"utf-8\\\"), encoding.Base64Encoder()))\ + encrypted = sealed_box.encrypt(secret_value_input.encode(\\\"utf-8\\\"))\ + return b64encode(encrypted).decode(\\\"utf-8\\\")\ +\ +\ +def get_public_key(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str) -> (str, str):\ + public_key_endpoint: str = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/public-key\\\"\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\"}\ + response = requests.get(url=public_key_endpoint, headers=headers)\ + if response.status_code != 200:\ + raise IOError(\ + f\\\"Could not get public key for repository {gh_owner}/{gh_repo}. The Response code was {response.status_code}\\\")\ +\ + public_key_json = response.json()\ + return public_key_json['key_id'], public_key_json['key']\ +\ +\ +def set_secret(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str, public_key_id: str, secret_key: str,\ + encrypted_secret_value: str):\ + secret_creation_url = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/{secret_key}\\\"\ + secret_creation_body = {\\\"key_id\\\": public_key_id, \\\"encrypted_value\\\": encrypted_secret_value}\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\", \\\"Content-Type\\\": \\\"application/json\\\"}\ +\ + secret_creation_response = requests.put(url=secret_creation_url, json=secret_creation_body, headers=headers)\ + if secret_creation_response.status_code == 201 or secret_creation_response.status_code == 204:\ + print(\\\"--Secret Created / Updated!--\\\")\ + else:\ + print(f\\\"-- Error creating / updating github secret, the reason was : {secret_creation_response.reason}\\\")\ +\ +\ +parser, _ = init_argparse()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +token = generate_github_token(parser.github_app_id, parser.github_app_private_key,\ + parser.github_app_installation_id) if len(\ + parser.git_password.strip()) == 0 else parser.git_password.strip()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +if not parser.secret_name.strip():\ + print(\\\"You must define the secret name\\\")\ + sys.exit(1)\ + \ +if not verify_new_repo(token, parser.git_organization, parser.repo):\ + print(\\\"Could not find the repo\\\")\ + sys.exit(1)\ +\ +key_id, public_key = get_public_key('https://api.github.com/repos', parser.git_organization, parser.repo,\ + token)\ +encrypted_secret: str = encrypt(public_key_for_repo=public_key, secret_value_input=parser.secret_value)\ +set_secret(gh_base_url='https://api.github.com/repos', gh_owner=parser.git_organization, gh_repo=parser.repo,\ + gh_auth_token=token, public_key_id=key_id, secret_key=parser.secret_name,\ + encrypted_secret_value=encrypted_secret)\ +\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + step { + condition = \"Success\" + name = \"Create Docker Hub Password Secret\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Create Docker Hub Password Secret\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"CreateGitHubSecret.Git.Credentials.Password\" = \"#{Git.Credentials.Password}\" + \"CreateGitHubSecret.GitHub.Secret.Name\" = \"DOCKERHUB_TOKEN\" + \"Octopus.Action.Script.Syntax\" = \"Python\" + \"CreateGitHubSecret.GitHub.Secret.Value\" = \"#{Docker.Credentials.Password}\" + \"CreateGitHubSecret.Git.Url.Organization\" = \"#{Git.Url.Organization}\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.RunOnServer\" = \"true\" + \"CreateGitHubSecret.Git.Url.Repo\" = \"#{Octopus.Action[Create Repo].Output.NewRepo}\" + \"Octopus.Action.Script.ScriptBody\" = \"# https://gist.github.com/comdotlinux/9a53bb00767a16d6646464c4b8249094\ +\ +# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid\ +# having to use a regular user account.\ +import subprocess\ +import sys\ +\ +# Install our own dependencies\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt', '--disable-pip-version-check'])\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pynacl', '--disable-pip-version-check'])\ +\ +import requests\ +import json\ +import subprocess\ +import sys\ +import os\ +import urllib.request\ +import base64\ +import re\ +import jwt\ +import time\ +import argparse\ +import urllib3\ +from base64 import b64encode\ +from typing import TypedDict\ +from nacl import public, encoding\ +\ +# Disable insecure http request warnings\ +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\ +\ +# If this script is not being run as part of an Octopus step, setting variables is a noop\ +if 'set_octopusvariable' not in globals():\ + def set_octopusvariable(variable, value):\ + pass\ +\ +# If this script is not being run as part of an Octopus step, return variables from environment variables.\ +# Periods are replaced with underscores, and the variable name is converted to uppercase\ +if \\\"get_octopusvariable\\\" not in globals():\ + def get_octopusvariable(variable):\ + return os.environ[re.sub('\\\\\\\\.', '_', variable.upper())]\ +\ +# If this script is not being run as part of an Octopus step, print directly to std out.\ +if 'printverbose' not in globals():\ + def printverbose(msg):\ + print(msg)\ +\ +\ +def printverbose_noansi(output):\ + \\\"\\\"\\\"\ + Strip ANSI color codes and print the output as verbose\ + :param output: The output to print\ + \\\"\\\"\\\"\ + output_no_ansi = re.sub(r'\\\\x1b\\\\[[0-9;]*m', '', output)\ + printverbose(output_no_ansi)\ +\ +\ +def get_octopusvariable_quiet(variable):\ + \\\"\\\"\\\"\ + Gets an octopus variable, or an empty string if it does not exist.\ + :param variable: The variable name\ + :return: The variable value, or an empty string if the variable does not exist\ + \\\"\\\"\\\"\ + try:\ + return get_octopusvariable(variable)\ + except:\ + return ''\ +\ +\ +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi, raise_on_non_zero=False,\ + append_to_path=None):\ + \\\"\\\"\\\"\ + The execute method provides the ability to execute external processes while capturing and returning the\ + output to std err and std out and exit code.\ + \\\"\\\"\\\"\ +\ + my_env = os.environ.copy() if env is None else env\ +\ + if append_to_path is not None:\ + my_env[\\\"PATH\\\"] = append_to_path + os.pathsep + my_env['PATH']\ +\ + process = subprocess.Popen(args,\ + stdout=subprocess.PIPE,\ + stderr=subprocess.PIPE,\ + stdin=open(os.devnull),\ + text=True,\ + cwd=cwd,\ + env=my_env)\ + stdout, stderr = process.communicate()\ + retcode = process.returncode\ +\ + if not retcode == 0 and raise_on_non_zero:\ + raise Exception('command returned exit code ' + retcode)\ +\ + if print_args is not None:\ + print_output(' '.join(args))\ +\ + if print_output is not None:\ + print_output(stdout)\ + print_output(stderr)\ +\ + return stdout, stderr, retcode\ +\ +\ +def init_argparse():\ + parser = argparse.ArgumentParser(\ + usage='%(prog)s [OPTION]',\ + description='Fork a GitHub repo'\ + )\ +\ + parser.add_argument('--secret-name', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.Secret.Name') or get_octopusvariable_quiet(\ + 'GitHub.Secret.Name'))\ + parser.add_argument('--secret-value', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.Secret.Value') or get_octopusvariable_quiet(\ + 'GitHub.Secret.Value'))\ +\ + parser.add_argument('--repo', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Url.Repo') or get_octopusvariable_quiet(\ + 'Git.Url.Repo') or get_octopusvariable_quiet('Octopus.Project.Name'))\ + parser.add_argument('--git-organization', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Url.Organization') or get_octopusvariable_quiet(\ + 'Git.Url.Organization'))\ + parser.add_argument('--github-app-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id'))\ + parser.add_argument('--github-app-installation-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.InstallationId') or get_octopusvariable_quiet(\ + 'GitHub.App.InstallationId'))\ + parser.add_argument('--github-app-private-key', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.PrivateKey') or get_octopusvariable_quiet(\ + 'GitHub.App.PrivateKey'))\ + parser.add_argument('--git-password', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Credentials.Password') or get_octopusvariable_quiet(\ + 'Git.Credentials.Password'),\ + help='The git password. This takes precedence over the --github-app-id, --github-app-installation-id, and --github-app-private-key')\ +\ + return parser.parse_known_args()\ +\ +\ +def generate_github_token(github_app_id, github_app_private_key, github_app_installation_id):\ + # Generate the tokens used by git and the GitHub API\ + app_id = github_app_id\ + signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8'))\ +\ + payload = {\ + # Issued at time\ + 'iat': int(time.time()),\ + # JWT expiration time (10 minutes maximum)\ + 'exp': int(time.time()) + 600,\ + # GitHub App's identifier\ + 'iss': app_id\ + }\ +\ + # Create JWT\ + jwt_instance = jwt.JWT()\ + encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256')\ +\ + # Create access token\ + url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens'\ + headers = {\ + 'Authorization': 'Bearer ' + encoded_jwt,\ + 'Accept': 'application/vnd.github+json',\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers, method='POST')\ + response = urllib.request.urlopen(request)\ + response_json = json.loads(response.read().decode())\ + return response_json['token']\ +\ +\ +def generate_auth_header(token):\ + auth = base64.b64encode(('x-access-token:' + token).encode('ascii'))\ + return 'Basic ' + auth.decode('ascii')\ +\ +\ +def verify_new_repo(token, cac_org, new_repo):\ + # Attempt to view the new repo\ + try:\ + url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo\ + headers = {\ + 'Accept': 'application/vnd.github+json',\ + 'Authorization': 'Bearer ' + token,\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers)\ + urllib.request.urlopen(request)\ + return True\ + except:\ + return False\ +\ +\ +def encrypt(public_key_for_repo: str, secret_value_input: str) -> str:\ + \\\"\\\"\\\"Encrypt a Unicode string using the public key.\\\"\\\"\\\"\ + sealed_box = public.SealedBox(public.PublicKey(public_key_for_repo.encode(\\\"utf-8\\\"), encoding.Base64Encoder()))\ + encrypted = sealed_box.encrypt(secret_value_input.encode(\\\"utf-8\\\"))\ + return b64encode(encrypted).decode(\\\"utf-8\\\")\ +\ +\ +def get_public_key(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str) -> (str, str):\ + public_key_endpoint: str = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/public-key\\\"\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\"}\ + response = requests.get(url=public_key_endpoint, headers=headers)\ + if response.status_code != 200:\ + raise IOError(\ + f\\\"Could not get public key for repository {gh_owner}/{gh_repo}. The Response code was {response.status_code}\\\")\ +\ + public_key_json = response.json()\ + return public_key_json['key_id'], public_key_json['key']\ +\ +\ +def set_secret(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str, public_key_id: str, secret_key: str,\ + encrypted_secret_value: str):\ + secret_creation_url = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/{secret_key}\\\"\ + secret_creation_body = {\\\"key_id\\\": public_key_id, \\\"encrypted_value\\\": encrypted_secret_value}\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\", \\\"Content-Type\\\": \\\"application/json\\\"}\ +\ + secret_creation_response = requests.put(url=secret_creation_url, json=secret_creation_body, headers=headers)\ + if secret_creation_response.status_code == 201 or secret_creation_response.status_code == 204:\ + print(\\\"--Secret Created / Updated!--\\\")\ + else:\ + print(f\\\"-- Error creating / updating github secret, the reason was : {secret_creation_response.reason}\\\")\ +\ +\ +parser, _ = init_argparse()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +token = generate_github_token(parser.github_app_id, parser.github_app_private_key,\ + parser.github_app_installation_id) if len(\ + parser.git_password.strip()) == 0 else parser.git_password.strip()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +if not parser.secret_name.strip():\ + print(\\\"You must define the secret name\\\")\ + sys.exit(1)\ + \ +if not verify_new_repo(token, parser.git_organization, parser.repo):\ + print(\\\"Could not find the repo\\\")\ + sys.exit(1)\ +\ +key_id, public_key = get_public_key('https://api.github.com/repos', parser.git_organization, parser.repo,\ + token)\ +encrypted_secret: str = encrypt(public_key_for_repo=public_key, secret_value_input=parser.secret_value)\ +set_secret(gh_base_url='https://api.github.com/repos', gh_owner=parser.git_organization, gh_repo=parser.repo,\ + gh_auth_token=token, public_key_id=key_id, secret_key=parser.secret_name,\ + encrypted_secret_value=encrypted_secret)\ +\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + step { + condition = \"Success\" + name = \"Create Docker Hub Password Username\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Create Docker Hub Password Username\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = false + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"CreateGitHubSecret.Git.Credentials.Password\" = \"#{Git.Credentials.Password}\" + \"Octopus.Action.RunOnServer\" = \"true\" + \"CreateGitHubSecret.GitHub.Secret.Value\" = \"#{Docker.Credentials.Username}\" + \"CreateGitHubSecret.Git.Url.Repo\" = \"#{Octopus.Action[Create Repo].Output.NewRepo}\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.ScriptBody\" = \"# https://gist.github.com/comdotlinux/9a53bb00767a16d6646464c4b8249094\ +\ +# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid\ +# having to use a regular user account.\ +import subprocess\ +import sys\ +\ +# Install our own dependencies\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt', '--disable-pip-version-check'])\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pynacl', '--disable-pip-version-check'])\ +\ +import requests\ +import json\ +import subprocess\ +import sys\ +import os\ +import urllib.request\ +import base64\ +import re\ +import jwt\ +import time\ +import argparse\ +import urllib3\ +from base64 import b64encode\ +from typing import TypedDict\ +from nacl import public, encoding\ +\ +# Disable insecure http request warnings\ +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\ +\ +# If this script is not being run as part of an Octopus step, setting variables is a noop\ +if 'set_octopusvariable' not in globals():\ + def set_octopusvariable(variable, value):\ + pass\ +\ +# If this script is not being run as part of an Octopus step, return variables from environment variables.\ +# Periods are replaced with underscores, and the variable name is converted to uppercase\ +if \\\"get_octopusvariable\\\" not in globals():\ + def get_octopusvariable(variable):\ + return os.environ[re.sub('\\\\\\\\.', '_', variable.upper())]\ +\ +# If this script is not being run as part of an Octopus step, print directly to std out.\ +if 'printverbose' not in globals():\ + def printverbose(msg):\ + print(msg)\ +\ +\ +def printverbose_noansi(output):\ + \\\"\\\"\\\"\ + Strip ANSI color codes and print the output as verbose\ + :param output: The output to print\ + \\\"\\\"\\\"\ + output_no_ansi = re.sub(r'\\\\x1b\\\\[[0-9;]*m', '', output)\ + printverbose(output_no_ansi)\ +\ +\ +def get_octopusvariable_quiet(variable):\ + \\\"\\\"\\\"\ + Gets an octopus variable, or an empty string if it does not exist.\ + :param variable: The variable name\ + :return: The variable value, or an empty string if the variable does not exist\ + \\\"\\\"\\\"\ + try:\ + return get_octopusvariable(variable)\ + except:\ + return ''\ +\ +\ +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi, raise_on_non_zero=False,\ + append_to_path=None):\ + \\\"\\\"\\\"\ + The execute method provides the ability to execute external processes while capturing and returning the\ + output to std err and std out and exit code.\ + \\\"\\\"\\\"\ +\ + my_env = os.environ.copy() if env is None else env\ +\ + if append_to_path is not None:\ + my_env[\\\"PATH\\\"] = append_to_path + os.pathsep + my_env['PATH']\ +\ + process = subprocess.Popen(args,\ + stdout=subprocess.PIPE,\ + stderr=subprocess.PIPE,\ + stdin=open(os.devnull),\ + text=True,\ + cwd=cwd,\ + env=my_env)\ + stdout, stderr = process.communicate()\ + retcode = process.returncode\ +\ + if not retcode == 0 and raise_on_non_zero:\ + raise Exception('command returned exit code ' + retcode)\ +\ + if print_args is not None:\ + print_output(' '.join(args))\ +\ + if print_output is not None:\ + print_output(stdout)\ + print_output(stderr)\ +\ + return stdout, stderr, retcode\ +\ +\ +def init_argparse():\ + parser = argparse.ArgumentParser(\ + usage='%(prog)s [OPTION]',\ + description='Fork a GitHub repo'\ + )\ +\ + parser.add_argument('--secret-name', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.Secret.Name') or get_octopusvariable_quiet(\ + 'GitHub.Secret.Name'))\ + parser.add_argument('--secret-value', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.Secret.Value') or get_octopusvariable_quiet(\ + 'GitHub.Secret.Value'))\ +\ + parser.add_argument('--repo', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Url.Repo') or get_octopusvariable_quiet(\ + 'Git.Url.Repo') or get_octopusvariable_quiet('Octopus.Project.Name'))\ + parser.add_argument('--git-organization', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Url.Organization') or get_octopusvariable_quiet(\ + 'Git.Url.Organization'))\ + parser.add_argument('--github-app-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id'))\ + parser.add_argument('--github-app-installation-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.InstallationId') or get_octopusvariable_quiet(\ + 'GitHub.App.InstallationId'))\ + parser.add_argument('--github-app-private-key', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.GitHub.App.PrivateKey') or get_octopusvariable_quiet(\ + 'GitHub.App.PrivateKey'))\ + parser.add_argument('--git-password', action='store',\ + default=get_octopusvariable_quiet(\ + 'CreateGitHubSecret.Git.Credentials.Password') or get_octopusvariable_quiet(\ + 'Git.Credentials.Password'),\ + help='The git password. This takes precedence over the --github-app-id, --github-app-installation-id, and --github-app-private-key')\ +\ + return parser.parse_known_args()\ +\ +\ +def generate_github_token(github_app_id, github_app_private_key, github_app_installation_id):\ + # Generate the tokens used by git and the GitHub API\ + app_id = github_app_id\ + signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8'))\ +\ + payload = {\ + # Issued at time\ + 'iat': int(time.time()),\ + # JWT expiration time (10 minutes maximum)\ + 'exp': int(time.time()) + 600,\ + # GitHub App's identifier\ + 'iss': app_id\ + }\ +\ + # Create JWT\ + jwt_instance = jwt.JWT()\ + encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256')\ +\ + # Create access token\ + url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens'\ + headers = {\ + 'Authorization': 'Bearer ' + encoded_jwt,\ + 'Accept': 'application/vnd.github+json',\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers, method='POST')\ + response = urllib.request.urlopen(request)\ + response_json = json.loads(response.read().decode())\ + return response_json['token']\ +\ +\ +def generate_auth_header(token):\ + auth = base64.b64encode(('x-access-token:' + token).encode('ascii'))\ + return 'Basic ' + auth.decode('ascii')\ +\ +\ +def verify_new_repo(token, cac_org, new_repo):\ + # Attempt to view the new repo\ + try:\ + url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo\ + headers = {\ + 'Accept': 'application/vnd.github+json',\ + 'Authorization': 'Bearer ' + token,\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers)\ + urllib.request.urlopen(request)\ + return True\ + except:\ + return False\ +\ +\ +def encrypt(public_key_for_repo: str, secret_value_input: str) -> str:\ + \\\"\\\"\\\"Encrypt a Unicode string using the public key.\\\"\\\"\\\"\ + sealed_box = public.SealedBox(public.PublicKey(public_key_for_repo.encode(\\\"utf-8\\\"), encoding.Base64Encoder()))\ + encrypted = sealed_box.encrypt(secret_value_input.encode(\\\"utf-8\\\"))\ + return b64encode(encrypted).decode(\\\"utf-8\\\")\ +\ +\ +def get_public_key(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str) -> (str, str):\ + public_key_endpoint: str = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/public-key\\\"\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\"}\ + response = requests.get(url=public_key_endpoint, headers=headers)\ + if response.status_code != 200:\ + raise IOError(\ + f\\\"Could not get public key for repository {gh_owner}/{gh_repo}. The Response code was {response.status_code}\\\")\ +\ + public_key_json = response.json()\ + return public_key_json['key_id'], public_key_json['key']\ +\ +\ +def set_secret(gh_base_url: str, gh_owner: str, gh_repo: str, gh_auth_token: str, public_key_id: str, secret_key: str,\ + encrypted_secret_value: str):\ + secret_creation_url = f\\\"{gh_base_url}/{gh_owner}/{gh_repo}/actions/secrets/{secret_key}\\\"\ + secret_creation_body = {\\\"key_id\\\": public_key_id, \\\"encrypted_value\\\": encrypted_secret_value}\ + headers: TypedDict[str, str] = {\\\"Authorization\\\": f\\\"Bearer {gh_auth_token}\\\", \\\"Content-Type\\\": \\\"application/json\\\"}\ +\ + secret_creation_response = requests.put(url=secret_creation_url, json=secret_creation_body, headers=headers)\ + if secret_creation_response.status_code == 201 or secret_creation_response.status_code == 204:\ + print(\\\"--Secret Created / Updated!--\\\")\ + else:\ + print(f\\\"-- Error creating / updating github secret, the reason was : {secret_creation_response.reason}\\\")\ +\ +\ +parser, _ = init_argparse()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +token = generate_github_token(parser.github_app_id, parser.github_app_private_key,\ + parser.github_app_installation_id) if len(\ + parser.git_password.strip()) == 0 else parser.git_password.strip()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +if not parser.secret_name.strip():\ + print(\\\"You must define the secret name\\\")\ + sys.exit(1)\ + \ +if not verify_new_repo(token, parser.git_organization, parser.repo):\ + print(\\\"Could not find the repo\\\")\ + sys.exit(1)\ +\ +key_id, public_key = get_public_key('https://api.github.com/repos', parser.git_organization, parser.repo,\ + token)\ +encrypted_secret: str = encrypt(public_key_for_repo=public_key, secret_value_input=parser.secret_value)\ +set_secret(gh_base_url='https://api.github.com/repos', gh_owner=parser.git_organization, gh_repo=parser.repo,\ + gh_auth_token=token, public_key_id=key_id, secret_key=parser.secret_name,\ + encrypted_secret_value=encrypted_secret)\ +\" + \"CreateGitHubSecret.Git.Url.Organization\" = \"#{Git.Url.Organization}\" + \"CreateGitHubSecret.GitHub.Secret.Name\" = \"DOCKERHUB_USERNAME\" + \"Octopus.Action.Script.Syntax\" = \"Python\" + } + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + features = [] + } + + properties = {} + target_roles = [] + } + step { + condition = \"Success\" + name = \"Generate and Push\" + package_requirement = \"LetOctopusDecide\" + start_trigger = \"StartAfterPrevious\" + + action { + action_type = \"Octopus.Script\" + name = \"Generate and Push\" + condition = \"Success\" + run_on_server = true + is_disabled = false + can_be_used_for_project_versioning = true + is_required = false + worker_pool_id = local.worker_pool_id + properties = { + \"PopulateGithubRepo.Yeoman.Generator.SubGenerator\" = \"nodejs-docker-webapp\" + \"PopulateGithubRepo.Yeoman.Generator.Arguments\" = \"--octopusUrl #{Octopus.Action[Get Variables].Output.Web.ServerUri} --octopusSpace \\\"#{Octopus.Action[Get Variables].Output.Space.Name}\\\" --octopusApi #{Octopus.ApiKey} --octopusProject \\\"#{Application.Octopus.Project}\\\" --dockerImage #{Application.Docker.Image}\" + \"PopulateGithubRepo.Yeoman.Generator.Name\" = \"octopus-reference-architecture-apps\" + \"Octopus.Action.Script.Syntax\" = \"Python\" + \"Octopus.Action.Script.ScriptSource\" = \"Inline\" + \"Octopus.Action.Script.ScriptBody\" = \"# This script forks a GitHub repo. It creates a token from a GitHub App installation to avoid\ +# having to use a regular user account.\ +import subprocess\ +import sys\ +\ +# Install our own dependencies\ +env_vars = os.environ.copy()\ +env_vars['PIP_ROOT_USER_ACTION'] = 'ignore'\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'jwt', '--disable-pip-version-check'], env=env_vars)\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'requests', '--disable-pip-version-check'], env=env_vars)\ +subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'anyascii', '--disable-pip-version-check'], env=env_vars)\ +\ +import requests\ +import json\ +import subprocess\ +import sys\ +import os\ +import urllib.request\ +import base64\ +import re\ +import jwt\ +import time\ +import argparse\ +import platform\ +import zipfile\ +import lzma\ +import tarfile\ +import shutil\ +import urllib3\ +from shlex import split\ +from anyascii import anyascii\ +\ +# Disable insecure http request warnings\ +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\ +\ +# If this script is not being run as part of an Octopus step, setting variables is a noop\ +if 'set_octopusvariable' not in globals():\ + def set_octopusvariable(variable, value):\ + pass\ +\ +# If this script is not being run as part of an Octopus step, return variables from environment variables.\ +# Periods are replaced with underscores, and the variable name is converted to uppercase\ +if \\\"get_octopusvariable\\\" not in globals():\ + def get_octopusvariable(variable):\ + return os.environ[re.sub('\\\\\\\\.', '_', variable.upper())]\ +\ +# If this script is not being run as part of an Octopus step, print directly to std out.\ +if 'printverbose' not in globals():\ + def printverbose(msg):\ + print(msg)\ +\ +\ +def printverbose_noansi(output):\ + \\\"\\\"\\\"\ + Strip ANSI color codes and print the output as verbose\ + :param output: The output to print\ + \\\"\\\"\\\"\ + output_no_ansi = re.sub(r'\\\\x1b\\\\[[0-9;]*m', '', output)\ + printverbose(output_no_ansi)\ +\ +\ +def get_octopusvariable_quiet(variable):\ + \\\"\\\"\\\"\ + Gets an octopus variable, or an empty string if it does not exist.\ + :param variable: The variable name\ + :return: The variable value, or an empty string if the variable does not exist\ + \\\"\\\"\\\"\ + try:\ + return get_octopusvariable(variable)\ + except Exception as inst:\ + return ''\ +\ +\ +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi, raise_on_non_zero=False,\ + append_to_path=None):\ + \\\"\\\"\\\"\ + The execute method provides the ability to execute external processes while capturing and returning the\ + output to std err and std out and exit code.\ + \\\"\\\"\\\"\ +\ + my_env = os.environ.copy() if env is None else env\ +\ + if append_to_path is not None:\ + my_env[\\\"PATH\\\"] = append_to_path + os.pathsep + my_env['PATH']\ +\ + process = subprocess.Popen(args,\ + stdout=subprocess.PIPE,\ + stderr=subprocess.PIPE,\ + stdin=open(os.devnull),\ + text=True,\ + cwd=cwd,\ + env=my_env)\ + stdout, stderr = process.communicate()\ + retcode = process.returncode\ +\ + if not retcode == 0 and raise_on_non_zero:\ + raise Exception('command returned exit code ' + retcode)\ +\ + if print_args is not None:\ + print_output(' '.join(args))\ +\ + if print_output is not None:\ + print_output(stdout)\ + print_output(stderr)\ +\ + return stdout, stderr, retcode\ +\ +\ +def init_argparse():\ + parser = argparse.ArgumentParser(\ + usage='%(prog)s [OPTION]',\ + description='Fork a GitHub repo'\ + )\ + parser.add_argument('--generator', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Yeoman.Generator.Name') or get_octopusvariable_quiet(\ + 'Yeoman.Generator.Name'))\ + parser.add_argument('--sub-generator', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Yeoman.Generator.SubGenerator') or get_octopusvariable_quiet(\ + 'Yeoman.Generator.SubGenerator'))\ + parser.add_argument('--generator-arguments', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Yeoman.Generator.Arguments') or get_octopusvariable_quiet(\ + 'Yeoman.Generator.Arguments'),\ + help='The arguments to pas to yo. Pass all arguments as a single string. This string is then parsed as if it were yo arguments.')\ + parser.add_argument('--repo', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Git.Url.Repo') or get_octopusvariable_quiet(\ + 'Git.Url.Repo'))\ + parser.add_argument('--git-organization', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Git.Url.Organization') or get_octopusvariable_quiet(\ + 'Git.Url.Organization'))\ + parser.add_argument('--github-app-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.GitHub.App.Id') or get_octopusvariable_quiet('GitHub.App.Id'))\ + parser.add_argument('--github-app-installation-id', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.GitHub.App.InstallationId') or get_octopusvariable_quiet(\ + 'GitHub.App.InstallationId'))\ + parser.add_argument('--github-app-private-key', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.GitHub.App.PrivateKey') or get_octopusvariable_quiet(\ + 'GitHub.App.PrivateKey'))\ + parser.add_argument('--git-password', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Git.Credentials.Password') or get_octopusvariable_quiet(\ + 'Git.Credentials.Password'),\ + help='The git password. This takes precedence over the --github-app-id, --github-app-installation-id, and --github-app-private-key')\ + parser.add_argument('--git-username', action='store',\ + default=get_octopusvariable_quiet(\ + 'PopulateGithubRepo.Git.Credentials.Username') or get_octopusvariable_quiet(\ + 'Git.Credentials.Username'),\ + help='The git username. This will be used for both the git authentication and the username associated with any commits.')\ +\ + return parser.parse_known_args()\ +\ +\ +def generate_github_token(github_app_id, github_app_private_key, github_app_installation_id):\ + # Generate the tokens used by git and the GitHub API\ + app_id = github_app_id\ + signing_key = jwt.jwk_from_pem(github_app_private_key.encode('utf-8'))\ +\ + payload = {\ + # Issued at time\ + 'iat': int(time.time()),\ + # JWT expiration time (10 minutes maximum)\ + 'exp': int(time.time()) + 600,\ + # GitHub App's identifier\ + 'iss': app_id\ + }\ +\ + # Create JWT\ + jwt_instance = jwt.JWT()\ + encoded_jwt = jwt_instance.encode(payload, signing_key, alg='RS256')\ +\ + # Create access token\ + url = 'https://api.github.com/app/installations/' + github_app_installation_id + '/access_tokens'\ + headers = {\ + 'Authorization': 'Bearer ' + encoded_jwt,\ + 'Accept': 'application/vnd.github+json',\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers, method='POST')\ + response = urllib.request.urlopen(request)\ + response_json = json.loads(response.read().decode())\ + return response_json['token']\ +\ +\ +def generate_auth_header(token):\ + auth = base64.b64encode(('x-access-token:' + token).encode('ascii'))\ + return 'Basic ' + auth.decode('ascii')\ +\ +\ +def verify_new_repo(token, cac_org, new_repo):\ + # Attempt to view the new repo\ + try:\ + url = 'https://api.github.com/repos/' + cac_org + '/' + new_repo\ + headers = {\ + 'Accept': 'application/vnd.github+json',\ + 'Authorization': 'Bearer ' + token,\ + 'X-GitHub-Api-Version': '2022-11-28'\ + }\ + request = urllib.request.Request(url, headers=headers)\ + urllib.request.urlopen(request)\ + return True\ + except Exception as inst:\ + return False\ +\ +\ +def is_windows():\ + return platform.system() == 'Windows'\ +\ +\ +def download_file(url, filename, verify_ssl=True):\ + r = requests.get(url, verify=verify_ssl)\ + with open(filename, 'wb') as file:\ + file.write(r.content)\ +\ +\ +def ensure_git_exists():\ + if is_windows():\ + print(\\\"Checking git is installed\\\")\ + try:\ + stdout, _, exit_code = execute(['git', 'version'])\ + printverbose(stdout)\ + if not exit_code == 0:\ + raise \\\"git not found\\\"\ + except:\ + print(\\\"Downloading git\\\")\ + download_file('https://www.7-zip.org/a/7zr.exe', '7zr.exe')\ + download_file(\ + 'https://github.com/git-for-windows/git/releases/download/v2.42.0.windows.2/PortableGit-2.42.0.2-64-bit.7z.exe',\ + 'PortableGit.7z.exe')\ + print(\\\"Installing git\\\")\ + print(\\\"Consider installing git on the worker or using a standard worker-tools image\\\")\ + execute(['7zr.exe', 'x', 'PortableGit.7z.exe', '-o' + os.path.join(os.getcwd(), 'git'), '-y'])\ + return os.path.join(os.getcwd(), 'git', 'bin', 'git')\ +\ + return 'git'\ +\ +\ +def install_npm_linux():\ + print(\\\"Downloading node\\\")\ + download_file(\ + 'https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz',\ + 'node.tar.xz')\ + print(\\\"Installing node on Linux\\\")\ + with lzma.open(\\\"node.tar.xz\\\", \\\"r\\\") as lzma_ref:\ + with open(\\\"node.tar\\\", \\\"wb\\\") as fdst:\ + shutil.copyfileobj(lzma_ref, fdst)\ + with tarfile.open(\\\"node.tar\\\", \\\"r\\\") as tar_ref:\ + tar_ref.extractall(os.getcwd())\ +\ + try:\ + _, _, exit_code = execute([os.getcwd() + '/node-v18.18.2-linux-x64/bin/npm', '--version'],\ + append_to_path=os.getcwd() + '/node-v18.18.2-linux-x64/bin')\ + if not exit_code == 0:\ + raise Exception(\\\"Failed to run npm\\\")\ + except Exception as ex:\ + print('Failed to install npm ' + str(ex))\ + sys.exit(1)\ + return os.getcwd() + '/node-v18.18.2-linux-x64/bin/npm', os.getcwd() + '/node-v18.18.2-linux-x64/bin'\ +\ +\ +def install_npm_windows():\ + print(\\\"Downloading node\\\")\ + download_file('https://nodejs.org/dist/v18.18.2/node-v18.18.2-win-x64.zip', 'node.zip', False)\ + print(\\\"Installing node on Windows\\\")\ + with zipfile.ZipFile(\\\"node.zip\\\", \\\"r\\\") as zip_ref:\ + zip_ref.extractall(os.getcwd())\ + try:\ + _, _, exit_code = execute([os.path.join(os.getcwd(), 'node-v18.18.2-win-x64', 'npm.cmd'), '--version'],\ + append_to_path=os.path.join(os.getcwd(), 'node-v18.18.2-win-x64'))\ + if not exit_code == 0:\ + raise Exception(\\\"Failed to run npm\\\")\ + except Exception as ex:\ + print('Failed to install npm ' + str(ex))\ + sys.exit(1)\ +\ + return (os.path.join(os.getcwd(), 'node-v18.18.2-win-x64', 'npm.cmd'),\ + os.path.join(os.getcwd(), 'node-v18.18.2-win-x64'))\ +\ +\ +def ensure_node_exists():\ + try:\ + print(\\\"Checking node is installed\\\")\ + _, _, exit_code = execute(['npm', '--version'])\ + if not exit_code == 0:\ + raise Exception(\\\"npm not found\\\")\ + except:\ + if is_windows():\ + return install_npm_windows()\ + else:\ + return install_npm_linux()\ +\ + return 'npm', None\ +\ +\ +def ensure_yo_exists(npm_executable, npm_path):\ + try:\ + print(\\\"Checking Yeoman is installed\\\")\ + _, _, exit_code = execute(['yo', '--version'])\ + if not exit_code == 0:\ + raise Exception(\\\"yo not found\\\")\ + except:\ + print('Installing Yeoman')\ +\ + _, _, retcode = execute([npm_executable, 'install', '-g', 'yo'], append_to_path=npm_path)\ +\ + if not retcode == 0:\ + print(\\\"Failed to set install Yeoman. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ + npm_bin, _, retcode = execute([npm_executable, 'config', 'get', 'prefix'], append_to_path=npm_path)\ +\ + if not retcode == 0:\ + print(\\\"Failed to set get the npm prefix directory. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ + try:\ + if is_windows():\ + _, _, exit_code = execute([os.path.join(npm_bin.strip(), 'yo.cmd'), '--version'],\ + append_to_path=npm_path)\ + else:\ + _, _, exit_code = execute([os.path.join(npm_bin.strip(), 'bin', 'yo'), '--version'],\ + append_to_path=npm_path)\ +\ + if not exit_code == 0:\ + raise Exception(\\\"Failed to run yo\\\")\ + except Exception as ex:\ + print('Failed to install yo ' + str(ex))\ + sys.exit(1)\ +\ + # Windows and Linux save NPM binaries in different directories\ + if is_windows():\ + return os.path.join(npm_bin.strip(), 'yo.cmd')\ +\ + return os.path.join(npm_bin.strip(), 'bin', 'yo')\ +\ + return 'yo'\ +\ +\ +git_executable = ensure_git_exists()\ +npm_executable, npm_path = ensure_node_exists()\ +yo_executable = ensure_yo_exists(npm_executable, npm_path)\ +parser, _ = init_argparse()\ +\ +if not parser.git_password.strip() and not (\ + parser.github_app_id.strip() and parser.github_app_private_key.strip() and parser.github_app_installation_id.strip()):\ + print(\\\"You must supply the GitHub token, or the GitHub App ID and private key and installation ID\\\")\ + sys.exit(1)\ +\ +if not parser.git_organization.strip():\ + print(\\\"You must define the organization\\\")\ + sys.exit(1)\ +\ +if not parser.repo.strip():\ + print(\\\"You must define the repo name\\\")\ + sys.exit(1)\ +\ +if not parser.generator.strip():\ + print(\\\"You must define the Yeoman generator\\\")\ + sys.exit(1)\ +\ +# Create a dir for the git clone\ +if os.path.exists('downstream'):\ + shutil.rmtree('downstream')\ +\ +os.mkdir('downstream')\ +\ +# Create a dir for yeoman to use\ +if os.path.exists('downstream-yeoman'):\ + shutil.rmtree('downstream-yeoman')\ +\ +os.mkdir('downstream-yeoman')\ +# Yeoman will use a less privileged user to write to this directory, so grant full access\ +if not is_windows():\ + os.chmod('downstream-yeoman', 0o777)\ +\ +downstream_dir = os.path.join(os.getcwd(), 'downstream')\ +downstream_yeoman_dir = os.path.join(os.getcwd(), 'downstream-yeoman')\ +\ +# The access token is generated from a github app or supplied directly as an access token\ +token = generate_github_token(parser.github_app_id, parser.github_app_private_key,\ + parser.github_app_installation_id) if len(\ + parser.git_password.strip()) == 0 else parser.git_password.strip()\ +\ +if not verify_new_repo(token, parser.git_organization, parser.repo):\ + print('Repo at https://github.com/' + parser.git_organization + '/' + parser.repo + ' could not be accessed')\ + sys.exit(1)\ +\ +# We need to disable the credentials helper prompt, which will pop open a GUI prompt that we can never close\ +if is_windows():\ + _, _, retcode = execute([git_executable, 'config', '--system', 'credential.helper', 'manager'])\ +\ + if not retcode == 0:\ + print(\\\"Failed to set the credential.helper setting. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ + _, _, retcode = execute([git_executable, 'config', '--system', 'credential.modalprompt', 'false'])\ +\ + if not retcode == 0:\ + print(\\\"Failed to srt the credential.modalprompt setting. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ + # We need to disable the credentials helper prompt, which will pop open a GUI prompt that we can never close\ + _, _, retcode = execute(\ + [git_executable, 'config', '--system', 'credential.microsoft.visualstudio.com.interactive', 'never'])\ +\ + if not retcode == 0:\ + print(\ + \\\"Failed to set the credential.microsoft.visualstudio.com.interactive setting. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +_, _, retcode = execute([git_executable, 'config', '--global', 'user.email', 'octopus@octopus.com'])\ +\ +if not retcode == 0:\ + print(\\\"Failed to set the user.email setting. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +_, _, retcode = execute([git_executable, 'config', '--global', 'core.autocrlf', 'input'])\ +\ +if not retcode == 0:\ + print(\\\"Failed to set the core.autocrlf setting. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +username = parser.git_username if len(parser.git_username) != 0 else 'Octopus'\ +_, _, retcode = execute([git_executable, 'config', '--global', 'user.name', username])\ +\ +if not retcode == 0:\ + print(\\\"Failed to set the git username. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +_, _, retcode = execute([git_executable, 'config', '--global', 'credential.helper', 'cache'])\ +\ +if not retcode == 0:\ + print(\\\"Failed to set the git credential helper. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +print('Cloning repo')\ +\ +_, _, retcode = execute(\ + [git_executable, 'clone',\ + 'https://' + username + ':' + token + '@github.com/' + parser.git_organization + '/' + parser.repo + '.git',\ + 'downstream'])\ +\ +if not retcode == 0:\ + print(\\\"Failed to clone the git repo. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +print('Configuring Yeoman Generator')\ +\ +_, _, retcode = execute([npm_executable, 'install'], cwd=os.path.join(os.getcwd(), 'YeomanGenerator'), append_to_path=npm_path)\ +\ +if not retcode == 0:\ + print(\\\"Failed to install the generator dependencies. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +_, _, retcode = execute([npm_executable, 'link'], cwd=os.path.join(os.getcwd(), 'YeomanGenerator'), append_to_path=npm_path)\ +\ +if not retcode == 0:\ + print(\\\"Failed to link the npm module. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +print('Running Yeoman Generator')\ +\ +# Treat the string of yo arguments as a raw input and parse it again. The resulting list of unknown arguments\ +# is then passed to yo. We have to convert the incoming values from utf to ascii when parsing a second time.\ +yo_args = split(anyascii(parser.generator_arguments))\ +\ +generator_name = parser.generator + ':' + parser.sub_generator if len(parser.sub_generator) != 0 else parser.generator\ +\ +yo_arguments = [yo_executable, generator_name, '--force', '--skip-install']\ +\ +# Yeoman has issues running as root, which it will often do in a container.\ +# So we run Yeoman in its own directory, and then copy the changes to the git directory.\ +_, _, retcode = execute(yo_arguments + yo_args, cwd=downstream_yeoman_dir, append_to_path=npm_path)\ +\ +if not retcode == 0:\ + print(\\\"Failed to run Yeoman. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +shutil.copytree(downstream_yeoman_dir, downstream_dir, dirs_exist_ok=True)\ +\ +print('Adding changes to git')\ +\ +_, _, retcode = execute([git_executable, 'add', '.'], cwd=downstream_dir)\ +\ +if not retcode == 0:\ + print(\\\"Failed to add the git changes. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ +# Check for pending changes\ +_, _, retcode = execute([git_executable, 'diff-index', '--quiet', 'HEAD'], cwd=downstream_dir)\ +\ +if not retcode == 0:\ + print('Committing changes to git')\ + _, _, retcode = execute([git_executable, 'commit', '-m',\ + 'Added files from Yeoman generator ' + parser.generator + ':' + parser.sub_generator],\ + cwd=downstream_dir)\ +\ + if not retcode == 0:\ + print(\\\"Failed to set commit the git changes. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\ + print('Pushing changes to git')\ +\ + _, _, retcode = execute([git_executable, 'push', 'origin', 'main'], cwd=downstream_dir)\ +\ + if not retcode == 0:\ + print(\\\"Failed to push the git changes. Check the verbose logs for details.\\\")\ + sys.exit(1)\ +\" + \"PopulateGithubRepo.Yeoman.Generator.Package\" = jsonencode({ + \"PackageId\" = \"OctopusSolutionsEngineering/ReferenceArchitectureAppGenerators\" + \"FeedId\" = local.github_feed_id + }) + \"Octopus.Action.RunOnServer\" = \"true\" + \"PopulateGithubRepo.Git.Url.Organization\" = \"#{Git.Url.Organization}\" + \"PopulateGithubRepo.Git.Url.Repo\" = \"#{Octopus.Action[Create Repo].Output.NewRepo}\" + \"PopulateGithubRepo.Git.Credentials.Password\" = \"#{Git.Credentials.Password}\" + } + + container { + feed_id = local.docker_hub_feed_id + image = \"octopussamples/node-workertools\" + } + + environments = [] + excluded_environments = [] + channels = [] + tenant_tags = [] + + package { + name = \"YeomanGenerator\" + package_id = \"OctopusSolutionsEngineering/ReferenceArchitectureAppGenerators\" + acquisition_location = \"Server\" + extract_during_deployment = false + feed_id = local.github_feed_id + properties = { + Extract = \"True\", PackageParameterName = \"PopulateGithubRepo.Yeoman.Generator.Package\", Purpose = \"\", + SelectionMode = \"deferred\" + } + } + features = [] + } + + properties = {} + target_roles = [] + } +} +#endregion + +#endregion +", + "Octopus.Action.Terraform.TemplateParameters": "{\"octopus_server\":\"#{ReferenceArchitecture.WebApp.Octopus.ServerUrl}\",\"octopus_apikey\":\"#{ReferenceArchitecture.WebApp.Octopus.ApiKey}\",\"octopus_space_id\":\"#{ReferenceArchitecture.WebApp.Octopus.SpaceId}\",\"feed_docker_hub_username\":\"#{ReferenceArchitecture.WebApp.Docker.Username}\",\"feed_docker_hub_password\":\"#{ReferenceArchitecture.WebApp.Docker.Password}\",\"github_access_token\":\"#{ReferenceArchitecture.WebApp.GitHub.AccessToken}\",\"azure_account_application_id\":\"#{ReferenceArchitecture.WebApp.Azure.ApplicationId}\",\"azure_account_subscription_id\":\"#{ReferenceArchitecture.WebApp.Azure.SubscriptionId}\",\"azure_account_tenant_id\":\"#{ReferenceArchitecture.WebApp.Azure.TenantId}\",\"azure_account_password\":\"#{ReferenceArchitecture.WebApp.Azure.AccountPassword}\"}", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Terraform.AdditionalInitParams": "#{if ReferenceArchitecture.Terraform.InitArgs}#{ReferenceArchitecture.Terraform.InitArgs}#{/if}", + "Octopus.Action.Terraform.AdditionalActionParams": "#{if ReferenceArchitecture.Terraform.ApplyArgs}#{ReferenceArchitecture.Terraform.ApplyArgs}#{/if}" + }, + "Parameters": [ + { + "Id": "e8ed2805-bd27-470d-9580-c5486201e5e4", + "Name": "ReferenceArchitecture.WebApp.Azure.ApplicationId", + "Label": "Azure account application ID", + "HelpText": "This is the The Azure account application ID. Refer to the [Azure documentation](https://learn.microsoft.com/en-us/purview/create-service-principal-azure) for details on creating a service principal.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "857dd051-4f27-4533-b3c1-4f60e2da208a", + "Name": "ReferenceArchitecture.WebApp.Azure.SubscriptionId", + "Label": "Azure account subscription ID", + "HelpText": "This is the The Azure account subscription ID. Refer to the [Azure documentation](https://learn.microsoft.com/en-us/purview/create-service-principal-azure) for details on creating a service principal.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "99a7b543-4b0e-49d6-9e48-bc176e07e75e", + "Name": "ReferenceArchitecture.WebApp.Azure.TenantId", + "Label": "Azure account tenant ID", + "HelpText": "This is the The Azure account tenant ID. Refer to the [Azure documentation](https://learn.microsoft.com/en-us/purview/create-service-principal-azure) for details on creating a service principal.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6ea2138c-d67e-477b-975d-622a5f5e0f33", + "Name": "ReferenceArchitecture.WebApp.Azure.AccountPassword", + "Label": "Azure account password", + "HelpText": "This is the The Azure account password. Refer to the [Azure documentation](https://learn.microsoft.com/en-us/purview/create-service-principal-azure) for details on creating a service principal.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "47a001cc-f17b-4905-bd73-91363d7b5f83", + "Name": "ReferenceArchitecture.WebApp.Docker.Username", + "Label": "Docker Hub Username", + "HelpText": "The Docker Hub username. See the [Docker docs](https://docs.docker.com/docker-id/) for more information on creating a Docker Hub account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d83f2e90-532a-4db0-97e9-f07e8167c3e7", + "Name": "ReferenceArchitecture.WebApp.Docker.Password", + "Label": "Docker Hub Password", + "HelpText": "The Docker Hub password. See the [Docker docs](https://docs.docker.com/docker-id/) for more information on creating a Docker Hub account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "4d8e6395-e298-42f2-b2ca-cfdbcd65ae2b", + "Name": "ReferenceArchitecture.WebApp.GitHub.AccessToken", + "Label": "GitHub Access Token", + "HelpText": "The GitHub access token. Find more details in the [GitHub documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). + + +This value is used when populating GitHub repos with template projects. It can be left blank if you do not use the `Create Template Github Project` runbooks.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e088e550-e179-4998-be04-0b6653bd8371", + "Name": "ReferenceArchitecture.WebApp.Octopus.ApiKey", + "Label": "Octopus API Key", + "HelpText": "The Octopus API key. See the [Octopus docs](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) for more details on creating an API Key.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "5c5c947b-2d3a-4f1f-a85c-1fa6a3b61bac", + "Name": "ReferenceArchitecture.WebApp.Octopus.SpaceId", + "Label": "Octopus Space ID", + "HelpText": "The Octopus space ID.", + "DefaultValue": "#{Octopus.Space.Id}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4b8f62d1-1c12-4027-bc60-44029c7df0cc", + "Name": "ReferenceArchitecture.WebApp.Octopus.ServerUrl", + "Label": "Octopus Server URL", + "HelpText": "The Octopus server URL.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ce1529b7-c366-4faa-a3bc-bade411237cc", + "Name": "ReferenceArchitecture.Terraform.ApplyArgs", + "Label": "Optional Terraform Apply Args", + "HelpText": "Optional arguments passed to the `terraform apply` command. See the [documentation](https://oc.to/wRvMoP) for details on any optional variables that can be defined here. Leave this field blank unless you have a specific reason to change it.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "14a42b42-2692-4850-943c-425d3036bf1c", + "Name": "ReferenceArchitecture.Terraform.InitArgs", + "Label": "Optional Terraform Init Args", + "HelpText": "Optional arguments passed to the `terraform init` command. See the [documentation](https://oc.to/wRvMoP) for details on any optional variables that can be defined here. Leave this field blank unless you have a specific reason to change it.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.TerraformApply", + "$Meta": { + "ExportedAt": "2023-11-12T23:48:11.062Z", + "OctopusVersion": "2024.1.1024", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-register-aks-k8s-cluster.json.human b/step-templates/octopus-register-aks-k8s-cluster.json.human new file mode 100644 index 000000000..1e06eb3f7 --- /dev/null +++ b/step-templates/octopus-register-aks-k8s-cluster.json.human @@ -0,0 +1,327 @@ +{ + "Id": "eff227d2-2cd6-4d86-90ae-6258cee53d0a", + "Name": "Register AKS Cluster with Octopus Deploy", + "Description": "Step template to Register an AKS Cluster with Octopus Deploy using the Octopus Deploy API", + "ActionType": "Octopus.Script", + "Version": 1, + "Author": "octobob", + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$OctopusAPIKey = $OctopusParameters[\"RegisterAzureCluster.Octopus.Api.Key\"] +$RegistrationName = $OctopusParameters[\"RegisterAzureCluster.AKS.Name\"] +$ClusterResourceGroup = $OctopusParameters[\"RegisterAzureCluster.ResourceGroup.Name\"] +$OctopusUrl = $OctopusParameters[\"RegisterAzureCluster.Octopus.Base.Url\"] +$Roles = $OctopusParameters[\"RegisterAzureCluster.Roles.List\"] +$Environments = $OctopusParameters[\"RegisterAzureCluster.Environment.List\"] +$SpaceId = $OctopusParameters[\"Octopus.Space.Id\"] +$MachinePolicyIdOrName = $OctopusParameters[\"RegisterAzureCluster.MachinePolicy.IdOrName\"] +$AzureAccountId = $OctopusParameters[\"RegisterAzureCluster.Azure.Account\"] +$Tenants = $OctopusParameters[\"RegisterAzureCluster.Tenant.List\"] +$DeploymentType = $OctopusParameters[\"RegisterAzureCluster.Tenant.DeploymentType\"] +$WorkerPoolNameOrId = $OctopusParameters[\"RegisterAzureCluster.WorkerPool.IdOrName\"] +$OverwriteExisting = $OctopusParameters[\"RegisterAzureCluster.Overwrite.Existing\"] +$OverwriteExisting = $OverwriteExisting -eq \"True\" + +Write-Host \"AKS Name: $RegistrationName\" +Write-Host \"Resoure Group Name: $ClusterResourceGroup\" +Write-Host \"Octopus Url: $OctopusUrl\" +Write-Host \"Role List: $Roles\" +Write-Host \"Environments: $Environments\" +Write-Host \"Machine Policy Name or Id: $MachinePolicyIdOrName\" +Write-Host \"Azure Account Id: $AzureAccountId\" +Write-Host \"Tenant List: $Tenants\" +Write-Host \"Deployment Type: $DeploymentType\" +Write-Host \"Worker Pool Name or Id: $WorkerPoolNameOrId\" +Write-Host \"Overwrite Existing: $OverwriteExisting\" + +$header = New-Object \"System.Collections.Generic.Dictionary[[String],[String]]\" +$header.Add(\"X-Octopus-ApiKey\", $OctopusAPIKey) + +$baseApiUrl = \"$OctopusUrl/api\" +$baseApiInformation = Invoke-RestMethod $baseApiUrl -Headers $header +if ((Get-Member -InputObject $baseApiInformation.Links -Name \"Spaces\" -MemberType Properties) -ne $null) +{ \t +\t$baseApiUrl = \"$baseApiUrl/$SpaceId\" +} + +Write-Host \"Base API Url: $baseApiUrl\" + +$existingMachineResultsUrl = \"$baseApiUrl/machines?partialName=$RegistrationName&skip=0&take=1000\" +Write-Host \"Attempting to find existing machine with similar name at $existingMachineResultsUrl\" +$existingMachineResponse = Invoke-RestMethod $existingMachineResultsUrl -Headers $header +Write-Host $existingMachineResponse + +$machineFound = $false +$machineId = $null +foreach ($item in $existingMachineResponse.Items) +{ +\tif ($item.Name -eq $RegistrationName) + { + \t$machineFound = $true + if ($OverwriteExisting) + { + \t$machineId = $item.Id + } + break + } +} + +if ($machineFound -and $OverwriteExisting -eq $false) +{ +\tWrite-Highlight \"Machine already exists, skipping registration\" + Exit 0 +} + +$roleList = $Roles -split \",\" +$environmentList = $Environments -split \",\" +$environmentIdList = @() +Write-Host \"Getting the ids for all environments specified\" +foreach($environment in $environmentList) +{ +\tWrite-Host \"Getting the id for the environment $environment\" + $environmentEscaped = $environment.Replace(\" \", \"%20\") + $environmentUrl = \"$baseApiUrl/environments?skip=0&take=1000&name=$environmentEscaped\" + $environmentResponse = Invoke-RestMethod $environmentUrl -Headers $header + + $environmentId = $environmentResponse.Items[0].Id + if ($environmentId -eq $null) + { + \tWrite-Host \"The environment $environment cannot be found in this space, exiting\" + exit 1 + } + Write-Host \"The id for environment $environment is $environmentId\" + $environmentIdList += $environmentId +} +$tenantList = $Tenants -split \",\" +$tenantIdList = @() + +foreach($tenant in $tenantList) +{ +\tif ([string]::IsNullOrWhiteSpace($tenant) -eq $false) + { + Write-Host \"Getting the id for tenant $tenant\" + $tenantEscaped = $tenant.Replace(\" \", \"%20\") + $tenantUrl = \"$baseApiUrl/tenants?skip=0&take=1000&name=$tenantEscaped\" + $tenantResponse = Invoke-RestMethod $tenantUrl -Headers $header + + $tenantId = $tenantResponse.Items[0].Id + Write-Host \"The id for tenant $tenant is $tenantId\" + $tenantIdList += $tenantId + } +} + +$machinePolicyId = $machinePolicyIdOrName +if ($machinePolicyIdOrName.StartsWith(\"MachinePolicies-\") -eq $false) +{ +\tWrite-Host \"The machine policy specified $machinePolicyIdOrName appears to be a name\" +\t$machinePolicyNameEscaped = $machinePolicyIdOrName.Replace(\" \", \"%20\") +\t$machinePolicyResponse = Invoke-RestMethod \"$baseApiUrl/machinepolicies?partialName=$machinePolicyNameEscaped\" -Headers $header + + $machinePolicyId = $machinePolicyResponse.Items[0].Id + Write-Host \"The machine policy id is $machinePolicyId\" +} + +if ([string]::IsNullOrWhiteSpace($machinePolicyId) -eq $true) +{ +\tWrite-Host \"The machine policy $machinePolicyIdOrName cannot be found, exiting\" + exit 1 +} + +$workerPoolId = $WorkerPoolNameOrId +if ([string]::IsNullOrWhiteSpace($workerPoolId) -eq $false -and $workerPoolId.StartsWith(\"WorkerPools-\") -eq $false) +{ +\tWrite-Host \"The worker pool $workerPoolId appears to be a name, looking it up\" + $workerPoolNameEscaped = $workerPoolId.Replace(\" \", \"%20\") + $workerPoolResponse = Invoke-RestMethod \"$baseApiUrl/workerpools?partialName=$workerPoolNameEscaped\" -Headers $header + + $workerPoolId = $workerPoolResponse.Items[0].Id + Write-Host \"The worker pool id is $workerPoolId\" +} + +$rawRequest = @{ +\tId = $machineId; + MachinePolicyId = $MachinePolicyId; + Name = $RegistrationName; +\tIsDisabled = $false; +\tHealthStatus = \"Unknown\"; +\tHasLatestCalamari = $true; +\tStatusSummary = $null; +\tIsInProcess = $true; +\tEndpoint = @{ + \tId = $null; +\t\tCommunicationStyle = \"Kubernetes\"; +\t\tLinks = $null; +\t\tAccountType = \"AzureServicePrincipal\"; + ClusterUrl = $null; + ClusterCertificate = $null; + SkipTlsVerification = $false; + DefaultWorkerPoolId = $workerPoolId; + Authentication = @{ + \tAuthenticationType = \"KubernetesAzure\"; + AccountId = $AzureAccountId; + ClusterName = $RegistrationName; + ClusterResourceGroup = $ClusterResourceGroup + }; + }; +\tLinks = $null;\t +\tRoles = $roleList; +\tEnvironmentIds = $environmentIdList; +\tTenantIds = $tenantIdList; + TenantedDeploymentParticipation = $DeploymentType; +\tTenantTags = @()} + +$jsonRequest = $rawRequest | ConvertTo-Json -Depth 10 + +Write-Host \"Sending in the request $jsonRequest\" + +$machineUrl = \"$baseApiUrl/machines\" +$method = \"POST\" +if ($OverwriteExisting -and $machineId -ne $null) +{ +\t$machineUrl = \"$machineUrl/$machineId\" + \t$method = \"PUT\" +} + +Write-Host \"Posting to url $machineUrl\" +$machineResponse = Invoke-RestMethod $machineUrl -Headers $header -Method $method -Body $jsonRequest + +Write-Host \"Create machine's response: $machineResponse\"" + }, + "Parameters": [ + { + "Id": "e98dc4e2-0766-4d2d-a753-eafe294fdeea", + "Name": "RegisterAzureCluster.Octopus.Base.Url", + "Label": "Octopus Base Url", + "HelpText": "The base url of your Octopus Deploy instance. Example: https://samples.octopus.app", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cb3cdd41-3d1f-49c6-820e-acfa24bf5a88", + "Name": "RegisterAzureCluster.Octopus.Api.Key", + "Label": "Octopus Api Key", + "HelpText": "The API key of a user in Octopus Deploy who has permissions to register the cluster.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "1d3d8695-27a3-4d3e-9457-6b483253a609", + "Name": "RegisterAzureCluster.Azure.Account", + "Label": "Azure Account", + "HelpText": "The Azure Account with permissions to access the AKS cluster", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "ca9d9733-2032-466b-9e80-2aa6abd3c977", + "Name": "RegisterAzureCluster.AKS.Name", + "Label": "AKS Cluster Name", + "HelpText": "The name of the AKS Cluster Name to register with Octopus Deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1e3c6a4a-09da-4cd1-989f-93be59ad7f16", + "Name": "RegisterAzureCluster.ResourceGroup.Name", + "Label": "AKS Resource Group Name", + "HelpText": "Name of the Azure Resource Group where the AKS cluster is located.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fa4bd89d-bb86-4d3f-87ff-17125cd88f24", + "Name": "RegisterAzureCluster.Roles.List", + "Label": "Role CSV List", + "HelpText": "Comma separated list of environments to assign to the AKS cluster in Octopus Deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0c80326d-cdc1-439d-be6b-fbab4da42cda", + "Name": "RegisterAzureCluster.Environment.List", + "Label": "Environment CSV List", + "HelpText": "Comma separated list of environments to assign to the AKS cluster in Octopus Deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "667a18b3-1694-4333-87a4-9be712b59122", + "Name": "RegisterAzureCluster.Tenant.List", + "Label": "Tenant CSV List", + "HelpText": "(Optional) If this is for a tenant, the a comma separated list of tenants to assign the AKS cluster to in Octopus Deploy", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "db2de612-45bb-4e4a-ab95-64083dd80393", + "Name": "RegisterAzureCluster.Tenant.DeploymentType", + "Label": "Tenanted Deployments", + "HelpText": "Choose the kind of deployment where this deployment target should be included.", + "DefaultValue": "Untenanted", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Untenanted|Exclude from tenanted deployments (default) +Tenanted|Include only in tenanted deployments +TenantedOrUntenanted|Include in both tenanted and untenanted deployments" + } + }, + { + "Id": "5ef91f37-b13b-413d-955c-872c7f274c7e", + "Name": "RegisterAzureCluster.MachinePolicy.IdOrName", + "Label": "Machine Policy Id Or Name", + "HelpText": "Enter in the name or the Id of the Machine Policy in Octopus Deploy for the AKS Cluster.", + "DefaultValue": "Default Machine Policy", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c4c45925-d7a4-4e41-9c2d-a8d73a6292b1", + "Name": "RegisterAzureCluster.WorkerPool.IdOrName", + "Label": "Worker Pool Id or Name", + "HelpText": "The name or id of the worker pool all communication will go through for the K8s cluster. Leave blank for the default worker pool.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "94b90acb-a054-4886-91e0-79e9881709f9", + "Name": "RegisterAzureCluster.Overwrite.Existing", + "Label": "Overwrite Existing Registration", + "HelpText": "Indicates if the existing cluster should be overwritten", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2020-04-13T15:37:29.866Z", + "OctopusVersion": "2020.1.10", + "Type": "ActionTemplate" + }, + "Category": "octopus" + } diff --git a/step-templates/octopus-register-listening-target.json.human b/step-templates/octopus-register-listening-target.json.human new file mode 100644 index 000000000..02bbf00b9 --- /dev/null +++ b/step-templates/octopus-register-listening-target.json.human @@ -0,0 +1,303 @@ +{ + "Id": "6f95a351-a1a1-43d1-a378-410a9acf0e60", + "Name": "Register Listening Deployment Target with Octopus", + "Description": "Step template to Register an Listening Deployment Target with Octopus Deploy using the API. Useful when spinning up machines and you want to wait to register until the machine finishes installing all additional software.", + "ActionType": "Octopus.Script", + "Version": 2, + "Author": "octobob", + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$OctopusAPIKey = $OctopusParameters[\"RegisterListeningTarget.Octopus.Api.Key\"] +$RegistrationName = $OctopusParameters[\"RegisterListeningTarget.Machine.Name\"] +$RegistrationAddress = $OctopusParameters[\"RegisterListeningTarget.Machine.Address\"] +$OctopusUrl = $OctopusParameters[\"RegisterListeningTarget.Octopus.Base.Url\"] +$Roles = $OctopusParameters[\"RegisterListeningTarget.Roles.List\"] +$Environments = $OctopusParameters[\"RegisterListeningTarget.Environment.List\"] +$SpaceId = $OctopusParameters[\"Octopus.Space.Id\"] +$MachinePolicyIdOrName = $OctopusParameters[\"RegisterListeningTarget.MachinePolicy.IdOrName\"] +$Tenants = $OctopusParameters[\"RegisterListeningTarget.Tenant.List\"] +$DeploymentType = $OctopusParameters[\"RegisterListeningTarget.Tenant.DeploymentType\"] +$PortNumber = $OctopusParameters[\"RegisterListeningTarget.Machine.Port\"] +$OverwriteExisting = $OctopusParameters[\"RegisterListeningTarget.Overwrite.Existing\"] +$OverwriteExisting = $OverwriteExisting -eq \"True\" + +Write-Host \"Machine Name: $RegistrationName\" +Write-Host \"Machine Address: $RegistrationAddress\" +Write-Host \"Machine Port: $PortNumber\" +Write-Host \"Octopus Url: $OctopusUrl\" +Write-Host \"Role List: $Roles\" +Write-Host \"Environments: $Environments\" +Write-Host \"Machine Policy Name or Id: $MachinePolicyIdOrName\" +Write-Host \"Tenant List: $Tenants\" +Write-Host \"Deployment Type: $DeploymentType\" +Write-Host \"Overwrite Existing: $OverwriteExisting\" + +$header = New-Object \"System.Collections.Generic.Dictionary[[String],[String]]\" +$header.Add(\"X-Octopus-ApiKey\", $OctopusAPIKey) + +# If tenanted only deployments is set then you require at least one tenant in the tenant list +if($tenantList -eq $null -and $DeploymentType -eq \"Tenanted\"){ +\tFail-Step \"Tenanted only deployments require at least one associated tenants!\" +} + +$baseApiUrl = \"$OctopusUrl/api\" +$baseApiInformation = Invoke-RestMethod $baseApiUrl -Headers $header +if ((Get-Member -InputObject $baseApiInformation.Links -Name \"Spaces\" -MemberType Properties) -ne $null) +{ \t +\t$baseApiUrl = \"$baseApiUrl/$SpaceId\" +} + +Write-Host \"Base API Url: $baseApiUrl\" + +$existingMachineResultsUrl = \"$baseApiUrl/machines?partialName=$RegistrationName&skip=0&take=1000\" +Write-Host \"Attempting to find existing machine with similar name at $existingMachineResultsUrl\" +$existingMachineResponse = Invoke-RestMethod $existingMachineResultsUrl -Headers $header +Write-Host $existingMachineResponse + +$machineFound = $false +$machineId = $null +foreach ($item in $existingMachineResponse.Items) +{ +\tif ($item.Name -eq $RegistrationName) + { + \t$machineFound = $true + if ($OverwriteExisting) + { + \t$machineId = $item.Id + } + break + } +} + +if ($machineFound -and $OverwriteExisting -eq $false) +{ +\tWrite-Highlight \"Machine already exists, skipping registration\" + Exit 0 +} + +$roleList = $Roles -split \",\" +$environmentList = $Environments -split \",\" +$environmentIdList = @() +Write-Host \"Getting the ids for all environments specified\" +foreach($environment in $environmentList) +{ +\tWrite-Host \"Getting the id for the environment $environment\" + $environmentEscaped = $environment.Replace(\" \", \"%20\") + $environmentUrl = \"$baseApiUrl/environments?skip=0&take=1000&name=$environmentEscaped\" + $environmentResponse = Invoke-RestMethod $environmentUrl -Headers $header + + $environmentId = $environmentResponse.Items[0].Id + Write-Host \"The id for environment $environment is $environmentId\" + $environmentIdList += $environmentId +} +$tenantList = $Tenants -split \",\" +$tenantIdList = @() + +# If tenant list is null then no need to go trhough this or it will pick the first tenant from all the tenants in Octopus +if($tenantIdList -ne $null){ + +\tforeach($tenant in $tenantList) +\t{ +\t\tWrite-Host \"Getting the id for tenant $tenant\" +\t\t$tenantEscaped = $tenant.Replace(\" \", \"%20\") +\t\t$tenantUrl = \"$baseApiUrl/tenants?skip=0&take=1000&name=$tenantEscaped\" +\t\t$tenantResponse = Invoke-RestMethod $tenantUrl -Headers $header +\t +\t\t$tenantId = $tenantResponse.Items[0].Id +\t\tWrite-Host \"The id for tenant $tenant is $tenantId\" +\t\t$tenantIdList += $tenantId +\t} +\t +} + + + +$machinePolicyId = $machinePolicyIdOrName +if ($machinePolicyIdOrName.StartsWith(\"MachinePolicies-\") -eq $false) +{ +\tWrite-Host \"The machine policy specified $machinePolicyIdOrName appears to be a name\" +\t$machinePolicyNameEscaped = $machinePolicyIdOrName.Replace(\" \", \"%20\") +\t$machinePolicyResponse = Invoke-RestMethod \"$baseApiUrl/machinepolicies?partialName=$machinePolicyNameEscaped\" -Headers $header + + $machinePolicyId = $machinePolicyResponse.Items[0].Id + Write-Host \"The machine policy id is $machinePolicyId\" +} + +$discoverUrl = \"$baseApiUrl/machines/discover?host=$RegistrationAddress&port=$PortNumber&type=TentaclePassive\" +Write-Host \"Discovering the machine $discoverUrl\" +$discoverResponse = Invoke-RestMethod $discoverUrl -Headers $header +Write-Host \"ProjectResponse: $discoverResponse\" + +$machineThumbprint = $discoverResponse.EndPoint.Thumbprint +Write-Host \"Thumbprint = $machineThumbprint\" + +$rawRequest = @{ +\tId = $machineId; +\tMachinePolicyId = $machinePolicyId; +\tName = $RegistrationName; +\tIsDisabled = $false; +\tHealthStatus = \"Unknown\"; +\tHasLatestCalamari = $true; +\tStatusSummary = $null; +\tIsInProcess = $true; +\tEndpoint = @{ + \t\tId = $null; +\t\tCommunicationStyle = \"TentaclePassive\"; +\t\tLinks = $null; +\t\tUri = \"https://$RegistrationAddress`:$PortNumber\"; +\t\tThumbprint = \"$machineThumbprint\"; +\t\tProxyId = $null +\t}; +\tLinks = $null;\t +\tRoles = $roleList; +\tEnvironmentIds = $environmentIdList; +\tTenantIds = $tenantIdList; +\tTenantTags = $null; +\tTenantedDeploymentParticipation = $DeploymentType +} + +$jsonRequest = $rawRequest | ConvertTo-Json -Depth 10 + +Write-Host \"Sending in the request $jsonRequest\" + +$machineUrl = \"$baseApiUrl/machines\" + +$method = \"POST\" +if ($OverwriteExisting -and $machineId -ne $null) +{ +\t$machineUrl = \"$machineUrl/$machineId\" + \t$method = \"PUT\" +} +Write-Host \"Posting to url $machineUrl\" +$machineResponse = Invoke-RestMethod $machineUrl -Headers $header -Method $method -Body $jsonRequest + +Write-Host \"Create machine's response: $machineResponse\"" + }, + "Parameters": [ + { + "Id": "e98dc4e2-0766-4d2d-a753-eafe294fdeea", + "Name": "RegisterListeningTarget.Octopus.Base.Url", + "Label": "Octopus Base Url", + "HelpText": "The base url of your Octopus Deploy instance. Example: https://samples.octopus.app", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cb3cdd41-3d1f-49c6-820e-acfa24bf5a88", + "Name": "RegisterListeningTarget.Octopus.Api.Key", + "Label": "Octopus Api Key", + "HelpText": "The API key of a user in Octopus Deploy who has permissions to register the cluster.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ca9d9733-2032-466b-9e80-2aa6abd3c977", + "Name": "RegisterListeningTarget.Machine.Name", + "Label": "Machine Name", + "HelpText": "The name of the machine to register with Octopus Deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1d3d8695-27a3-4d3e-9457-6b483253a609", + "Name": "RegisterListeningTarget.Machine.Address", + "Label": "Machine Address", + "HelpText": "The machine address (IP Address or Domain Name) to connect to", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ea762a1e-07ee-4eec-af7a-b6e29bf274d8", + "Name": "RegisterListeningTarget.Machine.Port", + "Label": "Port Number", + "HelpText": "The port the tentacle is listening on", + "DefaultValue": "10933", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fa4bd89d-bb86-4d3f-87ff-17125cd88f24", + "Name": "RegisterListeningTarget.Roles.List", + "Label": "Role CSV List", + "HelpText": "Comma separated list of environments to assign to the machine in Octopus Deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0c80326d-cdc1-439d-be6b-fbab4da42cda", + "Name": "RegisterListeningTarget.Environment.List", + "Label": "Environment CSV List", + "HelpText": "Comma separated list of environments to assign to the the machine in Octopus Deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "667a18b3-1694-4333-87a4-9be712b59122", + "Name": "RegisterListeningTarget.Tenant.List", + "Label": "Tenant CSV List", + "HelpText": "(Optional) If this is for a tenant, the a comma separated list of tenants to assign the machine to in Octopus Deploy", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "db2de612-45bb-4e4a-ab95-64083dd80393", + "Name": "RegisterListeningTarget.Tenant.DeploymentType", + "Label": "Tenanted Deployments", + "HelpText": "Choose the kind of deployment where this deployment target should be included.", + "DefaultValue": "Untenanted", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Untenanted|Exclude from tenanted deployments (default) +Tenanted|Include only in tenanted deployments +TenantedOrUntenanted|Include in both tenanted and untenanted deployments" + } + }, + { + "Id": "5ef91f37-b13b-413d-955c-872c7f274c7e", + "Name": "RegisterListeningTarget.MachinePolicy.IdOrName", + "Label": "Machine Policy Id Or Name", + "HelpText": "Enter in the name or the Id of the Machine Policy in Octopus Deploy for the AKS Cluster.", + "DefaultValue": "Default Machine Policy", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5b2e0993-7f96-4447-b47b-029c8f225688", + "Name": "RegisterListeningTarget.Overwrite.Existing", + "Label": "Overwrite Existing Registration", + "HelpText": "Indicates if the existing listening tentacle should be overwritten in Octopus.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "adamoctoclose", + "$Meta": { + "ExportedAt": "2021-03-17T15:37:29.866Z", + "OctopusVersion": "2020.5.5", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} diff --git a/step-templates/octopus-register-listening-worker.json.human b/step-templates/octopus-register-listening-worker.json.human new file mode 100644 index 000000000..0b8075fb6 --- /dev/null +++ b/step-templates/octopus-register-listening-worker.json.human @@ -0,0 +1,243 @@ +{ + "Id": "e83b3265-64f9-4870-8802-54884c43eaf0", + "Name": "Register Listening Worker with Octopus", + "Description": "Step template to Register an Listening Worker with Octopus Deploy using the API. Useful for when you need to wait to install additional software and a restart when spinning up a new worker.", + "ActionType": "Octopus.Script", + "Version": 1, + "Author": "octobob", + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$OctopusAPIKey = $OctopusParameters[\"RegisterListeningWorker.Octopus.Api.Key\"] +$RegistrationName = $OctopusParameters[\"RegisterListeningWorker.Machine.Name\"] +$RegistrationAddress = $OctopusParameters[\"RegisterListeningWorker.Machine.Address\"] +$OctopusUrl = $OctopusParameters[\"RegisterListeningWorker.Octopus.Base.Url\"] +$WorkerPools = $OctopusParameters[\"RegisterListeningWorker.WorkerPool.List\"] +$SpaceId = $OctopusParameters[\"Octopus.Space.Id\"] +$MachinePolicyIdOrName = $OctopusParameters[\"RegisterListeningWorker.MachinePolicy.IdOrName\"] +$PortNumber = $OctopusParameters[\"RegisterListeningWorker.Machine.Port\"] +$OverwriteExisting = $OctopusParameters[\"RegisterListeningWorker.Overwrite.Existing\"] +$OverwriteExisting = $OverwriteExisting -eq \"True\" + + +Write-Host \"Machine Name: $RegistrationName\" +Write-Host \"Machine Address: $RegistrationAddress\" +Write-Host \"Machine Port: $PortNumber\" +Write-Host \"Octopus Url: $OctopusUrl\" +Write-Host \"Worker Pools: $WorkerPools\" +Write-Host \"Environments: $Environments\" +Write-Host \"Machine Policy Name or Id: $MachinePolicyIdOrName\" +Write-Host \"Overwrite Existing: $OverwriteExisting\" + +$header = New-Object \"System.Collections.Generic.Dictionary[[String],[String]]\" +$header.Add(\"X-Octopus-ApiKey\", $OctopusAPIKey) + +$baseApiUrl = \"$OctopusUrl/api\" +$baseApiInformation = Invoke-RestMethod $baseApiUrl -Headers $header +if ((Get-Member -InputObject $baseApiInformation.Links -Name \"Spaces\" -MemberType Properties) -ne $null) +{ \t +\t$baseApiUrl = \"$baseApiUrl/$SpaceId\" +} + +Write-Host \"Base API Url: $baseApiUrl\" + +$existingMachineResultsUrl = \"$baseApiUrl/workers?partialName=$RegistrationName&skip=0&take=1000\" +Write-Host \"Attempting to find existing machine with similar name at $existingMachineResultsUrl\" +$existingMachineResponse = Invoke-RestMethod $existingMachineResultsUrl -Headers $header +Write-Host $existingMachineResponse + +$machineFound = $false +foreach ($item in $existingMachineResponse.Items) +{ +\tif ($item.Name -eq $RegistrationName) + { + \t$machineFound = $true + if ($OverwriteExisting) + { + \t$machineId = $item.Id + } + break + } +} + +if ($machineFound -and $OverwriteExisting -eq $false) +{ +\tWrite-Highlight \"Machine already exists, skipping registration\" + Exit 0 +} + +$workerPoolList = $WorkerPools -split \",\" +$workerPoolIdList = @() +Write-Host \"Getting the ids for all environments specified\" +foreach($workerPool in $workerPoolList) +{ +\tWrite-Host \"Getting the id for the worker pool $workerPool\" + + if ($workerPool.StartsWith(\"WorkerPools-\") -eq $true) + { + \tWrite-Host \"The worker pool is already an id, using that instead of looking it up\" + \t$workerPoolIdList += $workerPool + } + else + { + \t$workerPoolEscaped = $workerPool.Replace(\" \", \"%20\") + $workerPoolUrl = \"$baseApiUrl/workerpools?skip=0&take=1000&partialName=$workerPoolEscaped\" + $workerPoolResponse = Invoke-RestMethod $workerPoolUrl -Headers $header + + $workerPoolId = $workerPoolResponse.Items[0].Id + Write-Host \"The id for worker pool $workerPool is $workerPoolId\" + $workerPoolIdList += $workerPoolId + } +} + +$machinePolicyId = $machinePolicyIdOrName +if ($machinePolicyIdOrName.StartsWith(\"MachinePolicies-\") -eq $false) +{ +\tWrite-Host \"The machine policy specified $machinePolicyIdOrName appears to be a name\" +\t$machinePolicyNameEscaped = $machinePolicyIdOrName.Replace(\" \", \"%20\") +\t$machinePolicyResponse = Invoke-RestMethod \"$baseApiUrl/machinepolicies?partialName=$machinePolicyNameEscaped\" -Headers $header + + $machinePolicyId = $machinePolicyResponse.Items[0].Id + Write-Host \"The machine policy id is $machinePolicyId\" +} + +$discoverUrl = \"$baseApiUrl/machines/discover?host=$RegistrationAddress&port=$PortNumber&type=TentaclePassive\" +Write-Host \"Discovering the machine $discoverUrl\" +$discoverResponse = Invoke-RestMethod $discoverUrl -Headers $header +Write-Host \"ProjectResponse: $discoverResponse\" + +$machineThumbprint = $discoverResponse.EndPoint.Thumbprint +Write-Host \"Thumbprint = $machineThumbprint\" + +$rawRequest = @{ + Id = $machineId; + MachinePolicyId = $MachinePolicyId; + Name = $RegistrationName; + IsDisabled = $false; + HealthStatus = \"Unknown\"; + HasLatestCalamari = $true; + StatusSummary = $null; + IsInProcess = $true; + Links = $null; + WorkerPoolIds = $workerPoolIdList; + Endpoint = @{ + Id = $null; + CommunicationStyle = \"TentaclePassive\"; + Links = $null; + Uri = \"https://$RegistrationAddress`:$PortNumber\"; + Thumbprint = \"$machineThumbprint\"; + ProxyId = $null + } +} + +$jsonRequest = $rawRequest | ConvertTo-Json -Depth 10 + +Write-Host \"Sending in the request $jsonRequest\" + +$machineUrl = \"$baseApiUrl/workers\" +$method = \"POST\" +if ($OverwriteExisting -and $machineId -ne $null) +{ +\t$machineUrl = \"$machineUrl/$machineId\" + \t$method = \"PUT\" +} + +Write-Host \"Posting to url $machineUrl\" +$machineResponse = Invoke-RestMethod $machineUrl -Headers $header -Method $method -Body $jsonRequest + +Write-Host \"Create workers's response: $machineResponse\"" + }, + "Parameters": [ + { + "Id": "e98dc4e2-0766-4d2d-a753-eafe294fdeea", + "Name": "RegisterListeningWorker.Octopus.Base.Url", + "Label": "Octopus Base Url", + "HelpText": "The base url of your Octopus Deploy instance. Example: https://samples.octopus.app", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cb3cdd41-3d1f-49c6-820e-acfa24bf5a88", + "Name": "RegisterListeningWorker.Octopus.Api.Key", + "Label": "Octopus Api Key", + "HelpText": "The API key of a user in Octopus Deploy who has permissions to register the cluster.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ca9d9733-2032-466b-9e80-2aa6abd3c977", + "Name": "RegisterListeningWorker.Machine.Name", + "Label": "Machine Name", + "HelpText": "The name of the machine to register with Octopus Deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1d3d8695-27a3-4d3e-9457-6b483253a609", + "Name": "RegisterListeningWorker.Machine.Address", + "Label": "Machine Address", + "HelpText": "The machine address (IP Address or Domain Name) to connect to", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ea762a1e-07ee-4eec-af7a-b6e29bf274d8", + "Name": "RegisterListeningWorker.Machine.Port", + "Label": "Port Number", + "HelpText": "The port the tentacle is listening on", + "DefaultValue": "10933", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fa4bd89d-bb86-4d3f-87ff-17125cd88f24", + "Name": "RegisterListeningWorker.WorkerPool.List", + "Label": "Worker Pool CSV List", + "HelpText": "Comma separated list of Worker Pools to assign the worker to in Octopus Deploy. This can be the worker pool name or the id.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5ef91f37-b13b-413d-955c-872c7f274c7e", + "Name": "RegisterListeningWorker.MachinePolicy.IdOrName", + "Label": "Machine Policy Id Or Name", + "HelpText": "Enter in the name or the Id of the Machine Policy in Octopus Deploy for the AKS Cluster.", + "DefaultValue": "Default Machine Policy", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d456924c-3720-4893-9a40-36bb1c00b331", + "Name": "RegisterListeningWorker.Overwrite.Existing", + "Label": "Overwrite Existing Registration", + "HelpText": "Indicates if the existing worker should be overwritten", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2020-04-13T15:37:29.866Z", + "OctopusVersion": "2020.1.10", + "Type": "ActionTemplate" + }, + "Category": "octopus" + } diff --git a/step-templates/octopus-serialize-project-to-terraform.json.human b/step-templates/octopus-serialize-project-to-terraform.json.human new file mode 100644 index 000000000..60ac29eaf --- /dev/null +++ b/step-templates/octopus-serialize-project-to-terraform.json.human @@ -0,0 +1,615 @@ +{ + "Id": "e9526501-09d5-490f-ac3f-5079735fe041", + "Name": "Octopus - Serialize Project to Terraform", + "Description": "Serialize an Octopus project as a Terraform module and upload the resulting package to the Octopus built in feed. + +This step uses naming conventions to exclude resources from the generated module: + +* Variables starting with `Private.` are excluded +* Runbooks starting with `__ ` are excluded +* The environment called `Sync` is removed from any variable scopes + +Because serializing Terraform modules is done via the API, the values of any secret variables are not available, and are not included in the module generated by this step. + +However, by following a variable naming and scoping convention, it is possible to export and then apply a project in a Terraform module recreating secret variables, without ever including the secrets in the exported module. + +The project to be exported must define all secret variables with a unique name and a single value. For example, the secret variable `Test.Database.Password` can be scoped to the `Test` environment and the secret variable `Production.Database.Password` can be scoped to the `Production` environment. You can not have a single secret variable called `Database.Password` with two values for the different environments though. + +To collapse the unique secret variables into a single variable used by steps, it is possible to create a non-secret variable called `Database.Password` with two values `#{Test.Database.Password}` and `#{Production.Database.Password}` scoped to appropriate environments. + +In this way steps can still reference a single variable called `Database.Password`, but all secret variables have unique names and only one value. + +All secret variables are then scoped to an additional environment called `Sync`, which means all secret variables are exposed to runbooks run in the `Step` environment. The `Sync` environment is used to apply the Terraform module exported by this step, `Apply a Terraform template` step to perform variable replacements with secret variables. + +The secret values in the Terraform module then have default values set to the Octostache template referencing the secret variable. For example, the Octopus variables in the Terraform module have default values like `#{Test.Database.Password}` and `#{Production.Database.Password}`. These templates are replaced at runtime by the `Apply a Terraform template` step, run in the `Sync` environment, effectively injecting the secret values back into the newly created project. + +This allows secret variables to be recreated with their original values, without ever exporting the secret values. ", + "ActionType": "Octopus.Script", + "Version": 10, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptBody": "import argparse +import os +import stat +import re +import socket +import subprocess +import sys +from datetime import datetime +from urllib.parse import urlparse +import urllib.request +from itertools import chain +import platform +from urllib.request import urlretrieve +import zipfile +import json +import tarfile +import random, time + +# If this script is not being run as part of an Octopus step, return variables from environment variables. +# Periods are replaced with underscores, and the variable name is converted to uppercase +if \"get_octopusvariable\" not in globals(): + def get_octopusvariable(variable): + return os.environ[re.sub('\\\\.', '_', variable.upper())] + +# If this script is not being run as part of an Octopus step, print directly to std out. +if \"printverbose\" not in globals(): + def printverbose(msg): + print(msg) + + +def printverbose_noansi(output): + \"\"\" + Strip ANSI color codes and print the output as verbose + :param output: The output to print + \"\"\" + if not output: + return + + # https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python + output_no_ansi = re.sub(r'\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])', '', output) + printverbose(output_no_ansi) + + +def get_octopusvariable_quiet(variable): + \"\"\" + Gets an octopus variable, or an empty string if it does not exist. + :param variable: The variable name + :return: The variable value, or an empty string if the variable does not exist + \"\"\" + try: + return get_octopusvariable(variable) + except: + return '' + + +def retry_with_backoff(fn, retries=5, backoff_in_seconds=1): + x = 0 + while True: + try: + return fn() + except Exception as e: + + print(e) + + if x == retries: + raise + + sleep = (backoff_in_seconds * 2 ** x + + random.uniform(0, 1)) + time.sleep(sleep) + x += 1 + + +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi): + \"\"\" + The execute method provides the ability to execute external processes while capturing and returning the + output to std err and std out and exit code. + \"\"\" + process = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=cwd, + env=env) + stdout, stderr = process.communicate() + retcode = process.returncode + + if print_args is not None: + print_output(' '.join(args)) + + if print_output is not None: + print_output(stdout) + print_output(stderr) + + return stdout, stderr, retcode + + +def is_windows(): + return platform.system() == 'Windows' + + +def init_argparse(): + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTION] [FILE]...', + description='Serialize an Octopus project to a Terraform module' + ) + parser.add_argument('--ignore-all-changes', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Project.IgnoreAllChanges') or get_octopusvariable_quiet( + 'Exported.Project.IgnoreAllChanges') or 'false', + help='Set to true to set the \"lifecycle.ignore_changes\" ' + + 'setting on each exported resource to \"all\"') + parser.add_argument('--ignore-variable-changes', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Project.IgnoreVariableChanges') or get_octopusvariable_quiet( + 'Exported.Project.IgnoreVariableChanges') or 'false', + help='Set to true to set the \"lifecycle.ignore_changes\" ' + + 'setting on each exported octopus variable to \"all\"') + parser.add_argument('--terraform-backend', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.ThisInstance.Terraform.Backend') or get_octopusvariable_quiet( + 'ThisInstance.Terraform.Backend') or 'pg', + help='Set this to the name of the Terraform backend to be included in the generated module.') + parser.add_argument('--server-url', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.ThisInstance.Server.Url') or get_octopusvariable_quiet( + 'ThisInstance.Server.Url'), + help='Sets the server URL that holds the project to be serialized.') + parser.add_argument('--api-key', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.ThisInstance.Api.Key') or get_octopusvariable_quiet( + 'ThisInstance.Api.Key'), + help='Sets the Octopus API key.') + parser.add_argument('--space-id', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Space.Id') or get_octopusvariable_quiet( + 'Exported.Space.Id') or get_octopusvariable_quiet('Octopus.Space.Id'), + help='Set this to the space ID containing the project to be serialized.') + parser.add_argument('--project-name', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Project.Name') or get_octopusvariable_quiet( + 'Exported.Project.Name') or get_octopusvariable_quiet( + 'Octopus.Project.Name'), + help='Set this to the name of the project to be serialized.') + parser.add_argument('--upload-space-id', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Octopus.UploadSpace.Id') or get_octopusvariable_quiet( + 'Octopus.UploadSpace.Id') or get_octopusvariable_quiet('Octopus.Space.Id'), + help='Set this to the space ID of the Octopus space where ' + + 'the resulting package will be uploaded to.') + parser.add_argument('--ignore-cac-managed-values', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Project.IgnoreCacValues') or get_octopusvariable_quiet( + 'Exported.Project.IgnoreCacValues') or 'false', + help='Set this to true to exclude cac managed values like non-secret variables, ' + + 'deployment processes, and project versioning into the Terraform module. ' + + 'Set to false to have these values embedded into the module.') + parser.add_argument('--exclude-cac-project-settings', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Project.ExcludeCacProjectValues') or get_octopusvariable_quiet( + 'Exported.Project.ExcludeCacProjectValues') or 'false', + help='Set this to true to exclude CaC settings like git connections from the exported module.') + parser.add_argument('--ignored-library-variable-sets', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Project.IgnoredLibraryVariableSet') or get_octopusvariable_quiet( + 'Exported.Project.IgnoredLibraryVariableSet'), + help='A comma separated list of library variable sets to ignore.') + parser.add_argument('--ignored-accounts', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Project.IgnoredAccounts') or get_octopusvariable_quiet( + 'Exported.Project.IgnoredAccounts'), + help='A comma separated list of accounts to ignore.') + parser.add_argument('--include-step-templates', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Project.IncludeStepTemplates') or get_octopusvariable_quiet( + 'Exported.Project.IncludeStepTemplates') or 'false', + help='Set this to true to include step templates in the exported module. ' + + 'This disables the default behaviour of detaching step templates.') + parser.add_argument('--lookup-project-link-tenants', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Project.LookupProjectLinkTenants') or get_octopusvariable_quiet( + 'Exported.Project.LookupProjectLinkTenants') or 'false', + help='Set this option to link tenants and create tenant project variables.') + + return parser.parse_known_args() + + +def get_latest_github_release(owner, repo, filename): + url = f\"https://api.github.com/repos/{owner}/{repo}/releases/latest\" + releases = urllib.request.urlopen(url).read() + contents = json.loads(releases) + + download = [asset for asset in contents.get('assets') if asset.get('name') == filename] + + if len(download) != 0: + return download[0].get('browser_download_url') + + return None + + +def ensure_octo_cli_exists(): + if is_windows(): + print(\"Checking for the Octopus CLI\") + try: + stdout, _, exit_code = execute(['octo.exe', 'help']) + printverbose(stdout) + if not exit_code == 0: + raise \"Octo CLI not found\" + return \"\" + except: + print(\"Downloading the Octopus CLI\") + urlretrieve('https://download.octopusdeploy.com/octopus-tools/9.0.0/OctopusTools.9.0.0.win-x64.zip', + 'OctopusTools.zip') + with zipfile.ZipFile('OctopusTools.zip', 'r') as zip_ref: + zip_ref.extractall(os.getcwd()) + return os.getcwd() + else: + print(\"Checking for the Octopus CLI for Linux\") + try: + stdout, _, exit_code = execute(['octo', 'help']) + printverbose(stdout) + if not exit_code == 0: + raise \"Octo CLI not found\" + return \"\" + except: + print(\"Downloading the Octopus CLI for Linux\") + urlretrieve('https://download.octopusdeploy.com/octopus-tools/9.0.0/OctopusTools.9.0.0.linux-x64.tar.gz', + 'OctopusTools.tar.gz') + with tarfile.open('OctopusTools.tar.gz') as file: + file.extractall(os.getcwd()) + os.chmod(os.path.join(os.getcwd(), 'octo'), stat.S_IRWXO | stat.S_IRWXU | stat.S_IRWXG) + return os.getcwd() + + +def ensure_octoterra_exists(): + if is_windows(): + print(\"Checking for the Octoterra tool for Windows\") + try: + stdout, _, exit_code = execute(['octoterra.exe', '-version']) + printverbose(stdout) + if not exit_code == 0: + raise \"Octoterra not found\" + return \"\" + except: + print(\"Downloading Octoterra CLI for Windows\") + retry_with_backoff(lambda: urlretrieve( + \"https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport/releases/latest/download/octoterra_windows_amd64.exe\", + 'octoterra.exe'), 10, 30) + return os.getcwd() + else: + print(\"Checking for the Octoterra tool for Linux\") + try: + stdout, _, exit_code = execute(['octoterra', '-version']) + printverbose(stdout) + if not exit_code == 0: + raise \"Octoterra not found\" + return \"\" + except: + print(\"Downloading Octoterra CLI for Linux\") + retry_with_backoff(lambda: urlretrieve( + \"https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport/releases/latest/download/octoterra_linux_amd64\", + 'octoterra'), 10, 30) + os.chmod(os.path.join(os.getcwd(), 'octoterra'), stat.S_IRWXO | stat.S_IRWXU | stat.S_IRWXG) + return os.getcwd() + + +octocli_path = ensure_octo_cli_exists() +octoterra_path = ensure_octoterra_exists() +parser, _ = init_argparse() + +# Variable precondition checks +if len(parser.server_url) == 0: + print(\"--server-url, ThisInstance.Server.Url, or SerializeProject.ThisInstance.Server.Url must be defined\") + sys.exit(1) + +if len(parser.api_key) == 0: + print(\"--api-key, ThisInstance.Api.Key, or ThisInstance.Api.Key must be defined\") + sys.exit(1) + +print(\"Octopus URL: \" + parser.server_url) +print(\"Octopus Space ID: \" + parser.space_id) + +# Build the arguments to ignore library variable sets +ignores_library_variable_sets = parser.ignored_library_variable_sets.split(',') +ignores_library_variable_sets_args = [['-excludeLibraryVariableSet', x] for x in ignores_library_variable_sets] + +# Build the arguments to ignore accounts +ignored_accounts = parser.ignored_accounts.split(',') +ignored_accounts = [['-excludeAccounts', x] for x in ignored_accounts] + +os.mkdir(os.getcwd() + '/export') + +export_args = [os.path.join(octoterra_path, 'octoterra'), + # the url of the instance + '-url', parser.server_url, + # the api key used to access the instance + '-apiKey', parser.api_key, + # add a postgres backend to the generated modules + '-terraformBackend', parser.terraform_backend, + # dump the generated HCL to the console + '-console', + # dump the project from the current space + '-space', parser.space_id, + # the name of the project to serialize + '-projectName', parser.project_name, + # ignoreProjectChanges can be set to ignore all changes to the project, variables, runbooks etc + '-ignoreProjectChanges=' + parser.ignore_all_changes, + # use data sources to lookup external dependencies (like environments, accounts etc) rather + # than serialize those external resources + '-lookupProjectDependencies', + # for any secret variables, add a default value set to the octostache value of the variable + # e.g. a secret variable called \"database\" has a default value of \"#{database}\" + '-defaultSecretVariableValues', + # Any value that can't be replaced with an Octostache template, add a dummy value + '-dummySecretVariableValues', + # detach any step templates, allowing the exported project to be used in a new space + '-detachProjectTemplates=' + str(not parser.include_step_templates), + # allow the downstream project to move between project groups + '-ignoreProjectGroupChanges', + # allow the downstream project to change names + '-ignoreProjectNameChanges', + # CaC enabled projects will not export the deployment process, non-secret variables, and other + # CaC managed project settings if ignoreCacManagedValues is true. It is usually desirable to + # set this value to true, but it is false here because CaC projects created by Terraform today + # save some variables in the database rather than writing them to the Git repo. + '-ignoreCacManagedValues=' + parser.ignore_cac_managed_values, + # Excluding CaC values means the resulting module does not include things like git credentials. + # Setting excludeCaCProjectSettings to true and ignoreCacManagedValues to false essentially + # converts a CaC project back to a database project. + '-excludeCaCProjectSettings=' + parser.exclude_cac_project_settings, + # This value is always true. Either this is an unmanaged project, in which case we are never + # reapplying it; or it is a variable configured project, in which case we need to ignore + # variable changes, or it is a shared CaC project, in which case we don't use Terraform to + # manage variables. + '-ignoreProjectVariableChanges=' + parser.ignore_variable_changes, + # To have secret variables available when applying a downstream project, they must be scoped + # to the Sync environment. But we do not need this scoping in the downstream project, so the + # Sync environment is removed from any variable scopes when serializing it to Terraform. + '-excludeVariableEnvironmentScopes', 'Sync', + # Exclude any variables starting with \"Private.\" + '-excludeProjectVariableRegex', 'Private\\\\..*', + # Capture the octopus endpoint, space ID, and space name as output vars. This is useful when + # querying th Terraform state file to know which space and instance the resources were + # created in. The scripts used to update downstream projects in bulk work by querying the + # Terraform state, finding all the downstream projects, and using the space name to only process + # resources that match the current tenant (because space names and tenant names are the same). + # The output variables added by this option are octopus_server, octopus_space_id, and + # octopus_space_name. + '-includeOctopusOutputVars', + # Where steps do not explicitly define a worker pool and reference the default one, this + # option explicitly exports the default worker pool by name. This means if two spaces have + # different default pools, the exported project still uses the pool that the original project + # used. + '-lookUpDefaultWorkerPools', + # Link any tenants that were originally link to the project and create project tenant variables + '-lookupProjectLinkTenants=' + parser.lookup_project_link_tenants, + # Add support for experimental step templates + '-experimentalEnableStepTemplates=' + parser.include_step_templates, + # The directory where the exported files will be saved + '-dest', os.getcwd() + '/export', + # This is a management runbook that we do not wish to export + '-excludeRunbookRegex', '__ .*'] + list(chain(*ignores_library_variable_sets_args)) + list( + chain(*ignored_accounts)) + +print(\"Exporting Terraform module\") +_, _, octoterra_exit = execute(export_args) + +if not octoterra_exit == 0: + print(\"Octoterra failed. Please check the logs for more information.\") + sys.exit(1) + +date = datetime.now().strftime('%Y.%m.%d.%H%M%S') + +print(\"Creating Terraform module package\") +if is_windows(): + execute([os.path.join(octocli_path, 'octo.exe'), + 'pack', + '--format', 'zip', + '--id', re.sub('[^0-9a-zA-Z]', '_', parser.project_name), + '--version', date, + '--basePath', os.getcwd() + '\\\\export', + '--outFolder', os.getcwd()]) +else: + _, _, _ = execute([os.path.join(octocli_path, 'octo'), + 'pack', + '--format', 'zip', + '--id', re.sub('[^0-9a-zA-Z]', '_', parser.project_name), + '--version', date, + '--basePath', os.getcwd() + '/export', + '--outFolder', os.getcwd()]) + +print(\"Uploading Terraform module package\") +if is_windows(): + _, _, _ = execute([os.path.join(octocli_path, 'octo.exe'), + 'push', + '--apiKey', parser.api_key, + '--server', parser.server_url, + '--space', parser.upload_space_id, + '--package', os.getcwd() + \"\\\\\" + + re.sub('[^0-9a-zA-Z]', '_', parser.project_name) + '.' + date + '.zip', + '--replace-existing']) +else: + _, _, _ = execute([os.path.join(octocli_path, 'octo'), + 'push', + '--apiKey', parser.api_key, + '--server', parser.server_url, + '--space', parser.upload_space_id, + '--package', os.getcwd() + \"/\" + + re.sub('[^0-9a-zA-Z]', '_', parser.project_name) + '.' + date + '.zip', + '--replace-existing']) + +print(\"##octopus[stdout-default]\") + +print(\"Done\") +", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Python" + }, + "Parameters": [ + { + "Id": "ca62a702-6eb3-4d01-b645-73fbb2a1ea86", + "Name": "SerializeProject.Exported.Project.IgnoreAllChanges", + "Label": "Ignore All Changes", + "HelpText": "Selecting this option creates a Terraform module with the \"lifecycle.ignore_changes\" option set to \"all\" for all project resources. This allows the resources to be created if they do not exist, but won't update them if the module is reapplied. This value effectively enables the \"Ignore Variable Changes\" option.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "05fafe0b-b05f-4e7b-85c3-857b62dc4182", + "Name": "SerializeProject.Exported.Project.IgnoreVariableChanges", + "Label": "Ignore Variable Changes", + "HelpText": "Selecting this option creates a Terraform module with the \"lifecycle.ignore_changes\" option set to \"all\" for Octopus variables (i.e. \"octopusdeploy_variable\" resources)\". This allows Octopus variables to be created if they do not exist, but won't update Octopus variable values if the module is reapplied. This value effectively enabled if the \"Ignore All Changes\" option is enabled.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "3b8f35b6-fc1a-442b-ae0e-3036f5436a7a", + "Name": "SerializeProject.Exported.Project.IgnoreCacValues", + "Label": "Ignore CaC Settings", + "HelpText": "Enable this option to exclude any Config-as-Code managed resources from the exported module, such as non-secret variables, deployment process, and CaC defined project settings. This option is useful when you are exporting CaC enabled projects and do not wish to include any settings in the exported module that are managed by Git. Disable this option, and enable the \"Exclude CaC Settings\" option to essentially convert CaC projects to regular projects.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "5c315650-9ba8-48b8-a02c-269315277fea", + "Name": "SerializeProject.Exported.Project.ExcludeCacProjectValues", + "Label": "Exclude CaC Settings", + "HelpText": "Enable this option to exclude Config-as-Code settings from the exported module, such as Git credentials and the version controlled flag. Enable this option, and disable the \"Ignore CaC Settings\" option to essentially convert CaC projects to regular projects.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "9c18e779-bddc-4f74-81c6-9d75babc9c9c", + "Name": "SerializeProject.ThisInstance.Terraform.Backend", + "Label": "Terraform Backend", + "HelpText": "The [backed](https://developer.hashicorp.com/terraform/language/settings/backends/configuration) to define in the Terraform module.", + "DefaultValue": "s3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f96cd929-1c18-4f7c-9121-82904e64834e", + "Name": "SerializeProject.ThisInstance.Server.Url", + "Label": "Octopus Server URL", + "HelpText": "The URL of the Octopus Server hosting the project to be serialized.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "95e0f611-d1f2-4317-ad5e-9131de73bbbe", + "Name": "SerializeProject.ThisInstance.Api.Key", + "Label": "Octopus API Key", + "HelpText": "The Octopus API Key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "9473de1f-a633-41d0-ba2c-6b622ce65551", + "Name": "SerializeProject.Exported.Space.Id", + "Label": "Octopus Space ID", + "HelpText": "The Space ID containing the project to be exported", + "DefaultValue": "#{Octopus.Space.Id}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9d8af8e2-307a-4149-a28e-f75cec9ee044", + "Name": "SerializeProject.Exported.Project.Name", + "Label": "Octopus Project Name", + "HelpText": "The name of the project to serialize.", + "DefaultValue": "#{Octopus.Project.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1b8ce71f-0931-4966-805a-e6d0ec12e3a0", + "Name": "SerializeProject.Octopus.UploadSpace.Id", + "Label": "Octopus Upload Space ID", + "HelpText": "The ID of the space to upload the Terraform package to. Leave this blank to upload to the space defined in the `Octopus Space ID` parameter.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "aec82033-cae1-4a18-a315-c70468f71539", + "Name": "SerializeProject.Exported.Project.IgnoredAccounts", + "Label": "Ignored Accounts", + "HelpText": "A comma separated list of accounts that will not be included in the Terraform module. These accounts are often those used by Runbooks that are not included in the module, and so do not need to be referenced.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e45abab5-cb8f-4af2-b3e9-3cde057907df", + "Name": "Exported.Project.IgnoredLibraryVariableSet", + "Label": "Ignored Library Variables Sets", + "HelpText": "A comma separated list of library variables sets that will not be included in the Terraform module. These library variable sets are often those used by Runbooks that are not included in the module, and so do not need to be referenced.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e456bc3f-a537-4982-8963-a091d3f31cf0", + "Name": "SerializeProject.Exported.Project.IncludeStepTemplates", + "Label": "Include Step Templates", + "HelpText": "Enable this option to export step templates referenced by a project. Disable this option to have step templates detached in projects instead.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "d9dacd25-5b0f-4f3e-89e2-08eefc0ffb89", + "Name": "SerializeProject.Exported.Project.LookupProjectLinkTenants", + "Label": "Link Tenants and Create Tenant Variables", + "HelpText": "Enable this option to have each project link any tenants and create project tenant variables.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-09-11T21:06:55.101Z", + "OctopusVersion": "2023.4.2151", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-serialize-runbook-to-terraform.json.human b/step-templates/octopus-serialize-runbook-to-terraform.json.human new file mode 100644 index 000000000..380e19397 --- /dev/null +++ b/step-templates/octopus-serialize-runbook-to-terraform.json.human @@ -0,0 +1,402 @@ +{ + "Id": "07b966c3-130c-4f13-ae0f-5105af5b97a1", + "Name": "Octopus - Serialize Runbook to Terraform", + "Description": "Serialize an Octopus runbook as a Terraform module and upload the resulting package to the Octopus built in feed. + +Note the exported runbooks do not include project variables, so any project that the exported runbook is attached to must already have all project and library variables defined.", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptBody": "import argparse +import os +import re +import socket +import subprocess +import sys +from datetime import datetime +from urllib.parse import urlparse +from itertools import chain +import platform +from urllib.request import urlretrieve +import zipfile + +# If this script is not being run as part of an Octopus step, return variables from environment variables. +# Periods are replaced with underscores, and the variable name is converted to uppercase +if \"get_octopusvariable\" not in globals(): + def get_octopusvariable(variable): + return os.environ[re.sub('\\\\.', '_', variable.upper())] + +# If this script is not being run as part of an Octopus step, print directly to std out. +if \"printverbose\" not in globals(): + def printverbose(msg): + print(msg) + + +def printverbose_noansi(output): + \"\"\" + Strip ANSI color codes and print the output as verbose + :param output: The output to print + \"\"\" + output_no_ansi = re.sub('\\x1b\\[[0-9;]*m', '', output) + printverbose(output_no_ansi) + + +def get_octopusvariable_quiet(variable): + \"\"\" + Gets an octopus variable, or an empty string if it does not exist. + :param variable: The variable name + :return: The variable value, or an empty string if the variable does not exist + \"\"\" + try: + return get_octopusvariable(variable) + except: + return '' + + +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi): + \"\"\" + The execute method provides the ability to execute external processes while capturing and returning the + output to std err and std out and exit code. + \"\"\" + process = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=cwd, + env=env) + stdout, stderr = process.communicate() + retcode = process.returncode + + if print_args is not None: + print_output(' '.join(args)) + + if print_output is not None: + print_output(stdout) + print_output(stderr) + + return stdout, stderr, retcode + + +def is_windows(): + return platform.system() == 'Windows' + + +def init_argparse(): + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTION] [FILE]...', + description='Serialize an Octopus project to a Terraform module' + ) + parser.add_argument('--ignore-all-changes', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Project.IgnoreAllChanges') or get_octopusvariable_quiet( + 'Exported.Project.IgnoreAllChanges') or 'false', + help='Set to true to set the \"lifecycle.ignore_changes\" ' + + 'setting on each exported resource to \"all\"') + parser.add_argument('--terraform-backend', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.ThisInstance.Terraform.Backend') or get_octopusvariable_quiet( + 'ThisInstance.Terraform.Backend') or 'pg', + help='Set this to the name of the Terraform backend to be included in the generated module.') + parser.add_argument('--server-url', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.ThisInstance.Server.Url') or get_octopusvariable_quiet( + 'ThisInstance.Server.Url'), + help='Sets the server URL that holds the project to be serialized.') + parser.add_argument('--api-key', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.ThisInstance.Api.Key') or get_octopusvariable_quiet( + 'ThisInstance.Api.Key'), + help='Sets the Octopus API key.') + parser.add_argument('--space-id', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Space.Id') or get_octopusvariable_quiet( + 'Exported.Space.Id') or get_octopusvariable_quiet('Octopus.Space.Id'), + help='Set this to the space ID containing the project to be serialized.') + parser.add_argument('--project-name', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Project.Name') or get_octopusvariable_quiet( + 'Exported.Project.Name') or get_octopusvariable_quiet( + 'Octopus.Project.Name'), + help='Set this to the name of the project to be serialized.') + parser.add_argument('--runbook-name', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Exported.Runbook.Name') or get_octopusvariable_quiet( + 'Exported.Runbook.Name'), + help='Set this to the name of the project to be serialized.') + parser.add_argument('--upload-space-id', + action='store', + default=get_octopusvariable_quiet( + 'SerializeProject.Octopus.UploadSpace.Id') or get_octopusvariable_quiet( + 'Octopus.UploadSpace.Id') or get_octopusvariable_quiet('Octopus.Space.Id'), + help='Set this to the space ID of the Octopus space where ' + + 'the resulting package will be uploaded to.') + + return parser.parse_known_args() + + +def ensure_octo_cli_exists(): + if is_windows(): + print(\"Checking for the Octopus CLI\") + try: + stdout, _, exit_code = execute(['octo', 'help']) + printverbose(stdout) + if not exit_code == 0: + raise \"Octo CLI not found\" + except: + print(\"Downloading the Octopus CLI\") + urlretrieve('https://download.octopusdeploy.com/octopus-tools/9.0.0/OctopusTools.9.0.0.win-x64.zip', + 'OctopusTools.zip') + with zipfile.ZipFile('OctopusTools.zip', 'r') as zip_ref: + zip_ref.extractall(os.getcwd()) + + +def check_docker_exists(): + try: + stdout, _, exit_code = execute(['docker', 'version']) + printverbose(stdout) + if not exit_code == 0: + raise \"Docker not found\" + except: + print('Docker must be installed: https://docs.docker.com/get-docker/') + sys.exit(1) + + +check_docker_exists() +ensure_octo_cli_exists() +parser, _ = init_argparse() + +# Variable precondition checks +if len(parser.server_url) == 0: + print(\"--server-url, ThisInstance.Server.Url, or SerializeProject.ThisInstance.Server.Url must be defined\") + sys.exit(1) + +if len(parser.api_key) == 0: + print(\"--api-key, ThisInstance.Api.Key, or ThisInstance.Api.Key must be defined\") + sys.exit(1) + +octoterra_image = 'ghcr.io/octopussolutionsengineering/octoterra-windows' if is_windows() else 'ghcr.io/octopussolutionsengineering/octoterra' +octoterra_mount = 'C:/export' if is_windows() else '/export' + +print(\"Pulling the Docker images\") +execute(['docker', 'pull', octoterra_image]) + +if not is_windows(): + execute(['docker', 'pull', 'ghcr.io/octopusdeploylabs/octo']) + +# Find out the IP address of the Octopus container +parsed_url = urlparse(parser.server_url) +octopus = socket.getaddrinfo(parsed_url.hostname, '80')[0][4][0] + +print(\"Octopus hostname: \" + parsed_url.hostname) +print(\"Octopus IP: \" + octopus.strip()) + +os.mkdir(os.getcwd() + '/export') + +export_args = ['docker', 'run', + '--rm', + '--add-host=' + parsed_url.hostname + ':' + octopus.strip(), + '-v', os.getcwd() + '/export:' + octoterra_mount, + octoterra_image, + # the url of the instance + '-url', parser.server_url, + # the api key used to access the instance + '-apiKey', parser.api_key, + # add a postgres backend to the generated modules + '-terraformBackend', parser.terraform_backend, + # dump the generated HCL to the console + '-console', + # dump the project from the current space + '-space', parser.space_id, + # the name of the project to serialize + '-projectName', parser.project_name, + # the name of the runbook to serialize + '-runbookName', parser.runbook_name, + # ignoreProjectChanges can be set to ignore all changes to the project, variables, runbooks etc + '-ignoreProjectChanges=' + parser.ignore_all_changes, + # for any secret variables, add a default value set to the octostache value of the variable + # e.g. a secret variable called \"database\" has a default value of \"#{database}\" + '-defaultSecretVariableValues', + # detach any step templates, allowing the exported project to be used in a new space + '-detachProjectTemplates', + # Capture the octopus endpoint, space ID, and space name as output vars. This is useful when + # querying th Terraform state file to know which space and instance the resources were + # created in. The scripts used to update downstream projects in bulk work by querying the + # Terraform state, finding all the downstream projects, and using the space name to only process + # resources that match the current tenant (because space names and tenant names are the same). + # The output variables added by this option are octopus_server, octopus_space_id, and + # octopus_space_name. + '-includeOctopusOutputVars', + # Where steps do not explicitly define a worker pool and reference the default one, this + # option explicitly exports the default worker pool by name. This means if two spaces have + # different default pools, the exported project still uses the pool that the original project + # used. + '-lookUpDefaultWorkerPools', + # These tenants are linked to the project to support some management runbooks, but should not + # be exported + '-excludeAllTenants', + # The directory where the exported files will be saved + '-dest', octoterra_mount] + +print(\"Exporting Terraform module\") +_, _, octoterra_exit = execute(export_args) + +if not octoterra_exit == 0: + print(\"Octoterra failed. Please check the logs for more information.\") + sys.exit(1) + +date = datetime.now().strftime('%Y.%m.%d.%H%M%S') + +print(\"Creating Terraform module package\") +if is_windows(): + execute(['octo', + 'pack', + '--format', 'zip', + '--id', re.sub('[^0-9a-zA-Z]', '_', parser.project_name + \"_\" + parser.runbook_name), + '--version', date, + '--basePath', os.getcwd() + '\\\\export', + '--outFolder', 'C:\\\\export']) +else: + _, _, _ = execute(['docker', 'run', + '--rm', + '--add-host=' + parsed_url.hostname + ':' + octopus.strip(), + '-v', os.getcwd() + \"/export:/export\", + 'ghcr.io/octopusdeploylabs/octo', + 'pack', + '--format', 'zip', + '--id', re.sub('[^0-9a-zA-Z]', '_', parser.project_name + \"_\" + parser.runbook_name), + '--version', date, + '--basePath', '/export', + '--outFolder', '/export']) + +print(\"Uploading Terraform module package\") +if is_windows(): + _, _, _ = execute(['octo', + 'push', + '--apiKey', parser.api_key, + '--server', parser.server_url, + '--space', parser.upload_space_id, + '--package', 'C:\\\\export\\\\' + + re.sub('[^0-9a-zA-Z]', '_', parser.project_name + \"_\" + parser.runbook_name) + '.' + date + '.zip', + '--replace-existing']) +else: + _, _, _ = execute(['docker', 'run', + '--rm', + '--add-host=' + parsed_url.hostname + ':' + octopus.strip(), + '-v', os.getcwd() + \"/export:/export\", + 'ghcr.io/octopusdeploylabs/octo', + 'push', + '--apiKey', parser.api_key, + '--server', parser.server_url, + '--space', parser.upload_space_id, + '--package', '/export/' + + re.sub('[^0-9a-zA-Z]', '_', parser.project_name + \"_\" + parser.runbook_name) + '.' + date + '.zip', + '--replace-existing']) + +print(\"##octopus[stdout-default]\") + +print(\"Done\") +", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Python" + }, + "Parameters": [ + { + "Id": "070f2882-2911-4297-b9e3-2da81abf6e70", + "Name": "SerializeProject.Exported.Project.IgnoreAllChanges", + "Label": "Ignore All Changes", + "HelpText": "Selecting this option creates a Terraform module with the \"lifecycle.ignore_changes\" option set to \"all\". This allows the resources to be created if they do not exist, but won't update them if the module is reapplied.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "4cb3da75-7449-4adb-b81a-e87dff371a27", + "Name": "SerializeProject.ThisInstance.Terraform.Backend", + "Label": "Terraform Backend", + "HelpText": "The [backed](https://developer.hashicorp.com/terraform/language/settings/backends/configuration) to define in the Terraform module.", + "DefaultValue": "s3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "aa3df492-845a-4889-a7fc-c9c6f3a95a30", + "Name": "SerializeProject.ThisInstance.Server.Url", + "Label": "Octopus Server URL", + "HelpText": "The URL of the Octopus Server hosting the project to be serialized.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e92dbdff-dd5a-4c95-91a1-40c0ccbb3b3f", + "Name": "SerializeProject.ThisInstance.Api.Key", + "Label": "Octopus API Key", + "HelpText": "The Octopus API Key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c906ecbd-f304-48b8-83ea-fe75008c37df", + "Name": "SerializeProject.Exported.Space.Id", + "Label": "Octopus Space ID", + "HelpText": "The Space ID containing the project to be exported", + "DefaultValue": "#{Octopus.Space.Id}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fae1f2e4-9be5-4380-9fd3-409a1a538b37", + "Name": "SerializeProject.Exported.Project.Name", + "Label": "Octopus Project Name", + "HelpText": "The name of the project containing the runbook.", + "DefaultValue": "#{Octopus.Project.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a711f201-fa2f-4b32-9205-13f396c253d7", + "Name": "SerializeProject.Exported.Runbook.Name", + "Label": "Octopus Runbook Name", + "HelpText": "The name of the runbook to serialize.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "27b222da-690c-4da3-8c60-d06bc7d3505b", + "Name": "SerializeProject.Octopus.UploadSpace.Id", + "Label": "Octopus Upload Space ID", + "HelpText": "The ID of the space to upload the Terraform package to. Leave this blank to upload to the space defined in the `Octopus Space ID` parameter.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-11-08T23:36:23.610Z", + "OctopusVersion": "2024.1.895", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-serialize-space-to-terraform.json.human b/step-templates/octopus-serialize-space-to-terraform.json.human new file mode 100644 index 000000000..8d5ded23e --- /dev/null +++ b/step-templates/octopus-serialize-space-to-terraform.json.human @@ -0,0 +1,549 @@ +{ + "Id": "e03c56a4-f660-48f6-9d09-df07e1ac90bd", + "Name": "Octopus - Serialize Space to Terraform", + "Description": "Serialize an Octopus space, excluding all projects, as a Terraform module and upload the resulting package to the Octopus built in feed. + +This step is expected to be used in conjunction with the [Octopus - Serialize Project to Terraform](https://library.octopus.com/step-templates/e9526501-09d5-490f-ac3f-5079735fe041/actiontemplate-octopus-serialize-project-to-terraform) step. This step will serialize the global space resources, which typically do not change much, and have those resources recreated in a downstream space. The `Octopus - Serialize Project to Terraform` step then serializes a project, using `data` blocks to reference space level resources by name.", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptBody": "import argparse +import os +import stat +import re +import socket +import subprocess +import sys +from datetime import datetime +from urllib.parse import urlparse +from itertools import chain +import platform +from urllib.request import urlretrieve +import zipfile +import urllib.request +import urllib.parse +import json +import tarfile +import random, time + +# If this script is not being run as part of an Octopus step, return variables from environment variables. +# Periods are replaced with underscores, and the variable name is converted to uppercase +if \"get_octopusvariable\" not in globals(): + def get_octopusvariable(variable): + return os.environ[re.sub('\\\\.', '_', variable.upper())] + +# If this script is not being run as part of an Octopus step, print directly to std out. +if \"printverbose\" not in globals(): + def printverbose(msg): + print(msg) + + +def printverbose_noansi(output): + \"\"\" + Strip ANSI color codes and print the output as verbose + :param output: The output to print + \"\"\" + if not output: + return + + # https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python + output_no_ansi = re.sub(r'\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])', '', output) + printverbose(output_no_ansi) + + +def get_octopusvariable_quiet(variable): + \"\"\" + Gets an octopus variable, or an empty string if it does not exist. + :param variable: The variable name + :return: The variable value, or an empty string if the variable does not exist + \"\"\" + try: + return get_octopusvariable(variable) + except: + return '' + + +def retry_with_backoff(fn, retries=5, backoff_in_seconds=1): + x = 0 + while True: + try: + return fn() + except Exception as e: + + print(e) + + if x == retries: + raise + + sleep = (backoff_in_seconds * 2 ** x + + random.uniform(0, 1)) + time.sleep(sleep) + x += 1 + + +def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi): + \"\"\" + The execute method provides the ability to execute external processes while capturing and returning the + output to std err and std out and exit code. + \"\"\" + process = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=cwd, + env=env) + stdout, stderr = process.communicate() + retcode = process.returncode + + if print_args is not None: + print_output(' '.join(args)) + + if print_output is not None: + print_output(stdout) + print_output(stderr) + + return stdout, stderr, retcode + + +def is_windows(): + return platform.system() == 'Windows' + + +def init_argparse(): + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTION] [FILE]...', + description='Serialize an Octopus project to a Terraform module' + ) + parser.add_argument('--terraform-backend', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.ThisInstance.Terraform.Backend') or get_octopusvariable_quiet( + 'ThisInstance.Terraform.Backend') or 'pg', + help='Set this to the name of the Terraform backend to be included in the generated module.') + parser.add_argument('--server-url', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.ThisInstance.Server.Url') or get_octopusvariable_quiet( + 'ThisInstance.Server.Url'), + help='Sets the server URL that holds the project to be serialized.') + parser.add_argument('--api-key', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.ThisInstance.Api.Key') or get_octopusvariable_quiet( + 'ThisInstance.Api.Key'), + help='Sets the Octopus API key.') + parser.add_argument('--space-id', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.Exported.Space.Id') or get_octopusvariable_quiet( + 'Exported.Space.Id') or get_octopusvariable_quiet('Octopus.Space.Id'), + help='Set this to the space ID containing the project to be serialized.') + parser.add_argument('--upload-space-id', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.Octopus.UploadSpace.Id') or get_octopusvariable_quiet( + 'Octopus.UploadSpace.Id') or get_octopusvariable_quiet('Octopus.Space.Id'), + help='Set this to the space ID of the Octopus space where ' + + 'the resulting package will be uploaded to.') + parser.add_argument('--ignored-library-variable-sets', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.Exported.Space.IgnoredLibraryVariableSet') or get_octopusvariable_quiet( + 'Exported.Space.IgnoredLibraryVariableSet'), + help='A comma separated list of library variable sets to ignore.') + parser.add_argument('--ignored-tenants', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.Exported.Space.IgnoredTenants') or get_octopusvariable_quiet( + 'Exported.Space.IgnoredTenants'), + help='A comma separated list of tenants ignore.') + + parser.add_argument('--ignored-tenants-with-tag', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.Exported.Space.IgnoredTenantTags') or get_octopusvariable_quiet( + 'Exported.Space.IgnoredTenants'), + help='A comma separated list of tenant tags that identify tenants to ignore.') + parser.add_argument('--ignore-all-targets', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.Exported.Space.IgnoreTargets') or get_octopusvariable_quiet( + 'Exported.Space.IgnoreAllChanges') or 'false', + help='Set to true to exlude targets from the exported module') + + parser.add_argument('--dummy-secret-variables', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.Exported.Space.DummySecrets') or get_octopusvariable_quiet( + 'Exported.Space.DummySecrets') or 'false', + help='Set to true to set secret values, like account and feed passwords, to a dummy value by default') + parser.add_argument('--include-step-templates', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.Exported.Space.IncludeStepTemplates') or get_octopusvariable_quiet( + 'Exported.Space.IncludeStepTemplates') or 'false', + help='Set this to true to include step templates in the exported module. ' + + 'This disables the default behaviour of detaching step templates.') + parser.add_argument('--ignored-accounts', + action='store', + default=get_octopusvariable_quiet( + 'SerializeSpace.Exported.Space.IgnoredAccounts') or get_octopusvariable_quiet( + 'Exported.Space.IgnoredAccounts'), + help='A comma separated list of accounts to ignore.') + + return parser.parse_known_args() + + +def get_latest_github_release(owner, repo, filename): + url = f\"https://api.github.com/repos/{owner}/{repo}/releases/latest\" + releases = urllib.request.urlopen(url).read() + contents = json.loads(releases) + + download = [asset for asset in contents.get('assets') if asset.get('name') == filename] + + if len(download) != 0: + return download[0].get('browser_download_url') + + return None + + +def ensure_octo_cli_exists(): + if is_windows(): + print(\"Checking for the Octopus CLI\") + try: + stdout, _, exit_code = execute(['octo.exe', 'help']) + printverbose(stdout) + if not exit_code == 0: + raise \"Octo CLI not found\" + return \"\" + except: + print(\"Downloading the Octopus CLI\") + urlretrieve('https://download.octopusdeploy.com/octopus-tools/9.0.0/OctopusTools.9.0.0.win-x64.zip', + 'OctopusTools.zip') + with zipfile.ZipFile('OctopusTools.zip', 'r') as zip_ref: + zip_ref.extractall(os.getcwd()) + return os.getcwd() + else: + print(\"Checking for the Octopus CLI for Linux\") + try: + stdout, _, exit_code = execute(['octo', 'help']) + printverbose(stdout) + if not exit_code == 0: + raise \"Octo CLI not found\" + return \"\" + except: + print(\"Downloading the Octopus CLI for Linux\") + urlretrieve('https://download.octopusdeploy.com/octopus-tools/9.0.0/OctopusTools.9.0.0.linux-x64.tar.gz', + 'OctopusTools.tar.gz') + with tarfile.open('OctopusTools.tar.gz') as file: + file.extractall(os.getcwd()) + os.chmod(os.path.join(os.getcwd(), 'octo'), stat.S_IRWXO | stat.S_IRWXU | stat.S_IRWXG) + return os.getcwd() + + +def ensure_octoterra_exists(): + if is_windows(): + print(\"Checking for the Octoterra tool for Windows\") + try: + stdout, _, exit_code = execute(['octoterra.exe', '-version']) + printverbose(stdout) + if not exit_code == 0: + raise \"Octoterra not found\" + return \"\" + except: + print(\"Downloading Octoterra CLI for Windows\") + retry_with_backoff(lambda: urlretrieve( + \"https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport/releases/latest/download/octoterra_windows_amd64.exe\", + 'octoterra.exe'), 10, 30) + return os.getcwd() + else: + print(\"Checking for the Octoterra tool for Linux\") + try: + stdout, _, exit_code = execute(['octoterra', '-version']) + printverbose(stdout) + if not exit_code == 0: + raise \"Octoterra not found\" + return \"\" + except: + print(\"Downloading Octoterra CLI for Linux\") + retry_with_backoff(lambda: urlretrieve( + \"https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport/releases/latest/download/octoterra_linux_amd64\", + 'octoterra'), 10, 30) + os.chmod(os.path.join(os.getcwd(), 'octoterra'), stat.S_IRWXO | stat.S_IRWXU | stat.S_IRWXG) + return os.getcwd() + + +octocli_path = ensure_octo_cli_exists() +octoterra_path = ensure_octoterra_exists() +parser, _ = init_argparse() + +# Variable precondition checks +if len(parser.server_url) == 0: + print(\"--server-url, ThisInstance.Server.Url, or SerializeSpace.ThisInstance.Server.Url must be defined\") + sys.exit(1) + +if len(parser.api_key) == 0: + print(\"--api-key, ThisInstance.Api.Key, or SerializeSpace.ThisInstance.Api.Key must be defined\") + sys.exit(1) + + +print(\"Octopus URL: \" + parser.server_url) +print(\"Octopus Space ID: \" + parser.space_id) + +# Build the arguments to ignore library variable sets +ignores_library_variable_sets = parser.ignored_library_variable_sets.split(',') +ignores_library_variable_sets_args = [['-excludeLibraryVariableSetRegex', x] for x in ignores_library_variable_sets if + x.strip() != ''] + +# Build the arguments to ignore tenants +ignores_tenants = parser.ignored_tenants.split(',') +ignores_tenants_args = [['-excludeTenants', x] for x in ignores_tenants if x.strip() != ''] + +# Build the arguments to ignore tenants with tags +ignored_tenants_with_tag = parser.ignored_tenants_with_tag.split(',') +ignored_tenants_with_tag_args = [['-excludeTenantsWithTag', x] for x in ignored_tenants_with_tag if x.strip() != ''] + +# Build the arguments to ignore accounts +ignored_accounts = parser.ignored_accounts.split(',') +ignored_accounts = [['-excludeAccountsRegex', x] for x in ignored_accounts] + +os.mkdir(os.getcwd() + '/export') + +export_args = [os.path.join(octoterra_path, 'octoterra'), + # the url of the instance + '-url', parser.server_url, + # the api key used to access the instance + '-apiKey', parser.api_key, + # add a postgres backend to the generated modules + '-terraformBackend', parser.terraform_backend, + # dump the generated HCL to the console + '-console', + # dump the project from the current space + '-space', parser.space_id, + # Use default dummy values for secrets (e.g. a feed password). These values can still be overridden if known, + # but allows the module to be deployed and have the secrets updated manually later. + '-dummySecretVariableValues=' + parser.dummy_secret_variables, + # Add support for experimental step templates + '-experimentalEnableStepTemplates=' + parser.include_step_templates, + # Don't export any projects + '-excludeAllProjects', + # Output variables allow the Octopus space and instance to be determined from the Terraform state file. + '-includeOctopusOutputVars', + # Provide an option to ignore targets. + '-excludeAllTargets=' + parser.ignore_all_targets, + # The directory where the exported files will be saved + '-dest', os.getcwd() + '/export'] + list( + chain(*ignores_library_variable_sets_args, *ignores_tenants_args, *ignored_tenants_with_tag_args, + *ignored_accounts)) + +print(\"Exporting Terraform module\") +_, _, octoterra_exit = execute(export_args) + +if not octoterra_exit == 0: + print(\"Octoterra failed. Please check the verbose logs for more information.\") + sys.exit(1) + +date = datetime.now().strftime('%Y.%m.%d.%H%M%S') + +print('Looking up space name') +url = parser.server_url + '/api/Spaces/' + parser.space_id +headers = { + 'X-Octopus-ApiKey': parser.api_key, + 'Accept': 'application/json' +} +request = urllib.request.Request(url, headers=headers) + +# Retry the request for up to a minute. +response = None +for x in range(12): + response = urllib.request.urlopen(request) + if response.getcode() == 200: + break + time.sleep(5) + +if not response or not response.getcode() == 200: + print('The API query failed') + sys.exit(1) + +data = json.loads(response.read().decode(\"utf-8\")) +print('Space name is ' + data['Name']) + +print(\"Creating Terraform module package\") +if is_windows(): + execute([os.path.join(octocli_path, 'octo.exe'), + 'pack', + '--format', 'zip', + '--id', re.sub('[^0-9a-zA-Z]', '_', data['Name']), + '--version', date, + '--basePath', os.getcwd() + '\\\\export', + '--outFolder', os.getcwd()]) +else: + _, _, _ = execute([os.path.join(octocli_path, 'octo'), + 'pack', + '--format', 'zip', + '--id', re.sub('[^0-9a-zA-Z]', '_', data['Name']), + '--version', date, + '--basePath', os.getcwd() + '/export', + '--outFolder', os.getcwd()]) + +print(\"Uploading Terraform module package\") +if is_windows(): + _, _, _ = execute([os.path.join(octocli_path, 'octo.exe'), + 'push', + '--apiKey', parser.api_key, + '--server', parser.server_url, + '--space', parser.upload_space_id, + '--package', os.getcwd() + \"\\\\\" + + re.sub('[^0-9a-zA-Z]', '_', data['Name']) + '.' + date + '.zip', + '--replace-existing']) +else: + _, _, _ = execute([os.path.join(octocli_path, 'octo'), + 'push', + '--apiKey', parser.api_key, + '--server', parser.server_url, + '--space', parser.upload_space_id, + '--package', os.getcwd() + \"/\" + + re.sub('[^0-9a-zA-Z]', '_', data['Name']) + '.' + date + '.zip', + '--replace-existing']) + +print(\"##octopus[stdout-default]\") + +print(\"Done\") +", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Python" + }, + "Parameters": [ + { + "Id": "b9a96f7f-d8bc-408b-a614-5646bf44d092", + "Name": "SerializeSpace.ThisInstance.Terraform.Backend", + "Label": "Terraform Backend", + "HelpText": "The [backed](https://developer.hashicorp.com/terraform/language/settings/backends/configuration) to define in the Terraform module.", + "DefaultValue": "s3", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "941e2310-7ea0-4e76-a528-c65c9b68f8e7", + "Name": "SerializeSpace.ThisInstance.Server.Url", + "Label": "Octopus Server URL", + "HelpText": "The URL of the Octopus Server hosting the project to be serialized.", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6799b358-e3a5-4668-a12f-5e10e092c1c9", + "Name": "SerializeSpace.ThisInstance.Api.Key", + "Label": "Octopus API Key", + "HelpText": "The Octopus API Key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "950d7bb5-c80b-42a8-84a5-a7012d0fe7ca", + "Name": "SerializeSpace.Exported.Space.Id", + "Label": "Octopus Space ID", + "HelpText": "The Space ID containing the project to be exported", + "DefaultValue": "#{Octopus.Space.Id}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "64acb2fe-b713-4f69-b08b-34546dd808cd", + "Name": "SerializeSpace.Octopus.UploadSpace.Id", + "Label": "Octopus Upload Space ID", + "HelpText": "The ID of the space to upload the Terraform package to. Leave this blank to upload to the space defined in the `Octopus Space ID` parameter.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e6b3d298-87e8-4965-966e-230537b4dd4c", + "Name": "SerializeSpace.Exported.Space.IgnoredLibraryVariableSet", + "Label": "Ignored Library Variables Sets", + "HelpText": "A comma separated list of library variables sets that will not be included in the Terraform module. These library variable sets are often those used by Runbooks that are not included in the module, and so do not need to be referenced.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1208c54c-568a-4a48-9340-fbff710079b3", + "Name": "SerializeSpace.Exported.Space.IgnoredTenants", + "Label": "Ignored Tenants", + "HelpText": "A comma separated list of tenants that will not be included in the Terraform module. These tenants are often those used by Runbooks to identify managed spaces or instances, but which must not be recreated.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9121bc1d-df00-4ec7-ae1c-751261d2438d", + "Name": "SerializeSpace.Exported.Space.IgnoredAccounts", + "Label": "Ignored Accounts", + "HelpText": "A comma separated list of accounts that will not be included in the Terraform module. These accounts are often those used by Runbooks that are not included in the module, and so do not need to be referenced.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c33810ae-0f53-421d-b11a-9161bfdd0df8", + "Name": "SerializeSpace.Exported.Space.IgnoreTargets", + "Label": "Ignore All Targets", + "HelpText": "Check this option to ignore the targets when serializing the Terraform module. This is useful when downstream spaces require their own unique targets to work correctly.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "a8e8163e-0981-4496-a844-b16281d8ce1c", + "Name": "SerializeSpace.Exported.Space.DummySecrets", + "Label": "Default Secrets to Dummy Values", + "HelpText": "This option sets the default value of all secrets, like account and feed passwords, to a dummy value. This allows the resources to be created and then updated at a later time with the correct values. Note the default values can be overridden if they are known.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "e7cbd75d-39f1-40ad-a41a-8eed8a65902c", + "Name": "SerializeSpace.Exported.Space.IgnoredTenantTags", + "Label": "Ignore Tenants with Tag", + "HelpText": "A comma separated list of tenant tags that identify tenants to exclude from the export. Typically tenants that represent internal teams or business units will have a tag like `TenantType/InternalCustomer` to distinguish them from other kinds of tenants.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f312be91-cc8f-49d6-afd7-fc6a6e38926c", + "Name": "SerializeSpace.Exported.Space.IncludeStepTemplates", + "Label": "Include Step Templates", + "HelpText": "Enable this option to export step templates.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-10-24T05:39:19.004Z", + "OctopusVersion": "2023.4.6612", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mcasperson", + "Category": "octopus" +} diff --git a/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json.human b/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json.human new file mode 100644 index 000000000..fec20d77d --- /dev/null +++ b/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json.human @@ -0,0 +1,215 @@ +{ + "Id": "c6faf6be-296c-44ee-abf6-ce87331b2557", + "Name": "Set Octopus release notes from TFS query", + "Description": "Sets Octopus release notes from TFS query", + "ActionType": "Octopus.Script", + "Version": 6, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#TFS +$instance = $OctopusParameters[\"tfsInstance\"] +$collection = $OctopusParameters[\"tfsCollection\"] +$project = $OctopusParameters[\"tfsProject\"] +$PAT = $OctopusParameters[\"tfsPat\"] +$pathquery = $OctopusParameters[\"tfsPathQuery\"] + +#Octopus +$octopusAPIKey = $OctopusParameters['octopusAPIKey'] +$baseUri = $OctopusParameters['#{if Octopus.Web.ServerUri}Octopus.Web.ServerUri#{else}Octopus.Web.BaseUrl#{/if}'] +$octopusProjectId = $OctopusParameters['Octopus.Project.Id'] +$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number'] + +write-host \"Instance: $($instance)\" +write-host \"collection: $($collection)\" +write-host \"project: $($project)\" +write-host \"baseUri: $($baseUri)\" +write-host \"projectId: $($projectId)\" +write-host \"thisReleaseNumber: $($thisReleaseNumber)\" +write-host \"TFS path: $($pathquery)\" + +#Create HEADERS +$bytes = [System.Text.Encoding]::ASCII.GetBytes($PAT) +$base64 = [System.Convert]::ToBase64String($bytes) +$basicAuthValue = \"Basic $base64\" +$headers = @{ } +$headers.Add(\"Authorization\", $basicAuthValue) +$headers.Add(\"Accept\",\"application/json\") +$headers.Add(\"Content-Type\",\"application/json\") + +$reqheaders = @{\"X-Octopus-ApiKey\" = $octopusAPIKey } +$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $octopusAPIKey } + +function Test-SpacesApi { +\tWrite-Verbose \"Checking API compatibility\"; +\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json; + if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) { + \tWrite-Verbose \"Spaces API found\" + \treturn $true; + } + Write-Verbose \"Pre-spaces API found\" + return $false; +} + +if(Test-SpacesApi) { +\t$spaceId = $OctopusParameters['Octopus.Space.Id']; + if([string]::IsNullOrWhiteSpace($spaceId)) { + throw \"This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which isn't expected - please reach out to our support team at https://help.octopus.com if you encounter this error.\"; + } +\t$baseApiUrl = \"/api/$spaceId\" ; +} else { +\t$baseApiUrl = \"/api\" ; +} + +# Get the current release +$releaseUri = \"$baseUri$baseApiUrl/projects/$octopusProjectId/releases/$thisReleaseNumber\" +write-host \"Release uri $($releaseUri)\" +try { + $currentRelease = Invoke-RestMethod $releaseUri -Headers $reqheaders -UseBasicParsing +} catch { + if ($_.Exception.Response.StatusCode.Value__ -ne 404) { + $result = $_.Exception.Response.GetResponseStream() + $reader = New-Object System.Io.StreamReader($result); + $responseBody = $reader.ReadToEnd(); + throw \"Error occurred: $responseBody\" + } +} + +if(![string]::IsNullOrWhiteSpace($currentRelease.ReleaseNotes)){ +\twrite-host \"Release notes already filled in. $($currentRelease.ReleaseNotes)\" + Set-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes +\texit; +} + + +#Get projectid +$url = \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/projects/$($project)?&includeCapabilities=false&includeHistory=false&api-version=2.2\" +write-host \"Invoking url= $($url)\" +$projectresponse = Invoke-RestMethod $url -Method GET -Headers $headers + +$projectid = $projectresponse.id +write-host \"projectid $($projectid)\" + +#Get the ID of the query to execute +$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/$($pathquery)?$depth=1&api-version=2.2\" -Method GET -Headers $headers +write-host \"queryResult $($queryResult)\" + +#https://{instance}/DefaultCollection/[{project}/]_apis/wit/wiql/{id}?api-version={version} +$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/wit/wiql/$($queryResult.Id)?api-version=2.2\" -Method GET -Headers $headers + +Write-Host \"Found $($queryResult.workItems.Count) number of workitems for query: ReleaseNotes$($releaseTag)\" + +$releaseNotes = \"**Work Items:**\" + + +if($queryResult.workItems.Count -eq 0) +{ +\tWrite-Host \"No work items for release\" +\t$releaseNotes = \"`n no new work items\" +} +else +{ +\t#Create a list of ids +\t$ids = [string]::Join(\"%2C\", ($queryResult.workItems.id)) + +\t#Get all the work items +\t$workItems = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/_apis/wit/workItems?ids=$($ids)&fields=System.Title\" -Method GET -Headers $headers + +\tforeach($workItem in $workItems.value) +\t{ +\t\t#Add line for each work item +\t\t$releaseNotes = $releaseNotes + \"`n* [$($workItem.id)] (http://$($instance)/tfs/$($collection)/9981e67f-b27c-4628-b5cf-fba1d327aa07/_workitems/edit/$($workItem.id)) : $($workItem.fields.'System.Title')\" +\t} + +} + + + +# Update the release notes for the current release +$currentRelease.ReleaseNotes = $releaseNotes +write-host \"Release notes $($currentRelease.ReleaseNotes)\" +Write-Host \"Updating release notes for $thisReleaseNumber`:`n`n\" +try { + $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\" + write-host \"Release uri $($releaseUri)\" + $currentReleaseBody = $currentRelease | ConvertTo-Json + write-host \"Current release body $($currentReleaseBody)\" + $result = Invoke-RestMethod $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing +\twrite-host \"result $($result)\" +} catch { + $result = $_.Exception.Response.GetResponseStream() + $reader = New-Object System.Io.StreamReader($result); + $responseBody = $reader.ReadToEnd(); + Write-Host \"error $($responseBody)\" + throw \"Error occurred: $responseBody\" +} + +Set-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes +" + }, + "Parameters": [ + { + "Id": "08ce7e1a-7251-4b9f-bd07-f63cbfbc7f20", + "Name": "tfsInstance", + "Label": "TFS Instance", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "85eff544-5275-420f-81d8-cf01ea42d903", + "Name": "tfsCollection", + "Label": "Collection", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a423bf6c-52be-4994-8bab-2d64f05f167f", + "Name": "tfsProject", + "Label": "Project", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1db49bf8-d38a-4ae7-b858-693c584272d5", + "Name": "tfsPat", + "Label": "Personal access token", + "HelpText": "Personal access token to retrieve from TFS", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "0427c73a-2925-4c55-a2ce-5584cd24b1dd", + "Name": "octopusAPIKey", + "Label": "Octopus API key", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2023-02-16T15:38:44.043Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} diff --git a/step-templates/octopus-support-export.json.human b/step-templates/octopus-support-export.json.human new file mode 100644 index 000000000..6dc47648b --- /dev/null +++ b/step-templates/octopus-support-export.json.human @@ -0,0 +1,226 @@ +{ + "Id": "de1fb4d2-5cc7-43cc-8dfc-633d63e97c92", + "Name": "Octopus - Support Export", + "Description": "This step template will take an already exported project on your server and do the following: +- Change all sensitive variables to plain text variables, and the values will become \"Sensitive Variable Was Here\" +- All accounts will have their details nulled out +- All certificates will have junk data put into their json + +After the above is complete, the modified zip file will be attached to the task that you can download and send to support. Please remember, support will still need the password you used to create the original export, but now there will be no sensitive data available. + +NOTE: This step template currently DOES NOT purge any Tenant variables. If you have any Tenants connected to this project with variables you are concerned about sharing, you will need to import the modified zip into a blank space and remove the values, and re-export for now. This functionality will come in a future update to the step template. + +**NOTE: This template only works on Octopus Server 2021.1 and above, as it requires project import/export to be available.**", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Add-Type -AssemblyName System.Web + +#Gathering Parameters +[string]$SpaceName = $OctopusParameters[\"SupportExportSpaceName\"] +[string]$APIKey = $OctopusParameters[\"SupportExportAPIKey\"] +[string]$serverTaskId = $OctopusParameters[\"SupportExportTaskID\"] +[string]$OctopusURL = $OctopusParameters[\"SupportExportServerURL\"] + +if ([string]::IsNullOrEmpty($SpaceName)){ +\tWrite-Error \"You did not supply a space name. The space name is required.\" + exit +} +if ([string]::IsNullOrEmpty($APIKey)){ +\tWrite-Error \"You did not supply an API Key. An API Key is required.\" + exit +} +if ([string]::IsNullOrEmpty($serverTaskId)){ +\tWrite-Error \"You did not supply the task ID of the export task you want to be scrubbed. This parameter is required.\" + exit +} + +#If the Server URL parameter was empty, use the ServerURI if it exists, if not, use the BaseURL, which could result in a failed connection in some environments so it's a last resort. +if ([string]::IsNullOrEmpty($OctopusURL)) +{ + if ([string]::IsNullOrEmpty(\"#{Octopus.Web.ServerUri}\")){ + $OctopusURL = \"#{Octopus.Web.BaseUrl}\" + } + else{ + $OctopusURL = \"#{Octopus.Web.ServerUri}\" + } +} +$header = @{ \"X-Octopus-ApiKey\" = $APIKey } +$fileDownloadPath = \"#{env:OctopusCalamariWorkingDirectory}\" + +#Junk Certificate to overwrite the exported certs with +$junkCert = @\" + { + \"CertificateData\": \"ZV+cWXHpMqMfmZrMXndg+8TGup8TkDYQUrHVU8Q/b2wJwjpj5QqDry43RUWUX7FNbGwqCtLI5PtKZ9C6gfGdAUYK6frdal/IatPrVZV+uLQZKEE4S+ky5bu8U8WQ7KjQP70i39ayde2N1nnhXpdjbhYBqky3rFwdzRH3O1qOA+aJWOQs7tUgMP+A+2N+jRGanMAwhBNyAsk//84mgWxBFCdx6HJFAvfD4oxa6uQHURl34GFvk1CS2EKdfyqSjrz8nle1YYD68L4kBvERAyZvnzMW34fpUfNNEd9KepIKWbmNhO87yPgaspUFF2LV2tEsu2AXv1l4Rhzn+8cHsTsCJMEz1VPVPJ217+OZIudSJMo0Zx5YVJHpOsRwKL6n3fYYrFYqMZG/DIoRK+SlmgHcmsZ0YfA5NmCprppLxzOmWbsD6t6gOFUAhPj9G314GDfAmV3B54rWXqWnR/VEGc778rfw+mSHScpJAt0fA/xEZgsbd6CQYYmOGvwx2tOYSE3l56QTQEBatCv+zdWRH3dI7Tdr8Yrf2zrHACXDdk+/19oNBVSWsgwgDg/xu+4p9Y/ZyW1pZFbY6ZEgTTegeNbl5U6Lkb8QlMMFd98yDiQqhKSeDanF4N+yrwefhV+JGmfiOzhgcO3zEHf6gFALZnk9umf2y38fRnOc9rfOymG0rlDr+S6Rx3Xc0vvqH/FeS7e/ctc3dmKB7bYC0KeJNWxEkGpJw0bdudM+nJnjVvCmDfjBCW1ScKTpm9sZ02E/SPiL9gGUds1hAALtdZ70UDSjyDvlPKxPu/w0lPLUqPgSavCeoLTVfGZVVY9rHLOKDTvRlqSMzl90TdovuXGcDHqyQL8T5imRRAhxGaaVZxmLzGXoPWacNkAP79wrnGkKU6GHSNZfC9CtZ21FVGAyEZPyRuXc9nhWPcyl4/rdt+ne/5c96E/lwTmV2thRKdD0awDkQmWji+6+aJUtSCpw69xJcCIVzb3g/4j02ugZAL8/jjDPiLLytWRUNTTSH/RSpPnHUPi0/ciiGev8O7AblGA8Z7g7ok1WOl8FBERvUd3hpX6z0nbrKe4za7ydNivNNouWEpKZoenC82BoQAGkkCHcNio1UJYWn8Ri3W15Eml+bZ2Z4qbeUWp5ro2kwL4vwyte941FGOJEv4YBA4EKeRbr2oY1wzFFxq0kcLXdR42Bqpcn3HXdYSkl/ZmyNh33AWa4ooda51ouBJ6Ae43U201xt8OXow/w2mo913z3uclBG0pJcoHc4o1uQS6/ZTp6hxEPvOmJNc1c14E5gykUNuSs0u1TAVra49W486XOAniSb9RvnJ1iqja2aUeY4B835cjTeAx5xBIL+ESwxBpiFeCy5IyhNINOL3lWW+H1NruCC+otPVUVroYenPXRHXA0dng/koPgQ89Ck0/8dQ/SJM/IP0fyJE7NFZWu9XofcpfTSk6Pte17R3m4VuTYjx/oG0tkq3UL4s0Khj0s+H3CISK2Qc0BwE1S1l9bQ5Ukz1v7HFcn77mKgzk5PB4sowMrwWyBtLMcVF2d460Aa0eM8/OA1m9ZrkzAeBGRUMy9dLQAAowGMYui5TfGNUBlVeBaMc1IoKpJUgcvAu5sYcPJvOOLO1wRUFIhu9qwlHbAx/hl6ZfxShDx5BZV8eN60dMm7N34PuZpe/mL80P3z+SNLz9NuBjM5lg82O25dOBkTYRZFt6Hmmc3431vLLvbCMvhcRJNuaa/p3JPjY/uXEuzknQ2/hw9YvgY2+BrovE6SoWlPsppfisTcGhYx80hOGX4kzMpNNW+XeD0o7SWXvCIq2VrI1dQWgZ0AIgOv7oaVmQ2isaJyaGG4P6Uz39BOoBzH4k/beSFt3RwF2DELk1hVbk8F/XieQlzgrqBalFpJZ8sdlRDAKntdSolxQEMrwKAIHeiJxtV9rwOhubTFlcJEyDISwVTz1j5lWrj6j/dv4lzLVIY58lwzqhOrh734OU/N9pr0rCZViiAl3adh7gVLpMNRZ4RJPBAKbbZImkeA/eFAxXEhi5fS9R3gUyHvqxwOySTIVPXXRENRgjdnq9cmqtszpw/1Bm4cN/iD+H0kh7UA98X48iJ3VxdWfe7qjJFcz6euyRKdejxZMrk2rfHAu7kcMIcDuACsoqRxPADPVJFGbMxJDrB6B0MfLxettxX3crmLfzknkHWsVvCW3mdZcNYszf8N7VJ/fFI7oNhq6ptq3yaeJqOTBNFUD9PcITkH4P3iUYlOf4jS2rXuLmcGDqKGQ2EzUa7YFUa3iwp5iOQnnfG4xpGGYhUz/QZHKNNufklgzFpVONn2rCfxLeHxbaZodny3uAQJiqNPQzLaBLqt1Jh/F+l9h/TEfjfIsXwygoIsPXRW6dlxa/Zkqy7+4fGQp8L7wjYWmZi+icq4pnI6ai4sqIKuz+xQGtwV2JLy8kLKaXzA7jTDV0wzJ7pmwAzbkX24sLznBYD4wUbb0Ix7G8=|AAAAAAAAAAAAAAAAAAAAAA==\", + \"Password\": null, + \"CertificateMetadata\": { + \"Type\": \"Valid\", + \"CertificateDataFormat\": \"Pem\", + \"HasPrivateKey\": false, + \"RootCertificate\": { + \"Version\": 3, + \"SerialNumber\": \"01\", + \"SignatureAlgorithmName\": \"SHA-1withRSA\", + \"NotBefore\": \"2014-01-28T15:36:55-05:00\", + \"NotAfter\": \"2024-01-26T15:36:55-05:00\", + \"SubjectName\": \"CN=www.dimi.fr,O=Internet Widgits Pty Ltd,ST=Some-State,C=FR\", + \"IssuerName\": \"E=dimi@dimi.fr,CN=Dimi CA,OU=NSBU,O=Dimi,L=Paris,ST=Some-State,C=FR\", + \"Thumbprint\": \"AD42744CC010C079D9CC08A17A45414C8140B1E7\", + \"SubjectAlternativeNames\": [] + }, + \"CertificateChain\": [] + } + } + + +\"@ +$junkCert = $junkCert | ConvertFrom-Json + +Write-Host \"Getting the space information\" +$spaceList = Invoke-RestMethod -Method Get -Uri \"$OctopusUrl/api/spaces?skip=0&take=10&partialName=$([System.Web.HTTPUtility]::UrlEncode($spaceName))\" -Headers $header +$space = $spaceList.Items | Where-Object {$_.Name -eq $spaceName} +$spaceId = $space.Id + +#Getting artifact from the export task +$artifactList = Invoke-RestMethod -Method Get -Uri \"$OctopusUrl/api/$spaceId/artifacts?regarding=$serverTaskId\" -Headers $header +$artifact = $artifactList.Items | Where-Object {$_.Filename -like \"*Octopus-Export*\"} +$fullname = $artifact.Filename +$artifactId = $artifact.Id +Write-Host \"Found $artifactId that matches expected file name Octopus-Export\" + +Write-Host \"Getting file content\" +Invoke-RestMethod -Method Get -Uri \"$OctopusUrl/api/$spaceId/artifacts/$artifactId/content\" -Headers $header -OutFile \"$($filedownloadpath)\\$($fullname)\" +Write-Host \"File content written to $fileDownloadPath\" + +$ExportFile = \"$($filedownloadpath)\\$($fullname)\" + +$OriginalFileName = (Get-Item $ExportFile).BaseName +$Path = \"$($fileDownloadPath)\\export\" +Expand-Archive -Path $ExportFile -DestinationPath $Path +$SearchInFilesWithFileNameMatching = \"variableset-*\" +$TextToSearchFor = @(\"Sensitive*\",\"AzureAccount\",\"AmazonWebServicesAccount\",\"GoogleCloudAccount\",\"Certificate\") +$PathArray = @() + + +foreach ($text in $TextToSearchFor){ + Get-ChildItem $Path | Where-Object {$_.Name -match $SearchInFilesWithFileNameMatching} | Where-Object { $_.Attributes -ne \"Directory\"} | + ForEach-Object { + If (Get-Content $_.FullName | Select-String -Pattern $text) { + $PathArray += $_.FullName + } + } +} + + +foreach ($file in $pathArray) { + $variableSnapshot = Get-Content -Path $file | ConvertFrom-Json + foreach ($variable in $variableSnapshot.Variables) { + #Check if the variable is a sensitive variable and make it a string and put junk there + if ($variable.Type -eq \"Sensitive\"){ + $variable.Type = \"String\" + $variable.Value = \"Sensitive Variable Was Here\" + } + #Check if the variable is an account, if so, null out everything but the account type and save the json back + if (($variable.Type -eq \"AzureAccount\") -or ($variable.Type -eq \"AmazonWebServicesAccount\") -or ($variable.Type -eq\"GoogleCloudAccount\")){ + + if (Test-Path -Path \"$($Path)\\$($variable.value).json\"){ + $tempJson = Get-Content -Path \"$($Path)\\$($variable.value).json\" + $tempJson = $tempJson | ConvertFrom-Json + foreach ($item in $tempJson.Details){ + $item.PSObject.Properties | ForEach-Object { + if ($_.Name -ne \"AccountType\"){ + if ($_.Value -ne $null){ + $_.Value = $null + } + } + } + } + $tempJson = $tempJson | ConvertTo-Json -Depth 10 + Set-Content -Path \"$($Path)\\$($variable.value).json\" -Value $tempJson + } + } + + #Check if the variable is a certificate, if so, make it equal to the junk cert that we created in the beginning + if ($variable.Type -eq \"Certificate\"){ + + if (Test-Path -Path \"$($Path)\\$($variable.value).json\"){ + $tempJson = Get-Content -Path \"$($Path)\\$($variable.value).json\" + $tempJson = $tempJson | ConvertFrom-Json + + #overwriting pertinent data with junk + $tempJson.CertificateData = $junkCert.CertificateData + $tempJson.Password = $junkCert.Password + $tempJson.CertificateMetadata = $junkCert.CertificateMetadata + + + $tempJson = $tempJson | ConvertTo-Json -Depth 10 + Set-Content -Path \"$($Path)\\$($variable.value).json\" -Value $tempJson + + } + } + } + $variableSnapshot | ConvertTo-Json -Depth 100 | Out-File $file -Encoding UTF8 +} + + + +Compress-Archive -Path $Path\\* -DestinationPath \"$($fileDownloadPath)\\$($OriginalFileName)-Modified.zip\" +Remove-Item $Path -Recurse + +New-OctopusArtifact -Path \"$($fileDownloadPath)\\$($OriginalFileName)-Modified.zip\" -Name \"$($OriginalFileName)-Modified.zip\"" + }, + "Parameters": [ + { + "Id": "4833547d-71c7-4b60-bf3e-1bdfb2847f26", + "Name": "SupportExportSpaceName", + "Label": "Space Name", + "HelpText": "The Space that has the project and export task that you want to be scrubbed.", + "DefaultValue": "Default", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2aaa1afe-bad9-4eae-b79b-467fd4a19ec5", + "Name": "SupportExportAPIKey", + "Label": "API Key", + "HelpText": "API Key that has access to the project and task.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "68874953-98d4-4c45-8046-90208330e0c8", + "Name": "SupportExportTaskID", + "Label": "Task ID", + "HelpText": "Task ID of the export you want to be scrubbed", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d075d988-210a-4239-b3e3-1923679762c8", + "Name": "SupportExportServerURL", + "Label": "ServerURL", + "HelpText": "This will default to the Defined Server URI in Configuration->Nodes. In some cases, you might want to define the URL that you want the API call to run against rather than letting the default value be used. If you get a connection issue, try defining a known good URL here.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2022-06-01T15:54:33.140Z", + "OctopusVersion": "2022.1.2584", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "millerjn21", + "Category": "octopus" +} diff --git a/step-templates/octopus-validate-deploying-user.json.human b/step-templates/octopus-validate-deploying-user.json.human new file mode 100644 index 000000000..c5135a0d3 --- /dev/null +++ b/step-templates/octopus-validate-deploying-user.json.human @@ -0,0 +1,88 @@ +{ + "Id": "bbcf1894-9cf7-4968-a3e4-3bff08d8c75b", + "Name": "Octopus - Validate Deploying User", + "Description": "Verifies current deploying user didn't deploy previously to specified environment.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$releaseId = $OctopusParameters[\"Octopus.Release.Id\"] +$currentDeployerId = $OctopusParameters[\"Octopus.Deployment.CreatedBy.Id\"] +$userName = $OctopusParameters[\"Octopus.Deployment.CreatedBy.Username\"] +$environmentId = $OctopusParameters[\"Octopus.Environment.Id\"] +$spaceId = $OctopusParameters[\"Octopus.Space.Id\"] +$octopusURL = $OctopusParameters[\"ovdu_octopusURL\"] +$octopusAPIKey = $OctopusParameters[\"ovdu_octopusAPIKey\"] +$precedingEnvironment = $OctopusParameters[\"ovdu_precedingEnvironment\"] + +$header = @{ \"X-Octopus-ApiKey\" = $octopusAPIKey } + +$deploymentDetails = (Invoke-RestMethod -Method Get -Uri \"$octopusURL/api/$($spaceId)/releases/$($releaseId)/deployments/\" -Headers $header) + +# Get details for deployment to preceding environment +$allEnvironments = (Invoke-RestMethod -Method Get -Uri \"$octopusURL/api/$($spaceId)/environments\" -Headers $header) +$environmentItem = $allEnvironments.Items | where-object { $_.Name -eq $precedingEnvironment } +$environmentId = $environmentItem.Id + +# Load all deploys to the previous environment +$environmentDeploys = $deploymentDetails.Items | Where-Object {$_.EnvironmentId -eq $environmentId} + +# Iterate deployments to the previous environment to validate current deployer +foreach($prevdeployment in $environmentDeploys) + { + \tif($prevDeployment.Id -eq $OctopusParameters[\"Octopus.Deployment.Id\"]) + {continue} + \telse + { + \t\tif($prevdeployment.DeployedById -eq $currentDeployerId ) + \t{ + \tWrite-Highlight \"$userName previously deployed this project to $precedingEnvironment - deployment cancelled.\" + \tThrow \"$userName previously deployed this project to $precedingEnvironment - deployment cancelled.\" + \t} + } + }" + }, + "Parameters": [ + { + "Id": "e99ae8d8-f63c-45b5-967c-703cc89e620c", + "Name": "ovdu_octopusURL", + "Label": "Octopus Base Url", + "HelpText": "The base url of your Octopus Deploy instance. Example: https://samples.octopus.app", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "781b3394-138e-4aa5-bd26-5a835563bf88", + "Name": "ovdu_octopusAPIKey", + "Label": "Octopus Api Key", + "HelpText": "The API key of a user in Octopus Deploy who has permissions to manage releases.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "a05c4f99-ae6e-48ea-a11e-2cb3b538bf7e", + "Name": "ovdu_precedingEnvironment", + "Label": "Environment to validate deployer against", + "HelpText": "The environment preceding the controlled environment where if the user deployed this release, it will fail in this environment", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-10-22T15:08:53.042Z", + "OctopusVersion": "2021.1.7665", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "handplaned", + "Category": "octopus" +} diff --git a/step-templates/octopus-wait-for-deployment-target-registration.json.human b/step-templates/octopus-wait-for-deployment-target-registration.json.human new file mode 100644 index 000000000..e84f64f01 --- /dev/null +++ b/step-templates/octopus-wait-for-deployment-target-registration.json.human @@ -0,0 +1,220 @@ +{ + "Id": "dc573fd6-92be-46fa-aab1-861adcd37d82", + "Name": "Octopus - Wait for Deployment Target registration", + "Description": "This step will poll Octopus Deploy until it detects that the expected Deployment Target has been registered. + +The goal being that a deployment will be paused until the expected Deployment Target is available (eg [Transient Targets](https://octopus.com/docs/infrastructure/environments/elastic-and-transient-environments/deploying-to-transient-targets)). On subsequent deploys, the Deployment Target would quickly be identified as registered, and the deployment would continue as expected. + +With a couple of extra step templates you can: +- Create a new EC2 Instance (_AWS - Launch EC2 Instance_) +- Include the new Deployment Target in subsequent deployment steps (_Health Check_)", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +param( + [string]$odEnv, + [string]$odName, + [string]$odRole, + [int]$odTimeout, + [string]$odUrl, + [string]$odApiKey, + [switch]$whatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if (!$result -or $result -eq $null) { + if ($Default) { + $result = $Default + } elseif ($Required) { + throw \"Missing parameter value $Name\" + } + } + + return $result +} + +& { + param( + [string]$odEnv, + [string]$odName, + [string]$odRole, + [int]$odTimeout, + [string]$odUrl, + [string]$odApiKey + ) + + # If Octopus Deploy's URL/API Key are not provided as params, attempt to retrieve them from Environment Variables + if (!$odUrl) { + if ([Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")) { + $odUrl = [Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\") + } + } + + if (!$odUrl) { throw \"Octopus Deploy API URL was not available/provided.\" } + + if (!$odApiKey) { + if ([Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")) { + $odApiKey = [Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\") + } + } + + if (!$odApiKey) { throw \"Octopus Deploy API key was not available/provided.\" } + + $header = @{ \"X-Octopus-ApiKey\" = $odApiKey } + + Write-Verbose \"Checking API compatibility\"; + $rootDocument = Invoke-WebRequest \"$odUrl/api/\" -Header $header -UseBasicParsing | ConvertFrom-Json; + if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) { + Write-Verbose \"Spaces API found\" + $hasSpacesApi = $true; + } else { + Write-Verbose \"Pre-spaces API found\" + $hasSpacesApi = $false; + } + + if($hasSpacesApi) { + $spaceId = $OctopusParameters['Octopus.Space.Id']; + if([string]::IsNullOrWhiteSpace($spaceId)) { + throw \"This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which isn't expected - please reach out to our support team at https://help.octopus.com if you encounter this error.\"; + } + $baseApiUrl = \"/api/$spaceId\" ; + } else { + $baseApiUrl = \"/api\" ; + } + + $environments = (Invoke-WebRequest \"$odUrl$baseApiUrl/environments/all\" -Headers $header -UseBasicParsing).content | ConvertFrom-Json + $environment = $environments | Where-Object { $_.Name -contains $odEnv } + if (@($environment).Count -eq 0) { throw \"Could not find environment with the name '$odEnv'\" } + + $timeout = new-timespan -Seconds $odTimeout + $sw = [diagnostics.stopwatch]::StartNew() + + Write-Output (\"------------------------------\") + Write-Output (\"Checking the Deployment Target's registration status:\") + Write-Output (\"------------------------------\") + + while ($true) + { + if ($sw.elapsed -gt $timeout) { throw \"Timed out waiting for the Deployment Target to register\" } + + $machines = ((Invoke-WebRequest ($odUrl + $environment.Links.Self + \"/machines\") -Headers $header -UseBasicParsing).content | ConvertFrom-Json).items + if ($odName) { $machines = $machines | Where-Object { $_.Name -like \"*$odName*\" } } + if ($odRole) { $machines = $machines | Where-Object { $_.Roles -like \"*$odRole*\" } } + if (@($machines).Count -gt 0) { break } + + Write-Output (\"$(Get-Date) | Waiting for Deployment Target to register with the name '$odName' and role '$odRole'\") + + Start-Sleep -Seconds 5 + } + + Write-Output (\"$(Get-Date) | Deployment Target registered with the name '$odName' and role '$odRole'!\") + } ` + (Get-Param 'odEnv' -Required) ` + (Get-Param 'odName' -Required) ` + (Get-Param 'odRole') ` + (Get-Param 'odTimeout' -Required) ` + (Get-Param 'odUrl') ` + (Get-Param 'odApiKey')" + }, + "Parameters": [ + { + "Id": "46d497f7-f297-4329-a5a3-a32e32dfb85b", + "Name": "odEnv", + "Label": "Environment", + "HelpText": "The Environment you expect the Deployment Target to be registered in.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "28e05635-b343-4a00-a42e-55f5244ddd47", + "Name": "odName", + "Label": "Expected Display Name", + "HelpText": "The Display Name of the expected Deployment Target. +Note: _Partial matches for the supplied Display Name is enabled (eg \"Machine\" would match a Target called \"Machine01\")_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2df65d31-6a0e-47cb-bcc3-7b5320a23aee", + "Name": "odRole", + "Label": "Expected Role (Optional)", + "HelpText": "The Role of the expected Deployment Target. +Note: _Partial matches for the supplied Role is enabled (eg \"Database\" would match a Target with the Role \"Database01\")_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "ff029a44-af95-4580-95c1-889a06a83f4c", + "Name": "odTimeout", + "Label": "Timeout", + "HelpText": "The amount of time to wait for the expected Deployment Target to be registered. (aka the Timeout)", + "DefaultValue": "600", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "9fe91ebb-7521-4a91-9284-8398a8e9ad87", + "Name": "odUrl", + "Label": "Octopus Deploy URL (Kind-of Optional)", + "HelpText": "The base URL of your Octopus Deploy installation. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"OD\\_API\\_URL\"", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "054a2fca-6f12-46b2-a2a3-32045a50ea6a", + "Name": "odApiKey", + "Label": "API Key (Kind-of Optional)", + "HelpText": "An API Key with permissions access the Octopus Deploy API. +Note: If empty, this step will attempt to use the value contained in the Machine Environment Variable \"OD\\_API\\_KEY\" +Further Reading: +https://octopus.com/docs/api-and-integration/api/how-to-create-an-api-key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2020-09-03T08:23:42.851Z", + "OctopusVersion": "2020.4.0-rc0003", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} diff --git a/step-templates/octopus-worker-healthcheck.json.human b/step-templates/octopus-worker-healthcheck.json.human new file mode 100644 index 000000000..ba7d3dd8d --- /dev/null +++ b/step-templates/octopus-worker-healthcheck.json.human @@ -0,0 +1,136 @@ +{ + "Id": "c6c23c7b-876d-4758-a908-511f066156d7", + "Name": "Worker - Health check", + "Description": "Run a health check against a worker.", + "ActionType": "Octopus.Script", + "Version": 4, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define parameters +$baseUrl = $OctopusParameters['Octopus.Web.ServerUri'] +$apiKey = $WorkerApiKey +$spaceId = $OctopusParameters['Octopus.Space.Id'] +$spaceName = $OctopusParameters['Octopus.Space.Name'] +$environmentName = $OctopusParameters['Octopus.Environment.Name'] +$workerName = $OctopusParameters['WorkerName'] +$workerPoolName = $OctopusParameters['WorkerPoolName'] + +# Check for null or empty +if ([string]::IsNullOrEmpty($baseUrl)) +{ +\t$baseUrl = $OctopusParameters['#{if Octopus.Web.ServerUri}Octopus.Web.ServerUri#{else}Octopus.Web.BaseUrl#{/if}'] +} + +# Get worker +if (![string]::IsNullOrEmpty($workerPoolName)) +{ + # Get worker pool + $workerPool = (Invoke-RestMethod -Method Get -Uri \"$baseUrl/api/$spaceId/workerpools/all\" -Headers @{\"X-Octopus-ApiKey\"=\"$apiKey\"}) | Where-Object {$_.Name -eq $workerPoolName} + + # Check to make sure it exists + if ($null -ne $workerPool) + { + $worker = (Invoke-RestMethod -Method Get -Uri \"$baseUrl/api/$spaceId/workerpools/$($workerPool.Id)/workers\" -Headers @{\"X-Octopus-ApiKey\"=\"$apiKey\"}).Items | Where-Object {$_.Name -eq \"$workerName\"} + } + else + { + \tWrite-Error \"Worker pool $workerPoolName not found!\" + } +} +else +{ + $worker = (Invoke-RestMethod -Method Get -Uri \"$baseUrl/api/$spaceId/workers/all\" -Headers @{\"X-Octopus-ApiKey\"=\"$apiKey\"}) | Where-Object {$_.Name -eq \"$workerName\"} +} + +# Check to make sure something was returned +if ($null -eq $worker) +{ +\tif (![string]::IsNullOrEmpty($workerPoolName)) + { + \tWrite-Error \"Unable to find $workerName in $workerPoolName!\" + } + else + { + \tWrite-Error \"Unable to find $workerName!\" + } +} + +# Build payload +$jsonPayload = @{ +\tName = \"Health\" + Description = \"Check $workerName health\" + Arguments = @{ + \tTimeout = \"00:05:00\" + MachineIds = @( + \t$worker.Id + ) + OnlyTestConnection = \"false\" + } + SpaceId = \"$spaceId\" +} + +# Display message +Write-Output \"Beginning health check of $workerName ...\" + +# Execute health check +$healthCheck = (Invoke-RestMethod -Method Post -Uri \"$baseUrl/api/tasks\" -Body ($jsonPayload | ConvertTo-Json -Depth 10) -Headers @{\"X-Octopus-ApiKey\"=\"$apiKey\"}) + +# Check to see if the health check is queued +while ($healthCheck.IsCompleted -eq $false) +{ + $healthCheck = (Invoke-RestMethod -Method Get -Uri \"$baseUrl/api/tasks/$($healthCheck.Id)\" -Headers @{\"X-Octopus-ApiKey\"=\"$apiKey\"}) +} + +if ($healthCheck.State -eq \"Failed\") +{ +\tWrite-Error \"Health check failed!\" +} +else +{ +\tWrite-Output \"Health check completed with $($healthCheck.State).\" +} +" + }, + "Parameters": [ + { + "Id": "4ec84f25-8c23-4576-b7fe-3b60ca6f0f3c", + "Name": "WorkerName", + "Label": "Worker name", + "HelpText": "Name of the worker to do a health check on.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d876958b-1d93-45fa-996e-2005de2919ee", + "Name": "WorkerPoolName", + "Label": "Worker Pool", + "HelpText": "(Optional) Worker pool the worker belongs to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1939947b-e777-4e4a-8caf-729dacb25aed", + "Name": "WorkerApiKey", + "Label": "Api Key", + "HelpText": "Api Key to use to issue health check.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2023-02-16T15:38:44.043Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "octopus" + } diff --git a/step-templates/opslevel-create-deploy-event-bash.json.human b/step-templates/opslevel-create-deploy-event-bash.json.human new file mode 100644 index 000000000..1af95931e --- /dev/null +++ b/step-templates/opslevel-create-deploy-event-bash.json.human @@ -0,0 +1,148 @@ +{ + "Id": "9a22df59-75ea-4867-852e-fddc0d65fa1a", + "Name": "OpsLevel - Create Deploy Event - Bash", + "Description": "Track deploys to your services across different environments in [OpsLevel](https://www.opslevel.com/docs/insights/deploys/).", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Name": "OpsLevel", + "Id": "3f494661-a014-4602-9bb3-fd3b7dcc9cbe", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "OL_PACKAGE" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "if test -f \"#{Octopus.Action.Package[OpsLevel].ExtractedPath}/opslevel\"; then +\tchmod +x #{Octopus.Action.Package[OpsLevel].ExtractedPath}/opslevel +\tcat << EOF | #{Octopus.Action.Package[OpsLevel].ExtractedPath}/opslevel create deploy --log-level=WARN -i #{OL_INTEGRATION_URL} -f - +service: #{OL_SERVICE} +description: #{OL_DESCRIPTION} +environment: #{OL_ENVIRONMENT} +deploy-number: #{OL_DEPLOY_NUMBER} +deploy-url: #{OL_DEPLOY_URL} +dedup-id: #{OL_DEDUP_ID} +deployer: + name: #{OL_DEPLOYER_NAME} + email: #{OL_DEPLOYER_EMAIL} +#{if Octopus.Release.Package} +#{if Octopus.Release.Package[].Commits} +commit: + sha: \"#{Octopus.Release.Package[0].Commits[0].CommitId}\" + message: \"#{Octopus.Release.Package[0].Commits[0].Comment}\" +#{/if} +#{/if} +EOF +else +\techo \"Please ensure the `opslevel` CLI package is setup and installed!\" +fi" + }, + "Parameters": [ + { + "Id": "1f3a7c3e-246e-4767-b314-bfc46d77c9e7", + "Name": "OL_PACKAGE", + "Label": "OpsLevel CLI Package", + "HelpText": "The Package that contains the OpsLevel CLI", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "31ed8779-810d-43e0-a53c-90520b549dab", + "Name": "OL_SERVICE", + "Label": "Service", + "HelpText": "The service alias for the event", + "DefaultValue": "#{Octopus.Project.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "033217c3-d9fe-4bf3-a353-5d51768b77e5", + "Name": "OL_DESCRIPTION", + "Label": "Description", + "HelpText": "The description of the event", + "DefaultValue": "#{Octopus.Release.Notes}", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "1cc660c8-bb22-4ff3-bc09-d7488cd6c24a", + "Name": "OL_ENVIRONMENT", + "Label": "Environment", + "HelpText": "The environment for the event", + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c60fc4c7-b29c-4f0d-a7d3-691b0bad2a3e", + "Name": "OL_DEPLOY_NUMBER", + "Label": "Deploy Number", + "HelpText": "The deploy number of the event", + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "304310bc-600d-435d-9481-7a71bac9fc24", + "Name": "OL_DEPLOY_URL", + "Label": "Deploy URL", + "HelpText": "The deploy url of the event", + "DefaultValue": "#{Octopus.Web.ServerUri}#{Octopus.Web.DeploymentLink}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "53ee85df-3d25-4018-b3e2-0dd34eb721ea", + "Name": "OL_DEDUP_ID", + "Label": "Dedup Id", + "HelpText": "The dedup id for the event", + "DefaultValue": "#{Octopus.Release.Id}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "536caa2b-36b3-4a6a-b1ed-245ddea9e647", + "Name": "OL_DEPLOYER_EMAIL", + "Label": "Deployer Email", + "HelpText": "The email of the deployer who created the event", + "DefaultValue": "#{Octopus.Deployment.CreatedBy.EmailAddress}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "244d6d8f-a512-4211-8e9e-16ae7caca84a", + "Name": "OL_DEPLOYER_NAME", + "Label": "Deployer Name", + "HelpText": "The name of the deployer who created the event", + "DefaultValue": "#{Octopus.Deployment.CreatedBy.Username}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-06-27T16:56:46.770Z", + "OctopusVersion": "2021.1.7316", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "rocktavious", + "Category": "opslevel" +} diff --git a/step-templates/opslevel-create-deploy-event-ps.json.human b/step-templates/opslevel-create-deploy-event-ps.json.human new file mode 100644 index 000000000..82413ca52 --- /dev/null +++ b/step-templates/opslevel-create-deploy-event-ps.json.human @@ -0,0 +1,148 @@ +{ + "Id": "dbde3ef3-a81b-43ac-99ed-509660e54beb", + "Name": "OpsLevel - Create Deploy Event - Powershell", + "Description": "Track deploys to your services across different environments in [OpsLevel](https://www.opslevel.com/docs/insights/deploys/).", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Name": "OpsLevel", + "Id": "15dca245-862f-410f-b5f4-753dbba6e8f8", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "OL_PACKAGE" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "if (Test-Path -Path #{Octopus.Action.Package[OpsLevel].ExtractedPath}\\opslevel.exe -PathType Leaf) { +\t@\" +service: #{OL_SERVICE} +description: #{OL_DESCRIPTION} +environment: #{OL_ENVIRONMENT} +deploy-number: #{OL_DEPLOY_NUMBER} +deploy-url: #{OL_DEPLOY_URL} +dedup-id: #{OL_DEDUP_ID} +deployer: + name: #{OL_DEPLOYER_NAME} + email: #{OL_DEPLOYER_EMAIL} +#{if Octopus.Release.Package} +#{if Octopus.Release.Package[].Commits} +commit: + sha: \\\"#{Octopus.Release.Package[0].Commits[0].CommitId}\\\" + message: \\\"#{Octopus.Release.Package[0].Commits[0].Comment}\\\" +#{/if} +#{/if} +\"@ | #{Octopus.Action.Package[OpsLevel].ExtractedPath}\\opslevel.exe create deploy --log-level=WARN -i \"#{OL_INTEGRATION_URL}\" -f - +} else { +\tWrite-Host \"Please ensure the `opslevel` CLI package is setup and installed!\" +} +" + }, + "Parameters": [ + { + "Id": "1f3a7c3e-246e-4767-b314-bfc46d77c9e7", + "Name": "OL_PACKAGE", + "Label": "OpsLevel CLI Package", + "HelpText": "The Package that contains the OpsLevel CLI", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "31ed8779-810d-43e0-a53c-90520b549dab", + "Name": "OL_SERVICE", + "Label": "Service", + "HelpText": "The service alias for the event", + "DefaultValue": "#{Octopus.Project.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "033217c3-d9fe-4bf3-a353-5d51768b77e5", + "Name": "OL_DESCRIPTION", + "Label": "Description", + "HelpText": "The description of the event", + "DefaultValue": "#{Octopus.Release.Notes}", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "1cc660c8-bb22-4ff3-bc09-d7488cd6c24a", + "Name": "OL_ENVIRONMENT", + "Label": "Environment", + "HelpText": "The environment for the event", + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c60fc4c7-b29c-4f0d-a7d3-691b0bad2a3e", + "Name": "OL_DEPLOY_NUMBER", + "Label": "Deploy Number", + "HelpText": "The deploy number of the event", + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "304310bc-600d-435d-9481-7a71bac9fc24", + "Name": "OL_DEPLOY_URL", + "Label": "Deploy URL", + "HelpText": "The deploy url of the event", + "DefaultValue": "#{Octopus.Web.ServerUri}#{Octopus.Web.DeploymentLink}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "53ee85df-3d25-4018-b3e2-0dd34eb721ea", + "Name": "OL_DEDUP_ID", + "Label": "Dedup Id", + "HelpText": "The dedup id for the event", + "DefaultValue": "#{Octopus.Release.Id}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "536caa2b-36b3-4a6a-b1ed-245ddea9e647", + "Name": "OL_DEPLOYER_EMAIL", + "Label": "Deployer Email", + "HelpText": "The email of the deployer who created the event", + "DefaultValue": "#{Octopus.Deployment.CreatedBy.EmailAddress}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "244d6d8f-a512-4211-8e9e-16ae7caca84a", + "Name": "OL_DEPLOYER_NAME", + "Label": "Deployer Name", + "HelpText": "The name of the deployer who created the event", + "DefaultValue": "#{Octopus.Deployment.CreatedBy.Username}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-06-27T16:57:30.362Z", + "OctopusVersion": "2021.1.7316", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "rocktavious", + "Category": "opslevel" +} diff --git a/step-templates/oracle-add-user-to-role.json.human b/step-templates/oracle-add-user-to-role.json.human new file mode 100644 index 000000000..df7c3fc74 --- /dev/null +++ b/step-templates/oracle-add-user-to-role.json.human @@ -0,0 +1,241 @@ +{ + "Id": "6a0db144-3ad5-46bb-bd7d-02b22b98a559", + "Name": "Oracle - Add Database User To Role", + "Description": "Adds database user to a role", + "ActionType": "Octopus.Script", + "Version": 2, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-UserInRole +{ +\t# Define parameters + param ( + $Username, + $RoleName) + +\t# Execute query + $userRole = Invoke-SqlQuery \"SELECT * FROM DBA_ROLE_PRIVS WHERE GRANTEE = '$Username' AND GRANTED_ROLE = '$RoleName'\" + + # Check to see if anything was returned + if ($userRole.ItemArray.Count -gt 0) + { + # Found + return $true + } + + + # Not found + return $false +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +# Create credential object for the connection +$SecurePassword = ConvertTo-SecureString $oracleLoginPasswordWithAddRoleRights -AsPlainText -Force +$ServerCredential = New-Object System.Management.Automation.PSCredential ($oracleLoginWithAddRoleRights, $SecurePassword) + +try +{ +\t# Connect to MySQL + Open-OracleConnection -Datasource $oracleServerName -Credential $ServerCredential -Port $oracleServerPort -ServiceName $oracleServiceName + + # See if database exists + $userInRole = Get-UserInRole -Username $oracleUsername -RoleName $oracleRoleName + + if ($userInRole -eq $false) + { + # Create database + Write-Output \"Adding user $oracleUsername to role $oracleRoleName ...\" + $executionResults = Invoke-SqlUpdate \"GRANT `\"$oracleRoleName`\" TO `\"$oracleUsername`\"\" + + # See if it was created + $userInRole = Get-UserInRole -Username $oracleUsername -RoleName $oracleRoleName + + # Check array + if ($userInRole -eq $true) + { + # Success + Write-Output \"$oracleUserName added to $oracleRoleName successfully!\" + } + else + { + # Failed + Write-Error \"Failure adding $oracleUserName to $oracleRoleName!\" + } + } + else + { + \t# Display message + Write-Output \"User $oracleUsername is already in role $oracleRoleName\" + } +} +finally +{ + Close-SqlConnection +} + + +", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "66790019-ca40-41cc-8849-5995557e34c1", + "Name": "oracleServerName", + "Label": "Server name", + "HelpText": "Name of the Oracle server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d29f262e-74d2-4c7b-acb3-67b642b614c4", + "Name": "oracleServerPort", + "Label": "Port", + "HelpText": "Port that the Oracle server listens on.", + "DefaultValue": "1521", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "330f737b-dc2b-4ceb-9d27-168c1b5f6e18", + "Name": "oracleServiceName", + "Label": "Service Name", + "HelpText": "Service name for Oracle database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0531e03b-c152-46e4-ab2d-7cb8093aa641", + "Name": "oracleLoginWithAddRoleRights", + "Label": "Login name", + "HelpText": "Login name of a user that can add roles to other users.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5bddbafe-0ca0-4037-9dd1-d522abc5838e", + "Name": "oracleLoginPasswordWithAddRoleRights", + "Label": "Login password", + "HelpText": "Password for the login account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "2c6ee772-96f6-4803-b6a1-5d57a58b28f0", + "Name": "oracleUsername", + "Label": "User name", + "HelpText": "Name of the user to add the role to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7f806b37-e0d5-4560-873e-de6c73f80161", + "Name": "oracleRoleName", + "Label": "Role name", + "HelpText": "Name of the role to add to the user.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-21T21:54:08.753Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "oracle" + } diff --git a/step-templates/oracle-create-user-if-not-exists.json.human b/step-templates/oracle-create-user-if-not-exists.json.human new file mode 100644 index 000000000..5b0f081f9 --- /dev/null +++ b/step-templates/oracle-create-user-if-not-exists.json.human @@ -0,0 +1,229 @@ +{ + "Id": "d8b21b0b-1a07-4d47-9c72-4260e83a807c", + "Name": "Oracle - Create User If Not Exists", + "Description": "Creates a new user account on a Oracle database server", + "ActionType": "Octopus.Script", + "Version": 2, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-UserExists +{ +\t# Define parameters + param ($Hostname, + $Username) + +\t# Execute query + return Invoke-SqlQuery \"SELECT * FROM ALL_USERS WHERE USERNAME = '$UserName'\" +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +# Create credential object for the connection +$SecurePassword = ConvertTo-SecureString $oracleLoginPasswordWithAddUserRights -AsPlainText -Force +$ServerCredential = New-Object System.Management.Automation.PSCredential ($oracleLoginWithAddUserRights, $SecurePassword) + +try +{ +\t# Connect to MySQL + Open-OracleConnection -Datasource $oracleDBServerName -Credential $ServerCredential -Port $oracleDBServerPort -ServiceName $oracleServiceName + + # See if database exists + $userExists = Get-UserExists -Username $oracleNewUsername + + if ($userExists -eq $null) + { + # Create database + Write-Output \"Creating user $oracleNewUsername ...\" + $executionResults = Invoke-SqlUpdate \"CREATE USER `\"$oracleNewUsername`\" IDENTIFIED BY `\"$oracleNewUserPassword`\"\" + + # See if it was created + $userExists = Get-UserExists -Username $oracleNewUsername + + # Check array + if ($userExists -ne $null) + { + # Success + Write-Output \"$oracleNewUsername created successfully!\" + } + else + { + # Failed + Write-Error \"$oracleNewUsername was not created!\" + } + } + else + { + \t# Display message + Write-Output \"User $oracleNewUsername already exists.\" + } +} +finally +{ + Close-SqlConnection +} + + +", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "8123da26-a8ed-4b4e-bc04-b5c90546785a", + "Name": "oracleDBServerName", + "Label": "Oracle Server", + "HelpText": "Host name of the Oracle server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a4a4ee03-b166-4324-9bdf-4011c1f4f707", + "Name": "oracleDBServerPort", + "Label": "Port", + "HelpText": "Port number the Oracle server listens on.", + "DefaultValue": "1521", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "63cd98dc-359c-4c25-8b84-56ab33d9d05f", + "Name": "oracleServiceName", + "Label": "Service Name", + "HelpText": "Service name for Oracle database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "307a4f0e-92b7-4b2f-8cac-b12771d2cb23", + "Name": "oracleLoginWithAddUserRights", + "Label": "Admin Login name", + "HelpText": "Login name of a user with rights to create user accounts.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "78ccf489-2c68-419e-9b53-bdf96ff9ae8c", + "Name": "oracleLoginPasswordWithAddUserRights", + "Label": "Login Password", + "HelpText": "Password Login name.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "8bf12f2a-7d7b-4b8c-bced-2b438fbb21e4", + "Name": "oracleNewUsername", + "Label": "New user name", + "HelpText": "Name of the new user account to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "45b96db6-4523-4b07-b8e7-848dc2b63053", + "Name": "oracleNewUserPassword", + "Label": "New user password", + "HelpText": "Password for the new user account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-21T21:52:45.217Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "oracle" + } diff --git a/step-templates/oracle-run-script-through-sqlplus.json.human b/step-templates/oracle-run-script-through-sqlplus.json.human new file mode 100644 index 000000000..936d3dee4 --- /dev/null +++ b/step-templates/oracle-run-script-through-sqlplus.json.human @@ -0,0 +1,93 @@ +{ + "Id": "c7cd3ab4-5dfb-4f8d-957e-1940ed30359c", + "Name": "Run Oracle SQLPlus Script", + "Description": "This step will run a script file on an Oracle database using SQLPlus. This script assumes you have SQLPlus installed and a TNS entry for the database you wish to connect to.", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$scriptFile = $OctopusParameters[\"Oracle.ScriptFile.Location\"] +$server = $OctopusParameters[\"Oracle.Server.Name\"] +$user = $OctopusParameters[\"Oracle.User.Name\"] +$password = $OctopusParameters[\"Oracle.User.Password\"] + +Write-Host \"Script File: $scriptFile\" +Write-Host \"Oracle Server: $server\" +Write-Host \"Oracle Username: $user\" +Write-Host \"Oracle Password not shown\" + +If ((Test-Path $scriptFile) -eq $true){ + Write-Host \"Script file found, running on the database\" + + $maskedConnectionString = \"$user/*****@$server/$deploymentSchema\" + $unmaskedConnectionString = \"$user/$password@$server\" + Write-Host \"Running the script against: $maskedConnectionString\" + + Write-Host \"Adding to the top of the script file WHENEVER SQLERROR EXIT SQL.SQLCODE\" + $scriptToHandleErrors = \"WHENEVER SQLERROR EXIT SQL.SQLCODE + \" + + $old = Get-Content $scriptFile + Set-Content -Path $scriptFile -Value $scriptToHandleErrors + Add-Content -Path $scriptFile -Value $old + + echo exit | sqlplus $unmaskedConnectionString @$scriptFile +} +else { +\tWrite-Highlight \"No script file was found. If the script file should be there please verify the location and try again.\" +}" + }, + "Parameters": [ + { + "Id": "2aa011b3-ab2b-4de9-a09c-abb20cbbd55e", + "Name": "Oracle.ScriptFile.Location", + "Label": "Script File To Run", + "HelpText": "The script file to run on the Oracle server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "36161c93-a70d-472d-a31a-d8cba42ee087", + "Name": "Oracle.Server.Name", + "Label": "TNS Name", + "HelpText": "The TNS entry in tnsnames.ora containing the necessary connection information.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1e1cc9f3-486e-4df2-bb9c-e7dd7d1918f7", + "Name": "Oracle.User.Name", + "Label": "Oracle Username", + "HelpText": "The user who has permissions to run the script file on the server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bbf4eb93-fbda-4740-b782-88480183d77c", + "Name": "Oracle.User.Password", + "Label": "Oracle User Password", + "HelpText": "The password of the user who will run the script", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedOn": "2019-03-21T14:34Z", + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2019-03-21T20:12:11.956Z", + "OctopusVersion": "2018.9.5", + "Type": "ActionTemplate" + }, + "Category": "oracle" +} diff --git a/step-templates/pagerduty-close-maintenance-window.json.human b/step-templates/pagerduty-close-maintenance-window.json.human new file mode 100644 index 000000000..a9dff5ae7 --- /dev/null +++ b/step-templates/pagerduty-close-maintenance-window.json.human @@ -0,0 +1,95 @@ +{ + "Id": "4841c8e6-3f23-4b52-90d0-c363eb0bc526", + "Name": "PagerDuty - Close Maintenance Window", + "Description": "Closes a maintenance window by Id.", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "param( + [string]$OpeningStepName = \"\", + [string]$Token = \"\" +) + +function Get-Param($Name, [switch]$Required, $Default) { + $Result = $null + + if ($OctopusParameters -ne $null) { + $Result = $OctopusParameters[$Name] + } + + if ($Result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $Result = $variable.Value + } + } + + if ($Result -eq $null -or $Result -eq \"\") { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $Result = $Default + } + } + + return $Result +} + +& { + param([string]$OpeningStepName, [string]$Token) + +\t$WindowId = $OctopusParameters[\"Octopus.Action[$OpeningStepName].Output.WindowId\"] + $Uri = \"https://api.pagerduty.com/maintenance_windows/$WindowId\" + $Headers = @{ + \"Authorization\" = \"Token token=$Token\" + \"Accept\" = \"application/vnd.pagerduty+json;version=2\" +\t\t} + +\ttry { +\t\tInvoke-RestMethod -Uri $Uri -Method Delete -ContentType \"application/json\" -Headers $Headers +\t\tWrite-Host \"PagerDuty window closed for window_id: $WindowId\" +\t} catch [System.Exception] { + Write-Host $_.Exception.Message + + $ResponseStream = $_.Exception.Response.GetResponseStream() + $Reader = New-Object System.IO.StreamReader($ResponseStream) + $Reader.ReadToEnd() | Write-Host + +\t\tExit 0 + } +} (Get-Param 'OpeningStepName' -Required) (Get-Param 'Token' -Required)" + }, + "Parameters": [ + { + "Name": "Token", + "Label": "Token", + "HelpText": "The API token of the PagerDuty instance. + +Found here: https://mydomain.pagerduty.com/api_keys", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "OpeningStepName", + "Label": "OpeningStepName", + "HelpText": "The **previous** step in which the window to close was opened.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + } + ], + "LastModifiedOn": "2020-07-23T07:38:49.056Z", + "LastModifiedBy": "tfbryan", + "$Meta": { + "ExportedAt": "2020-07-23T07:38:49.056Z", + "OctopusVersion": "2020.1.14", + "Type": "ActionTemplate" + }, + "Category": "pagerduty" +} diff --git a/step-templates/pagerduty-open-maintenance-window.json.human b/step-templates/pagerduty-open-maintenance-window.json.human new file mode 100644 index 000000000..017d282a1 --- /dev/null +++ b/step-templates/pagerduty-open-maintenance-window.json.human @@ -0,0 +1,180 @@ +{ + "Id": "93a10982-f675-42cd-ac3a-46ef28a46afa", + "Name": "PagerDuty - Open Maintenance Window", + "Description": "Open a new maintenance window for the specified services, using the PagerDuty v2 API. + +No new incidents will be created for a service that is currently in maintenance. + +This script sets an output variable **WindowId** that can be used in the _PagerDuty - Close Maintenance Window_ template.", + "ActionType": "Octopus.Script", + "Version": 16, + "Properties": { + "Octopus.Action.Script.ScriptBody": "param( + [array]$ServiceIds = @(\"\"), + [string]$RequesterId = \"\", + [string]$Description = \"\", + [int]$Minutes = 10, + [string]$Token = \"\" +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $Result = $null + + if ($OctopusParameters -ne $null) { + $Result = $OctopusParameters[$Name] + } + + if ($Result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $Result = $variable.Value + } + } + + if ($Result -eq $null -or $Result -eq \"\") { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $Result = $Default + } + } + + return $Result +} + +& { + param([array]$ServiceIds, [string]$RequesterId, [string]$Description, [int]$Minutes, [string]$Token) + + Write-Host \"Opening PagerDuty window for $Description\" + + try { + $ArrayOfServices = $ServiceIds.split(\",\") | foreach { $_.trim() } + $Start = ((Get-Date)).ToString(\"yyyy-MM-ddTHH:mm:sszzzzZ\"); + $End = ((Get-Date).AddMinutes($Minutes)).ToString(\"yyyy-MM-ddTHH:mm:sszzzzZ\"); + $ServiceIdArray = @() + + foreach($ServiceId in $ArrayOfServices){ + \t$ServiceIdArray += @{\"id\"=$ServiceId; \"type\"=\"service_reference\"} + } + + $Uri = \"https://api.pagerduty.com/maintenance_windows\" + $Headers = @{ + \"Authorization\" = \"Token token=$Token\" + \"Accept\" = \"application/vnd.pagerduty+json;version=2\" + \"From\" = $RequesterId +\t\t} + + Write-Host \"Window will be open from $Start -> $End\" + + $Post = @{ + maintenance_window= @{ + \ttype = 'maintenance_window' + start_time = $Start + end_time = $End + description = $Description + services = $ServiceIdArray + } + } | ConvertTo-Json -Depth 4 + + $ResponseObj = Invoke-RestMethod -Uri $Uri -Method Post -Body $Post -ContentType \"application/json\" -Headers $Headers + $WindowId = $ResponseObj.maintenance_window.id + + Write-Host \"Window Id $WindowId created\" + + if(Get-Command -name \"Set-OctopusVariable\" -ErrorAction SilentlyContinue) { + Set-OctopusVariable -name \"WindowId\" -value $WindowId + } else { + Write-Host \"Octopus output variable not set\" + } + } catch [System.Exception] { + Write-Host \"Error while opening PagerDuty window\" + Write-Host $_.Exception.Message + + $ResponseStream = $_.Exception.Response.GetResponseStream() + $Reader = New-Object System.IO.StreamReader($ResponseStream) + $Reader.ReadToEnd() | Write-Host + + Exit 1 + } +} (Get-Param 'ServiceIds' -Required) (Get-Param 'RequesterId' -Required) (Get-Param 'Description' -Required) (Get-Param 'Minutes' -Required) (Get-Param 'Token' -Required)", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Name": "Minutes", + "Label": "Minutes", + "HelpText": "Please set an estimated number of minutes for the maintenance window. The maintenance window will remain open for this duration unless it's proactively closed sooner.", + "DefaultValue": "60", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "RequesterId", + "Label": "RequesterId", + "HelpText": "Please configure an email address of the user creating the maintenance window. + +Example: Octopus@mydomain.com", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Token", + "Label": "Token", + "HelpText": "Please supply the API token of your PagerDuty instance. + +Found here: https://mydomain.pagerduty.com/api_keys", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Description", + "Label": "Description", + "HelpText": "Please supply a short description for this maintenance window.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ServiceIds", + "Label": "ServiceIds", + "HelpText": "Please supply a comma separated list of the PagerDuty service ids that will be included in this maintenance window. + +Found here: https://mydomain.pagerduty.com/services/**ABC123** + +Example: ABC123, ABC456", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Subdomain", + "Label": "Subdomain", + "HelpText": "The subdomain of the PagerDuty instance. + +Found here: https://**mydomain**.pagerduty.com/", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2020-07-23T08:13:54.382Z", + "LastModifiedBy": "tfbryan", + "$Meta": { + "ExportedAt": "2020-07-23T08:13:54.382Z", + "OctopusVersion": "2020.1.14", + "Type": "ActionTemplate" + }, + "Category": "pagerduty" +} diff --git a/step-templates/pause-resume-pingdom-check.json.human b/step-templates/pause-resume-pingdom-check.json.human new file mode 100644 index 000000000..14f9b3002 --- /dev/null +++ b/step-templates/pause-resume-pingdom-check.json.human @@ -0,0 +1,77 @@ +{ + "Id": "a2d3b49a-84aa-4dc4-8da1-fbb3d8ff205b", + "Name": "Pause or Resume Pingdom check", + "Description": null, + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$webclient = New-Object System.Net.WebClient +$webclient.Credentials = New-Object System.Net.NetworkCredential($UserName, $Password) +$webClient.Headers.add('App-Key',$AppKey) +$url = \"https://api.pingdom.com/api/2.0/checks/$CheckId\" +$actionBody = \"paused=\" + ($Action -eq \"Pause\").tostring().tolower() + +$checkResult = $webclient.DownloadString($url) | ConvertFrom-Json +Write-Host \"Attempting to\" $Action.tolower() \"check\" $CheckId \"-\" $checkResult.check.name + +$result = $webclient.UploadString($url, \"PUT\", $actionBody) | ConvertFrom-Json + +Write-Host $result.message" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "UserName", + "Label": "User Name", + "HelpText": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Password", + "Label": "Pingdom Password", + "HelpText": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "AppKey", + "Label": "Application Key", + "HelpText": "You generate your application key inside the Pingdom control panel.", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "CheckId", + "Label": "Check Id", + "HelpText": "Check Id to be paused or resumed", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Action", + "Label": null, + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Pause +Resume" + } + } + ], + "LastModifiedOn": "2015-07-31T12:23:52.329+00:00", + "LastModifiedBy": "timorzadir", + "$Meta": { + "ExportedAt": "2015-07-31T12:23:52.329+00:00", + "OctopusVersion": "3.0.7.2204", + "Type": "ActionTemplate" + }, + "Category": "pingdom" +} diff --git a/step-templates/pingdom-create-uptime-check.json.human b/step-templates/pingdom-create-uptime-check.json.human new file mode 100644 index 000000000..a2d67f610 --- /dev/null +++ b/step-templates/pingdom-create-uptime-check.json.human @@ -0,0 +1,436 @@ +{ + "Id": "70ea4820-7d02-4015-a691-8c77b5ab14d5", + "Name": "Pingdom - Create Uptime Check", + "Description": "Creates Pingdom http check using [Create New Check API method](https://www.pingdom.com/resources/api#MethodCreate+New+Check).", + "ActionType": "Octopus.Script", + "Version": 43, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null, + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Ref https://www.pingdom.com/resources/api#MethodCreate+New+Check + +Function Get-Parameter() { + Param( + [parameter(Mandatory=$true)] + [string]$Name, + [switch]$Required, + $Default, + [switch]$FailOnValidate + ) + + $result = $null + $errMessage = [string]::Empty + + If ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + Write-Host (\"Octopus parameter value for \" + $Name + \": \" + $result) + } + + If ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + If ($result -eq $null -or [string]::IsNullOrEmpty($result)) { + If ($Required) { + $errMessage = \"Missing value for $Name\" + } ElseIf (-Not $Default -eq $null) { + Write-Host (\"Default value: \" + $Default) + $result = $Default + } + } + + If (-Not [string]::IsNullOrEmpty($errMessage)) { + If ($FailOnValidate) { + Throw $errMessage + } Else { + Write-Warning $errMessage + } + } + + return $result +} + +Function Test-Any() { + begin { + $any = $false + } + process { + $any = $true + } + end { + $any + } +} + +& { + Write-Host \"Start PingdomCreateUptimeCheck\" + + Add-Type -AssemblyName System.Web + + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + $throwErrorWhenFailed = [System.Convert]::ToBoolean([string](Get-Parameter -Name \"Pingdom.ThrowErrorWhenFailed\" -Default \"False\")) + + $pingdomUsername = [string] (Get-Parameter -Name \"Pingdom.Username\" -Required -FailOnValidate:$throwErrorWhenFailed) + $pingdomPassword = [string] (Get-Parameter -Name \"Pingdom.Password\" -Required -FailOnValidate:$throwErrorWhenFailed) + $pingdomAppKey = [string] (Get-Parameter -Name \"Pingdom.AppKey\" -Required -FailOnValidate:$throwErrorWhenFailed) + + $pingdomCheckName = [string] (Get-Parameter -Name \"Pingdom.CheckName\" -Required -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckTarget = [string] (Get-Parameter -Name \"Pingdom.CheckTarget\" -Required -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckType = \"http\" + $pingdomCheckIntervalMinutes = [System.Nullable[int]] (Get-Parameter -Name \"Pingdom.CheckIntervalMinutes\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckContactIds = [string] (Get-Parameter -Name \"Pingdom.CheckContactIds\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckSendNotificationWhenDown = [System.Nullable[int]] (Get-Parameter -Name \"Pingdom.CheckSendNotificationWhenDown\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckNotifyAgainEvery = [System.Nullable[int]] (Get-Parameter -Name \"Pingdom.CheckNotifyAgainEvery\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckNotifyWhenBackUp = [string](Get-Parameter -Name \"Pingdom.CheckNotifyWhenBackUp\" -Default \"True\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckTags = [string] (Get-Parameter -Name \"Pingdom.CheckTags\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckHttpUrl = [string] (Get-Parameter -Name \"Pingdom.CheckHttpUrl\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckHttpEncryptionEnabled = [string](Get-Parameter -Name \"Pingdom.CheckHttpEncryptionEnabled\" -Default \"False\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckHttpTargetPort = [System.Nullable[int]] (Get-Parameter -Name \"Pingdom.CheckHttpTargetPort\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckAuth = [string] (Get-Parameter -Name \"Pingdom.CheckAuth\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckShouldContain = [string] (Get-Parameter -Name \"Pingdom.CheckShouldContain\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckShouldNotContain = [string] (Get-Parameter -Name \"Pingdom.CheckShouldNotContain\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckPostData = [string] (Get-Parameter -Name \"Pingdom.CheckPostData\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckIntegrationIds = [string] (Get-Parameter -Name \"Pingdom.CheckIntegrationIds\" -FailOnValidate:$throwErrorWhenFailed) + + $apiVersion = \"2.1\" + $url = \"https://api.pingdom.com/api/{0}/checks\" -f $apiVersion + $securePassword = ConvertTo-SecureString $pingdomPassword -AsPlainText -Force + $credential = New-Object System.Management.Automation.PSCredential($pingdomUsername, $securePassword) + $headers = @{ + \"App-Key\" = $pingdomAppKey + } + + # Validate if check already registered + + Write-Host \"Getting uptime check list to find check by name: $url\" + Try { + $response = Invoke-RestMethod -Uri $url -Method Get -ContentType \"application/json\" -Credential $credential -Headers $headers + } Catch { + $errMessage = \"Error occured when getting uptime check in Pingdom: \" + $_.Exception.Message + If($throwErrorWhenFailed -eq $true) { + Write-Error $errMessage + } + Else { + Write-Warning $errMessage + } + } + + $checkExists = $response.checks | Where-Object { $_.name -eq $pingdomCheckName } | Test-Any + If ($checkExists -eq $true) { + Write-Host \"Check with name $pingdomCheckName already registered. New check will not be created!\" + Exit + } + + # Create new check + + $apiParameters = @{} + $apiParameters.Add(\"name\", $pingdomCheckName) + $apiParameters.Add(\"host\", $pingdomCheckTarget) + $apiParameters.Add(\"type\", $pingdomCheckType) + $apiParameters.Add(\"contactids\", $pingdomCheckContactIds) + $apiParameters.Add(\"integrationids\", $pingdomCheckIntegrationIds) + If ($pingdomCheckIntervalMinutes -ne $null) { + $apiParameters.Add(\"resolution\", $pingdomCheckIntervalMinutes) + } + If ($pingdomCheckSendNotificationWhenDown -ne $null) { + $apiParameters.Add(\"sendnotificationwhendown\", $pingdomCheckSendNotificationWhenDown) + } + If ($pingdomCheckNotifyAgainEvery -ne $null) { + $apiParameters.Add(\"notifyagainevery\", $pingdomCheckNotifyAgainEvery) + } + If ($pingdomCheckNotifyWhenBackUp -ne $null) { + $apiParameters.Add(\"notifywhenbackup\", $pingdomCheckNotifyWhenBackUp.ToLower()) + } + If ($pingdomCheckTags -ne $null) { + $apiParameters.Add(\"tags\", $pingdomCheckTags) + } + If (-Not [string]::IsNullOrEmpty($pingdomCheckHttpUrl)) { + $apiParameters.Add(\"url\", $pingdomCheckHttpUrl) + } + If ($pingdomCheckHttpEncryptionEnabled -ne $null) { + $apiParameters.Add(\"encryption\", $pingdomCheckHttpEncryptionEnabled.ToLower()) + } + If ($pingdomCheckHttpTargetPort -ne $null) { + $apiParameters.Add(\"port\", $pingdomCheckHttpTargetPort) + } + If (-Not [string]::IsNullOrEmpty($pingdomCheckAuth)) { + $apiParameters.Add(\"auth\", $pingdomCheckAuth) + } + If (-Not [string]::IsNullOrEmpty($pingdomCheckShouldContain)) { + $apiParameters.Add(\"shouldcontain\", $pingdomCheckShouldContain) + } + If (-Not [string]::IsNullOrEmpty($pingdomCheckShouldNotContain)) { + $apiParameters.Add(\"shouldnotcontain\", $pingdomCheckShouldNotContain) + } + If (-Not [string]::IsNullOrEmpty($pingdomCheckPostData )) { + $apiParameters.Add(\"postdata\", $pingdomCheckPostData) + } + + If ($apiParameters.Count -gt 0) { + $queryString = \"\" + $apiParameters.Keys | ForEach-Object { + $queryString += ($_ + \"=\" + [System.Web.HttpUtility]::UrlEncode($apiParameters.Item($_)) + \"&\") + } + $requestBody = $queryString.Substring(0, $queryString.Length - 1) + $url += \"?$requestBody\" + } + + Write-Host \"Creating new uptime check: $url\" + Write-Host \"Request body: $requestBody\" + Try { + $response = Invoke-RestMethod -Uri $url -Method Post -Body $requestBody -ContentType \"application/json\" -Credential $credential -Headers $headers + Write-Host (\"Successfully added uptime check in Pingdom: Name = \" + $response.check.name + \", Id = \" + $response.check.id) + } Catch { + $errMessage = \"Error occured when adding uptime check in Pingdom: \" + $_.Exception + \"`n\" + $errMessage += \"Response: \" + $_ + If($throwErrorWhenFailed -eq $true) { + Write-Error $errMessage + } + Else { + Write-Warning $errMessage + } + } + + Write-Host \"End PingdomCreateUptimeCheck\" +}" + }, + "Parameters": [ + { + "Id": "16cdd666-9822-428c-9c1e-fb8943039a98", + "Name": "Pingdom.Username", + "Label": "Username", + "HelpText": "Mandatory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "d5b36e0f-3c55-4b63-9971-4c9147218514", + "Name": "Pingdom.Password", + "Label": "Password", + "HelpText": "Mandatory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "2f33c3ed-202a-4711-8897-a47329c18b8d", + "Name": "Pingdom.AppKey", + "Label": "App key", + "HelpText": "Mandatory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "754d2183-3b3b-4170-898b-f759af555f69", + "Name": "Pingdom.CheckName", + "Label": "Name", + "HelpText": "Mandatory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c04286a9-d4e4-4c33-985e-67f52b3a9549", + "Name": "Pingdom.CheckTarget", + "Label": "Target", + "HelpText": "Mandatory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "96e4c38b-4d43-4083-ace3-510f945a4202", + "Name": "Pingdom.CheckIntervalMinutes", + "Label": "Interval", + "HelpText": "Check interval in minutes. Integer (1, 5, 15, 30, 60)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "7fc1f49f-446c-4836-8966-b1786d45f6b1", + "Name": "Pingdom.CheckContactIds", + "Label": "Contact Ids", + "HelpText": "Pingdom contact identifiers. For example contactids=154325,465231,765871.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f76811df-7a79-4fee-a491-46c6ce4bc813", + "Name": "Pingdom.CheckIntegrationIds", + "Label": "", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "963c6ff8-b79e-408d-9729-036b58c42857", + "Name": "Pingdom.CheckSendNotificationWhenDown", + "Label": "Send notification when down", + "HelpText": "Send notification when down n times.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "6ba72782-3eee-465c-9176-659de024f8dd", + "Name": "Pingdom.CheckNotifyAgainEvery", + "Label": "Notify every", + "HelpText": "Notify again every n result. 0 means that no extra notifications will be sent.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "9ad13abe-13ab-466c-bd0e-a51637bece3b", + "Name": "Pingdom.CheckSendToEmail", + "Label": "Send to email", + "HelpText": null, + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "8a958bb9-5a61-4956-b79d-7f990271b127", + "Name": "Pingdom.CheckNotifyWhenBackUp", + "Label": "Notify when back up", + "HelpText": "Notify when back up again.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "93e9f063-b0b9-495b-8774-fd140b96a640", + "Name": "Pingdom.CheckTags", + "Label": "Tags", + "HelpText": "Pingdom check tags. Example: octopus,azure.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "9d579e60-2228-4521-9178-2da35817d0b8", + "Name": "Pingdom.CheckHttpEncryptionEnabled", + "Label": "Http encryption enabled", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "e289c1f6-cfbc-4774-ac97-395ac353b3b3", + "Name": "Pingdom.CheckHttpTargetPort", + "Label": "Http target port", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "dcda6fd8-9e42-40c4-9213-f77e9e67b974", + "Name": "Pingdom.CheckAuth", + "Label": "Auth", + "HelpText": "Username and password for target HTTP authentication. Example: user:password.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "bab9c934-dfb4-42c3-817f-22b58a8ef3d5", + "Name": "Pingdom.CheckShouldContain", + "Label": "Should contain", + "HelpText": "Target site should contain this string.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "96e83352-1dce-4d16-bce7-0f8d4929ced7", + "Name": "Pingdom.CheckShouldNotContain", + "Label": "Should not contain", + "HelpText": "Target site should NOT contain this string. If shouldcontain is also set, this parameter is not allowed.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "60570a61-678f-4ef2-90e5-c70d7b53ecb4", + "Name": "Pingdom.CheckPostData", + "Label": "Post data", + "HelpText": "Data that should be posted to the web page, for example submission data for a sign-up or login form. The data needs to be formatted in the same way as a web browser would send it to the web server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "fdb84b63-f86b-4aff-b9e9-e9166d512f51", + "Name": "Pingdom.ThrowErrorWhenFailed", + "Label": "Error when failed", + "HelpText": "When checked, throws Octopus exception if template script has failed.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "LastModifiedBy": "sarbis", + "$Meta": { + "ExportedAt": "2018-06-11T13:49:31.508Z", + "OctopusVersion": "2018.4.12", + "Type": "ActionTemplate" + }, + "Category": "pingdom" +} diff --git a/step-templates/pingdom-modify-uptime-check.json.human b/step-templates/pingdom-modify-uptime-check.json.human new file mode 100644 index 000000000..3ea3f6f41 --- /dev/null +++ b/step-templates/pingdom-modify-uptime-check.json.human @@ -0,0 +1,300 @@ +{ + "Id": "e2324451-0488-4dbf-acc8-656cc2f8defb", + "Name": "Pingdom - Modify Uptime Check", + "Description": "Modifies Pingdom http check using [Modify Check API method](https://www.pingdom.com/resources/api#MethodModify+Check).", + "ActionType": "Octopus.Script", + "Version": 26, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null, + "Octopus.Action.Package.DownloadOnTentacle": "False", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Ref: https://www.pingdom.com/resources/api#MethodModify+Check + +Function Get-Parameter() { + Param( + [parameter(Mandatory=$true)] + [string]$Name, + [switch]$Required, + $Default, + [switch]$FailOnValidate + ) + + $result = $null + $errMessage = [string]::Empty + + If ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + Write-Host (\"Octopus parameter value for \" + $Name + \": \" + $result) + } + + If ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + If ($result -eq $null -or [string]::IsNullOrEmpty($result)) { + If ($Required) { + $errMessage = \"Missing value for $Name\" + } ElseIf (-Not $Default -eq $null) { + Write-Host (\"Default value: \" + $Default) + $result = $Default + } + } + + If (-Not [string]::IsNullOrEmpty($errMessage)) { + If ($FailOnValidate) { + Throw $errMessage + } Else { + Write-Warning $errMessage + } + } + + return $result +} + +& { + Write-Host \"Start PingdomModifyUptimeCheck\" + + Add-Type -AssemblyName System.Web + + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + $throwErrorWhenFailed = [System.Convert]::ToBoolean([string](Get-Parameter -Name \"Pingdom.ThrowErrorWhenFailed\" -Default \"False\")) + + $pingdomUsername = [string] (Get-Parameter -Name \"Pingdom.Username\" -Required -FailOnValidate:$throwErrorWhenFailed) + $pingdomPassword = [string] (Get-Parameter -Name \"Pingdom.Password\" -Required -FailOnValidate:$throwErrorWhenFailed) + $pingdomAppKey = [string] (Get-Parameter -Name \"Pingdom.AppKey\" -Required -FailOnValidate:$throwErrorWhenFailed) + + $pingdomCheckId = [string] (Get-Parameter -Name \"Pingdom.CheckId\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckPaused = [string] (Get-Parameter -Name \"Pingdom.CheckPaused\" -Default \"False\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckName = [string] (Get-Parameter -Name \"Pingdom.CheckName\" -Required -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckTarget = [string] (Get-Parameter -Name \"Pingdom.CheckTarget\" -Required -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckIntervalMinutes = [System.Nullable[int]] (Get-Parameter -Name \"Pingdom.CheckIntervalMinutes\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckContactIds = [string] (Get-Parameter -Name \"Pingdom.CheckContactIds\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckSendNotificationWhenDown = [System.Nullable[int]] (Get-Parameter -Name \"Pingdom.CheckSendNotificationWhenDown\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckNotifyAgainEvery = [System.Nullable[int]] (Get-Parameter -Name \"Pingdom.CheckNotifyAgainEvery\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckNotifyWhenBackUp = [string](Get-Parameter -Name \"Pingdom.CheckNotifyWhenBackUp\" -Default \"True\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckTags = [string] (Get-Parameter -Name \"Pingdom.CheckTags\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckHttpUrl = [string] (Get-Parameter -Name \"Pingdom.CheckHttpUrl\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckHttpEncryptionEnabled = [string](Get-Parameter -Name \"Pingdom.CheckHttpEncryptionEnabled\" -Default \"False\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckHttpTargetPort = [System.Nullable[int]] (Get-Parameter -Name \"Pingdom.CheckHttpTargetPort\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckAuth = [string] (Get-Parameter -Name \"Pingdom.CheckAuth\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckShouldContain = [string] (Get-Parameter -Name \"Pingdom.CheckShouldContain\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckShouldNotContain = [string] (Get-Parameter -Name \"Pingdom.CheckShouldNotContain\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckPostData = [string] (Get-Parameter -Name \"Pingdom.CheckPostData\" -FailOnValidate:$throwErrorWhenFailed) + $pingdomCheckIntegrationIds = [string] (Get-Parameter -Name \"Pingdom.CheckIntegrationIds\" -FailOnValidate:$throwErrorWhenFailed) + + $apiVersion = \"2.1\" + $url = \"https://api.pingdom.com/api/{0}/checks\" -f $apiVersion + $securePassword = ConvertTo-SecureString $pingdomPassword -AsPlainText -Force + $credential = New-Object System.Management.Automation.PSCredential($pingdomUsername, $securePassword) + $headers = @{ + \"App-Key\" = $pingdomAppKey + } + + If ([string]::IsNullOrEmpty($pingdomCheckId) -and [string]::IsNullOrEmpty($pingdomCheckName)) { + $errMessage = \"Please specify CheckId or CheckName!\" + If($throwErrorWhenFailed -eq $true) { + Write-Error $errMessage + } + Else { + Write-Warning $errMessage + } + Exit + } + + # Find check id by name + + If (-Not [string]::IsNullOrEmpty($pingdomCheckName)) { + Write-Host \"Getting uptime check list to find check by name: $url\" + Try { + $response = Invoke-RestMethod -Uri $url -Method Get -ContentType \"application/json\" -Credential $credential -Headers $headers + } Catch { + Write-Host \"Error occured when getting uptime check list in Pingdom: \" + $_.Exception.Message + } + + $checkFiltered = $response.checks | Where-Object { $_.name -eq $pingdomCheckName } + If ($checkFiltered -eq $null) { + Write-Warning \"Check with name $pingdomCheckName not found!\" + Exit + } + + $pingdomCheckId = $checkFiltered.id + } + + If ([string]::IsNullOrEmpty($pingdomCheckId)) { + Write-Warning \"Check with name $pingdomCheckName was not found!\" + Exit + } + + # Pause or resume check + + $url += \"/$pingdomCheckId\" + + $apiParameters = @{} + $apiParameters.Add(\"host\", $pingdomCheckTarget) + $apiParameters.Add(\"contactids\", $pingdomCheckContactIds) + $apiParameters.Add(\"integrationids\", $pingdomCheckIntegrationIds) + If ($pingdomCheckPaused -eq \"True\") { + $apiParameters.Add(\"paused\", \"true\") + } Else { + $apiParameters.Add(\"paused\", \"false\") + } + If ($pingdomCheckIntervalMinutes -ne $null) { + $apiParameters.Add(\"resolution\", $pingdomCheckIntervalMinutes) + } + If ($pingdomCheckSendNotificationWhenDown -ne $null) { + $apiParameters.Add(\"sendnotificationwhendown\", $pingdomCheckSendNotificationWhenDown) + } + If ($pingdomCheckNotifyAgainEvery -ne $null) { + $apiParameters.Add(\"notifyagainevery\", $pingdomCheckNotifyAgainEvery) + } + If ($pingdomCheckNotifyWhenBackUp -ne $null) { + $apiParameters.Add(\"notifywhenbackup\", $pingdomCheckNotifyWhenBackUp.ToLower()) + } + If ($pingdomCheckTags -ne $null) { + $apiParameters.Add(\"tags\", $pingdomCheckTags) + } + If ($pingdomCheckHttpUrl -ne $null) { + $apiParameters.Add(\"url\", $pingdomCheckHttpUrl) + } + If ($pingdomCheckHttpEncryptionEnabled -ne $null) { + $apiParameters.Add(\"encryption\", $pingdomCheckHttpEncryptionEnabled.ToLower()) + } + If ($pingdomCheckHttpTargetPort -ne $null) { + $apiParameters.Add(\"port\", $pingdomCheckHttpTargetPort) + } + If ($pingdomCheckAuth -ne $null) { + $apiParameters.Add(\"auth\", $pingdomCheckAuth) + } + If (-Not [string]::IsNullOrEmpty($pingdomCheckShouldContain)) { + $apiParameters.Add(\"shouldcontain\", $pingdomCheckShouldContain) + } + If (-Not [string]::IsNullOrEmpty($pingdomCheckShouldNotContain)) { + $apiParameters.Add(\"shouldnotcontain\", $pingdomCheckShouldNotContain) + } + If ($pingdomCheckPostData -ne $null) { + $apiParameters.Add(\"postdata\", $pingdomCheckPostData) + } + + If ($apiParameters.Count -gt 0) { + $queryString = \"\" + $apiParameters.Keys | ForEach-Object { + $queryString += ($_ + \"=\" + [Web.HttpUtility]::UrlEncode($apiParameters.Item($_)) + \"&\") + } + $queryString = $queryString.Substring(0, $queryString.Length - 1) + $url += \"?$queryString\" + } + + Write-Host \"Modifying uptime check: $url\" + Try { + $response = Invoke-RestMethod -Uri $url -Method Put -ContentType \"application/json\" -Credential $credential -Headers $headers + Write-Host $response.message + } Catch { + $errMessage = \"Error occured when adding uptime check in Pingdom: \" + $_.Exception + \"`n\" + $errMessage += \"Response: \" + $_ + If($throwErrorWhenFailed -eq $true) { + Write-Error $errMessage + } + Else { + Write-Warning $errMessage + } + } + + Write-Host \"End PingdomModifyUptimeCheck\" +}" + }, + "Parameters": [ + { + "Id": "cc77291a-308c-414a-9e68-3a23ea2207da", + "Name": "Pingdom.Username", + "Label": "Username", + "HelpText": "Mandatory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "815e7620-2f99-4231-a975-5a074b77260c", + "Name": "Pingdom.Password", + "Label": "Password", + "HelpText": "Mandatory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "ac459f1c-7688-481c-a954-05779adeac61", + "Name": "Pingdom.AppKey", + "Label": "App key", + "HelpText": "Mandatory.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "df91b159-d961-4a45-bb8b-9521b3e8848f", + "Name": "Pingdom.CheckId", + "Label": "Id", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "9a3d6294-799b-474c-9cd4-3404e281acb5", + "Name": "Pingdom.CheckName", + "Label": "Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "4508116a-fb26-4500-b1d1-b75d13838204", + "Name": "Pingdom.CheckPaused", + "Label": "Paused", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "603710c6-e717-44e0-9eac-b84c06ff9351", + "Name": "Pingdom.ThrowErrorWhenFailed", + "Label": "Error when failed", + "HelpText": "When checked, throws Octopus exception if template script has failed.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "LastModifiedBy": "sarbis", + "$Meta": { + "ExportedAt": "2018-06-11T13:50:51.699Z", + "OctopusVersion": "2018.4.12", + "Type": "ActionTemplate" + }, + "Category": "pingdom" +} diff --git a/step-templates/postgres-add-database-user-to-role.json.human b/step-templates/postgres-add-database-user-to-role.json.human new file mode 100644 index 000000000..7415e45eb --- /dev/null +++ b/step-templates/postgres-add-database-user-to-role.json.human @@ -0,0 +1,339 @@ +{ + "Id": "72f8bfaf-14c3-4807-b687-c07738c14ba1", + "Name": "Postgres - Add Database User To Role", + "Description": "Adds database user to a role. + +Note: +- AWS EC2 IAM Role authentication requires the AWS CLI be installed.", + "ActionType": "Octopus.Script", + "Version": 6, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-UserInRole +{ +\t# Define parameters + param ( + $Username, + $RoleName) + +\t# Execute query + $userRole = Invoke-SqlQuery \"SELECT r.rolname, r1.rolname as `\"role`\" FROM pg_catalog.pg_roles r JOIN pg_catalog.pg_auth_members m ON (m.member = r.oid) JOIN pg_roles r1 ON (m.roleid=r1.oid) WHERE r.rolcanlogin AND r1.rolname = '$RoleName' AND r.rolname = '$Username'\" + + # Check to see if anything was returned + if ($userRole.ItemArray.Count -gt 0) + { + # Found + return $true + } + + + # Not found + return $false +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +# Get whether trust certificate is necessary +$addTrustSSL = [System.Convert]::ToBoolean(\"$addTrustSSL\") + +try +{ +\t# Declare initial connection string + $connectionString = \"Server=$addPostgresServerName;Port=$addPostgresServerPort;Database=postgres;\" + +\t# Check to see if we need to trust the ssl cert +\tif ($addTrustSSL -eq $true) +\t{ + # Append SSL connection string components + $connectionString += \"SSL Mode=Require;Trust Server Certificate=true;\" +\t} + + # Update the connection string based on authentication method + switch ($postgreSqlAuthenticationMethod) + { + \"azuremanagedidentity\" + { + \t# Get login token + Write-Host \"Generating Azure Managed Identity token ...\" + $token = Invoke-RestMethod -Method GET -Uri \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://ossrdbms-aad.database.windows.net\" -Headers @{\"MetaData\" = \"true\"} + + # Append remaining portion of connection string + $connectionString += \";User Id=$addLoginWithAddRoleRights;Password=`\"$($token.access_token)`\";\" + + break + } + \"awsiam\" + { + # Region is part of the RDS endpoint, extract + $region = ($addPostgresServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $addLoginPasswordWithAddRoleRights = (aws rds generate-db-auth-token --hostname $addPostgresServerName --region $region --port $addPostgresServerPort --username $addLoginWithAddRoleRights) + + # Append remaining portion of connection string + $connectionString += \";User Id=$addLoginWithAddRoleRights;Password=`\"$addLoginPasswordWithAddRoleRights`\";\" + + break + } + \"gcpserviceaccount\" + { + # Define header + $header = @{ \"Metadata-Flavor\" = \"Google\"} + + # Retrieve service accounts + $serviceAccounts = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\" -Headers $header + + # Results returned in plain text format, get into array and remove empty entries + $serviceAccounts = $serviceAccounts.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + + # Retreive the specific service account assigned to the VM + $serviceAccount = $serviceAccounts | Where-Object {$_.Contains(\"iam.gserviceaccount.com\") } + + Write-Host \"Generating GCP IAM token ...\" + # Retrieve token for account + $token = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/$serviceAccount/token\" -Headers $header + + # Check to see if there was a username provided + if ([string]::IsNullOrWhitespace($addLoginWithAddRoleRights)) + { + \t# Use the service account name, but strip off the .gserviceaccount.com part + $addLoginWithAddRoleRights = $serviceAccount.SubString(0, $serviceAccount.IndexOf(\".gserviceaccount.com\")) + } + + # Append remaining portion of connection string + $connectionString += \";User Id=$addLoginWithAddRoleRights;Password=`\"$($token.access_token)`\";\" + + break + } + \"usernamepassword\" + { + # Append remaining portion of connection string + $connectionString += \";User Id=$addLoginWithAddRoleRights;Password=`\"$addLoginPasswordWithAddRoleRights`\";\" + + break + } + + \"windowsauthentication\" + { + # Append remaining portion of connection string + $connectionString += \";Integrated Security=True;\" + } + } + +\t# Open connection + Open-PostGreConnection -ConnectionString $connectionString + + # See if database exists + $userInRole = Get-UserInRole -Username $addUsername -RoleName $addRoleName + + if ($userInRole -eq $false) + { + # Create database + Write-Output \"Adding user $addUsername to role $addRoleName ...\" + $executionResults = Invoke-SqlUpdate \"GRANT $addRoleName TO `\"$addUsername`\";\" + + # See if it was created + $userInRole = Get-UserInRole -Username $addUsername -RoleName $addRoleName + + # Check array + if ($userInRole -eq $true) + { + # Success + Write-Output \"$addUserName added to $addRoleName successfully!\" + } + else + { + # Failed + Write-Error \"Failure adding $addUserName to $addRoleName!\" + } + } + else + { + \t# Display message + Write-Output \"User $addUsername is already in role $addRoleName\" + } +} +finally +{ + Close-SqlConnection +} + + +" + }, + "Parameters": [ + { + "Id": "802920ef-155f-4d43-aba7-0c32b09e90e8", + "Name": "addPostgresServerName", + "Label": "Server name", + "HelpText": "Name of the PostreSQL server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "89dcd174-d798-49be-8a09-d8a528cb6847", + "Name": "addPostgresServerPort", + "Label": "Port", + "HelpText": "Port that the PostgreSQL server listens on.", + "DefaultValue": "5432", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e357a82c-a25d-4f0e-ac99-b7928d884c25", + "Name": "postgreSqlAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the PostgreSQL server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +azuremanagedidentity|Azure Managed Identity +gcpserviceaccount|GCP Service Account +usernamepassword|Username\\Password +windowsauthentication|Windows Authentication" + } + }, + { + "Id": "9a975cd9-3891-4150-b747-bb78a76fcfb8", + "Name": "addLoginWithAddRoleRights", + "Label": "Login name", + "HelpText": "Login name of a user that can add roles to other users.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "76b5d5a6-4953-4dc4-92c9-3487b7852a2f", + "Name": "addLoginPasswordWithAddRoleRights", + "Label": "Login password", + "HelpText": "Password for the login account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "cf31a6ae-244f-49bb-b60b-e80987b96cdf", + "Name": "addUsername", + "Label": "User name", + "HelpText": "Name of the user to add the role to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "631a3a6c-203c-4925-9ee9-d2086debde37", + "Name": "addRoleName", + "Label": "Role name", + "HelpText": "Name of the role to add to the user.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c2fa3736-8d0c-4de6-9d11-72d0910c9cb7", + "Name": "addTrustSSL", + "Label": "Trust SSL certificate", + "HelpText": "Force trusting the SSL certificate.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2022-06-14T15:09:44.806Z", + "OctopusVersion": "2022.2.5111", + "Type": "ActionTemplate" + }, + "Category": "postgresql" + } diff --git a/step-templates/postgres-create-database.json.human b/step-templates/postgres-create-database.json.human new file mode 100644 index 000000000..4fa79cf9a --- /dev/null +++ b/step-templates/postgres-create-database.json.human @@ -0,0 +1,342 @@ +{ + "Id": "0a1208c7-4a12-4da1-a60d-2b3197b377c4", + "Name": "Postgres - Create Database If Not Exists", + "Description": "Creates a Postgres database if it doesn't already exist. + +Note: +- AWS EC2 IAM Role authentication requires the AWS CLI be installed.", + "ActionType": "Octopus.Script", + "Version": 10, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define variables +$connectionName = \"OctopusDeploy\" + +# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-DatabaseExists +{ +\t# Define parameters + param ($DatabaseName) + +\t# Execute query + return Invoke-SqlQuery -Query \"SELECT datname FROM pg_catalog.pg_database where datname = '$DatabaseName';\" -CommandTimeout $postgresCommandTimeout -ConnectionName $connectionName +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific version + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +# Get whether trust certificate is necessary +$createTrustSSL = [System.Convert]::ToBoolean(\"$createTrustSSL\") + +try +{ +\t# Declare initial connection string + $connectionString = \"Server=$createPosgreSQLServerName;Port=$createPort;Database=postgres;\" + +\t# Check to see if we need to trust the ssl cert +\tif ($createTrustSSL -eq $true) +\t{ + # Append SSL connection string components + $connectionString += \"SSL Mode=Require;Trust Server Certificate=true;\" +\t} + + # Update the connection string based on authentication method + switch ($postgreSqlAuthenticationMethod) + { + \"azuremanagedidentity\" + { + \t# Get login token + Write-Host \"Generating Azure Managed Identity token ...\" + $token = Invoke-RestMethod -Method GET -Uri \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://ossrdbms-aad.database.windows.net\" -Headers @{\"MetaData\" = \"true\"} + + # Append remaining portion of connection string + $connectionString += \";User Id=$createUsername;Password=`\"$($token.access_token)`\";\" + + break + } + \"awsiam\" + { + # Region is part of the RDS endpoint, extract + $region = ($createPosgreSQLServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $createUserPassword = (aws rds generate-db-auth-token --hostname $createPosgreSQLServerName --region $region --port $createPort --username $createUsername) + + # Append remaining portion of connection string + $connectionString += \";User Id=$createUsername;Password=`\"$createUserPassword`\";\" + + break + } + \"gcpserviceaccount\" + { + # Define header + $header = @{ \"Metadata-Flavor\" = \"Google\"} + + # Retrieve service accounts + $serviceAccounts = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\" -Headers $header + + # Results returned in plain text format, get into array and remove empty entries + $serviceAccounts = $serviceAccounts.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + + # Retreive the specific service account assigned to the VM + $serviceAccount = $serviceAccounts | Where-Object {$_.Contains(\"iam.gserviceaccount.com\") } + + Write-Host \"Generating GCP IAM token ...\" + # Retrieve token for account + $token = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/$serviceAccount/token\" -Headers $header + + # Check to see if there was a username provided + if ([string]::IsNullOrWhitespace($createUsername)) + { + \t# Use the service account name, but strip off the .gserviceaccount.com part + $createUsername = $serviceAccount.SubString(0, $serviceAccount.IndexOf(\".gserviceaccount.com\")) + } + + # Append remaining portion of connection string + $connectionString += \";User Id=$createUsername;Password=`\"$($token.access_token)`\";\" + + break + } + \"usernamepassword\" + { + # Append remaining portion of connection string + $connectionString += \";User Id=$createUsername;Password=`\"$createUserPassword`\";\" + + break + } + + \"windowsauthentication\" + { + # Append remaining portion of connection string + $connectionString += \";Integrated Security=True;\" + } + } + +\t# Open connection + Open-PostGreConnection -ConnectionString $connectionString -ConnectionName $connectionName + + # See if database exists + $databaseExists = Get-DatabaseExists -DatabaseName $createDatabaseName + + if ($databaseExists.ItemArray.Count -eq 0) + { + # Create database + Write-Output \"Creating database $createDatabaseName ...\" + $executionResult = Invoke-SqlUpdate -Query \"CREATE DATABASE `\"$createDatabaseName`\";\" -CommandTimeout $postgresCommandTimeout -ConnectionName $connectionName + + # Check result + if ($executionResult -ne -1) + { + # Commit transaction + Write-Error \"Create schema failed.\" + } + else + { + \t# See if it was created + $databaseExists = Get-DatabaseExists -DatabaseName $createDatabaseName + + # Check array + if ($databaseExists.ItemArray.Count -eq 1) + { + \t# Success + Write-Output \"$createDatabaseName created successfully!\" + } + else + { + \t# Failed + Write-Error \"$createDatabaseName was not created!\" + } + } + } + else + { + \t# Display message + Write-Output \"Database $createDatabaseName already exists.\" + } +} +finally +{ +\t# Close connection if open + if ((Test-SqlConnection -ConnectionName $connectionName) -eq $true) + { + \tClose-SqlConnection -ConnectionName $connectionName + } +} + + +" + }, + "Parameters": [ + { + "Id": "8fc92b80-5122-44a0-b3d8-a1d022a35055", + "Name": "createPosgreSQLServerName", + "Label": "Server", + "HelpText": "Hostname (or IP) of the MySQL database server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "32abc8e8-486c-4afb-abe1-f1e84125afc8", + "Name": "postgreSqlAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the PostgreSQL server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +azuremanagedidentity|Azure Managed Identity +gcpserviceaccount|GCP Service Account +usernamepassword|Username\\Password +windowsauthentication|Windows Authentication" + } + }, + { + "Id": "df993ccf-71ab-48de-9a67-e2af6653d35e", + "Name": "createUsername", + "Label": "Username", + "HelpText": "Username to use for the connection", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8a07f25d-a7db-466e-a356-9155cbc5f258", + "Name": "createUserPassword", + "Label": "Password", + "HelpText": "Password for the user account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "2af18465-c8d1-48f6-afce-1b1b30ae9559", + "Name": "createDatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to create", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f55e2a34-7a82-4d92-83bb-a19f304774d8", + "Name": "createPort", + "Label": "Port", + "HelpText": "Port for the database instance.", + "DefaultValue": "5432", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0366424c-ab0b-4895-8d48-2902e3f6de39", + "Name": "createTrustSSL", + "Label": "Trust SSL Certificate", + "HelpText": "Force trusting an SSL Certificate.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "99f987cb-97da-4f0a-8056-33a93ab908dc", + "Name": "postgresCommandTimeout", + "Label": "Command Timeout", + "HelpText": "Timeout value (in seconds) for SQL commands", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2024-05-23T15:30:50.319Z", + "OctopusVersion": "2024.3.164", + "Type": "ActionTemplate" + }, + "Category": "postgresql" +} diff --git a/step-templates/postgres-create-user.json.human b/step-templates/postgres-create-user.json.human new file mode 100644 index 000000000..1e614267a --- /dev/null +++ b/step-templates/postgres-create-user.json.human @@ -0,0 +1,361 @@ +{ + "Id": "6e676055-fb63-450f-9d98-ac99c4a68023", + "Name": "Postgres- Create User If Not Exists", + "Description": "Creates a new user account on a Postgres database server. + +Note: +- AWS EC2 IAM Role authentication requires the AWS CLI be installed.", + "ActionType": "Octopus.Script", + "Version": 8, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-UserExists +{ +\t# Define parameters + param ($Hostname, + $Username) + +\t# Execute query + return Invoke-SqlQuery \"SELECT * FROM pg_roles WHERE rolname = '$Username';\" +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific location + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +# Get whether trust certificate is necessary +$createTrustSSL = [System.Convert]::ToBoolean(\"$createTrustSSL\") + +try +{ +\t# Declare initial connection string + $connectionString = \"Server=$createPostgresSQLServerName;Port=$createPostgreSQLServerPort;Database=postgres;\" + +\t# Check to see if we need to trust the ssl cert +\tif ($createTrustSSL -eq $true) +\t{ + # Append SSL connection string components + $connectionString += \"SSL Mode=Require;Trust Server Certificate=true;\" +\t} + + # Update the connection string based on authentication method + switch ($postgreSqlAuthenticationMethod) + { + \"azuremanagedidentity\" + { + \t# Get login token + Write-Host \"Generating Azure Managed Identity token ...\" + $token = Invoke-RestMethod -Method GET -Uri \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://ossrdbms-aad.database.windows.net\" -Headers @{\"MetaData\" = \"true\"} + + # Append remaining portion of connection string + $connectionString += \";User Id=$createLoginWithAddUserRights;Password=`\"$($token.access_token)`\";\" + + break + } + \"awsiam\" + { + # Region is part of the RDS endpoint, extract + $region = ($createPostgresSQLServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $createLoginPasswordWithAddUserRights = (aws rds generate-db-auth-token --hostname $createPostgresSQLServerName --region $region --port $createPostgreSQLServerPort --username $createLoginWithAddUserRights) + + # Append remaining portion of connection string + $connectionString += \";User Id=$createLoginWithAddUserRights;Password=`\"$createLoginPasswordWithAddUserRights`\";\" + + break + } + \"gcpserviceaccount\" + { + # Define header + $header = @{ \"Metadata-Flavor\" = \"Google\"} + + # Retrieve service accounts + $serviceAccounts = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\" -Headers $header + + # Results returned in plain text format, get into array and remove empty entries + $serviceAccounts = $serviceAccounts.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + + # Retreive the specific service account assigned to the VM + $serviceAccount = $serviceAccounts | Where-Object {$_.Contains(\"iam.gserviceaccount.com\") } + + Write-Host \"Generating GCP IAM token ...\" + # Retrieve token for account + $token = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/$serviceAccount/token\" -Headers $header + + # Check to see if there was a username provided + if ([string]::IsNullOrWhitespace($createLoginWithAddUserRights)) + { + \t# Use the service account name, but strip off the .gserviceaccount.com part + $createLoginWithAddUserRights = $serviceAccount.SubString(0, $serviceAccount.IndexOf(\".gserviceaccount.com\")) + } + + # Append remaining portion of connection string + $connectionString += \";User Id=$createLoginWithAddUserRights;Password=`\"$($token.access_token)`\";\" + + break + } + \"usernamepassword\" + { + # Append remaining portion of connection string + $connectionString += \";User Id=$createLoginWithAddUserRights;Password=`\"$createLoginPasswordWithAddUserRights`\";\" + + break + } + + \"windowsauthentication\" + { + # Append remaining portion of connection string + $connectionString += \";Integrated Security=True;\" + } + } + +\t# Open connection + Open-PostGreConnection -ConnectionString $connectionString + + # See if database exists + $userExists = Get-UserExists -Username $createNewUsername + + if ($userExists -eq $null) + { + # Create user + Write-Output \"Creating user $createNewUsername ...\" + $createSql = \"\" + + switch ($postgreSqlAccountType) + { + \"awsiam\" + { +\t\t\t\t$createSql = \"CREATE ROLE `\"$createNewUsername`\" WITH LOGIN; GRANT rds_iam TO `\"$createNewUsername`\";\" + break + } + + \"usernamepassword\" + { +\t\t\t\t$createSql = \"CREATE ROLE `\"$createNewUsername`\" WITH LOGIN PASSWORD '$createNewUserPassword';\" + break + } + + \"windowsauthentication\" + { + \t$createSql = \"CREATE ROLE `\"$createNewUsername`\" WITH LOGIN;\" + \tbreak + } + } + + + $executionResults = Invoke-SqlUpdate $createSql + + # See if it was created + $userExists = Get-UserExists -Username $createNewUsername + + # Check array + if ($userExists -ne $null) + { + # Success + Write-Output \"$createNewUsername created successfully!\" + } + else + { + # Failed + Write-Error \"$createNewUsername was not created!\" + } + } + else + { + \t# Display message + Write-Output \"User $createNewUsername already exists.\" + } +} +finally +{ + Close-SqlConnection +}" + }, + "Parameters": [ + { + "Id": "3ff05fe1-7873-443e-ae6b-f52c7d994664", + "Name": "createPostgresSQLServerName", + "Label": "Host name of the Postgres server", + "HelpText": "Name of the Postgres server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7db491c2-441a-455e-849d-dbb6b61b24e2", + "Name": "createPostgreSQLServerPort", + "Label": "Port", + "HelpText": "Port number the Postgres server listens on.", + "DefaultValue": "5432", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cbb7e84e-1492-4e76-b588-b65c76f3bf2b", + "Name": "createTrustSSL", + "Label": "Trust SSL Certificate", + "HelpText": "Force trusting an SSL Certificate.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "c0e8a9e1-e68b-4af7-8dfa-5f1c9872e615", + "Name": "postgreSqlAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the PostgreSQL server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +azuremanagedidentity|Azure Managed Identity +gcpserviceaccount|GCP Service Account +usernamepassword|Username\\Password +windowsauthentication|Windows Authentication" + } + }, + { + "Id": "0e3cb957-541d-436c-b3b5-047ddda28ec8", + "Name": "createLoginWithAddUserRights", + "Label": "Login name", + "HelpText": "Login name of a user with rights to create user accounts.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b9029a8c-e4bd-4993-a163-1f95dfbaf01d", + "Name": "createLoginPasswordWithAddUserRights", + "Label": "Login Password", + "HelpText": "Password for Login name.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "b9986f38-a587-46bd-82d7-97d16baf529f", + "Name": "postgreSqlAccountType", + "Label": "Account type", + "HelpText": null, + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +usernamepassword|Username\\Password +windowsauthentication|Windows Authentication" + } + }, + { + "Id": "43b9e471-e792-49e1-a761-a8fa95679a24", + "Name": "createNewUsername", + "Label": "New user name", + "HelpText": "Name of the new user account to create.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "aeda6f0b-d9e9-4f66-8c61-84cf42c26f27", + "Name": "createNewUserPassword", + "Label": "New user password", + "HelpText": "Password for the new user account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2022-06-14T15:09:44.806Z", + "OctopusVersion": "2022.2.5111", + "Type": "ActionTemplate" + }, + "Category": "postgresql" + } diff --git a/step-templates/postgres-execute-sql.json.human b/step-templates/postgres-execute-sql.json.human new file mode 100644 index 000000000..b3b8e9ef0 --- /dev/null +++ b/step-templates/postgres-execute-sql.json.human @@ -0,0 +1,304 @@ +{ + "Id": "9a9c8c2c-d50e-4dc8-8e7e-b561f6e8fc15", + "Name": "Postgres - Execute SQL", + "Description": "Creates a Postgres database if it doesn't already exist. + +Note: +- AWS EC2 IAM Role authentication requires the AWS CLI be installed.", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define functions +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" +$PowerShellModuleName = \"SimplySql\" + +# Set secure protocols +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +# Check to see if SimplySql module is installed +if ((Get-ModuleInstalled -PowerShellModuleName $PowerShellModuleName) -ne $true) +{ + # Tell user what we're doing + Write-Output \"PowerShell module $PowerShellModuleName is not installed, downloading temporary copy ...\" + + # Install temporary copy + Install-PowerShellModule -PowerShellModuleName $PowerShellModuleName -LocalModulesPath $LocalModules +} + +# Display +Write-Output \"Importing module $PowerShellModuleName ...\" + +# Check to see if it was downloaded +if ((Test-Path -Path \"$LocalModules\\$PowerShellModuleName\") -eq $true) +{ +\t# Use specific version + $PowerShellModuleName = \"$LocalModules\\$PowerShellModuleName\" +} + +# Import the module +Import-Module -Name $PowerShellModuleName + +# Get whether trust certificate is necessary +$postgresqlTrustSSL = [System.Convert]::ToBoolean(\"$postgresqlTrustSSL\") + +try +{ +\t# Declare initial connection string + $connectionString = \"Server=$postgresqlServerName;Port=$postgresqlServerPort;Database=$postgresqlDatabaseName;\" + +\t# Check to see if we need to trust the ssl cert +\tif ($postgresqlTrustSSL -eq $true) +\t{ + # Append SSL connection string components + $connectionString += \"SSL Mode=Require;Trust Server Certificate=true;\" +\t} + + # Update the connection string based on authentication method + switch ($postgreSqlAuthenticationMethod) + { + \"azuremanagedidentity\" + { + \t# Get login token + Write-Host \"Generating Azure Managed Identity token ...\" + $token = Invoke-RestMethod -Method GET -Uri \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://ossrdbms-aad.database.windows.net\" -Headers @{\"MetaData\" = \"true\"} + + # Append remaining portion of connection string + $connectionString += \";User Id=$postgresqlUsername;Password=`\"$($token.access_token)`\";\" + + break + } + \"awsiam\" + { + # Region is part of the RDS endpoint, extract + $region = ($createPosgreSQLServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $createUserPassword = (aws rds generate-db-auth-token --hostname $postgresqlServerName --region $region --port $createPort --username $postgresqlUsername) + + # Append remaining portion of connection string + $connectionString += \";User Id=$postgresqlUsername;Password=`\"$postgesqlUserPassword`\";\" + + break + } + \"gcpserviceaccount\" + { + # Define header + $header = @{ \"Metadata-Flavor\" = \"Google\"} + + # Retrieve service accounts + $serviceAccounts = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/\" -Headers $header + + # Results returned in plain text format, get into array and remove empty entries + $serviceAccounts = $serviceAccounts.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + + # Retreive the specific service account assigned to the VM + $serviceAccount = $serviceAccounts | Where-Object {$_.Contains(\"iam.gserviceaccount.com\") } + + Write-Host \"Generating GCP IAM token ...\" + # Retrieve token for account + $token = Invoke-RestMethod -Method Get -Uri \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/$serviceAccount/token\" -Headers $header + + # Check to see if there was a username provided + if ([string]::IsNullOrWhitespace($postgresqlUsername)) + { + \t# Use the service account name, but strip off the .gserviceaccount.com part + $postgresqlUsername = $serviceAccount.SubString(0, $serviceAccount.IndexOf(\".gserviceaccount.com\")) + } + + # Append remaining portion of connection string + $connectionString += \";User Id=$postgresqlUsername;Password=`\"$($token.access_token)`\";\" + + break + } + \"usernamepassword\" + { + # Append remaining portion of connection string + $connectionString += \";User Id=$postgresqlUsername;Password=`\"$postgesqlUserPassword`\";\" + + break + } + + \"windowsauthentication\" + { + # Append remaining portion of connection string + $connectionString += \";Integrated Security=True;\" + } + } + +\t# Open connection + Open-PostGreConnection -ConnectionString $connectionString + + # Execute the statement + $executionResult = Invoke-SqlUpdate -Query \"$postgresqlCommand\" -CommandTimeout $postgresqlCommandTimeout + + # Display the result + Get-SqlMessage +} +finally +{ + Close-SqlConnection +} + + +" + }, + "Parameters": [ + { + "Id": "3adf249d-aef2-41dc-922d-a3cfb67c4afe", + "Name": "postgresqlServerName", + "Label": "Server", + "HelpText": "Hostname (or IP) of the MySQL database server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0fbfc453-3495-42b9-b13a-c7db532bc030", + "Name": "postgresqlAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the PostgreSQL server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +azuremanagedidentity|Azure Managed Identity +gcpserviceaccount|GCP Service Account +usernamepassword|Username\\Password +windowsauthentication|Windows Authentication" + } + }, + { + "Id": "cba7ecb6-e23d-462e-97d8-2d72e28d3233", + "Name": "postgresqlUsername", + "Label": "Username", + "HelpText": "Username to use for the connection", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0fc033f0-7446-4698-b003-bc18549d45b3", + "Name": "postgesqlUserPassword", + "Label": "Password", + "HelpText": "Password for the user account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "0f2cea01-dd43-4f3e-87aa-1d1ca2974e3e", + "Name": "postgresqlDatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to execute against.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ef1a940d-cd67-49b6-b584-c79d918b4707", + "Name": "postgresqlServerPort", + "Label": "Port", + "HelpText": "Port for the database instance.", + "DefaultValue": "5432", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "355fb14a-2dfd-4b41-98a3-87b7d177f716", + "Name": "postgresqlTrustSSL", + "Label": "Trust SSL Certificate", + "HelpText": "Force trusting an SSL Certificate.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "4d7dbc06-697c-4585-b18d-043ed1c120a7", + "Name": "postgresqlCommandTimeout", + "Label": "Command Timeout", + "HelpText": "Timeout value (in seconds) for SQL commands", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5a810fee-d9e9-4200-8540-2a22b3681be9", + "Name": "postgresqlCommand", + "Label": "Command", + "HelpText": "SQL statement(s) to execute.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2022-06-15T21:51:29.119Z", + "OctopusVersion": "2022.1.2849", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "postgresql" + } diff --git a/step-templates/proxmox-deploy-lxc.json.human b/step-templates/proxmox-deploy-lxc.json.human new file mode 100644 index 000000000..3df49aebd --- /dev/null +++ b/step-templates/proxmox-deploy-lxc.json.human @@ -0,0 +1,416 @@ +{ + "Id": "ab90cb93-96ae-4841-8058-1cc5ec6feebe", + "Name": "Proxmox Deploy LXC Container", + "Description": "Creates a new Proxmox LXC container using the Proxmox API. + +Requires a Proxmox [API token](https://pve.proxmox.com/wiki/Proxmox_VE_API#API_Tokens) to authenticate to the Proxmox Server/Cluster", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Proxmox Connection Variables +$ProxmoxHost = $OctopusParameters[\"Proxmox.Host\"]; +$ProxmoxPort = [int]$OctopusParameters[\"Proxmox.Port\"]; +$ProxmoxUser = $OctopusParameters[\"Proxmox.User\"]; + +$ProxmoxNode = $OctopusParameters[\"Proxmox.Node\"]; + +$ProxmoxTokenID = $OctopusParameters[\"Proxmox.TokenID\"]; +$ProxmoxToken = $OctopusParameters[\"Proxmox.Token\"]; + +# LXC Variables +$LXC_VMID = [int]$OctopusParameters[\"Proxmox.LXC.VMID\"]; +$LXC_Hostname = $OctopusParameters[\"Proxmox.LXC.Hostname\"]; +$LXC_OSTemplate = $OctopusParameters[\"Proxmox.LXC.OSTemplate\"]; +$LXC_Storage = $OctopusParameters[\"Proxmox.LXC.Storage\"]; +$LXC_CPU = [int]$OctopusParameters[\"Proxmox.LXC.Cores\"]; +$LXC_Memory = [int]$OctopusParameters[\"Proxmox.LXC.Memory\"]; +$LXC_RootSize = [int]$OctopusParameters[\"Proxmox.LXC.RootSize\"]; +$LXC_Networks = $OctopusParameters[\"Proxmox.LXC.Network\"]; +$LXC_Password = $OctopusParameters[\"Proxmox.LXC.Password\"]; + +$BaseURL = \"https://$($ProxmoxHost):$($ProxmoxPort)/api2/json\" + +$header = @{ +\t\"Authorization\" = \"PVEAPIToken=$($ProxmoxUser)!$($ProxmoxTokenID)=$($ProxmoxToken)\" +} + + +Write-Host \"Testing Connection To Proxmox Server/Cluster ...\" + +try{ +\tInvoke-RestMethod -Method GET -uri \"$($BaseURL)\" -Headers $header | out-null +}catch{ +\tthrow \"Couldn't Connect to the Proxmox Server/Cluster\" +} + +Write-Host \"Successfully Connected To Proxmox Server/Cluster\" + +$LXC_Start = 0 +try { + $Start = [System.Convert]::ToBoolean($OctopusParameters[\"Proxmox.LXC.StartOnCreate\"]) + + if($Start -eq $True){ + \t$LXC_Start = 1 + } + +} catch {} + +$LXC_Force = 0 +try { + $Force = [System.Convert]::ToBoolean($OctopusParameters[\"Proxmox.LXC.Force\"]) + + if($Force -eq $True){ + \t$LXC_Force = 1 + } + +} catch {} + +if($LXC_CPU -lt 1){ +\t$LXC_CPU=1; +} + +if($LXC_Memory -lt 16){ +\t$LXC_Memory = 16; +} + +if($LXC_RootSize -lt 1){ +\t$LXC_RootSize = 1; +} + +if($LXC_Hostname -eq $null -or $LXC_Hostname -eq \"\"){ +\tthrow \"LXC Hostname must be provided!\" +} + +if($LXC_OSTemplate -eq $null -or $LXC_OSTemplate -eq \"\"){ +\tthrow \"LXC OS Template must be provided!\" +} + +if($LXC_Storage -eq $null -or $LXC_Storage -eq \"\"){ +\tthrow \"LXC Storage must be provided!\" +} + +if($LXC_Networks -eq $null){ +\tthrow \"You must provide at least one network property\" +} + +if($LXC_Password -eq $null -or $LXC_Password -eq \"\"){ +\tthrow \"LXC Password must be provided!\" +} + +if($LXC_VMID -eq \"-1\"){ +\t$LXC_VMID=(Invoke-RestMethod -Method GET -uri \"$($BaseURL)/cluster/nextid\" -headers $header).data + Write-Host \"Found next vm id: $($LXC_VMID)\" +} + +if($LXC_VMID -lt 1){ +\tthrow \"The LXC VMID was not valid ($LXC_VMID), Set this to -1 to automatically find the next id\" +} + +$LXCData = @{ +\t\"vmid\" = $LXC_VMID + \"hostname\" = $LXC_Hostname + \"ostemplate\" = $LXC_OSTemplate + \"rootfs\" = \"volume=$($LXC_Storage):$($LXC_RootSize)\" + \"cores\" = $LXC_CPU + \"memory\" = $LXC_Memory + \"storage\" = $LXC_Storage + \"password\" = $LXC_Password + \"start\" = $LXC_Start + \"force\" = $LXC_Force +} + +$NetworkIndex = 0; + +$Networks = $LXC_Networks.replace(\"\ +\", \"`n\").split(\"`n\") + +if($Networks.Count -lt 1){ +\tthrow \"You must provide at least one network property\" +} + +foreach ($network in $Networks){ + $LXCData[\"net$($NetworkIndex)\"] = $network; + $NetworkIndex++; +} + +$existingLXC = $null; + +try{ + $existingLXC = Invoke-RestMethod -Method GET -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/lxc/$($LXCData.vmid)\" -Headers $header +}catch{} + +if($existingLXC -ne $null -and $LXCData.force -eq 0){ + throw \"LXC with VMID: $($LXCData.vmid) already exists. Use Force parameter to overwrite this LXC.\" + +}elseif($existingLXC -ne $null -and $LXCData.force -eq 1){ + + Write-host \"Deleting existing LXC with VMID: $($LXCData.vmid)\" + $LXCDestroyAsyncTask =Invoke-RestMethod -Method DELETE -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/lxc/$($LXCData.vmid)\" -Headers $header + + $count = 1; + $maxCount = 10; + $TaskID = $LXCDestroyAsyncTask.Data; + + DO + { + Write-Host \"Checking if LXC has finished Deleting..\" + $LXCDestroyAsyncTaskStatus = (Invoke-RestMethod -Method GET -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/tasks/$($TaskID)/status\" -Headers $header).data + + if($LXCDestroyAsyncTaskStatus.status -eq \"stopped\"){ + \t if($LXCDestroyAsyncTaskStatus.exitstatus -ne \"OK\"){ + \t Write-Error \"LXC destroy task finished with error: $($LXCDestroyAsyncTaskStatus.exitstatus)\" + }else{ + \t Write-Host \"LXC destroy task has successfully completed!\" + } + + break; + } + +\t Write-Host \"LXC destroy task has not finished yet, retrying in 5 seconds..\" + Write-Host \"Task Status: $($LXCDestroyAsyncTaskStatus.status)\" + sleep 5 + + If($count -gt $maxCount) { + Write-Warning \"Task Timed out!\" + break; + } + $count++ + + } While ($count -le $maxCount) +} + +Write-Host \"\" + +Write-Host \"New LXC Summary:\" + +$LXCData | Convertto-json -depth 10 + +$LXCCreateAsyncTask = (Invoke-RestMethod -Method POST -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/lxc\" -Headers $header -Body $LXCData) + + +$count = 1; +$maxCount = 10; + +Write-Host \"\" + +DO +{ + + $TaskID = $LXCCreateAsyncTask.Data; + Write-Host \"Checking if LXC has finished creating..\" + $LXCCreateAsyncTaskStatus = (Invoke-RestMethod -Method GET -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/tasks/$($TaskID)/status\" -Headers $header).data + + if($LXCCreateAsyncTaskStatus.status -eq \"stopped\"){ + \tif($LXCCreateAsyncTaskStatus.exitstatus -ne \"OK\"){ + \tWrite-Error \"LXC create task finished with error: $($LXCCreateAsyncTaskStatus.exitstatus)\" + }else{ + \tWrite-Host \"LXC create task has successfully completed!\" + } + + break; + } + +\tWrite-Host \"LXC create task has not finished yet, retrying in 5 seconds..\" + Write-Host \"Task Status: $($LXCCreateAsyncTaskStatus.status)\" + sleep 5 + + If($count -gt $maxCount) { + Write-Warning \"Task Timed out!\" + break; + } + $count++ + +} While ($count -le $maxCount) +" + }, + "Parameters": [ + { + "Id": "c75c69c5-bdfb-4cbf-8b5a-aeb6ef93c274", + "Name": "Proxmox.Host", + "Label": "Proxmox Host", + "HelpText": "The hostname or IP address of the Proxmox cluster/host", + "DefaultValue": "1.2.3.4", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "eddd73e2-cb63-4c1b-8100-05e8e9586180", + "Name": "Proxmox.Port", + "Label": "Proxmox Port", + "HelpText": "Port number for Proxmox Cluster/Host", + "DefaultValue": "8006", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "eae18957-8977-4f51-8e09-2402453fd531", + "Name": "Proxmox.User", + "Label": "Proxmox User Account", + "HelpText": "The Proxmox user account associated with the api token.", + "DefaultValue": "root@pam", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b413ee30-ba94-4ff3-8b4e-148c1fbc52f6", + "Name": "Proxmox.Node", + "Label": "Proxmox Node", + "HelpText": "The Proxmox node in the cluster.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3f70fe63-2b55-44f5-af8e-27d8717ade20", + "Name": "Proxmox.TokenID", + "Label": "Proxmox Token ID", + "HelpText": "This is token id that was used to create an API token in proxmox.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "741c3b80-af85-47c6-b49e-36aea9b2bc9a", + "Name": "Proxmox.Token", + "Label": "Proxmox API Token", + "HelpText": "The API Token secret key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "003db066-99a7-4d4c-a794-7a6310c2c86b", + "Name": "Proxmox.LXC.VMID", + "Label": "LXC VM ID", + "HelpText": "The new VMID for the new LXC container default is -1 to find the next ID", + "DefaultValue": "-1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "63ba1450-8c9e-4d06-bedf-00389a63ef3f", + "Name": "Proxmox.LXC.Hostname", + "Label": "LXC Hostname", + "HelpText": "The new LXC hostname", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0a13d075-3a2f-4718-9793-f9b2f5963455", + "Name": "Proxmox.LXC.OSTemplate", + "Label": "LXC OS Template", + "HelpText": "The template image or backup image for the LXC", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "55f7937b-4a5f-49bb-b129-f589c3ce51cc", + "Name": "Proxmox.LXC.Password", + "Label": "LXC Root Password", + "HelpText": "This will be the root password once the LXC container has been created", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "671751a5-d213-445a-a73f-dd9f1f33d754", + "Name": "Proxmox.LXC.Storage", + "Label": "LXC Storage", + "HelpText": "Where the rootfs for this LXC will be stored.", + "DefaultValue": "local", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e796b021-0c31-4e03-8825-23c19cbf8876", + "Name": "Proxmox.LXC.Network", + "Label": "LXC Networks", + "HelpText": "The list of network connections this LXC has. Each network connection on a new line. + + +`name=,bridge=,firewall=<0|1>,gw=,ip=` + + +More Info: [https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/lxc](https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/lxc)", + "DefaultValue": "name=eth0,bridge=vmbr0", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "c9e865ea-a7b2-42d3-aca6-8772e54b893a", + "Name": "Proxmox.LXC.Cores", + "Label": "LXC CPU Cores", + "HelpText": "The amount of CPU cores the LXC is assigned", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "74b64687-8aed-433d-bce3-714bfd738927", + "Name": "Proxmox.LXC.Memory", + "Label": "LXC Memory", + "HelpText": "The amount of Memory the LXC is assigned.", + "DefaultValue": "2048", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "85c52eb6-a25f-4f8e-a068-2a165160d94d", + "Name": "Proxmox.LXC.RootSize", + "Label": "LXC Rootfs Size", + "HelpText": "The size of the root volume for this LXC, Size is in GB", + "DefaultValue": "60", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fd072ced-1c8b-4205-a724-e9456b5152c6", + "Name": "Proxmox.LXC.StartOnCreate", + "Label": "LXC Start Once Created", + "HelpText": "Should the LXC start once the LXC has been created", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "dfea6eea-47ff-4489-9434-8ff80bbb8694", + "Name": "Proxmox.LXC.Force", + "Label": "LXC Overwrite Container", + "HelpText": "Overwrites an existing LXC with the same VMID", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-05-04T10:44:27.968Z", + "OctopusVersion": "2023.2.9087", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "domrichardson", + "Category": "proxmox" + } diff --git a/step-templates/proxmox-destroy-lxc.json.human b/step-templates/proxmox-destroy-lxc.json.human new file mode 100644 index 000000000..3035bc024 --- /dev/null +++ b/step-templates/proxmox-destroy-lxc.json.human @@ -0,0 +1,186 @@ +{ + "Id": "5facb9eb-5a33-4a67-a254-7ba938d0b5a4", + "Name": "Proxmox Destroy LXC Container", + "Description": "Destroys a Proxmox LXC container using the Proxmox API. + +Requires a Proxmox [API token](https://pve.proxmox.com/wiki/Proxmox_VE_API#API_Tokens) to authenticate to the Proxmox Server/Cluster", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Proxmox Connection Variables +$ProxmoxHost = $OctopusParameters[\"Proxmox.Host\"]; +$ProxmoxPort = [int]$OctopusParameters[\"Proxmox.Port\"]; +$ProxmoxUser = $OctopusParameters[\"Proxmox.User\"]; + +$ProxmoxNode = $OctopusParameters[\"Proxmox.Node\"]; + +$ProxmoxTokenID = $OctopusParameters[\"Proxmox.TokenID\"]; +$ProxmoxToken = $OctopusParameters[\"Proxmox.Token\"]; + +# LXC Variables +$LXC_VMID = [int]$OctopusParameters[\"Proxmox.LXC.VMID\"]; + +$BaseURL = \"https://$($ProxmoxHost):$($ProxmoxPort)/api2/json\" + +$header = @{ +\t\"Authorization\" = \"PVEAPIToken=$($ProxmoxUser)!$($ProxmoxTokenID)=$($ProxmoxToken)\" +} + + +Write-Host \"Testing Connection To Proxmox Server/Cluster ...\" + +try{ +\tInvoke-RestMethod -Method GET -uri \"$($BaseURL)\" -Headers $header | out-null +}catch{ +\tthrow \"Couldn't Connect to the Proxmox Server/Cluster\" +} + +Write-Host \"Successfully Connected To Proxmox Server/Cluster\" + +$CheckLXCExists = Invoke-RestMethod -Method GET -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/lxc/$($LXC_VMID)/status/current\" -Headers $header + +if($CheckLXCExists.data -eq $null){ +\tthrow \"The LXC container with vmid ($LXC_VMID) does not exist!\" +} + + +$LXC_Force = 0 +try { + $Force = [System.Convert]::ToBoolean($OctopusParameters[\"Proxmox.LXC.Force\"]) + + if($Force -eq $True){ + \t$LXC_Force = 1 + } + +} catch {} + +$LXCDestroyAsyncTask = (Invoke-RestMethod -Method DELETE -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/lxc/$($LXC_VMID)?force=$($LXC_Force)\" -Headers $header) + +$count = 1; +$maxCount = 10; + +$TaskID = $LXCDestroyAsyncTask.Data; + +DO +{ + Write-Host \"Checking if LXC has finished Destroying..\" + $LXCSDestroyAsyncTaskStatus = (Invoke-RestMethod -Method GET -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/tasks/$($TaskID)/status\" -Headers $header).data + + if($LXCSDestroyAsyncTaskStatus.status -eq \"stopped\"){ + \tif($LXCSDestroyAsyncTaskStatus.exitstatus -ne \"OK\"){ + \tWrite-Error \"LXC destroy task finished with error: $($LXCSDestroyAsyncTaskStatus.exitstatus)\" + }else{ + \tWrite-Host \"LXC destroy task has successfully completed!\" + } + + break; + } + +\tWrite-Host \"LXC destroy task has not finished yet, retring in 5 seconds..\" + Write-Host \"Task Status: $($LXCSDestroyAsyncTaskStatus.status)\" + sleep 5 + + If($count -gt $maxCount) { + Write-Warning \"Task Timed out!\" + break; + } + $count++ + +} While ($count -le $maxCount) +" + }, + "Parameters": [ + { + "Id": "9f1a3789-a4fe-4fc4-9a66-78e73d384248", + "Name": "Proxmox.Host", + "Label": "Proxmox Host", + "HelpText": "The hostname or IP address of the Proxmox cluster/host", + "DefaultValue": "1.2.3.4", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "240b1180-7b10-48ac-90fb-796fc8eaaf41", + "Name": "Proxmox.Port", + "Label": "Proxmox Port", + "HelpText": "Port number for Proxmox Cluster/Host", + "DefaultValue": "8006", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "37d5fad7-2bd3-45da-b30c-62116f0b1940", + "Name": "Proxmox.User", + "Label": "Proxmox User Account", + "HelpText": "The Proxmox user account associated with the api token.", + "DefaultValue": "root@pam", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "72a89f72-2571-435b-b0f7-a383f08572b9", + "Name": "Proxmox.Node", + "Label": "Proxmox Node", + "HelpText": "The Proxmox node in the cluster.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b33b88c3-3949-4563-91a2-4ae264bc2d7f", + "Name": "Proxmox.TokenID", + "Label": "Proxmox Token ID", + "HelpText": "This is token id that was used to create an API token in proxmox.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "eff9ca9b-b3ea-44e9-aa79-489d1d9161ee", + "Name": "Proxmox.Token", + "Label": "Proxmox API Token", + "HelpText": "The API Token secret key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "f7be6495-cc3b-4492-bb1b-e8a178a36a91", + "Name": "Proxmox.LXC.VMID", + "Label": "LXC VM ID", + "HelpText": "The LXC VMID you want to destroy", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "94de0cbc-b722-4788-858f-fb2b32b57fd8", + "Name": "Proxmox.LXC.Force", + "Label": "Force Destroy LXC", + "HelpText": "Do you want to force destroy the LXC container even if its running", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-03-16T12:26:59.412Z", + "OctopusVersion": "2023.2.2028", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "domrichardson", + "Category": "proxmox" + } diff --git a/step-templates/proxmox-start-lxc.json.human b/step-templates/proxmox-start-lxc.json.human new file mode 100644 index 000000000..a44aec164 --- /dev/null +++ b/step-templates/proxmox-start-lxc.json.human @@ -0,0 +1,192 @@ +{ + "Id": "f68d54f4-ddcb-4bda-b743-1182b0d82335", + "Name": "Proxmox Start LXC Container", + "Description": "Starts or Reboots a Proxmox LXC container using the Proxmox API. + +Requires a Proxmox [API token](https://pve.proxmox.com/wiki/Proxmox_VE_API#API_Tokens) to authenticate to the Proxmox Server/Cluster", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Proxmox Connection Variables +$ProxmoxHost = $OctopusParameters[\"Proxmox.Host\"]; +$ProxmoxPort = [int]$OctopusParameters[\"Proxmox.Port\"]; +$ProxmoxUser = $OctopusParameters[\"Proxmox.User\"]; + +$ProxmoxNode = $OctopusParameters[\"Proxmox.Node\"]; + +$ProxmoxTokenID = $OctopusParameters[\"Proxmox.TokenID\"]; +$ProxmoxToken = $OctopusParameters[\"Proxmox.Token\"]; + +# LXC Variables +$LXC_VMID = [int]$OctopusParameters[\"Proxmox.LXC.VMID\"]; + +$BaseURL = \"https://$($ProxmoxHost):$($ProxmoxPort)/api2/json\" + +$header = @{ +\t\"Authorization\" = \"PVEAPIToken=$($ProxmoxUser)!$($ProxmoxTokenID)=$($ProxmoxToken)\" +} + + +Write-Host \"Testing Connection To Proxmox Server/Cluster ...\" + +try{ +\tInvoke-RestMethod -Method GET -uri \"$($BaseURL)\" -Headers $header | out-null +}catch{ +\tthrow \"Couldn't Connect to the Proxmox Server/Cluster\" +} + +Write-Host \"Successfully Connected To Proxmox Server/Cluster\" + +$CheckLXCExists = Invoke-RestMethod -Method GET -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/lxc/$($LXC_VMID)/status/current\" -Headers $header + +if($CheckLXCExists.data -eq $null){ +\tthrow \"The LXC container with vmid ($LXC_VMID) does not exist!\" +} + + +$LXC_Reboot = $False +try { + $Start = [System.Convert]::ToBoolean($OctopusParameters[\"Proxmox.LXC.Reboot\"]) + + if($Start -eq $True){ + \t$LXC_Reboot = $True + } + +} catch {} + +$LXCData = @{} + +if($LXC_Reboot -eq $True){ +\t$LXCStartAsyncTask = (Invoke-RestMethod -Method POST -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/lxc/$($LXC_VMID)/status/reboot\" -Headers $header -Body $LXCData) +} else{ +\t$LXCStartAsyncTask = (Invoke-RestMethod -Method POST -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/lxc/$($LXC_VMID)/status/start\" -Headers $header -Body $LXCData) +} + +$count = 1; +$maxCount = 10; + +$TaskID = $LXCStartAsyncTask.Data; + +DO +{ + Write-Host \"Checking if LXC has finished Starting..\" + $LXCStartAsyncTaskStatus = (Invoke-RestMethod -Method GET -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/tasks/$($TaskID)/status\" -Headers $header).data + + if($LXCStartAsyncTaskStatus.status -eq \"stopped\"){ + \tif($LXCStartAsyncTaskStatus.exitstatus -ne \"OK\"){ + \tWrite-Error \"LXC start task finished with error: $($LXCStartAsyncTaskStatus.exitstatus)\" + }else{ + \tWrite-Host \"LXC start task has successfully completed!\" + } + + break; + } + +\tWrite-Host \"LXC start task has not finished yet, retring in 5 seconds..\" + Write-Host \"Task Status: $($LXCStartAsyncTaskStatus.status)\" + sleep 5 + + If($count -gt $maxCount) { + Write-Warning \"Task Timed out!\" + break; + } + $count++ + +} While ($count -le $maxCount) +" + }, + "Parameters": [ + { + "Id": "9f1a3789-a4fe-4fc4-9a66-78e73d384248", + "Name": "Proxmox.Host", + "Label": "Proxmox Host", + "HelpText": "The hostname or IP address of the Proxmox cluster/host", + "DefaultValue": "1.2.3.4", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "240b1180-7b10-48ac-90fb-796fc8eaaf41", + "Name": "Proxmox.Port", + "Label": "Proxmox Port", + "HelpText": "Port number for Proxmox Cluster/Host", + "DefaultValue": "8006", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "37d5fad7-2bd3-45da-b30c-62116f0b1940", + "Name": "Proxmox.User", + "Label": "Proxmox User Account", + "HelpText": "The Proxmox user account associated with the api token.", + "DefaultValue": "root@pam", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "72a89f72-2571-435b-b0f7-a383f08572b9", + "Name": "Proxmox.Node", + "Label": "Proxmox Node", + "HelpText": "The Proxmox node in the cluster.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b33b88c3-3949-4563-91a2-4ae264bc2d7f", + "Name": "Proxmox.TokenID", + "Label": "Proxmox Token ID", + "HelpText": "This is token id that was used to create an API token in proxmox.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "eff9ca9b-b3ea-44e9-aa79-489d1d9161ee", + "Name": "Proxmox.Token", + "Label": "Proxmox API Token", + "HelpText": "The API Token secret key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "f7be6495-cc3b-4492-bb1b-e8a178a36a91", + "Name": "Proxmox.LXC.VMID", + "Label": "LXC VM ID", + "HelpText": "The LXC VMID you want to start", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "94de0cbc-b722-4788-858f-fb2b32b57fd8", + "Name": "Proxmox.LXC.Reboot", + "Label": "Reboot LXC", + "HelpText": "Do you want to reboot the LXC container", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-03-16T12:29:02.350Z", + "OctopusVersion": "2023.2.2028", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "domrichardson", + "Category": "proxmox" + } diff --git a/step-templates/proxmox-stop-lxc.json.human b/step-templates/proxmox-stop-lxc.json.human new file mode 100644 index 000000000..b2c5f8b7a --- /dev/null +++ b/step-templates/proxmox-stop-lxc.json.human @@ -0,0 +1,193 @@ +{ + "Id": "402a0407-9fa7-4d6a-8e13-9bbfa7228960", + "Name": "Proxmox Shutdown LXC Container", + "Description": "Shutdown or Stop a Proxmox LXC container using the Proxmox API. + +Requires a Proxmox [API token](https://pve.proxmox.com/wiki/Proxmox_VE_API#API_Tokens) to authenticate to the Proxmox Server/Cluster", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Proxmox Connection Variables +$ProxmoxHost = $OctopusParameters[\"Proxmox.Host\"]; +$ProxmoxPort = [int]$OctopusParameters[\"Proxmox.Port\"]; +$ProxmoxUser = $OctopusParameters[\"Proxmox.User\"]; + +$ProxmoxNode = $OctopusParameters[\"Proxmox.Node\"]; + +$ProxmoxTokenID = $OctopusParameters[\"Proxmox.TokenID\"]; +$ProxmoxToken = $OctopusParameters[\"Proxmox.Token\"]; + +# LXC Variables +$LXC_VMID = [int]$OctopusParameters[\"Proxmox.LXC.VMID\"]; + +$BaseURL = \"https://$($ProxmoxHost):$($ProxmoxPort)/api2/json\" + +$header = @{ +\t\"Authorization\" = \"PVEAPIToken=$($ProxmoxUser)!$($ProxmoxTokenID)=$($ProxmoxToken)\" +} + + +Write-Host \"Testing Connection To Proxmox Server/Cluster ...\" + +try{ +\tInvoke-RestMethod -Method GET -uri \"$($BaseURL)\" -Headers $header | out-null +}catch{ +\tthrow \"Couldn't Connect to the Proxmox Server/Cluster\" +} + +Write-Host \"Successfully Connected To Proxmox Server/Cluster\" + +$CheckLXCExists = Invoke-RestMethod -Method GET -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/lxc/$($LXC_VMID)/status/current\" -Headers $header + +if($CheckLXCExists.data -eq $null){ +\tthrow \"The LXC container with vmid ($LXC_VMID) does not exist!\" +} + + +$LXC_Stop = $False +try { + $Stop = [System.Convert]::ToBoolean($OctopusParameters[\"Proxmox.LXC.Stop\"]) + + if($Stop -eq $True){ + \t$LXC_Stop = $True + } + +} catch {} + +$LXCData = @{} + +if($LXC_Stop -eq $True){ +\t$LXCStopAsyncTask = (Invoke-RestMethod -Method POST -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/lxc/$($LXC_VMID)/status/stop\" -Headers $header -Body $LXCData) +} else{ +\t$LXCStopAsyncTask = (Invoke-RestMethod -Method POST -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/lxc/$($LXC_VMID)/status/shutdown\" -Headers $header -Body $LXCData) +} + +$count = 1; +$maxCount = 10; + +$TaskID = $LXCStopAsyncTask.Data; + +DO +{ + Write-Host \"Checking if LXC has finished Shutting Down..\" + $LXCStopAsyncTaskStatus = (Invoke-RestMethod -Method GET -uri \"$($BaseURL)/nodes/$($ProxmoxNode)/tasks/$($TaskID)/status\" -Headers $header).data + + if($LXCStopAsyncTaskStatus.status -eq \"stopped\"){ + \tif($LXCStopAsyncTaskStatus.exitstatus -ne \"OK\"){ + \tWrite-Error \"LXC shutdown task finished with error: $($LXCStopAsyncTaskStatus.exitstatus)\" + }else{ + \tWrite-Host \"LXC shutdown task has successfully completed!\" + } + + break; + } + +\tWrite-Host \"LXC shutdown task has not finished yet, retring in 5 seconds..\" + Write-Host \"Task Status: $($LXCStopAsyncTaskStatus.status)\" + sleep 5 + + If($count -gt $maxCount) { + Write-Warning \"Task Timed out!\" + break; + } + $count++ + +} While ($count -le $maxCount) +" + }, + "Parameters": [ + { + "Id": "9f1a3789-a4fe-4fc4-9a66-78e73d384248", + "Name": "Proxmox.Host", + "Label": "Proxmox Host", + "HelpText": "The hostname or IP address of the Proxmox cluster/host", + "DefaultValue": "1.2.3.4", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "240b1180-7b10-48ac-90fb-796fc8eaaf41", + "Name": "Proxmox.Port", + "Label": "Proxmox Port", + "HelpText": "Port number for Proxmox Cluster/Host", + "DefaultValue": "8006", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "37d5fad7-2bd3-45da-b30c-62116f0b1940", + "Name": "Proxmox.User", + "Label": "Proxmox User Account", + "HelpText": "The Proxmox user account associated with the api token.", + "DefaultValue": "root@pam", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "72a89f72-2571-435b-b0f7-a383f08572b9", + "Name": "Proxmox.Node", + "Label": "Proxmox Node", + "HelpText": "The Proxmox node in the cluster.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b33b88c3-3949-4563-91a2-4ae264bc2d7f", + "Name": "Proxmox.TokenID", + "Label": "Proxmox Token ID", + "HelpText": "This is token id that was used to create an API token in proxmox.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "eff9ca9b-b3ea-44e9-aa79-489d1d9161ee", + "Name": "Proxmox.Token", + "Label": "Proxmox API Token", + "HelpText": "The API Token secret key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "f7be6495-cc3b-4492-bb1b-e8a178a36a91", + "Name": "Proxmox.LXC.VMID", + "Label": "LXC VM ID", + "HelpText": "The LXC VMID you want to shutdown", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "94de0cbc-b722-4788-858f-fb2b32b57fd8", + "Name": "Proxmox.LXC.Stop", + "Label": "Force Stop LXC", + "HelpText": "If enabled it will Force Stop the LXC Container Non-gracefully. +If disabled it will gracefully shutdown the LXC Container.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-03-16T12:30:15.168Z", + "OctopusVersion": "2023.2.2028", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "domrichardson", + "Category": "proxmox" + } diff --git a/step-templates/pushalot-send-notification.json.human b/step-templates/pushalot-send-notification.json.human new file mode 100644 index 000000000..0198e5631 --- /dev/null +++ b/step-templates/pushalot-send-notification.json.human @@ -0,0 +1,76 @@ +{ + "Id": "5e837cf6-27de-436b-a2e6-b2f9d0c7411c", + "Name": "Pushalot - Send a notification", + "Description": "Sends a notification using the Pushalot REST API.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[int]$timeoutSec = $null +if(-not [int]::TryParse($Timeout, [ref]$timeoutSec)) { $timeoutSec = 60 } + +if($Source -eq $null) { + $input = @{AuthorizationToken = $AuthorizationToken; Title = $Title; Body = $Body } +} +else { + $input = @{AuthorizationToken = $AuthorizationToken; Title = $Title; Body = $Body; Source = $Source } +} + +Invoke-RestMethod -Method Post -Uri \"https://pushalot.com/api/sendmessage\" -Body $input -TimeoutSec $timeoutSec " + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "AuthorizationToken", + "Label": "Specify the authorization token as registered in the Pushalot Dashboard", + "HelpText": "Goto [Apps+Tokens](https://pushalot.com) and create a token for your application, e.g. Octopus.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "Body", + "Label": "The body of the message", + "HelpText": "Body of the message. Will show in client app in message listing in shortened version and in message detail page in full version, so when possible try to keep most important information in the beginning of the text.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "Title", + "Label": "The title for the message", + "HelpText": "Title for the message. Will show in client app in message listing in shortened version and in message detail in full version, so when possible try to keep most important information in the beginning of the text.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Source", + "Label": "Source", + "HelpText": "Notification source name that will be displayed instead of authorization token's app name. When empty, the parameter will be omitted in the Pushalot API call.", + "DefaultValue": 0, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Timeout", + "Label": "Timeout in seconds", + "HelpText": "The maximum timout in seconds for the request.", + "DefaultValue": "60", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "HumanPrinter", + "$Meta": { + "ExportedAt": "2017-01-31T08:00:00.000Z", + "OctopusVersion": "3.8.0", + "Type": "ActionTemplate" + }, + "Category": "pushalot" +} diff --git a/step-templates/pushover-send-notification.json.human b/step-templates/pushover-send-notification.json.human new file mode 100644 index 000000000..3f76f5938 --- /dev/null +++ b/step-templates/pushover-send-notification.json.human @@ -0,0 +1,85 @@ +{ + "Id": "610f3e98-e0ee-4c94-8f0f-b6417c16169e", + "Name": "Pushover - Send a notification", + "Description": "Sends a notification using the Pushover REST API.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[int]$timeoutSec = $null +if(-not [int]::TryParse($Timeout, [ref]$timeoutSec)) { $timeoutSec = 60 } + +if($Title -eq $null) { + $input = @{token = $APIToken; user = $UserKey; message = $Message; priority = $Priority } +} +else { + $input = @{token = $APIToken; user = $UserKey; message = $Message; priority = $Priority; title = $Title } +} + +Invoke-RestMethod -Method Post -Uri \"https://api.pushover.net/1/messages.json\" -Body $input -TimeoutSec $timeoutSec " + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "APIToken", + "Label": "Specify the App token as registered in the Pushover Dashboard", + "HelpText": "Goto [Create New Pushover Application/Plugin](https://pushover.net/apps/build) and create a token for your application, e.g. Octopus.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "UserKey", + "Label": "The User Key of the Pushover User to send the notification to", + "HelpText": "The **User Key** can be found on the [Pushover dashboard](https://pushover.net/) after logging in.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "Message", + "Label": "The message that should be send", + "HelpText": "The message that must be displayed in the notification.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "Title", + "Label": "Title", + "HelpText": "The title of the notification. When empty, the parameter will be omitted in the Pushover API call.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Priority", + "Label": "Priority", + "HelpText": "The priority of the notification. See Pushover API documentation for valid values.", + "DefaultValue": 0, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Timeout", + "Label": "Timeout in seconds", + "HelpText": "The maximum timout in seconds for the request.", + "DefaultValue": "60", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "HumanPrinter", + "$Meta": { + "ExportedAt": "2017-01-31T08:00:00.000Z", + "OctopusVersion": "3.8.0", + "Type": "ActionTemplate" + }, + "Category": "pushover" +} diff --git a/step-templates/rabbitmq-notify-deploy.json.human b/step-templates/rabbitmq-notify-deploy.json.human new file mode 100644 index 000000000..0b8d2f3be --- /dev/null +++ b/step-templates/rabbitmq-notify-deploy.json.human @@ -0,0 +1,146 @@ +{ + "Id": "309f8497-7f79-4979-89a6-5d7e15d83ae2", + "Name": "RabbitMQ - Notify Deploy", + "Description": "Notifies a deploy by sending a message into rabbitMQ. The message contains all octopus variables and these can be used to have some insight on the deploy. The step is very beta, it is advised to improve it to match real case scenarios.", + "ActionType": "Octopus.Script", + "Version": 0, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Add-ValueToHashtable\r +{\r + param(\r + [Parameter(Mandatory = 1)][object]$variable,\r + [Parameter(Mandatory = 1)][hashtable]$hashtable\r + )\r +\r + if ($variable.value.GetType() -eq [System.String])\r + {\r + $hashtable.Add($variable.Name, $variable.value)\r + return\r + }\r +\r + if (($variable.value.GetType() -eq (New-Object 'System.Collections.Generic.Dictionary[String,String]').GetType()) -or ($variable.value.GetType() -eq [Hashtable]))\r + {\r + foreach ($element in $variable.Value.GetEnumerator())\r + {\r + $obj = New-Object PsObject -Property @{ Name = $element.Key; Value = $element.Value }\r + Add-ValueToHashtable -variable $obj -hashtable $hashtable\r + }\r + return\r + }\r +\r + throw \"Add-ValueToHashtable method does not know what to do with type \" + $variable.value.GetType().Name\r +}\r +\r +function Get-UnixDate\r +{\r + $epoch = Get-Date -Year 1970 -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0\t\r + $now = Get-Date\r + return ([math]::truncate($now.ToUniversalTime().Subtract($epoch).TotalMilliSeconds))\r +}\r +\r +function Get-IsRollback\r +{\r + $currentVersion = New-Object -TypeName System.Version -ArgumentList $OctopusReleaseNumber\r + $prevVersion = New-Object -TypeName System.Version -ArgumentList $OctopusReleasePreviousNumber\r +\r + return ($currentVersion.CompareTo($prevVersion) -lt 0)\r +}\r +\r +function Get-OctopusVariablesJson\r +{\r + $octoVariables = @{}\r +\r + foreach ($var in (Get-Variable -Name OctopusParameters*))\r + {\r + Add-ValueToHashtable -variable $var -hashtable $octoVariables\r + }\r +\r + $octoVariables.Add(\"isrollback\", (Get-IsRollback))\r + $octoVariables.Add(\"timestamp\", (Get-UnixDate))\r + $octoVariables.Add(\"safeprojectname\", $OctopusParameters[\"Octopus.Project.Name\"].Replace(\" \", \"_\"))\r +\r + return ($octoVariables | ConvertTo-Json -Compress)\r +}\r +\r +function ConvertTo-AsciiString\r +{\r + param( \r + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]\r + [string]$input)\r + \r + process {\r + #custom desired transformation\r + $tmp = $input.Replace([char]0x00EC, \"i\")\r + #fallback\r + $bytes = [System.Text.Encoding]::UTF8.GetBytes($tmp)\r + $asciiArray = [System.Text.Encoding]::Convert([System.Text.Encoding]::UTF8, [System.Text.Encoding]::ASCII, $bytes)\r + $ascistring = [System.Text.Encoding]::ASCII.GetString($asciiArray)\r + return $ascistring\r + }\r +}\r +\r +$json = Get-OctopusVariablesJson | ConvertTo-AsciiString\r +$body = New-Object PsObject -Property @{ properties = @{}; routing_key = \"#\"; payload = $json; payload_encoding = \"string\" } | ConvertTo-Json -Compress\r +\r +$securepassword = ConvertTo-SecureString $rabbitPassword -AsPlainText -Force\r +$cred = New-Object System.Management.Automation.PSCredential ($rabbitUsername, $securepassword)\r +\r +Invoke-RestMethod -Uri \"$rabbitUrl/api/exchanges/$rabbitVirtualHost/$rabbitExchange/publish\" -Method Post -Credential $cred -Body $body -ContentType \"application/json\"" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "rabbitUsername", + "Label": "rabbitmq username", + "HelpText": "username used to publish message", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "rabbitPassword", + "Label": "rabbitmq user password", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "rabbitExchange", + "Label": "rabbitMQ exchange", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "rabbitVirtualHost", + "Label": "rabbitmq virtual host", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "rabbitUrl", + "Label": "rabbitmq url endpoint", + "HelpText": null, + "DefaultValue": "http://localhost:15672", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "alfhenrik", + "$Meta": { + "ExportedAt": "2015-07-05T08:47:05.572+00:00", + "OctopusVersion": "3.0.0.1666", + "Type": "ActionTemplate" + }, + "Category": "rabbitmq" +} diff --git a/step-templates/rackspace-update-load-balancer.json.human b/step-templates/rackspace-update-load-balancer.json.human new file mode 100644 index 000000000..ee7caaa66 --- /dev/null +++ b/step-templates/rackspace-update-load-balancer.json.human @@ -0,0 +1,147 @@ +{ + "Id": "94aa35a3-0a0c-4c45-8781-98006bda3bcd", + "Name": "Rackspace - Update Load Balancer", + "Description": "Change the condition of a node in a Rackspace Cloud Load Balancer.", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$username = $OctopusParameters['Username'] +$apiKey = $OctopusParameters['ApiKey'] +$loadbalanderId = $OctopusParameters['LoadBalancerID'] +$newNodeCondition = $OctopusParameters['NewCondition'] +$ipAddress = $OctopusParameters['NodeIpAddress'] + +if ($newNodeCondition -ne \"ENABLED\" -and $newNodeCondition -ne \"DISABLED\" -and $newNodeCondition -ne \"DRAINING\") +{ + throw \"Condition must be one of 'ENABLED', 'DISABLED' or 'DRAINING'\" +} + +# Get token and manipulation URL + +$tokensUri = \"https://lon.identity.api.rackspacecloud.com/v2.0/tokens\" +$tokensBody = @\" +{ + \"auth\": + { + \"RAX-KSKEY:apiKeyCredentials\": + { + \"username\": \"$username\", + \"apiKey\": \"$apiKey\" + } + } +} +\"@ + +Write-Host \"Sending request $tokensBody to $tokensUri\" + +$tokensResponse = Invoke-WebRequest -Uri $tokensUri -Method Post -Body $tokensBody -ContentType \"application/json\" -UseBasicParsing + +if ($tokensResponse.StatusCode -ne 200) +{ + throw \"Authorisation failed\" +} + +$tokensObj = ConvertFrom-Json -InputObject $tokensResponse.Content + +$loadBalancerDetails = $tokensObj.access.serviceCatalog | Where {$_.name -eq \"cloudLoadBalancers\"} +$endpoints = $loadBalancerDetails.endpoints | Select -First 1 +$loadbalancerUrl = $endpoints.publicURL +$token = $tokensObj.access.token.id + +# Update node + +$header = @{} +$header.Add(\"X-Auth-Token\", $token) +$nodesUrl = \"$loadbalancerUrl/loadbalancers/$loadbalancerId/nodes\" + +Write-Host \"Getting node details from $nodesUrl\" + +$nodesResponse = Invoke-WebRequest -Uri $nodesUrl -Method Get -Headers $header -ContentType \"application/json\" -UseBasicParsing + +if ($nodesResponse.StatusCode -ne 200) +{ + throw \"Getting load balancer details failed\" +} + +$nodesObj = ConvertFrom-Json -InputObject $nodesResponse.Content +$node = $nodesObj.nodes | Where {$_.address -eq $ipAddress} +$nodeId = $node.id + +$updateBody = @\" +{ + \"node\": { + \"condition\" : \"$newNodeCondition\" + } +} +\"@ +$updateUrl = \"$loadbalancerUrl/loadbalancers/$loadbalancerId/nodes/$nodeId\" + +Write-Host \"Updating node $nodeId to $newNodeCondition\" +Write-Host \"$updateBody\" +Write-Host \"$updateUrl\" + +$updateResponse = Invoke-WebRequest -Uri $updateUrl -Body $updateBody -Method Put -Headers $header -ContentType \"application/json\" -UseBasicParsing + +if ($updateResponse.StatusCode -ne 202) +{ + throw \"Updating load balancer failed\" +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Username", + "Label": "Username", + "HelpText": "Rackspace control panel username", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApiKey", + "Label": "API key", + "HelpText": "Rackspace control panel user API key", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "LoadBalancerID", + "Label": "Load Balancer ID", + "HelpText": "ID of the load balancer, found on the details page in the Rackspace control panel", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NodeIpAddress", + "Label": "Node IP address", + "HelpText": "IP address of load balanced node. Found on the load balancer details page of the Rackspace control panel", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NewCondition", + "Label": "New Node Condition", + "HelpText": "Condition to set the node to. Can either be 'ENABLED', 'DISABLED' or 'DRAINING'", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-16T10:29:42.481+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "rackspace" +} diff --git a/step-templates/ravendb-create-database.json.human b/step-templates/ravendb-create-database.json.human new file mode 100644 index 000000000..020c3a5e6 --- /dev/null +++ b/step-templates/ravendb-create-database.json.human @@ -0,0 +1,240 @@ +{ + "Id": "ffa7c244-f835-42d1-ad38-15f96037f114", + "Name": "RavenDB - Create Database", + "Description": "Used to create a new database on a server", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#--------------------------------------------------------------------\r +#Octopus Variables\r +\r +#URL where the database will be create\r +$ravenDatabaseURL = $OctopusParameters[\"ravenDatabaseURL\"]\r +\r +#Name of the new database\r +$ravenDatabaseName = $OctopusParameters[\"ravenDatabaseName\"]\r +\r +#Name of Active Bundles (Replication; Versioning; etc) (Default is null)\r +$ravenActiveBundles = $OctopusParameters[\"ravenActiveBundles\"]\r +\r +#storage Type Name (esent or voron)\r +$ravenStorageTypeName = $OctopusParameters[\"ravenStorageTypeName\"]\r +\r +#directory the database will be located on the server\r +$ravenDataDir = $OctopusParameters[\"ravenDataDir\"]\r +\r +#allow incremental back ups: boolean\r +$allowIncrementalBackups = $OctopusParameters[\"allowIncrementalBackups\"]\r +\r +#temporary files will be created at this location\r +$voronTempPath = $OctopusParameters[\"voronTempPath\"]\r +\r +#The path for the esent logs\r +$esentLogsPath = $OctopusParameters[\"esentLogsPath\"]\r +\r +#The path for the indexes on a disk\r +$indexStoragePath = $OctopusParameters[\"indexStoragePath\"]\r +\r +\r +#--------------------------------------------------------------------\r +\r +#checks to see if the entered database exists, return a boolean value depending on the outcome\r +function doesRavenDBExist([string] $databaseChecking, [string]$URL)\r +{\r + #retrieves the list of databases at the specified URL\r + $database_list = Invoke-RestMethod -Uri \"$URL/databases\" -Method Get\r + #checks if the database is at the specified URL\r + if ($database_list -contains $databaseChecking.ToString()) \r + {\r + return $TRUE\r + }\r + else \r + {\r + return $FALSE\r + }\r +\r + \r +}#ends does ravenDB exist function\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +\r +#--------------------------------------------------------------------\r +\r +#check to see if database exists\r +Write-Output \"Checking to see if $ravenDatabaseName exists\"\r +\r +$database_exists = doesRavenDBExist -databaseChecking $ravenDatabaseName -URL $ravenDatabaseURL\r +\r +if($database_exists -eq $TRUE)\r +{\r + Write-Error \"$ravenDatabaseName already exists\" -ErrorId E4\r + Exit 1\r +}\r +\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +#--------------------------------------------------------------------\r +#check setting variables \r +\r +if($ravenActiveBundles -eq $null)\r +{\r + $ravenActiveBundles = \"\" \r +}\r +\r +if($ravenDataDir -eq \"\")\r +{\r + Write-Warning \"A directory for the database has NOT been entered. The default directory ~\\Databases\\System is being used.\"\r + $ravenDataDir = \"~\\Databases\\System\"\r +}\r +\r +if($esentLogsPath -eq \"\")\r +{\r + Write-Warning \"The path for the esent logs has NOT been entered. The default path of ~/Data/Logs will be used\"\r + $esentLogsPath = \"~/Data/Logs\"\r +}\r +\r +if($indexStoragePath -eq \"\")\r +{\r + Write-Warning \"The path for the indexes has NOT been entered. The default path of ~/Data/Indexes will be used\"\r + $indexStoragePath = \"~/Data/Indexes\"\r +}\r +\r +\r +$ravenDataDir = $ravenDataDir.Replace(\"\\\", \"\\\\\")\r +\r +$voronTempPath = $voronTempPath.Replace(\"\\\", \"\\\\\")\r +\r +$esentLogsPath = $esentLogsPath.Replace(\"\\\", \"\\\\\")\r +\r +$indexStoragePath = $indexStoragePath.Replace(\"\\\", \"\\\\\")\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#database Settings \r +\r +$db_settings = @\"\r +{\r + \"Settings\":\r + {\r + \"Raven/ActiveBundles\": \"$ravenActiveBundles\",\r + \"Raven/StorageTypeName\": \"$ravenStorageTypeName\",\r + \"Raven/DataDir\": \"$ravenDataDir\",\r + \"Raven/Voron/AllowIncrementalBackups\": \"$allowIncrementalBackups\",\r + \"Raven/Voron/TempPath\": \"$voronTempPath\",\r + \"Raven/Esent/LogsPath\": \"$esentLogsPath\",\r + \"Raven/IndexStoragePath\": \"$indexStoragePath\"\r + }\r +}\r +\"@\r +\r +#--------------------------------------------------------------------\r +#create Database\r +\r +Write-Output \"Create database: $ravenDatabaseName\"\r +\r +$createURI = \"$ravenDatabaseURL/admin/databases/$ravenDatabaseName\"\r +\r +Invoke-RestMethod -Uri $createURI -Body $db_settings -Method Put\r +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ravenDatabaseURL", + "Label": "URL of the RavenDB", + "HelpText": "URL of the RavenDB, where you would like the database to be created. + +For example **http://localhost:8080/**", + "DefaultValue": "http://localhost:8080/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ravenDatabaseName", + "Label": "Name of the Database", + "HelpText": "The name the database will be called. Make sure that name is not already being used.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ravenActiveBundles", + "Label": "Active Bundles", + "HelpText": "A settings option, Active Bundles refers bundles that RavenDB has (Replication;Versioning; etc)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ravenStorageTypeName", + "Label": "Name of the Storage Type", + "HelpText": "What storage type to use", + "DefaultValue": "Esent", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Esent| Esent +Voron| Voron" + } + }, + { + "Name": "ravenDataDir", + "Label": "Path for the database directory", + "HelpText": "The path for the database directory on the server", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "allowIncrementalBackups", + "Label": "Allow Incremental Backups", + "HelpText": "Allow Incremental Backups to be perform on this Database", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "voronTempPath", + "Label": "Path to Temporary Files", + "HelpText": "A different path where the temporary files will be stored", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "esentLogsPath", + "Label": "Path for the Esent Logs", + "HelpText": "The path for the Esent logs", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "indexStoragePath", + "Label": "Path for the Indexes", + "HelpText": "The path for the indexes on a disk, if you want to store the indexes on another HDD for performance reasons", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-11-12T22:39:11.556+00:00", + "LastModifiedBy": "timhunt303", + "$Meta": { + "ExportedAt": "2015-11-15T22:07:42.439+00:00", + "OctopusVersion": "3.1.7", + "Type": "ActionTemplate" + }, + "Category": "ravendb" +} diff --git a/step-templates/ravendb-create-filesystem.json.human b/step-templates/ravendb-create-filesystem.json.human new file mode 100644 index 000000000..9ee2d3be6 --- /dev/null +++ b/step-templates/ravendb-create-filesystem.json.human @@ -0,0 +1,258 @@ +{ + "Id": "35e74a1b-3d6a-4d68-be07-51d0dfb56f23", + "Name": "RavenDB - Create File System", + "Description": "Used to create a new file system on a server", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#--------------------------------------------------------------------\r +#File System Octopus Variables\r +\r +#URL of RavenFS that is being deleted \r +$ravenFileSystemURL = $OctopusParameters[\"ravenFileSystemURL\"]\r +\r +#name of the RavenFS that is being deleted\r +$ravenFileSystemName = $OctopusParameters[\"ravenFileSystemName\"]\r +\r +#--------------------------------------------------------------------\r +#Settings Octopus Variables\r +\r +#Name of Active Bundles (Replication; Versioning; etc) (Default is none)\r +$ravenActiveBundles = $OctopusParameters[\"ravenActiveBundles\"]\r +\r +#storage Type Name (esent or voron)\r +$ravenStorageTypeName = $OctopusParameters[\"ravenStorageTypeName\"]\r +\r +#directory the database will be located on the server\r +$ravenDataDir = $OctopusParameters[\"ravenDataDir\"]\r +\r +#allow incremental back ups: boolean\r +$allowIncrementalBackups = $OctopusParameters[\"allowIncrementalBackups\"]\r +\r +#temporary files will be created at this location\r +$voronTempPath = $OctopusParameters[\"voronTempPath\"]\r +\r +#The path for the esent logs\r +$esentLogsPath = $OctopusParameters[\"esentLogsPath\"]\r +\r +#The path for the indexes on a disk\r +$indexStoragePath = $OctopusParameters[\"indexStoragePath\"]\r +\r +\r +#--------------------------------------------------------------------\r +\r +#checks to see if the entered file system exists, return a Boolean value depending on the outcome\r +function doesRavenFSExist([string] $FSChecking, [string]$URL)\r +{\r + #retrieves the list of File Systems at the specified URL\r + $fs_list = Invoke-RestMethod -Uri \"$URL/fs\" -Method Get\r + #checks if the File System is at the specified URL\r + if ($fs_list -contains $FSChecking.ToString()) \r + {\r + return $TRUE\r + }\r + else \r + {\r + return $FALSE\r + }\r +\r + \r +}#ends does File System exist function\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#check to see if File System exists\r +\r +Write-Output \"Checking if $ravenFileSystemName exists\"\r +\r +$fs_exists = doesRavenFSExist -FSChecking $ravenFileSystemName -URL $ravenFileSystemURL\r +\r +if($fs_exists -eq $TRUE)\r +{\r + Write-Error \"$ravenFileSystemName already exists\" -ErrorId E4\r + Exit 1\r +}\r +else\r +{\r + Write-Output \"$ravenFileSystemName doesn't exist. Creating $ravenFileSystemName now\"\r +}\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#check setting variables \r +\r +if($ravenActiveBundles -eq $null)\r +{\r + $ravenActiveBundles = \"\" \r +}\r +\r +if($ravenDataDir -eq \"\")\r +{\r + Write-Warning \"A directory for the database has NOT been entered. The default directory ~\\Databases\\System is being used.\"\r + $ravenDataDir = \"~\\Databases\\System\"\r +}\r +\r +if($esentLogsPath -eq \"\")\r +{\r + Write-Warning \"The path for the esent logs has NOT been entered. The default path of ~/Data/Logs will be used\"\r + $esentLogsPath = \"~/Data/Logs\"\r +}\r +\r +if($indexStoragePath -eq \"\")\r +{\r + Write-Warning \"The path for the indexes has NOT been entered. The default path of ~/Data/Indexes will be used\"\r + $indexStoragePath = \"~/Data/Indexes\"\r +}\r +\r +\r +$ravenDataDir = $ravenDataDir.Replace(\"\\\", \"\\\\\")\r +\r +$voronTempPath = $voronTempPath.Replace(\"\\\", \"\\\\\")\r +\r +$esentLogsPath = $esentLogsPath.Replace(\"\\\", \"\\\\\")\r +\r +$indexStoragePath = $indexStoragePath.Replace(\"\\\", \"\\\\\")\r +\r +$ravenDataDir = $ravenDataDir.Replace(\"DB\", \"FS\")\r +\r +$voronTempPath = $voronTempPath.Replace(\"DB\", \"FS\")\r +\r +$esentLogsPath = $esentLogsPath.Replace(\"DB\", \"FS\")\r +\r +$indexStoragePath = $indexStoragePath.Replace(\"DB\", \"FS\")\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#file system settings\r +\r +\r +$fs_settings = @\"\r +{\r + \"Settings\":\r + {\r + \"Raven/ActiveBundles\": \"$ravenActiveBundles\",\r + \"Raven/StorageTypeName\": \"$ravenStorageTypeName\",\r + \"Raven/DataDir\": \"$ravenDataDir\",\r + \"Raven/Voron/AllowIncrementalBackups\": \"$allowIncrementalBackups\",\r + \"Raven/Voron/TempPath\": \"$voronTempPath\",\r + \"Raven/Esent/LogsPath\": \"$esentLogsPath\",\r + \"Raven/IndexStoragePath\": \"$indexStoragePath\"\r + }\r +}\r +\"@\r +\r +#--------------------------------------------------------------------\r +#Create File System\r +\r +Write-Output \"Creating File System: $ravenFileSystemName\"\r +\r +$createURI = \"$ravenFileSystemURL/admin/fs/$ravenFileSystemName\"\r +\r +Invoke-RestMethod -Uri $createURI -Body $fs_settings -Method Put\r +\r +Write-Output \"$ravenFileSystemName created.\"\r +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ravenFileSystemURL", + "Label": "URL of RavenDB", + "HelpText": "URL of the RavenDB, where you would like the file system to be created. + +For example **http://localhost:8080/**", + "DefaultValue": "http://localhost:8080/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ravenFileSystemName", + "Label": "Name of the File System", + "HelpText": "Name the new File System will be called. Make sure the name is not being used already within this instance of RavenDB", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ravenActiveBundles", + "Label": "Active Bundles", + "HelpText": "A settings option, Active Bundles refers bundles that RavenDB has (Replication;Versioning; etc)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ravenStorageTypeName", + "Label": "Name of the Storage Type", + "HelpText": "What storage type to use", + "DefaultValue": "Esent", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Esent| Esent +Voron| Voron" + } + }, + { + "Name": "ravenDataDir", + "Label": "Path for the database directory", + "HelpText": "The path for the database directory on the server", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "allowIncrementalBackups", + "Label": "Allow Incremental Backups", + "HelpText": "Allow Incremental Backups to be perform on this Database", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "voronTempPath", + "Label": "Path to Temporary Files", + "HelpText": "A different path where the temporary files will be stored", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "esentLogsPath", + "Label": "Path for the Esent Logs", + "HelpText": "The path for the Esent logs", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "indexStoragePath", + "Label": "Path for the Indexes", + "HelpText": "The path for the indexes on a disk, if you want to store the indexes on another HDD for performance reasons", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-11-12T22:39:11.556+00:00", + "LastModifiedBy": "timhunt303", + "$Meta": { + "ExportedAt": "2015-11-15T22:10:59.092+00:00", + "OctopusVersion": "3.1.7", + "Type": "ActionTemplate" + }, + "Category": "ravendb" +} diff --git a/step-templates/ravendb-delete-database.json.human b/step-templates/ravendb-delete-database.json.human new file mode 100644 index 000000000..e672a8179 --- /dev/null +++ b/step-templates/ravendb-delete-database.json.human @@ -0,0 +1,161 @@ +{ + "Id": "474d4dff-e91b-4580-b074-c39763741cd6", + "Name": "RavenDB - Delete Database", + "Description": "Used to delete a database from a server, with a possibility to remove all the data from hard drive.", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#--------------------------------------------------------------------\r +#Octopus Variables\r +\r +#URL where the database can be found\r +$ravenDatabaseURL = $OctopusParameters[\"ravenDatabaseURL\"]\r +\r +#Name of the Database\r +$ravenDatabaseName = $OctopusParameters[\"ravenDatabaseName\"]\r +\r +#hard delete (true or false)\r +$hardDelete = $OctopusParameters[\"hardDelete\"]\r +\r +#Allow Database to be deleted\r +$allowDelete = $OctopusParameters[\"allowDelete\"]\r +\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +#--------------------------------------------------------------------\r +#checks to see if the database can be deleted\r +\r +if($allowDelete -eq $FALSE)\r +{\r + Write-Error \"$ravenDatabaseName cannot be deleted. Please try this on a database that can be delete.\" -ErrorId E4\r + Exit 1\r +}\r +\r +\r +#--------------------------------------------------------------------\r +\r +#checks to see if the entered database exists, return a boolean value depending on the outcome\r +function doesRavenDBExist([string] $databaseChecking, [string]$URL)\r +{\r + #retrieves the list of databases at the specified URL\r + $database_list = Invoke-RestMethod -Uri \"$URL/databases\" -Method Get\r + #checks if the database is at the specified URL\r + if ($database_list -contains $databaseChecking.ToString()) \r + {\r + return $TRUE\r + }\r + else \r + {\r + return $FALSE\r + }\r +\r + \r +}#ends does ravenDB exist function\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +\r +#--------------------------------------------------------------------\r +\r +#check to see if database exists\r +Write-Output \"Checking to see if $ravenDatabaseName exists\"\r +\r +$database_exists = doesRavenDBExist -databaseChecking $ravenDatabaseName -URL $ravenDatabaseURL\r +\r +if($database_exists -eq $TRUE)\r +{\r + Write-Output \"$ravenDatabaseName exists\"\r + $doWork = $TRUE\r +}\r +else\r +{\r + Write-Warning \"$ravenDatabaseName does not exist already.\" \r + $doWork = $FALSE\r +}\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +\r +#--------------------------------------------------------------------\r +#hard delete option\r +\r +$hardDeleteString = $hardDelete.ToString().ToLower()\r +\r +\r +\r +#--------------------------------------------------------------------\r +#Delete database\r +\r +if($doWork -eq $TRUE)\r +{\r +\r + Write-Output \"Deleting Database: $ravenDatabaseName\"\r +\r + $deleteURI = \"$ravenDatabaseURL/admin/databases/$ravenDatabaseName\" + \"?hard-delete=$hardDeleteString\"\r +\r + Invoke-RestMethod -Uri $deleteURI -Method Delete\r +\r + #Waits 10 seconds before it continues\r + Start-Sleep -Seconds 10\r + \r + Write-Output \"Database has successfuly been deleted\"\r +\r +}" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ravenDatabaseURL", + "Label": "URL of the Raven Database", + "HelpText": "The URL of the Raven database, where the Database is located. + +For example **http://localhost:8080/**", + "DefaultValue": "http://localhost:8080/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ravenDatabaseName", + "Label": "Name of the Raven Database", + "HelpText": "Name of the Database in Raven.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "hardDelete", + "Label": "Hard Delete", + "HelpText": "Should all of the data be removed from the hard drive as well", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "allowDelete", + "Label": "Allow Deletion", + "HelpText": "Is the database allowed to be deleted. **TRUE OR FALSE** value must be entered + +**For example**: you don't want a production server to be deleted, the script will stop it from happening + +**HINT**: have a variable within Octopus that returns true if it is allowed to be deleted and vice versa.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2015-11-26T22:39:11.556+00:00", + "LastModifiedBy": "timhunt303", + "$Meta": { + "ExportedAt": "2015-11-26T03:54:46.614+00:00", + "OctopusVersion": "3.1.7", + "Type": "ActionTemplate" + }, + "Category": "ravendb" +} diff --git a/step-templates/ravendb-delete-filesystem.json.human b/step-templates/ravendb-delete-filesystem.json.human new file mode 100644 index 000000000..624f1c8cd --- /dev/null +++ b/step-templates/ravendb-delete-filesystem.json.human @@ -0,0 +1,156 @@ +{ + "Id": "365ae5c6-8f97-4e99-82c8-9973b9b0d8ff", + "Name": "RavenDB - Delete File System", + "Description": "Used to delete a file system from a server, with a possibility to remove its all data from the hard drive.", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#--------------------------------------------------------------------\r +#Octopus Variables\r +\r +#URL of RavenFS that is being deleted \r +$ravenFileSystemURL = $OctopusParameters[\"ravenFileSystemURL\"]\r +\r +#name of the RavenFS that is being deleted\r +$ravenFileSystemName = $OctopusParameters[\"ravenFileSystemName\"]\r +\r +#hard delete (true or false)\r +$hardDelete = $OctopusParameters[\"hardDelete\"]\r +\r +#Allow File System to be deleted\r +$allowDelete = $OctopusParameters[\"allowDelete\"]\r +\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +#--------------------------------------------------------------------\r +#checks to see if the File System can be deleted\r +\r +if($allowDelete -eq $FALSE)\r +{\r + Write-Error \"$ravenFileSystemName cannot be deleted. Please try this on a database that can be delete.\" -ErrorId E4\r + Exit 1\r +}\r +\r +\r +#--------------------------------------------------------------------\r +\r +#checks to see if the entered file system exists, return a Boolean value depending on the outcome\r +function doesRavenFSExist([string] $FSChecking, [string]$URL)\r +{\r + #retrieves the list of File Systems at the specified URL\r + $fs_list = Invoke-RestMethod -Uri \"$URL/fs\" -Method Get\r + #checks if the File System is at the specified URL\r + if ($fs_list -contains $FSChecking.ToString()) \r + {\r + return $TRUE\r + }\r + else \r + {\r + return $FALSE\r + }\r +\r + \r +}#ends does File System exist function\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#check to see if File System exists\r +\r +Write-Output \"Checking if $ravenFileSystemName exists\"\r +\r +$fs_exists = doesRavenFSExist -FSChecking $ravenFileSystemName -URL $ravenFileSystemURL\r +\r +if($fs_exists -eq $TRUE)\r +{\r + Write-Output \"$ravenFileSystemName exists\"\r + $doWork = $TRUE\r +}\r +else\r +{\r + Write-Warning \"$ravenFileSystemName doesn't exist already.\"\r + $doWork = $FALSE\r +}\r +\r +#--------------------------------------------------------------------\r +#converts hard delete option to a string\r +\r +$hardDeleteString = $hardDelete.ToString().ToLower()\r +\r +#--------------------------------------------------------------------\r +#Delete File System\r +\r +if($doWork -eq $TRUE)\r +{\r +\r + Write-Output \"Deleting File System: $ravenFileSystemName\"\r +\r + $deleteURI = \"$ravenFileSystemURL/admin/fs/$ravenFileSystemName\" + \"?hard-delete=$hardDeleteString\"\r +\r + Invoke-RestMethod -Uri $deleteURI -Method Delete\r +\r +\r + #Waits 10 seconds before it continues\r + Start-Sleep -Seconds 10\r + \r + Write-Output \"File System has successfuly been deleted\"\r +\r +}" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ravenFileSystemURL", + "Label": "URL of the Raven Database", + "HelpText": "The URL of the Raven File System, where the File System is located. + +For example: **http://localhost:8080/**", + "DefaultValue": "http://localhost:8080/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ravenFileSystemName", + "Label": "Name of the File System", + "HelpText": "Name of the File System in Raven", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "hardDelete", + "Label": "Hard Delete", + "HelpText": "Should all of the data be removed from the hard drive as well", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "allowDelete", + "Label": "Allow Deletion", + "HelpText": "Is the File System allowed to be deleted. **TRUE OR FALSE** value must be entered + +**For example**: you don't want a production server to be deleted, the script will stop it from happening + +**HINT**: have a variable within Octopus that returns true if it is allowed to be deleted and vice versa.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2015-11-26T22:39:11.556+00:00", + "LastModifiedBy": "timhunt303", + "$Meta": { + "ExportedAt": "2015-11-26T03:55:48.036+00:00", + "OctopusVersion": "3.1.7", + "Type": "ActionTemplate" + }, + "Category": "ravendb" +} diff --git a/step-templates/ravendb-smuggler-move-data-between-database.json.human b/step-templates/ravendb-smuggler-move-data-between-database.json.human new file mode 100644 index 000000000..0faf5cbfc --- /dev/null +++ b/step-templates/ravendb-smuggler-move-data-between-database.json.human @@ -0,0 +1,422 @@ +{ + "Id": "82f804fe-682e-4e0b-8c2a-a5f289a9cabc", + "Name": "RavenDB - Smuggler - Move Data Between Databases", + "Description": "To move data directly between two instances (or different databases in the same instance) using the between option introduced in Smuggler version 3.", + "ActionType": "Octopus.Script", + "Version": 10, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "\r +# Variables\r +\r +#Location of the Raven Smuggler exe\r +$ravenSmugglerPath = $OctopusParameters[\"ravenSmugglerPath\"]\r +\r +\r +#--------------------------------------------------------------------\r +# Source Database Variables\r +\r +#URL of RavenDB that is being backed up \r +$sourceDatabaseURL = $OctopusParameters[\"sourceDatabaseURL\"]\r +\r +#name of the RavenDB that is being backed up\r +$sourceDatabaseName = $OctopusParameters[\"sourceDatabaseName\"]\r +\r +#API Key for the Source Database\r +$sourceDatabaseApiKey = $OctopusParameters[\"sourceDatabaseApiKey\"]\r +\r +\r +#--------------------------------------------------------------------\r +#Destination Database Variables\r +\r +#URL of destination RavenDB \r +$destinationDatabaseURL = $OctopusParameters[\"destinationDatabaseURL\"]\r +\r +#Name of the destination RavenDB\r +$destinationDatabaseName = $OctopusParameters[\"destinationDatabaseName\"]\r +\r +#API Key for the Destination Database\r +$destinationDatabaseAPIKey = $OctopusParameters[\"destinationDatabaseAPIKey\"]\r +\r +\r +\r +#------------------------------------------------------------------------------\r +# Other Variables retrieved from Octopus\r +\r +#Limit the back up to different types in the database\r +#Get document option (true/false)\r +$operateOnDocuments = $OctopusParameters[\"operateOnDocuments\"]\r +\r +#Get attachments option (true/false)\r +$operateOnAttachments = $OctopusParameters[\"operateOnAttachments\"]\r +\r +#Get indexes option (true/false)\r +$operateOnIndexes = $OctopusParameters[\"operateOnIndexes\"]\r +\r +#Get transformers option (true/false)\r +$operateOnTransformers = $OctopusParameters[\"operateOnTransformers\"]\r +\r +#Get timeout option \r +$timeout = $OctopusParameters[\"timeout\"]\r +\r +#Get wait for indexing option (true/false)\r +$waitForIndexing = $OctopusParameters[\"waitForIndexing\"]\r +\r +\r +#--------------------------------------------------------------------\r +\r +#checks to see if the entered database exists, return a boolean value depending on the outcome\r +function doesRavenDBExist([string] $databaseChecking, [string]$URL)\r +{\r + #retrieves the list of databases at the specified URL\r + $database_list = Invoke-RestMethod -Uri \"$URL/databases\" -Method Get\r + #checks if the database is at the specified URL\r + if ($database_list -contains $databaseChecking.ToString()) \r + {\r + return $TRUE\r + }\r + else \r + {\r + return $FALSE\r + }\r +\r + \r +}#ends does ravenDB exist function\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#Checking the versions of Raven Server of both databases to see if they are compatible \r +\r +Write-Output \"Checking that both $sourceDatabaseName and $destinationDatabaseName are running the same version of RavenDB\"\r +\r +#Getting Source Database's build version\r +$sourceVersionURL = \"$sourceDatabaseURL/databases/$sourceDatabaseName/build/version\"\r +\r +$sourceDatabaseVersion = Invoke-RestMethod -Uri $sourceVersionURL -Method Get\r +\r +#Getting destination Database's build version\r +$destinationVersionURL = \"$destinationDatabaseURL/databases/$destinationDatabaseName/build/version\"\r +\r +$destinationDatabaseVersion = Invoke-RestMethod -Uri $destinationVersionURL -Method Get\r +\r +#Checks to see if they are the same version and build number\r +if(($sourceDatabaseVersion.ProductVersion -eq $destinationDatabaseVersion.ProductVersion) -and ($sourceDatabaseVersion.BuildVersion -eq $destinationDatabaseVersion.BuildVersion))\r +{\r + \r + Write-Output \"Source Database Product Version:\" $sourceDatabaseVersion.ProductVersion \r + Write-Output \"Source Database Build Version:\" $sourceDatabaseVersion.BuildVersion\r + Write-Output \"Destination Database Version:\" $destinationDatabaseVersion.ProductVersion \r + Write-Output \"Destination Database Build Version:\" $destinationDatabaseVersion.BuildVersion\r + Write-Output \"Source and destination Databases are running the same version of Raven Server\"\r + \r +}\r +else \r +{\r + Write-Warning \"Source Database Version: $sourceDatabaseVersion\"\r + Write-Warning \"Destination Database Version: $destinationDatabaseVersion\"\r + Write-Warning \"The databases are running different versions of Raven Server\"\r +}\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +\r +#Check path to smuggler\r +Write-Output \"Checking if Smuggler path is correct`n\"\r +\r +$smugglerPath = \"$ravenSmugglerPath\"\r +\r +$smuggler_Exists = Test-Path -Path $smugglerPath\r +\r +\r +\r +#if the path is correct, the script continues, throws an error if the path is wrong\r +If($smuggler_Exists -eq $TRUE)\r +{\r + Write-Output \"Smuggler exists\"\r +\r +}#ends if smuggler exists \r +else\r +{\r + Write-Error \"Smuggler can not be found `nCheck the directory: $ravenSmugglerPath\" -ErrorId E4\r + Exit 1\r +}#ends else, smuggler can't be found\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#Checking the version of smuggler\r +\r +$SmugglerVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($ravenSmugglerPath).FileVersion\r +\r +if($SmugglerVersion -cgt \"3\")\r +{\r + Write-Output \"Smuggler Version: $SmugglerVersion\"\r +}\r +else\r +{\r + Write-Error \"The version of Smuggler that is installed can NOT complete this step `nPlease update Smuggler before continuing\" -ErrorId E4\r + Exit 1\r +}\r +Write-Output \"`n-------------------------`n\"\r +\r +\r +\r +\r +\r +#--------------------------------------------------------------------\r +\r +\r +#Checks if both Source database and destination database exist\r +Write-Output \"Checking if both $sourceDatabaseName and $destinationDatabaseName exist`n\"\r +\r +$sourceDatabase_exists = doesRavenDBExist -databaseChecking $sourceDatabaseName -URL $sourceDatabaseURL \r +\r +$destinationDatabase_exists = doesRavenDBExist -databaseChecking $destinationDatabaseName -URL $destinationDatabaseURL\r +\r +\r +#if both database exist a backup occurs\r +if(($sourceDatabase_exists -eq $TRUE) -and ($destinationDatabase_exists -eq $TRUE))\r +{\r +\r + Write-Output \"Both $sourceDatabaseName and $destinationDatabaseName exist`n\"\r +\r +}#ends if \r +#if the source database doesn’t exist an error is throw\r +elseIf(($sourceDatabase_exists -eq $FALSE) -and ($destinationDatabase_exists -eq $TRUE))\r +{\r +\r + Write-Error \"$sourceDatabaseName does not exist. `nMake sure the database exists before continuing\" -ErrorId E4\r + Exit 1\r +\r +}\r +#if the destination database doesn’t exist an error is throw\r +elseIf(($destinationDatabase_exists -eq $FALSE) -and ($sourceDatabase_exists -eq $TRUE))\r +{\r +\r + Write-Error \"$destinationDatabaseName does not exist. `nMake sure the database exists before continuing\" -ErrorId E4\r + Exit 1\r +\r +}#ends destination db not exists\r +else\r +{\r +\r + Write-Error \"Neither $sourceDatabaseName or $destinationDatabaseName exists. `nMake sure both databases exists\" -ErrorId E4\r + Exit 1\r +\r +}#ends else\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#changing the types to export/import\r +\r +$operateTypes = @()\r +\r +\r +if($operateOnDocuments -eq $TRUE)\r +{\r + $operateTypes += \"Documents\"\r +}\r +if($operateOnIndexes -eq $TRUE)\r +{\r + $operateTypes += \"Indexes\"\r +}\r +if($operateOnAttachments -eq $TRUE)\r +{\r + $operateTypes += \"Attachments\"\r +}\r +if($operateOnTransformers -eq $TRUE)\r +{\r + $operateTypes += \"Transformers\"\r +}\r +\r +$Types = $operateTypes -join \",\"\r +\r +if($Types -ne \"\")\r +{\r + Write-Output \"This back up is only operating on $Types\"\r +\r + Write-Output \"`n-------------------------`n\"\r +}\r +\r +\r +#--------------------------------------------------------------------\r +#check if wait for indexing is selected\r +$Indexing = \"\"\r +\r +if($waitForIndexing -eq $TRUE)\r +{\r + $Indexing = \"--wait-for-indexing\"\r +}\r +\r +#--------------------------------------------------------------------\r +#backing up source database into the destination database\r +\r +try\r +{\r + Write-Output \"Attempting Backup up now\"\r + Write-Output \"`n-------------------------`n\"\r + & $ravenSmugglerPath between $sourceDatabaseURL $destinationDatabaseURL --database=$sourceDatabaseName --database2=$destinationDatabaseName --api-key=$sourceDatabaseApiKey --api-key2=$destinationDatabaseAPIKey --timeout=$Timeout $Indexing \r + Write-Output \"`n-------------------------`n\"\r + Write-Output \"Backup successful\" \r +}#ends try\r +catch\r +{\r + Write-Error \"An error occurred during backup, please try again\" -ErrorId E4\r + Exit 1\r +}#ends catch \r +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ravenSmugglerPath", + "Label": "Raven Smuggler Path", + "HelpText": "Full path to the Smuggler EXE. + +For example **C:\\RavenDB\\Smuggler\\Raven.Smuggler.exe**", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "sourceDatabaseUrl", + "Label": "Source Database URL", + "HelpText": "The URL of the Raven database, where the **Source Database** is located. + +For example **http://localhost:8080/**", + "DefaultValue": "http://localhost:8080/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "sourceDatabaseName", + "Label": "Name of the Source Database", + "HelpText": "Name of the **Source Database** in Raven.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "sourceDatabaseApiKey", + "Label": "API Key for the Source Database", + "HelpText": "API Key needed to access the **Source Database**. + +If key is not provided, anonymous authentication will be used. + +For more information: http://ravendb.net/docs/article-page/3.0/csharp/studio/accessing-studio", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "destinationDatabaseUrl", + "Label": "Destination Database URL", + "HelpText": "The URL of the Raven database, where the **Destination Database** is located. + +For example **http://localhost:8080/**", + "DefaultValue": "http://localhost:8080/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "destinationDatabaseName", + "Label": "Name of the Destination Database", + "HelpText": "Name of the **Destination Database** in Raven.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "destinationDatabaseApiKey", + "Label": "API Key for the Destination Database", + "HelpText": "API Key needed to access the **Destination Database**. + +If key is not provided, anonymous authentication will be used. + +For more information: http://ravendb.net/docs/article-page/3.0/csharp/studio/accessing-studio", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "OperateDocuments", + "Label": "Operate on Documents", + "HelpText": "With Raven backup, you can choose which types are operated during the backup. + +Unselect this option to exclude **Documents** from the copying process.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "OperateAttachments", + "Label": "Operate on Attachments", + "HelpText": "With Raven backup, you can choose which types are operated during the backup. + +Unselect this option to exclude **Attachments** from the copying process.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "OperateIndexes", + "Label": "Operate on Indexes", + "HelpText": "With Raven backup, you can choose which types are operated during the backup. + +Unselect this option to exclude **Indexes** from the copying process.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "OperateTransformers", + "Label": "Operate on Transformers", + "HelpText": "With Raven backup, you can choose which types are operated during the backup. + +Unselect this option to exclude **Transformers** from the copying process.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "timeout", + "Label": "Timeout", + "HelpText": "The timeout (in milliseconds) to use for requests.", + "DefaultValue": "300000", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "WaitIndexing", + "Label": "Wait for Indexing", + "HelpText": "Wait until all indexing activity has been completed (Import Only).", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2015-11-12T22:39:11.556+00:00", + "LastModifiedBy": "timhunt303", + "$Meta": { + "ExportedAt": "2015-11-15T22:01:49.385+00:00", + "OctopusVersion": "3.1.7", + "Type": "ActionTemplate" + }, + "Category": "ravendb" +} diff --git a/step-templates/ravendb-smuggler-move-data-between-filesystems.json.human b/step-templates/ravendb-smuggler-move-data-between-filesystems.json.human new file mode 100644 index 000000000..08105b494 --- /dev/null +++ b/step-templates/ravendb-smuggler-move-data-between-filesystems.json.human @@ -0,0 +1,274 @@ +{ + "Id": "4ec55f5e-ab8b-409d-b404-dc0bf705d057", + "Name": "RavenDB - Smuggler - Moving Data between File Systems", + "Description": "To move data directly between two instances (or different file systems in the same instance) using the between option introduced in Smuggler version 3.", + "ActionType": "Octopus.Script", + "Version": 0, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "\r +# Variables\r +\r +#Location of the Raven Smuggler exe\r +$ravenSmugglerPath = $OctopusParameters[\"ravenSmugglerPath\"]\r +\r +\r +#--------------------------------------------------------------------\r +# Source File System Variables\r +\r +#URL of RavenFS that is being backed up \r +$sourceFileSystemURL = $OctopusParameters[\"sourceFileSystemURL\"]\r +\r +#name of the RavenFS that is being backed up\r +$sourceFileSystemName = $OctopusParameters[\"sourceFileSystemName\"]\r +\r +#API Key for the Source File System\r +$sourceFileSystemApiKey = $OctopusParameters[\"sourceFileSystemApiKey\"]\r +\r +\r +\r +\r +#--------------------------------------------------------------------\r +#Destination File System Variables\r +\r +#URL of destination RavenFS \r +$destinationFileSystemURL = $OctopusParameters[\"destinationFileSystemURL\"]\r +\r +#Name of the destination RavenFS\r +$destinationFileSystemName = $OctopusParameters[\"destinationFileSystemName\"]\r +\r +#API Key for the Destination File System\r +$destinationFileSystemAPIKey = $OctopusParameters[\"destinationFileSystemAPIKey\"]\r +\r +\r +#--------------------------------------------------------------------\r +# Other Variables retrieved from Octopus\r +\r +#Get timeout variable\r +$timeout = $OctopusParameters[\"timeout\"]\r +\r +\r +\r +#--------------------------------------------------------------------\r +\r +#checks to see if the entered file system exists, return a Boolean value depending on the outcome\r +function doesRavenFSExist([string] $FSChecking, [string]$URL)\r +{\r + #retrieves the list of File Systems at the specified URL\r + $fs_list = Invoke-RestMethod -Uri \"$URL/fs\" -Method Get\r + #checks if the File System is at the specified URL\r + if ($fs_list -contains $FSChecking.ToString()) \r + {\r + return $TRUE\r + }\r + else \r + {\r + return $FALSE\r + }\r +\r + \r +}#ends does File System exist function\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +\r +#Check path to smuggler\r +Write-Output \"Checking if Smuggler path is correct`n\"\r +\r +$smuggler_Exists = Test-Path -Path $ravenSmugglerPath\r +\r +\r +\r +#if the path is correct, the script continues, throws an error if the path is wrong\r +If($smuggler_Exists -eq $TRUE)\r +{\r + Write-Output \"Smuggler exists\"\r +\r +}#ends if smuggler exists \r +else\r +{\r + Write-Error \"Smuggler cannot be found `nCheck the directory: $ravenSmugglerPath\" -ErrorId E4\r + Exit 1\r +}#ends else, smuggler can't be found\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#Checking the version of smuggler\r +\r +$SmugglerVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($ravenSmugglerPath).FileVersion\r +\r +if($SmugglerVersion -cgt \"3\")\r +{\r + Write-Host \"Smuggler Version: $SmugglerVersion\"\r +}\r +else\r +{\r + Write-Error \"The version of Smuggler that is installed can NOT complete this step. `nPlease update Smuggler before continuing\" -ErrorId E4\r + Exit 1\r +}\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +\r +#Check if Source File System and destination File System exists\r +Write-Output \"Checking if both $sourceFileSystemName and $destinationFileSystemName exist`n\"\r +\r +$sourceFS_exists = doesRavenFSExist -FSChecking $sourceFileSystemName -URL $sourceFileSystemURL \r +\r +$DestinationFS_Exist = doesRavenFSExist -FSChecking $destinationFileSystemName -URL $destinationFileSystemURL\r +\r +\r +#if both File System exist a backup occurs\r +if(($sourceFS_exists -eq $TRUE) -and ($DestinationFS_Exist -eq $TRUE))\r +{\r +\r + Write-Output \"Both $sourceFileSystemName and $destinationFileSystemName exist`n\"\r +\r +}#ends if \r +#if the source File System doesn’t exist an error is throw\r +elseIf(($sourceFS_exists -eq $FALSE) -and ($DestinationFS_Exist -eq $TRUE))\r +{\r +\r + Write-Error \"$sourceFileSystemName does not exist. `nMake sure the File System exists before continuing\" -ErrorId E4\r + Exit 1\r +\r +}\r +#if the destination File System doesn’t exist an error is throw\r +elseIf(($DestinationFS_Exist -eq $FALSE) -and ($sourceFS_exists -eq $TRUE))\r +{\r +\r + Write-Error \"$destinationFileSystemName does not exist. `nMake sure the File System exists before continuing\" -ErrorId E4\r + Exit 1\r +\r +}#ends destination FS not exists\r +else\r +{\r + \r + Write-Error \"Neither $sourceFileSystemName or $destinationFileSystemName exists. `nMake sure both File Systems exists\" -ErrorId E4\r + Exit 1\r +\r +}#ends else\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#start Backup\r +\r +try\r +{\r + Write-Output \"Attempting Backup up now\"\r + Write-Output \"`n-------------------------`n\"\r + & $ravenSmugglerPath between $sourceFileSystemURL $destinationFileSystemURL --filesystem=$sourceFileSystemName --filesystem2=$destinationFileSystemName --api-key=$sourceFileSystemApiKey --api-key2=$destinationFileSystemAPIKey --timeout=$timeout\r + Write-Output \"`n-------------------------`n\"\r + Write-Output \"Backup successful\"\r +\r +\r +}#ends try\r +catch\r +{\r + Write-Error \"An error occurred during backup, please try again\" -ErrorId E4\r + Exit 1\r +}#ends catch\r +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ravenSmugglerPath", + "Label": "Raven Smuggler Path", + "HelpText": "Full path to the Smuggler EXE. + +For example: **C:\\RavenDB\\Smuggler\\Raven.Smuggler.exe**", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "sourceFileSystemURL", + "Label": "Source File System URL", + "HelpText": "The URL of the Raven File System, where the **Source File System** is located. + +For example: **http://localhost:8080/**", + "DefaultValue": "http://localhost:8080/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "sourceFileSystemName", + "Label": "Name of the Source File System", + "HelpText": "Name of the **Source File System** in Raven.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "sourceFileSystemApiKey", + "Label": "Api Key for the Source File System", + "HelpText": "API Key needed to access the **Source File System**. + +If key is not provided, anonymous authentication will be used. + +For more information: http://ravendb.net/docs/article-page/3.0/csharp/studio/accessing-studio", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "destinationFileSystemURL", + "Label": "Destination File System URL", + "HelpText": "The URL for the Raven File System where the **Destination File System** is located. + +For example: **http://localhost:8080/**", + "DefaultValue": "http://localhost:8080/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "destinationFileSystemName", + "Label": "Name of the Destination File System", + "HelpText": "Name of the **Destination File System** in Raven.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "destinationFileSystemAPIKey", + "Label": "API Key for the Destination File System", + "HelpText": "API Key needed to access the **Destination File System**. + +If key is not provided, anonymous authentication will be used. + +For more information: http://ravendb.net/docs/article-page/3.0/csharp/studio/accessing-studio", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "timeout", + "Label": "Timeout", + "HelpText": "The timeout (in milliseconds) to use for requests.", + "DefaultValue": "300000", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-11-12T22:39:11.556+00:00", + "LastModifiedBy": "timhunt303", + "$Meta": { + "ExportedAt": "2015-11-15T22:06:33.737+00:00", + "OctopusVersion": "3.1.7", + "Type": "ActionTemplate" + }, + "Category": "ravendb" +} diff --git a/step-templates/ravendb-update-properties-within-a-document.json.human b/step-templates/ravendb-update-properties-within-a-document.json.human new file mode 100644 index 000000000..ed3847a9b --- /dev/null +++ b/step-templates/ravendb-update-properties-within-a-document.json.human @@ -0,0 +1,377 @@ +{ + "Id": "89806198-6216-4034-a934-6de6a3f445b0", + "Name": "RavenDB - Update Properties within a Document", + "Description": "Retrieves the specified document by a Raven Query, updates selected variables with values from octopus Variables. Replaces the current document with the newly created document with the updated values. +**IMPORTANT**: Any variable that is being updated **MUST** have an Octopus Variable that has exactly the same name (including capitals, any special characters, etc.) prefixed with “Property_”. This is the case of **BOTH** document variables and Metadata variables. I.E. if you wanted TestMode change, you **MUST** have an Octopus Variable named Property_TestMode (same name, and capitals, etc.). +", + "ActionType": "Octopus.Script", + "Version": 50, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "\r +#Variables\r +\r +#--------------------------------------------------------------------\r +#RavenDB database variables\r +\r +#URL address of RavenDB\r +$ravenDatabaseURL = $OctopusParameters[\"ravenDatabaseURL\"]\r +\r +#Name of the database\r +$ravenDatabaseName = $OctopusParameters[\"ravenDatabaseName\"]\r +\r +#--------------------------------------------------------------------\r +#RavenDB Query variables\r +\r +#Raven Query\r +#$ravenQuery = $OctopusParameters[\"ravenQuery\"]\r +\r +#Name of the settings document\r +$ravenDocumentName = $OctopusParameters[\"ravenDocumentName\"]\r +\r +#--------------------------------------------------------------------\r +#Setting Variables\r +\r +#list of settings variables that are to be changed\r +$includeSettingList = $OctopusParameters[\"includeSettingList\"]\r +\r +#list of settings variables that are NOT to be changed\r +$excludeSettingList = $OctopusParameters[\"excludeSettingList\"]\r +\r +#--------------------------------------------------------------------\r +#Metadata variables\r +\r +#list of metadata variables that are to be changed\r +$includeMetadataList = $OctopusParameters[\"includeMetadataList\"]\r +\r +#list of metadata variables that are NOT to be changed\r +$excludeMetadataList = $OctopusParameters[\"excludeMetadataList\"]\r +\r +\r +#--------------------------------------------------------------------\r +#other variables\r +\r +$octopusVariableList = $OctopusParameters.GetEnumerator()\r +\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +#--------------------------------------------------------------------\r +#checks to see if the entered database exists, return a Boolean value depending on the outcome\r +function doesRavenDBExist([string] $databaseChecking, [string]$URL)\r +{\r + #retrieves the list of databases at the specified URL\r + $database_list = Invoke-RestMethod -Uri \"$ravenDatabaseURL/databases\" -Method Get\r + #checks if the database is at the specified URL\r + if ($database_list -contains $databaseChecking.ToString()) \r + {\r + return $TRUE\r + }\r + else \r + {\r + return $FALSE\r + }\r +\r + \r +\r +}#ends does ravenDB exist function\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +#-------------------------------------------------------------------- \r +#check to see if the database exists\r + \r +\r +Write-Output \"Checking if $ravenDatabaseName exists\"\r +\r +$database_exists = doesRavenDBExist -databaseChecking $ravenDatabaseName -URL $ravenDatabaseURL\r +\r +\r +#only proceeds if database exists\r +if ($database_exists -eq $TRUE)\r +{\r + Write-Output \"$ravenDatabaseName exists\"\r + \r +}#ends database exists if statement \r +else \r +{\r + Write-Error \"$ravenDatabaseName doesn't exists. `nMake sure the database exists before continuing\" -ErrorId E4\r + Exit 1\r +}\r +\r +\r +Write-Output \"`n-------------------------`n\" \r + \r +#--------------------------------------------------------------------\r +#Get current setings and change them accordingly\r +\r +$allSettingsJSON = $null\r +\r +Write-Output \"Getting Document: $ravenDatabaseName\"\r +\r +$settingsURI = \"$ravenDatabaseURL/databases/$ravenDatabaseName/docs/$ravenDocumentName\"\r +\r + \r +\r +try {\r + #Gets settings from the specific Uri\r + $allSettings = Invoke-RestMethod -Uri $settingsURI -Method Get\r +\r +} catch {\r + if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\r + \r + $_.Exception\r + }\r +}\r +\r +#check to make sure the query return some results\r +if($allSettings -eq $null)\r +{\r + Write-Error \"An error occurred while querying the database. `nThe query did not return any values. `nPlease enter a new query\" -ErrorId E4\r + Exit 1\r +}\r +\r +$includeList = @()\r +\r +($includeSettingList.Split(\", \") | ForEach {\r + $includeList += $_.ToString()\r +})\r +\r + \r +Write-Output \"Updating the Settings document\"\r +try\r +{\r + \r +\r + #changes the values of the included settings within the original settings document to values from Octopus Variables\r + for($i = 0; $i -lt $includeList.length; $i++)\r + {\r + \r + \r + #checks if the any of the include setting list is in the exclude setting list\r + if($excludeSettingList -notcontains $includeList[$i])\r + {\r + \r + \r + $octopusVariableList = $OctopusParameters.GetEnumerator()\r + \r + #loops through the variable list to find the corresponding value to the settings variable\r + foreach($varKey in $octopusVariableList)\r + {\r + \r + \r + $newSettingVar = $includeList[$i].ToString()\r + \r + $newSettingVar = \"Property_$newSettingVar\"\r + \r + #sets the setting variable to the correct variable in octopus\r + if($varKey.Key -eq $newSettingVar)\r + {\r + \r + \r +\r + $allSettings.($includeList[$i]) = $varKey.Value \r +\r + }#ends if\r +\r + }#ends for each\r +\r +\r +\r + }#ends check if settings in excluded list\r +\r +\r + }#ends for\r +}#ends try\r +catch\r +{\r + Write-Error \"An error occurred while trying to find the Setting Variables.\" -ErrorId E4\r + Exit 1\r +}\r +\r +\r +Write-Output \"Update complete\"\r +\r +Write-Output \"`n-----------------------------\"\r +\r +#--------------------------------------------------------------------\r +#set update metadata information\r +\r +Write-Output \"Updating the Metadata of the document\"\r +\r +$metadata = @{}\r +\r +$metadataList = @()\r +\r +($includeMetadataList.Split(\", \") | Foreach {\r + $metadataList += $_.ToString()\r +})\r +\r +\r +try\r +{\r + for($i = 0; $i -lt $metadataList.length; $i++)\r + {\r + \r + if($excludeMetadataList -notcontains $metadataList[$i])\r + {\r + \r + $octopusVariableList = $OctopusParameters.GetEnumerator()\r + \r + foreach($varKey in $octopusVariableList)\r + {\r + \r + $newMetadataVar = $metadataList[$i]\r + \r + $newMetadataVar = \"Property_$newMetadataVar\"\r +\r + if($varKey.Key -eq $newMetadataVar)\r + {\r + \r + $temp = $metadataList[$i].ToString()\r + \r + $metadata.Add(\"$temp\", $varKey.Value)\r + \r + \r + }\r + \r + }#ends foreach\r +\r + }#ends if\r +\r + }#Ends for \r +}#ends try\r +catch\r +{\r + Write-Error \"An error occurred while trying to find the Metadata Variables.\" -ErrorId E4\r + Exit 1\r +}\r +\r +\r +Write-Output \"Metadata update complete\"\r +\r +\r +\r +#--------------------------------------------------------------------\r +#converting settings to a JSON document\r +\r +Write-Output \"Converting settings to a JSON document\"\r +\r +#Converts allSettings to JSON so it can be added to RavenDB\r +if ($allSettingsJSON -eq $null) \r +{\r + $allSettingsJSON = ConvertTo-Json -InputObject $allSettings\r +}\r +\r +\r +\r +Write-Output \"`n-------------------------`n\"\r +\r +#--------------------------------------------------------------------\r +#inserting settings document\r +\r +Write-Output \"Restoring Document: $ravenDatabaseName . Inserting the new settings document to the database\"\r +\r +#URL to put the JSON document\r +$putSettingsURI = \"$ravenDatabaseURL/databases/$ravenDatabaseName/docs/$ravenDocumentName\"\r +\r +#Puts the settings and metadata in the specified RavenDB\r +try\r +{\r +\r + Invoke-RestMethod -Uri $putSettingsURI -Headers $metadata -Body $allSettingsJSON -Method Put\r + \r + Write-Output \"New settings have been successfully added to the database\"\r +}\r +catch\r +{\r + Write-Error \"An error occurred while inserting the new settings document to the database\" -ErrorId E4\r +} \r +\r +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ravenDatabaseURL", + "Label": "URL of the Database", + "HelpText": "The URL of the database. + +For example: **http://localhost:8080/**", + "DefaultValue": "http://localhost:8080/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ravenDatabaseName", + "Label": "Name of the Database", + "HelpText": "Name of the database in Raven", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ravenDocumentName", + "Label": "Name of the Document", + "HelpText": "Name of the document in Raven that the program will retrieve.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "includeSettingList", + "Label": "List of Included Document Variables", + "HelpText": "A List of document variables that the program will update based on values within Octopus Variables + +**IMPORTANT:** The names of the variables **MUST** be the same (including capitals, special characters, etc.)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "excludeSettingList", + "Label": "List of Excluded Document Variables", + "HelpText": "A list of document variables that the step will exclude from the updated version. + +For example: if TestMode is in both the include and exclude document list, then TestMode will be excluded from the update.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "includeMetadataList", + "Label": "List of Included Metadata Variables", + "HelpText": "A List of Metadata variables that the program will update based on values within Octopus Variables + +**IMPORTANT:** The names of the variables **MUST** be the same (including capitals, special characters, etc.)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "excludeMetadataList", + "Label": "List of Excluded Metadata Variables", + "HelpText": "A list of Metadata variables that the step will exclude from the updated version. + +For example: if Raven-Entity-Name is in both the include and exclude metadata lists, then Raven-Entity-Name will be excluded from the update.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-11-26T22:39:11.556+00:00", + "LastModifiedBy": "timhunt303", + "$Meta": { + "ExportedAt": "2015-11-26T22:00:53.359+00:00", + "OctopusVersion": "3.1.7", + "Type": "ActionTemplate" + }, + "Category": "ravendb" +} diff --git a/step-templates/raygun-api-register-deployment.json.human b/step-templates/raygun-api-register-deployment.json.human new file mode 100644 index 000000000..8471ca3d3 --- /dev/null +++ b/step-templates/raygun-api-register-deployment.json.human @@ -0,0 +1,124 @@ +{ + "Id": "bb7de751-edba-4c5f-9845-1bd1b26f6b62", + "Name": "Raygun API - Register Deployment", + "Description": "Notifies [Raygun](https://raygun.com) of a deployment using their [Deployments API](https://raygun.com/documentation/product-guides/deployment-tracking/powershell/). +Sends the release number, deployer, release notes from the Octopus deployment.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Function Get-Parameter($Name, [switch]$Required, $Default, [switch]$FailOnValidate) { + $result = $null + $errMessage = [string]::Empty + + If ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + Write-Host \"Octopus paramter value for $Name : $result\" + } + + If ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + If ($result -eq $null) { + If ($Required) { + $errMessage = \"Missing parameter value $Name\" + } Else { + $result = $Default + } + } + + If (-Not [string]::IsNullOrEmpty($errMessage)) { + If ($FailOnValidate) { + Throw $errMessage + } Else { + Write-Warning $errMessage + } + } + + return $result +} + +& { + Write-Host \"Start AddInRaygun\" + + $deploymentId = [string] (Get-Parameter \"Octopus.Release.Number\" $true [string]::Empty $true) + $ownerName = [string] (Get-Parameter \"Octopus.Deployment.CreatedBy.DisplayName\" $true [string]::Empty $true) + $emailAddress = [string] (Get-Parameter \"Octopus.Deployment.CreatedBy.EmailAddress\" $false [string]::Empty $true) + $releaseNotes = [string] (Get-Parameter \"Octopus.Release.Notes\" $false [string]::Empty $true) + $personAccessToken = [string] (Get-Parameter \"Raygun.PersonalAccessToken\" $true [string]::Empty $true) + $apiKey = [string] (Get-Parameter \"Raygun.ApiKey\" $true [string]::Empty $true) + $deployedAt = Get-Date -Format \"o\" + + Write-Host \"Registering deployment with Raygun\" + + # Some older API keys may contain URL reserved characters (eg '/', '=', '+') and will need to be encoded. + # If your API key does not contain any reserved characters you can exclude the following line. + $urlEncodedApiKey = [System.Uri]::EscapeDataString($apiKey); + + $url = \"https://api.raygun.com/v3/applications/api-key/\" + $urlEncodedApiKey + \"/deployments\" + + $headers = @{ + Authorization=\"Bearer \" + $personAccessToken + } + + $payload = @{ + version = $deploymentId + ownerName = $ownerName + emailAddress = $emailAddress + comment = $releaseNotes + deployedAt = $deployedAt + } + + $payloadJson = $payload | ConvertTo-Json + + + try { + Invoke-RestMethod -Uri $url -Body $payloadJson -Method Post -Headers $headers -ContentType \"application/json\" -AllowInsecureRedirect + Write-Host \"Deployment registered with Raygun\" + } catch { + Write-Host \"Tried to send a deployment to \" $url \" with payload \" $payloadJson + Write-Error \"Error received when registering deployment with Raygun: $_\" + } + + Write-Host \"End AddInRaygun\" +}" + }, + "Parameters": [ + { + "Id": "0dd429d3-28f6-46b8-8fb7-e2ceb9124c15", + "Name": "Raygun.ApiKey", + "Label": "Api Key", + "HelpText": "Raygun Application's ApiKey (the same one you use to set Raygun up within your app)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "22d41dfb-f8f3-479d-8e72-08f456529f04", + "Name": "Raygun.PersonalAccessToken", + "Label": "Personal Access Token", + "HelpText": "Personal Access Token to use from your [Raygun User Settings page](https://app.raygun.io/user).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2024-04-09T08:20:04.075Z", + "OctopusVersion": "2024.2.4248", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "benjimac93", + "Category": "raygun" +} diff --git a/step-templates/re-prioritize-octopus-deploy-tasks.json.human b/step-templates/re-prioritize-octopus-deploy-tasks.json.human new file mode 100644 index 000000000..1a7252963 --- /dev/null +++ b/step-templates/re-prioritize-octopus-deploy-tasks.json.human @@ -0,0 +1,803 @@ +{ + "Id": "c9d5c96f-f731-4e6c-b9b3-d93f84a9bb74", + "Name": "Re-prioritize Octopus Deploy Tasks", + "Description": "This step will allow you to re-prioritize tasks in the Octopus Deploy queue. + +It will check the task queue for pending tasks and if it finds a task that should be prioritized based on the matching criteria it will cancel any tasks before it to move it to the top of the queue. + +The matching logic supports: +- Task Id - you provide a list of task ids and it will move those to the top of the queue +- Space, Environment, Project, Tenant - you provide a list of spaces, environments, projects, or tenants and the step will find matching deployments or runbook runs and move them to the top of the queue. + +How it works: +1) The step template pulls the list of unscheduled queued tasks (if you schedule a task to run at 7 PM it will exclude that from the check). +2) Attempts to find matching tasks based on task id or space/environment/project/tenant criteria. +3) If it finds one or more matching tasks it checks the position of those tasks in the queue. Any tasks ahead of them are cancelled. +4) if the step template cancels any deployments or runbook runs it will resubmit them using the same criteria as before (this moves them to the bottom of the queue). + +**Important** It will not cancel running tasks.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +$octopusApiKey = $OctopusParameters[\"TaskPriority.Api.Key\"] +$spaceList = $OctopusParameters[\"TaskPriority.Space.List\"] +$environmentList = $OctopusParameters[\"TaskPriority.Environment.List\"] +$projectList = $OctopusParameters[\"TaskPriority.Project.List\"] +$tenantList = $OctopusParameters[\"TaskPriority.Tenant.List\"] +$matchType = $OctopusParameters[\"TaskPriority.Match.Type\"] +$taskType = $OctopusParameters[\"TaskPriority.Task.Type\"] +$octopusUrl = $OctopusParameters[\"TaskPriority.Octopus.Url\"] +$taskIdList = $OctopusParameters[\"TaskPriority.TaskId.List\"] + +$cachedResults = @{} + +function Write-OctopusVerbose +{ + param($message) + + Write-Verbose $message +} + +function Write-OctopusInformation +{ + param($message) + + Write-Host $message +} + +function Write-OctopusSuccess +{ + param($message) + + Write-Highlight $message +} + +function Write-OctopusWarning +{ + param($message) + + Write-Warning \"$message\" +} + +function Write-OctopusCritical +{ + param ($message) + + Write-Error \"$message\" +} + +function Invoke-OctopusApi +{ + param + ( + $octopusUrl, + $endPoint, + $spaceId, + $apiKey, + $method, + $item, + $ignoreCache + ) + + $octopusUrlToUse = $OctopusUrl + if ($OctopusUrl.EndsWith(\"/\")) + { + $octopusUrlToUse = $OctopusUrl.Substring(0, $OctopusUrl.Length - 1) + } + + if ([string]::IsNullOrWhiteSpace($SpaceId)) + { + $url = \"$octopusUrlToUse/api/$EndPoint\" + } + else + { + $url = \"$octopusUrlToUse/api/$spaceId/$EndPoint\" + } + + try + { + if ($null -ne $item) + { + $body = $item | ConvertTo-Json -Depth 10 + Write-OctopusVerbose $body + + Write-OctopusInformation \"Invoking $method $url\" + return Invoke-RestMethod -Method $method -Uri $url -Headers @{\"X-Octopus-ApiKey\" = \"$ApiKey\" } -Body $body -ContentType 'application/json; charset=utf-8' + } + + if (($null -eq $ignoreCache -or $ignoreCache -eq $false) -and $method.ToUpper().Trim() -eq \"GET\") + { + Write-OctopusVerbose \"Checking to see if $url is already in the cache\" + if ($cachedResults.ContainsKey($url) -eq $true) + { + Write-OctopusVerbose \"$url is already in the cache, returning the result\" + return $cachedResults[$url] + } + } + else + { + Write-OctopusVerbose \"Ignoring cache.\" + } + + Write-OctopusVerbose \"No data to post or put, calling bog standard invoke-restmethod for $url\" + $result = Invoke-RestMethod -Method $method -Uri $url -Headers @{\"X-Octopus-ApiKey\" = \"$ApiKey\" } -ContentType 'application/json; charset=utf-8' + + if ($cachedResults.ContainsKey($url) -eq $true) + { + $cachedResults.Remove($url) + } + Write-OctopusVerbose \"Adding $url to the cache\" + $cachedResults.add($url, $result) + + return $result + + + } + catch + { + if ($null -ne $_.Exception.Response) + { + if ($_.Exception.Response.StatusCode -eq 401) + { + Write-OctopusCritical \"Unauthorized error returned from $url, please verify API key and try again\" + } + elseif ($_.Exception.Response.statusCode -eq 403) + { + Write-OctopusCritical \"Forbidden error returned from $url, please verify API key and try again\" + } + else + { + Write-OctopusVerbose -Message \"Error calling $url $($_.Exception.Message) StatusCode: $($_.Exception.Response.StatusCode )\" + } + } + else + { + Write-OctopusVerbose $_.Exception + } + } + + Throw \"There was an error calling the Octopus API please check the log for more details\" +} + +function Get-FilteredOctopusItem +{ + param( + $itemList, + $itemName + ) + + if ($itemList.Items.Count -eq 0) + { + Write-OctopusCritical \"Unable to find $itemName. Exiting with an exit code of 1.\" + return $null + } + + $item = $itemList.Items | Where-Object { $_.Name -eq $itemName} + + if ($null -eq $item) + { + Write-OctopusCritical \"Unable to find $itemName. Exiting with an exit code of 1.\" + return $null + } + + return $item +} + +function Get-OctopusItemByName +{ + param( + $itemName, + $itemType, + $endpoint, + $spaceId, + $octopusUrl, + $octopusApiKey + ) + + if ([string]::IsNullOrWhiteSpace($itemName)) + { + return $null + } + + Write-OctopusInformation \"Attempting to find $itemType with the name of $itemName\" + + $itemList = Invoke-OctopusApi -octopusUrl $octopusUrl -endPoint \"$($endPoint)?partialName=$([uri]::EscapeDataString($itemName))&skip=0&take=100\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + $item = Get-FilteredOctopusItem -itemList $itemList -itemName $itemName + + if ($null -eq $item) + { + Write-OctopusInformation \"Unable to find $itemType $itemName\" + return $null + } + + Write-OctopusInformation \"Successfully found $itemType $itemName with an id of $($item.Id)\" + + return $item +} + +function Get-SplitItemIntoArray +{ + param ( + $itemToSplit + ) + + if ($itemToSplit -like \"*`n*\") + { + return @(($itemToSplit -Split \"`n\").Trim()) + } + + if ($itemToSplit -like \"*,*\") + { + return @(($itemToSplit -Split \",\").Trim()) + } + + return @($itemToSplit) +} + +function Get-OctopusSpaceList +{ +\tparam( + \t$spaceList, + $octopusUrl, + $octopusApiKey + ) + + if ([string]::IsNullOrWhiteSpace($spaceList)) + { + $rawOctopusSpaceList = Invoke-OctopusApi -octopusUrl $octopusUrl -endPoint \"spaces?skip=0&take=10000\" -spaceId $null -apiKey $octopusApiKey -method \"GET\" + + return $rawOctopusSpaceList.Items + } + + $spaceListSplit = @(Get-SplitItemIntoArray -itemToSplit $spaceList) + $returnList = @() + + foreach ($spaceName in $spaceListSplit) + { + if ([string]::IsNullOrWhiteSpace($spaceName) -eq $false) + { + $octopusSpace = Get-OctopusItemByName -itemName $spaceName -itemType \"Space\" -endpoint \"spaces\" -spaceId $null -octopusUrl $octopusUrl -octopusApiKey $octopusApiKey + + if ($null -ne $octopusSpace) + { + $returnList += $octopusSpace + } + } + } + + return $returnList +} + +function Get-OctopusItemList +{ + param( + $octopusSpaceList, + $itemList, + $itemType, + $endpoint, + $octopusApiKey, + $octopusUrl + ) + + if ([string]::IsNullOrWhiteSpace($itemList)) + { + Write-Host \"The list for $itemType was empty\" + return @() + } + + $itemListSplit = @(Get-SplitItemIntoArray -itemToSplit $itemList) + $returnList = @() + + foreach ($itemName in $itemListSplit) + { + $splitItem = $itemName -split \"::\" + if ($splitItem.Count -gt 1 -and [string]::IsNullOrWhiteSpace($splitItem[1]) -eq $false) + { + Write-OctopusInformation \"The item $itemName included a space name, only pulling back the information for that space\" + $spaceId = $octopusSpaceList | Where-Object { $_.Name.ToLower().Trim() -eq $splitItem[1].ToLower().Trim() } + + if ($null -eq $spaceId) + { + Write-OctopusInformation \"The space name $($splitItem[1]) was not included in the space filter. Skipping this option.\" + continue + } + + $octopusItem = Get-OctopusItemByName -itemName $splitItem[0] -itemType $itemType -endpoint $endpoint -spaceId $spaceId -octopusUrl $octopusUrl -octopusApiKey $octopusApiKey + + if ($null -ne $octopusItem) + { + $returnList += $octopusItem + } + + continue + } + + foreach ($space in $octopusSpaceList) + { + $octopusItem = Get-OctopusItemByName -itemName $itemName -itemType $itemType -endpoint $endpoint -spaceId $space.Id -octopusUrl $octopusUrl -octopusApiKey $octopusApiKey + + if ($null -ne $octopusItem) + { + $returnList += $octopusItem + } + } + } + + return $returnList +} + +function Get-QueuedOctopusTasks +{ + param ( + $octopusApiKey, + $octopusUrl + ) + + $queuedTasks = Invoke-OctopusApi -octopusUrl $octopusUrl -endPoint \"Tasks?states=Queued&skip=0&take=1000\" -spaceId $null -apiKey $octopusApiKey -method \"GET\" -ignoreCache $true + + $returnList = @() + $currentTime = $(Get-Date).ToUniversalTime() + + Write-OctopusInformation \"Looping through the found items in reverse order because the Queue is FIFO but the return object is ordered by date DESC\" + + for($i = $queuedTasks.Items.Count - 1; $i -ge 0; $i--) + { + $task = $queuedTasks.Items[$i] + + if ($null -ne $task.QueueTime) + { + $compareTime = [DateTime]::Parse($task.QueueTime) + $compareTime = $compareTime.ToUniversalTime() + + Write-OctopusVerbose \"Checking to see if $compareTime is ahead of the $currentTime\" + if ($compareTime -gt $currentTime) + { + Write-OctopusInformation \"The queued task $($task.Id) has a queue time $($task.QueueTime) in the future. That means this is a scheduled deployment. Skipping this task.\" + continue + } + } + + if ($null -ne $task.StartTime) + { + Write-OctopusInformation \"The queued task $($task.Id) has a start time, meaning it was picked up, work was done, then it was added back to the queue. Skipping.\" + continue + } + + if ($true -eq $task.HasPendingInterruptions) + { + Write-OctopusInformation \"The task $($task.Id) has pending interruptions, this means the deployment has started and is awaiting someone to respond. Skipping this task.\" + continue + } + + $returnList += $task + } + + return $returnList +} + +function Test-OctopusListHasId +{ + param ( + $octopusList, + $octopusId + ) + + $findItem = $octopusList | Where-Object { $_.Id -eq $octopusId } + + if ($null -eq $findItem) + { + return $false + } + + return $true +} + +function Get-RunbookRunDetailsFromTask +{ + param ( + $runbookTask, + $octopusUrl, + $octopusApiKey + ) + + return Invoke-OctopusApi -endPoint \"runbookRuns/$($runbookTask.Arguments.RunbookRunId)\" -octopusUrl $octopusUrl -spaceId $runbookTask.SpaceId -apiKey $octopusApiKey -method \"GET\" +} + +function Get-DeploymentDetailsFromTask +{ + param ( + $deploymentTask, + $octopusUrl, + $octopusApiKey + ) + + return Invoke-OctopusApi -endPoint \"deployments/$($deploymentTask.Arguments.DeploymentId)\" -octopusUrl $octopusUrl -spaceId $deploymentTask.SpaceId -apiKey $octopusApiKey -method \"GET\" +} + +Write-OctopusInformation \"Space List: $spaceList\" +Write-OctopusInformation \"Environment List: $environmentList\" +Write-OctopusInformation \"Project List: $projectList\" +Write-OctopusInformation \"Tenant List: $tenantList\" +Write-OctopusInformation \"Octopus URL: $octopusUrl\" +Write-OctopusInformation \"Match Type: $matchType\" +Write-OctopusInformation \"Task Id List: $taskIdList\" + +$queuedTasks = @(Get-QueuedOctopusTasks -octopusApiKey $octopusApiKey -octopusUrl $octopusUrl) + +if ($queuedTasks.Length -eq 0) +{ + Write-OctopusSuccess \"No queued tasks found that can block a deployment. Exiting.\" + exit 0 +} + +$octopusInformation = @{ + TaskIdList = @(Get-SplitItemIntoArray -itemToSplit $taskIdList) +} + +if ([string]::IsNullOrWhiteSpace($taskIdList)) +{ + $octopusInformation.SpaceList = @(Get-OctopusSpaceList -spaceList $spaceList -octopusUrl $octopusUrl -octopusApiKey $octopusApiKey) + + $octopusInformation.EnvironmentList = @(Get-OctopusItemList -octopusSpaceList $octopusInformation.SpaceList -itemList $environmentList -itemType \"Environment\" -endpoint \"environments\" -octopusApiKey $octopusApiKey -octopusUrl $octopusUrl) + $octopusInformation.HasEnvironmentFilter = $octopusInformation.EnvironmentList.Count -gt 0 + + $octopusInformation.ProjectList = @(Get-OctopusItemList -octopusSpaceList $octopusInformation.SpaceList -itemList $projectList -itemType \"Project\" -endpoint \"projects\" -octopusApiKey $octopusApiKey -octopusUrl $octopusUrl) + $octopusInformation.HasProjectFilter = $octopusInformation.ProjectList.Count -gt 0 + + $octopusInformation.TenantList = @(Get-OctopusItemList -octopusSpaceList $octopusInformation.SpaceList -itemList $tenantList -itemType \"Tenant\" -endpoint \"tenants\" -octopusApiKey $octopusApiKey -octopusUrl $octopusUrl) + $octopusInformation.HasTenantFilter = $octopusInformation.TenantList.Count -gt 0 + + if ($octopusInformation.EnvironmentList.Count -eq 0 -and $octopusInformation.ProjectList.Count -eq 0 -and $octopusInformation.TenantList.Count -eq 0) + { + Write-OctopusCritical \"No environments OR projects OR tenants provided to filter on. You must provide at least one environment OR project OR tenant.\" + exit 1 + } + + Write-OctopusSuccess \"Going to look for any $taskType in the spaces ($(($octopusInformation.SpaceList | Select-Object -ExpandProperty Id) -join \", \")) matching \" + Write-OctopusSuccess \"Environments ($(($octopusInformation.EnvironmentList | Select-Object -ExpandProperty Id) -join \" OR \")) $matchType\" + Write-OctopusSuccess \"Projects ($(($octopusInformation.ProjectList | Select-Object -ExpandProperty Id) -join \" OR \")) $matchType\" + Write-OctopusSuccess \"Tenants ($(($octopusInformation.TenantList | Select-Object -ExpandProperty Id) -join \" OR \"))\" +} +else +{ + Write-OctopusSuccess \"Going to look for the tasks ($($octopusInformation.TaskIdList -join \", \"))\" +} + +$matchingTasks = @() + +Write-OctopusInformation \"Attempting to find any matching tasks based on the filtering criteria.\" +foreach ($task in $queuedTasks) +{ + if ($octopusInformation.TaskIdList -contains $task.Id) + { + Write-OctopusInformation \"The task $($task.Id) was found in the list of task ids. Adding to list.\" + $matchingTasks += $task + + continue + } + + if ($task.Name -ne \"Deploy\" -and $task.Name -ne \"RunbookRun\") + { + Write-Information \"The task not a deployment or a runbook run. It is $($task.Description). Moving onto next task.\" + continue + } + + if ($taskType -ne \"Both\" -and $taskType -ne $task.Name) + { + Write-Information \"You have selected to filter on $taskType only and this task is a $($task.Name). Moving onto the next task.\" + continue + } + + if ((Test-OctopusListHasId -octopusList $octopusInformation.SpaceList -octopusId $task.SpaceId) -eq $false) + { + Write-Information \"The task is not for any spaces specified. Moving onto the next task.\" + continue + } + + if ($task.Name -eq \"RunbookRun\") + { + $itemDetails = Get-RunbookRunDetailsFromTask -runbookTask $task -octopusUrl $octopusUrl -octopusApiKey $octopusApiKey + } + else + { + $itemDetails = Get-DeploymentDetailsFromTask -deploymentTask $task -octopusUrl $octopusUrl -octopusApiKey $octopusApiKey + } + + $matchesEnvironmentFilter = $octopusInformation.HasEnvironmentFilter -eq $true -and (Test-OctopusListHasId -octopusList $octopusInformation.EnvironmentList -octopusId $itemDetails.EnvironmentId) + Write-OctopusInformation \"$($task.Name) $($itemDetails.Id) Matches Environment Filter $matchesEnvironmentFilter\" + + $matchesProjectFilter = $octopusInformation.HasProjectFilter -eq $true -and (Test-OctopusListHasId -octopusList $octopusInformation.ProjectList -octopusId $itemDetails.ProjectId) + Write-OctopusInformation \"$($task.Name) $($itemDetails.Id) Matches Project Filter $matchesProjectFilter\" + + $matchesTenantFilter = $octopusInformation.HasTenantFilter -eq $true -and $null -ne $itemDetails.TenantId -and (Test-OctopusListHasId -octopusList $octopusInformation.TenantList -octopusId $itemDetails.TenantId) + Write-OctopusInformation \"$($task.Name) $($itemDetails.Id) Matches Tenant Filter $matchesTenantFilter\" + + if ($matchType -eq \"Or\" -and ($matchesTenantFilter -eq $true -or $matchesProjectFilter -eq $true -or $matchesEnvironmentFilter -eq $true)) + { + Write-OctopusInformation \"The match type was OR and one of the filters matched, adding this task to the matching list\" + $matchingTasks += $task + + continue + } + + Write-OctopusInformation \"The match type is AND, checking to see if the task matches all the filters\" + + if ($octopusInformation.HasEnvironmentFilter -eq $true -and $matchesEnvironmentFilter -eq $false) + { + Write-OctopusInformation \"The environment filter was provided and the environment $($itemDetails.EnvironmentId) didn't match any environments. Moving onto next task.\" + continue + } + + if ($octopusInformation.HasProjectFilter -eq $true -and $matchesProjectFilter -eq $false) + { + Write-OctopusInformation \"The project filter was provided and the project $($itemDetails.ProjectId) didn't match any projects. Moving onto next task.\" + continue + } + + if ($octopusInformation.HasTenantFilter -eq $true -and $matchesTenantFilter -eq $false) + { + Write-OctopusInformation \"The tenant filter was provided and the tenant $($itemDetails.TenantId) didn't match any tenants. Moving onto next task.\" + continue + } + + $matchingTasks += $task +} + +if ($matchingTasks.Count -eq 0) +{ + Write-OctopusSuccess \"No matching tasks found, exiting.\" + exit 0 +} + +Write-OctopusSuccess \"Matching tasks found, checking where they are in the queue.\" + +$matchingTaskCounter = 0 + +Write-OctopusInformation \"Looping through all the queued tasks again to find which tasks to cancel.\" +foreach ($task in $queuedTasks) +{ + if ((Test-OctopusListHasId -octopusList $matchingTasks -octopusId $task.Id)) + { + $matchingTaskCounter += 1 + Write-OctopusInformation \"The task $($task.Id) is one we want to move to the top of queue, leaving as is.\" + + if ($matchingTaskCounter -eq $matchingTasks.Count) + { + Write-OctopusSuccess \"All the matching tasks we want to move to the top of the queue have been found, exiting\" + break + } + + continue + } + + $updatedTask = Invoke-OctopusApi -endPoint \"tasks/$($task.Id)\" -octopusUrl $octopusUrl -spaceId $null -apiKey $octopusApiKey -method \"GET\" -ignoreCache $true + + if ($updatedTask.HasBeenPickedUpByProcssor -eq $true) + { + Write-OctopusInformation \"The task $($task.Id) has already been picked up and started processing, moving on.\" + continue + } + + $canceledTaskResult = Invoke-OctopusApi -endPoint \"tasks/$($task.Id)/cancel\" -octopusUrl $octopusUrl -spaceId $null -apiKey $octopusApiKey -method \"POST\" -ignoreCache $true + + Write-OctopusSuccess \"Task $($canceledTaskResult.Description) has been successfully cancelled\" + + if ($task.Name -eq \"Deploy\") + { + Write-OctopusInformation \"Task $($task.Id) is a deployment, setting up a redeploy.\" + + $deploymentInfo = Get-DeploymentDetailsFromTask -deploymentTask $task -octopusUrl $octopusUrl -octopusApiKey $octopusApiKey + + $bodyRaw = @{ + EnvironmentId = $deploymentInfo.EnvironmentId + ExcludedMachineIds = $deploymentInfo.ExcludedMachineIds + ForcePackageDownload = $deploymentInfo.ForcePackageDownload + ForcePackageRedeployment = $deploymentInfo.ForcePackageRedeployment + FormValues = $deploymentInfo.FormValues + QueueTime = $null + QueueTimeExpiry = $null + ReleaseId = $deploymentInfo.ReleaseId + SkipActions = $deploymentInfo.SkipActions + SpecificMachineIds = $deploymentInfo.SpecificMachineIds + TenantId = $deploymentInfo.TenantId + UseGuidedFailure = $deploymentInfo.UseGuidedFailure + } + + $newDeployment = Invoke-OctopusApi -endPoint \"deployments\" -spaceId $task.SpaceId -octopusUrl $octopusUrl -apiKey $octopusApiKey -method \"POST\" -item $bodyRaw + + Write-OctopusSuccess \"$($task.Description) has been successfully resubmitted with the new id $($newDeployment.TaskId)\" + } + + if ($task.Name -eq \"RunbookRun\") + { + Write-OctopusInformation \"Task $($task.Id) is a runbook run, configuring a re-run.\" + + $runbookInfo = Get-RunbookRunDetailsFromTask -runbookTask $task -octopusUrl $octopusUrl -octopusApiKey $octopusApiKey + + $bodyRaw = @{ + EnvironmentId = $runbookInfo.EnvironmentId + ExcludedMachineIds = $runbookInfo.ExcludedMachineIds + ForcePackageDownload = $runbookInfo.ForcePackageDownload + ForcePackageRedeployment = $runbookInfo.ForcePackageRedeployment + FormValues = $runbookInfo.FormValues + QueueTime = $null + QueueTimeExpiry = $null + RunbookId = $runbookInfo.RunbookId + SkipActions = $runbookInfo.SkipActions + SpecificMachineIds = $runbookInfo.SpecificMachineIds + TenantId = $runbookInfo.TenantId + UseGuidedFailure = $runbookInfo.UseGuidedFailure + FrozenRunbookProcessId = $runbookInfo.FrozenRunbookProcessId + RunbookSnapshotId = $runbookInfo.RunbookSnapshotId + } + + $newDeployment = Invoke-OctopusApi -endPoint \"runbookRuns\" -spaceId $task.SpaceId -octopusUrl $octopusUrl -apiKey $octopusApiKey -method \"POST\" -item $bodyRaw + + Write-OctopusSuccess \"$($task.Description) has been successfully resubmitted with the new id $($newDeployment.TaskId)\" + } +} + +Write-OctopusSuccess \"Finished re-prioritizing tasks\"" + }, + "Parameters": [ + { + "Id": "f7357d18-33c3-4f1e-883d-613e13e098cd", + "Name": "TaskPriority.Api.Key", + "Label": "Octopus API Key", + "HelpText": "*Required* + + +The API key of the user authorized to cancel tasks and resubmit them.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "82ef3721-aef1-4624-a306-9f20ad2ef64d", + "Name": "TaskPriority.Octopus.Url", + "Label": "Octopus URL", + "HelpText": "*Required* + +The URL of the Octopus Deploy instance you wish to query. + +Please only pass in the base URL, eg `https://samples.octopus.app`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b2e14858-7309-46ab-b624-ec811ad95618", + "Name": "TaskPriority.TaskId.List", + "Label": "Task Id List", + "HelpText": "A comma-separated or new-line separated list of task ids to move to the top of the queue. + +**IMPORTANT** when this is provided all other matching logic is ignored, including the task type and the space list. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "2434bd05-791b-45e9-9ffa-7fbde9524e5f", + "Name": "TaskPriority.Space.List", + "Label": "Space List", + "HelpText": "List of spaces to monitor. Separate spaces by using a new line. + +When left empty this step will query all spaces.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "87a7ec4a-128b-4b74-8f6b-ea66c9261dec", + "Name": "TaskPriority.Environment.List", + "Label": "Environment List", + "HelpText": "List of environment names to monitor for. Separate environment names by a new line. + +Options: +- `EnvironmentName` - looks for matching environment names in all spaces in the space list. +- `EnvironmentName::SpaceName` - looks for the environment name in the specified space. Use this when the same environment appears in multiple spaces and you want to filter to only apply to an item in a specific space. + +The default `Production`. + +When not specifying a value for the Task Id parameter; a entry must appear in the Environment List, Project List, OR Tenant List. Otherwise, this step will not do anything.", + "DefaultValue": "Production", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "b94bd70b-6554-4ea5-86f1-fbfb1827dfc5", + "Name": "TaskPriority.Project.List", + "Label": "Project List", + "HelpText": "List of projects to monitor. Separate project names by a new line. + +Options: +- `ProjectName` - looks for the specified project name in all spaces in the space list. +- `ProjectName::SpaceName` - looks for that project in a specific space. Use this when the same project appears in multiple spaces and you want to filter to only apply to an item in a specific space. + +When left empty it will not look for any projects. + +When not specifying a value for the Task Id parameter; a entry must appear in the Environment List, Project List, OR Tenant List. Otherwise, this step will not do anything.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "0958b7b3-94b6-4a3e-b78c-f2436950728a", + "Name": "TaskPriority.Tenant.List", + "Label": "Tenant List", + "HelpText": "List of tenant names to monitor. Separate tenant names by a new line. + +Options: +- `TenantName` - looks for the tenant name in any spaces in the space list +- `TenantName::SpaceName` - looks for the tenant name in the specified space. Use this when the same tenant appears in multiple spaces and you want to filter to only apply to an item in a specific space. + +When left empty it will not filter anything by the tenant. + +When not specifying a value for the Task Id parameter; a entry must appear in the Environment List, Project List, OR Tenant List. Otherwise, this step will not do anything. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "a6382a5c-df7f-4dee-bdc9-df89459e1d14", + "Name": "TaskPriority.Task.Match", + "Label": "Task Match", + "HelpText": "Indicates how the matching will occur. + +Options: +- `And` - means the deployment has to be for the Environment AND the project AND the tenant (when they are specified) +- `Or` - means the deployment can be for Environment OR the project OR the tenant + +Default is `Or` +", + "DefaultValue": "Or", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Or|Or +And|And" + } + }, + { + "Id": "1e8d3fc8-36b5-4c84-bfe5-4bfc5b4f5869", + "Name": "TaskPriority.Task.Type", + "Label": "Task Type", + "HelpText": "The type of task to move to the top of the queue. + +Options: +- `Deploy` - monitors deployments only +- `RunbookRun` - monitors runbook runs only +- `Both` - monitors both deployments and runbooks + +Defaults to `Both`", + "DefaultValue": "Both", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Both|Both +Deploy|Deploy +RunbookRun|RunbookRun" + } + } + ], + "$Meta": { + "ExportedAt": "2021-02-22T15:40:24.604Z", + "OctopusVersion": "2021.1.0-ci1022", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "octobob", + "Category": "octopus" + } diff --git a/step-templates/readyroll-deploy-database-package.json.human b/step-templates/readyroll-deploy-database-package.json.human new file mode 100644 index 000000000..8c0ccb11e --- /dev/null +++ b/step-templates/readyroll-deploy-database-package.json.human @@ -0,0 +1,97 @@ +{ + "Id": "14e87c33-b34a-429f-be2c-e44d3d631649", + "Name": "ReadyRoll - Deploy Database Package", + "Description": "Deploy database changes packaged with Redgate's [ReadyRoll](http://www.ready-roll.com/). Requires the Microsoft SQL Command Line Utilities 11 or later to be installed on the tentacle. + +*Version date: 14th January, 2016*", + "ActionType": "Octopus.TentaclePackage", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "536b0ad2-6439-4e6a-aff0-64ba07a33733", + "Name": "", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "SelectionMode": "deferred", + "PackageParameterName": "PackageName" + } + } + ], + "Properties": { + "Octopus.Action.Package.DownloadOnTentacle": "False" + }, + "Parameters": [ + { + "Id": "43af2bc5-668d-482f-a23b-1e46189fcd69", + "Name": "PackageName", + "Label": "Package to deploy", + "HelpText": "The package you want to deploy. If using NuGet, this matches the package ID from the NuSpec file in your ReadyRoll project.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "c7d2a8f5-0b33-4b1d-94cd-0f0f11ecf9d1", + "Name": "DatabaseServer", + "Label": "Target SQL Server instance", + "HelpText": "The fully qualified SQL Server instance name for the target database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a0c8e52f-e7f2-4859-9769-f749f6705a08", + "Name": "DatabaseName", + "Label": "Target database name", + "HelpText": "The name of the database to deploy to. ReadyRoll will create a new database if it does not exist.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8c448c0e-27c8-4572-8a96-0e9dad5c8091", + "Name": "UseWindowsAuth", + "Label": "Use Windows Authentication", + "HelpText": "If you check this field, Windows authentication will be used to connect, using the account that runs the Tentacle service. Otherwise, SQL Server authentication will be used and you will need to specify a username and password below.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "53358be8-b6fb-4dce-b107-a501c5ef5b1e", + "Name": "DatabaseUsername", + "Label": "Username", + "HelpText": "The SQL Server username used to connect to the database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "60688c6e-db91-4a6f-971f-e52901d7b732", + "Name": "DatabasePassword", + "Label": "Password", + "HelpText": "The SQL Server password used to connect to the database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedBy": "harrisonmeister", + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "StepPackageId": "Octopus.TentaclePackage", + "$Meta": { + "ExportedAt": "2023-11-15T14:12:59.832Z", + "OctopusVersion": "2024.1.1169", + "Type": "ActionTemplate" + }, + "Category": "readyroll" +} diff --git a/step-templates/redgate-create-database-release-worker-friendly.json.human b/step-templates/redgate-create-database-release-worker-friendly.json.human new file mode 100644 index 000000000..1c62a7a61 --- /dev/null +++ b/step-templates/redgate-create-database-release-worker-friendly.json.human @@ -0,0 +1,561 @@ +{ + "Id": "47d29b57-5bca-4205-ac62-ce10cdf8bab9", + "Name": "Redgate - Create Database Release (Worker Friendly)", + "Description": "Creates the resources (including the SQL update script) to deploy database changes using Redgate's [SQL Change Automation](https://www.red-gate.com/products/sql-development/sql-change-automation/), and exports them as Octopus artifacts so you can review the changes before deploying. + +Requires SQL Change Automation version 3.0.2 or later. + +*Version date: 2019-07-26* + +This step template is worker friendly, you can pass in a package reference rather than having to reference a previous step which downloaded the package. This step requires Octopus Deploy **2019.10.0** or higher.", + "ActionType": "Octopus.Script", + "Author": "octobob", + "Version": 3, + "Packages": [ + { + "Id": "86e63d39-4d9f-4d9d-a0f6-4d830d37811e", + "Name": "DLMAutomation.Package.Name", + "PackageId": null, + "FeedId": "#{DLMAutomationFeedId}", + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "DLMAutomationPackageName" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptBody": "$DlmAutomationModuleName = \"DLMAutomation\" +$SqlChangeAutomationModuleName = \"SqlChangeAutomation\" +$ModulesFolder = \"$Home\\Documents\\WindowsPowerShell\\Modules\" + +if ([string]::IsNullOrWhiteSpace($DLMModuleInstallLocation) -eq $false) +{ +\tif ((Test-Path $DLMModuleInstallLocation -IsValid) -eq $false) + { + \tWrite-Error \"The path $DLMModuleInstallLocation is not valid, please use a relative or absolute path.\" + exit 1 + } + + $ModulesFolder = [System.IO.Path]::GetFullPath($DLMModuleInstallLocation) +} + +Write-Host \"Modules will be installed into $ModulesFolder\" + +$LocalModules = (New-Item \"$ModulesFolder\" -ItemType Directory -Force).FullName +Write-Host \"LocalModules: $LocalModules\" +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\" +Write-Host $env:PSModulePath + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +function IsScaAvailable +{ + if ((Get-Module $SqlChangeAutomationModuleName) -ne $null) { + return $true + } + + return $false +} + +function InstallCorrectSqlChangeAutomation +{ + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Version]$requiredVersion + ) + + $moduleName = $SqlChangeAutomationModuleName + + # this will be null if $requiredVersion is not specified - which is exactly what we want + $maximumVersion = $requiredVersion + + if ($requiredVersion) { + if ($requiredVersion.Revision -eq -1) { + #If provided with a 3 part version number (the 4th part, revision, == -1), we should allow any value for the revision + $maximumVersion = [Version]\"$requiredVersion.$([System.Int32]::MaxValue)\" + } + + if ($requiredVersion.Major -lt 3) { + # If the specified version is below V3 then the user is requesting a version of DLMA. We should look for that module name instead + $moduleName = $DlmAutomationModuleName + } + } + + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion + + if (!$installedModule) { + #Either SCA isn't installed at all or $requiredVersion is specified but that version of SCA isn't installed + Write-Verbose \"$moduleName $requiredVersion not available - attempting to download from gallery\" + InstallLocalModule -moduleName $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion + } + elseif (!$requiredVersion) { + #We've got a version of SCA installed, but $requiredVersion isn't specified so we might be able to upgrade + $newest = GetHighestInstallableModule $moduleName + if ($newest -and ($installedModule.Version -lt $newest.Version)) { + Write-Verbose \"Updating $moduleName to version $($newest.Version)\" + InstallLocalModule -moduleName $moduleName -minimumVersion $newest.Version + } + } + + # Now we're done with install/upgrade, try to import the highest available module that matches our version requirements + + # We can't just use -minimumVersion and -maximumVersion arguments on Import-Module because PowerShell 3 doesn't have them, + # so we have to find the precise matching installed version using our code, then import that specifically. Note that + # $requiredVersion and $maximumVersion might be null when there's no specific version we need. + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion + + if (!$installedModule -and !$requiredVersion) { + #Did not find SCA, and we don't have a required version so we might be able to use an installed DLMA instead. + Write-Verbose \"$moduleName is not installed - trying to fall back to $DlmAutomationModuleName\" + $installedModule = GetHighestInstalledModule $DlmAutomationModuleName + } + + if ($installedModule) { + Write-Verbose \"Importing installed $($installedModule.Name) version $($installedModule.Version)\" + Import-Module $installedModule -Force + } + else { + throw \"$moduleName $requiredVersion is not installed, and could not be downloaded from the PowerShell gallery\" + } +} + +function InstallPowerShellGet { + [CmdletBinding()] + Param() + $psget = GetHighestInstalledModule PowerShellGet + if (!$psget) + { + Write-Warning @\" +Cannot access the PowerShell Gallery because PowerShellGet is not installed. +To install PowerShellGet, either upgrade to PowerShell 5 or install the PackageManagement MSI. +See https://docs.microsoft.com/en-us/powershell/gallery/installing-psget for more details. +\"@ + throw \"PowerShellGet is not available\" + } + + if ($psget.Version -lt [Version]'1.6') { + #Bootstrap the NuGet package provider, which updates NuGet without requiring admin rights + Write-Debug \"Installing NuGet package provider\" + Get-PackageProvider NuGet -ForceBootstrap | Out-Null + + #Use the currently-installed version of PowerShellGet + Import-PackageProvider PowerShellGet + + #Download the version of PowerShellGet that we actually need + Write-Debug \"Installing PowershellGet\" + Save-Module -Name PowerShellGet -Path $LocalModules -MinimumVersion 1.6 -Force -ErrorAction SilentlyContinue + } + + Write-Debug \"Importing PowershellGet\" + Import-Module PowerShellGet -MinimumVersion 1.6 -Force + #Make sure we're actually using the package provider from the imported version of PowerShellGet + Import-PackageProvider ((Get-Module PowerShellGet).Path) | Out-Null +} + +function InstallLocalModule { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$moduleName, + [Parameter(Mandatory = $false)] + [Version]$minimumVersion, + [Parameter(Mandatory = $false)] + [Version]$maximumVersion + ) + try { + InstallPowerShellGet + + Write-Debug \"Install $moduleName $requiredVersion\" + Save-Module -Name $moduleName -Path $LocalModules -Force -AcceptLicense -MinimumVersion $minimumVersion -MaximumVersion $maximumVersion -ErrorAction Stop + } + catch { + Write-Warning \"Could not install $moduleName $requiredVersion from any registered PSRepository\" + } +} + +function GetHighestInstalledModule { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [string] $moduleName, + + [Parameter(Mandatory = $false)] + [Version]$minimumVersion, + [Parameter(Mandatory = $false)] + [Version]$maximumVersion + ) + + return Get-Module $moduleName -ListAvailable | + Where {(!$minimumVersion -or ($_.Version -ge $minimumVersion)) -and (!$maximumVersion -or ($_.Version -le $maximumVersion))} | + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} | + Select -First 1 +} + +function GetHighestInstallableModule { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [string] $moduleName + ) + + try { + InstallPowerShellGet + Find-Module SqlChangeAutomation -AllVersions | + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} | + Select -First 1 + } + catch { + Write-Warning \"Could not find any suitable versions of $moduleName from any registered PSRepository\" + } +} + +function GetInstalledSqlChangeAutomationVersion {\t + $scaModule = (Get-Module $SqlChangeAutomationModuleName) + + if ($scaModule -ne $null) { + return $scaModule.Version + } +\t + $dlmaModule = (Get-Module $DlmAutomationModuleName) + + if ($dlmaModule -ne $null) { + return $dlmaModule.Version + } + + return $null +} + + +$ErrorActionPreference = 'Stop' +$VerbosePreference = 'Continue' + +# Set process level FUR environment +$env:REDGATE_FUR_ENVIRONMENT = \"Octopus Step Templates\" + +#Helper functions for paramter handling +function Required() { + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $true)][string]$Name + ) + if ([string]::IsNullOrWhiteSpace($Parameter)) { throw \"You must enter a value for '$Name'\" } +} +function Optional() { + #Default is untyped here - if we specify [string] powershell will convert nulls into empty string + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $false)]$Default + ) + if ([string]::IsNullOrWhiteSpace($Parameter)) { + $Default + } else { + $Parameter + } +} +function RequireBool() { + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $true)][string]$Name + ) + $Result = $False + if (![bool]::TryParse($Parameter , [ref]$Result )) { throw \"'$Name' must be a boolean value.\" } + $Result +} +function RequirePositiveNumber() { + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $true)][string]$Name + ) + $Result = 0 + if (![int32]::TryParse($Parameter , [ref]$Result )) { throw \"'$Name' must be a numerical value.\" } + if ($Result -lt 0) { throw \"'$Name' must be >= 0.\" } + $Result +} + +$SpecificModuleVersion = Optional -Parameter $SpecificModuleVersion +InstallCorrectSqlChangeAutomation -requiredVersion $SpecificModuleVersion + +# Check if SQL Change Automation is installed.\t +$powershellModule = Get-Module -Name SqlChangeAutomation\t +if ($powershellModule -eq $null) { \t + throw \"Cannot find SQL Change Automation on your Octopus Tentacle. If SQL Change Automation is installed, try restarting the Tentacle service for it to be detected.\"\t +} + +$currentVersion = $powershellModule.Version\t +$minimumRequiredVersion = [version] '3.0.3'\t +if ($currentVersion -lt $minimumRequiredVersion) { \t + throw \"This step requires SQL Change Automation version $minimumRequiredVersion or later. The current version is $currentVersion. The latest version can be found at http://www.red-gate.com/sca/productpage\"\t +} + +$minimumRequiredVersionDataCompareOptions = [version] '3.3.0' + +# Check the parameters. +Required -Parameter $DLMAutomationDeploymentResourcesPath -Name 'Export Path' +Required -Parameter $DLMAutomationDeleteExistingFiles -Name 'Delete files in export folder' +Required -Parameter $DLMAutomationDatabaseServer -Name 'Target SQL Server instance' +Required -Parameter $DLMAutomationDatabaseName -Name 'Target database name' +$DLMAutomationDatabaseUsername = Optional -Parameter $DLMAutomationDatabaseUsername +$DLMAutomationDatabasePassword = Optional -Parameter $DLMAutomationDatabasePassword +$DLMAutomationFilterPath = Optional -Parameter $DLMAutomationFilterPath +$DLMAutomationCompareOptions = Optional -Parameter $DLMAutomationCompareOptions +$DLMAutomationDataCompareOptions = Optional -Parameter $DLMAutomationDataCompareOptions +$DLMAutomationTransactionIsolationLevel = Optional -Parameter $DLMAutomationTransactionIsolationLevel -Default 'Serializable' +$DLMAutomationIgnoreStaticData = Optional -Parameter $DLMAutomationIgnoreStaticData -Default 'False' +$DLMAutomationIncludeIdenticalsInReport = Optional -Parameter $DLMAutomationIncludeIdenticalsInReport -Default 'False' + +# Constructing the unique export path. +$projectId = $OctopusParameters[\"Octopus.Project.Id\"] +$releaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$nugetPackageId = $OctopusParameters[\"Octopus.Action.Package[DLMAutomation.Package.Name].PackageId\"] +$exportPath = Join-Path (Join-Path (Join-Path $DLMAutomationDeploymentResourcesPath $projectId) $releaseNumber) $nugetPackageId + +# Make sure the directory we're about to create doesn't already exist, and delete any files if requested. +if ((Test-Path $exportPath) -AND ((Get-ChildItem $exportPath | Measure-Object).Count -ne 0)) { + if ($DLMAutomationDeleteExistingFiles -eq 'True') { + Write-Host \"Deleting all files in $exportPath\" + rmdir $exportPath -Recurse -Force + } else { + throw \"The export path is not empty: $exportPath. Select the 'Delete files in export folder' option to overwrite the existing folder contents.\" + } +} + +# Determine whether or not to include identical objects in the report. +$targetDB = New-DatabaseConnection -ServerInstance $DLMAutomationDatabaseServer -Database $DLMAutomationDatabaseName -Username $DLMAutomationDatabaseUsername -Password $DLMAutomationDatabasePassword | Test-DatabaseConnection + +$packagePath = $OctopusParameters[\"Octopus.Action.Package[DLMAutomation.Package.Name].ExtractedPath\"] + +$importedBuildArtifact = Import-DatabaseBuildArtifact -Path $packagePath + +# Only allow sqlcmd variables that don't have special characters like spaces, colon or dashes +$regex = '^[a-zA-Z_][a-zA-Z0-9_]+$' +$sqlCmdVariables = @{} +$OctopusParameters.Keys | Where { $_ -match $regex } | ForEach { +\t$sqlCmdVariables[$_] = $OctopusParameters[$_] +} + +# Create the deployment resources from the database to the NuGet package +$releaseParams = @{ + Target = $targetDB + Source = $importedBuildArtifact + TransactionIsolationLevel = $DLMAutomationTransactionIsolationLevel + IgnoreStaticData = [bool]::Parse($DLMAutomationIgnoreStaticData) + FilterPath = $DLMAutomationFilterPath + SQLCompareOptions = $DLMAutomationCompareOptions + IncludeIdenticalsInReport = [bool]::Parse($DLMAutomationIncludeIdenticalsInReport) + SqlCmdVariables = $sqlCmdVariables +} + +if($currentVersion -ge $minimumRequiredVersionDataCompareOptions) { + $releaseParams.SQLDataCompareOptions = $DLMAutomationDataCompareOptions +} elseif(-not [string]::IsNullOrWhiteSpace($DLMAutomationDataCompareOptions)) { + Write-Warning \"SQL Data Compare options requires SQL Change Automation version $minimumRequiredVersionDataCompareOptions or later. The current version is $currentVersion.\" +} + +$release = New-DatabaseReleaseArtifact @releaseParams + +# Export the deployment resources to disk +$release | Export-DatabaseReleaseArtifact -Path $exportPath + +# Import the changes summary, deployment warnings, and update script as Octopus artifacts, so you can review them. +function UploadIfExists() { + Param( + [Parameter(Mandatory = $true)] + [string]$ArtifactPath, + [Parameter(Mandatory = $true)] + [string]$Name + ) + if (Test-Path $ArtifactPath) { + New-OctopusArtifact $ArtifactPath -Name $Name + } +} + +UploadIfExists -ArtifactPath \"$exportPath\\Reports\\Changes.html\" -Name \"Changes-$DLMAutomationDatabaseName.html\" +UploadIfExists -ArtifactPath \"$exportPath\\Reports\\Drift.html\" -Name \"Drift-$DLMAutomationDatabaseName.html\" +UploadIfExists -ArtifactPath \"$exportPath\\Reports\\Warnings.xml\" -Name \"Warnings-$DLMAutomationDatabaseName.xml\" +UploadIfExists -ArtifactPath \"$exportPath\\Update.sql\" -Name \"Update-$DLMAutomationDatabaseName.sql\" +UploadIfExists -ArtifactPath \"$exportPath\\TargetedDeploymentScript.sql\" -Name \"TargetedDeploymentScript-$DLMAutomationDatabaseName.sql\" +UploadIfExists -ArtifactPath \"$exportPath\\DriftRevertScript.sql\" -Name \"DriftRevertScript-$DLMAutomationDatabaseName.sql\" + +# Sets a variable if there are changes to deploy. Useful if you want to have steps run only when this is set +if ($release.UpdateSQL -notlike '*This script is empty because the Target and Source schemas are equivalent*') +{ + Set-OctopusVariable -name \"ChangesToDeploy\" -value \"True\" +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "851de66e-5ebd-49f3-ae26-d7e276438313", + "Name": "DLMAutomationDeploymentResourcesPath", + "Label": "Export path", + "HelpText": "The path that the database deployment resources will be exported to. + +This path is used in the \"Redgate - Deploy from Database Release\" step, and must be accessible to all tentacles used in database deployment steps.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0d3a301d-dbe3-4e49-a3cd-4d5c8cee8483", + "Name": "DLMAutomationPackageName", + "Label": "Package", + "HelpText": "The name of the package to extract", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "011c4343-85f0-4b10-8789-e8654723f355", + "Name": "DLMAutomationDeleteExistingFiles", + "Label": "Delete files in export folder", + "HelpText": "If the folder that the deployment resources are exported to isn't empty, this step will fail. Select this option to delete any files in the folder.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "b3bf1847-5167-4aa6-a559-d72d563569ae", + "Name": "DLMAutomationDatabaseServer", + "Label": "Target SQL Server instance", + "HelpText": "The fully qualified SQL Server instance name for the target database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "dfec97ea-de71-4b80-92cc-406830b278be", + "Name": "DLMAutomationDatabaseName", + "Label": "Target database name", + "HelpText": "The name of the database that the source schema (the database package) will be compared with to generate the deployment resources. This must be an existing database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cdeee900-0313-4a40-9256-9432ae3d6e7a", + "Name": "DLMAutomationDatabaseUsername", + "Label": "Username (optional)", + "HelpText": "The SQL Server username used to connect to the database. If you leave this field and 'Password' blank, Windows authentication will be used to connect instead, using the account that runs the Tentacle service.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "552ee2df-2581-4061-a406-449b28de6bf6", + "Name": "DLMAutomationDatabasePassword", + "Label": "Password (optional)", + "HelpText": "You must enter a password if you entered a username.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "45bfb972-d44b-476b-8988-934bcc56da1f", + "Name": "DLMAutomationFilterPath", + "Label": "Filter path (optional)", + "HelpText": "Specify the location of a SQL Compare filter file (.scpf), which defines objects to include/exclude in the schema comparison. Filter files are generated by SQL Source Control. + +For more help see [Using SQL Compare filters in SQL Change Automation](http://www.red-gate.com/sca/ps/help/filters).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d867564b-2134-4a41-8c39-b3828e168444", + "Name": "DLMAutomationCompareOptions", + "Label": "SQL Compare options (optional)", + "HelpText": "Enter SQL Compare options to apply when generating the update script. Use a comma-separated list to enter multiple values. For more help see [Using SQL Compare options in SQL Change Automation](http://www.red-gate.com/sca/add-ons/compare-options).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "26764c18-7f04-4b69-b53e-4c61b1d8eeda", + "Name": "DLMAutomationDataCompareOptions", + "Label": "SQL Data Compare options (optional)", + "HelpText": "Enter SQL Data Compare options to apply when generating the update script. Use a comma-separated list to enter multiple values. For more help see [Using SQL Data Compare options in SQL Change Automation](https://documentation.red-gate.com/sca4/deploying-database-changes/automated-deployment-with-sql-source-control-projects/using-comparison-options-with-sql-change-automation-powershell-module-for-sql-source-control-projects).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "53313b06-05ef-4a17-9d65-91ffa17bd3c4", + "Name": "DLMAutomationTransactionIsolationLevel", + "Label": "Transaction isolation level (optional)", + "HelpText": "Select the transaction isolation level to be used in deployment scripts.", + "DefaultValue": "Serializable", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Serializable +Snapshot +RepeatableRead +ReadCommitted +ReadUncommitted" + } + }, + { + "Id": "e462b1ae-76f5-400e-b772-c36f36fae241", + "Name": "DLMAutomationIgnoreStaticData", + "Label": "Ignore static data", + "HelpText": "Exclude changes to static data when generating the deployment resources.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "13252389-53ff-4cea-8651-76c9cde85ca5", + "Name": "DLMAutomationIncludeIdenticalsInReport", + "Label": "Include identical objects in the change report", + "HelpText": "By default, the change report only includes added, modified and removed objects. Choose this option to also include identical objects.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "91d18ab6-88bf-4f9f-baec-baf44ec53cf1", + "Name": "SpecificModuleVersion", + "Label": "SQL Change Automation version (optional)", + "HelpText": "If you wish to use a specific version of SQL Change Automation rather than the latest, enter the version number here.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "61adc6ec-4216-41c8-ab30-dba6cfcd37d0", + "Name": "DLMModuleInstallLocation", + "Label": "SQL Change Automation Install Location (optional)", + "HelpText": "The SQL Change Automation cmdlets will be downloaded from the [PowerShell gallery](https://www.powershellgallery.com/packages/SqlChangeAutomation). Please specify the folder folder where those packages will be saved to. It can be relative or absolute. + + +If this is empty it will default `$Home\\Documents\\WindowsPowerShell\\Modules` which is the [recommended location](https://docs.microsoft.com/en-us/powershell/scripting/developer/module/installing-a-powershell-module?view=powershell-7#where-to-install-modules) from Microsoft.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2020-05-01T15:18:51.685Z", + "OctopusVersion": "2020.1.10", + "Type": "ActionTemplate" + }, + "Category": "redgate" + } diff --git a/step-templates/redgate-create-database-release.json.human b/step-templates/redgate-create-database-release.json.human new file mode 100644 index 000000000..0be1e005d --- /dev/null +++ b/step-templates/redgate-create-database-release.json.human @@ -0,0 +1,627 @@ +{ + "Id": "c20b70dc-69aa-42a1-85db-6d37341b63e3", + "Name": "Redgate - Create Database Release", + "Description": "Creates the resources (including the SQL update script) to deploy database changes using Redgate\u0027s [SQL Change Automation](http://www.red-gate.com/sca/productpage), and exports them as Octopus artifacts so you can review the changes before deploying.\r +\r +Requires SQL Change Automation version 3.0.2 or later.\r +\r +*Version date: 2020-12-21*", + "ActionType": "Octopus.Script", + "Version": 22, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function GetModuleInstallationFolder\r +{\r + if (ModuleInstallationFolderIsValid)\r + {\r + return [System.IO.Path]::GetFullPath($DLMAutomationModuleInstallationFolder)\r + }\r +\r + return \"$PSScriptRoot\\Modules\"\r +}\r +\r +function ModuleInstallationFolderIsValid\r +{\r + if ([string]::IsNullOrWhiteSpace($DLMAutomationModuleInstallationFolder))\r + {\r + return $false\r + }\r +\r + return (Test-Path $DLMAutomationModuleInstallationFolder -IsValid) -eq $true;\r +}\r +\r +$DlmAutomationModuleName = \"DLMAutomation\"\r +$SqlChangeAutomationModuleName = \"SqlChangeAutomation\"\r +$ModulesFolder = GetModuleInstallationFolder\r +$LocalModules = (New-Item \"$ModulesFolder\" -ItemType Directory -Force).FullName\r +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\"\r +\r +function IsScaAvailable\r +{\r + if ((Get-Module $SqlChangeAutomationModuleName) -ne $null) {\r + return $true\r + }\r +\r + return $false\r +}\r +\r +function InstallCorrectSqlChangeAutomation\r +{\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $false)]\r + [Version]$requiredVersion,\r + [Parameter(Mandatory = $false)]\r + [bool]$useInstalledVersion\r + )\r +\r + $moduleName = $SqlChangeAutomationModuleName\r +\r + # this will be null if $requiredVersion is not specified - which is exactly what we want\r + $maximumVersion = $requiredVersion\r +\r + if ($requiredVersion) {\r + if ($requiredVersion.Revision -eq -1) {\r + #If provided with a 3 part version number (the 4th part, revision, == -1), we should allow any value for the revision\r + $maximumVersion = [Version]\"$requiredVersion.$([System.Int32]::MaxValue)\"\r + }\r +\r + if ($requiredVersion.Major -lt 3) {\r + # If the specified version is below V3 then the user is requesting a version of DLMA. We should look for that module name instead\r + $moduleName = $DlmAutomationModuleName\r + }\r + }\r +\r + if ($useInstalledVersion) {\r + Write-Verbose \"Option to use installed version is selected. Skipping update/install using PowerShellGet.\"\r + }\r + else {\r + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r +\r + if (!$installedModule) {\r + #Either SCA isn\u0027t installed at all or $requiredVersion is specified but that version of SCA isn\u0027t installed\r + Write-Verbose \"$moduleName $requiredVersion not available - attempting to download from gallery\"\r + InstallLocalModule -moduleName $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r + }\r + elseif (!$requiredVersion) {\r + #We\u0027ve got a version of SCA installed, but $requiredVersion isn\u0027t specified so we might be able to upgrade\r + $newest = GetHighestInstallableModule $moduleName\r + if ($newest -and ($installedModule.Version -lt $newest.Version)) {\r + Write-Verbose \"Updating $moduleName to version $($newest.Version)\"\r + InstallLocalModule -moduleName $moduleName -minimumVersion $newest.Version\r + }\r + }\r + }\r +\r + # Now we\u0027re done with install/upgrade, try to import the highest available module that matches our version requirements\r +\r + # We can\u0027t just use -minimumVersion and -maximumVersion arguments on Import-Module because PowerShell 3 doesn\u0027t have them,\r + # so we have to find the precise matching installed version using our code, then import that specifically. Note that\r + # $requiredVersion and $maximumVersion might be null when there\u0027s no specific version we need.\r + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r +\r + if (!$installedModule -and !$requiredVersion) {\r + #Did not find SCA, and we don\u0027t have a required version so we might be able to use an installed DLMA instead.\r + Write-Verbose \"$moduleName is not installed - trying to fall back to $DlmAutomationModuleName\"\r + $installedModule = GetHighestInstalledModule $DlmAutomationModuleName\r + }\r +\r + if ($installedModule) {\r + Write-Verbose \"Importing installed $($installedModule.Name) version $($installedModule.Version)\"\r + Import-Module $installedModule -Force\r + }\r + else {\r + throw \"$moduleName $requiredVersion is not installed, and could not be downloaded from the PowerShell gallery\"\r + }\r +}\r +\r +function InstallPowerShellGet {\r + [CmdletBinding()]\r + Param()\r +\r + ConfigureProxyIfVariableSet\r + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12\r +\r + $psget = GetHighestInstalledModule PowerShellGet\r + if (!$psget)\r + {\r + Write-Warning @\"\r +Cannot access the PowerShell Gallery because PowerShellGet is not installed.\r +To install PowerShellGet, either upgrade to PowerShell 5 or install the PackageManagement MSI.\r +See https://docs.microsoft.com/en-us/powershell/gallery/installing-psget for more details.\r +\"@\r + throw \"PowerShellGet is not available\"\r + }\r +\r + if ($psget.Version -lt [Version]\u00271.6\u0027) {\r + #Bootstrap the NuGet package provider, which updates NuGet without requiring admin rights\r + Write-Debug \"Installing NuGet package provider\"\r + Get-PackageProvider NuGet -ForceBootstrap | Out-Null\r +\r + #Use the currently-installed version of PowerShellGet\r + Import-PackageProvider PowerShellGet\r +\r + #Download the version of PowerShellGet that we actually need\r + Write-Debug \"Installing PowershellGet\"\r + Save-Module -Name PowerShellGet -Path $LocalModules -MinimumVersion 1.6 -Force -ErrorAction SilentlyContinue\r + }\r +\r + Write-Debug \"Importing PowershellGet\"\r + Import-Module PowerShellGet -MinimumVersion 1.6 -Force\r + #Make sure we\u0027re actually using the package provider from the imported version of PowerShellGet\r + Import-PackageProvider ((Get-Module PowerShellGet).Path) | Out-Null\r +}\r +\r +function InstallLocalModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true)]\r + [string]$moduleName,\r + [Parameter(Mandatory = $false)]\r + [Version]$minimumVersion,\r + [Parameter(Mandatory = $false)]\r + [Version]$maximumVersion\r + )\r + try {\r + InstallPowerShellGet\r +\r + Write-Debug \"Install $moduleName $requiredVersion\"\r + Save-Module -Name $moduleName -Path $LocalModules -Force -AcceptLicense -MinimumVersion $minimumVersion -MaximumVersion $maximumVersion -ErrorAction Stop\r + }\r + catch {\r + Write-Warning \"Could not install $moduleName $requiredVersion from any registered PSRepository\"\r + }\r +}\r +\r +function GetHighestInstalledModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true, Position = 0)]\r + [string] $moduleName,\r +\r + [Parameter(Mandatory = $false)]\r + [Version]$minimumVersion,\r + [Parameter(Mandatory = $false)]\r + [Version]$maximumVersion\r + )\r +\r + return Get-Module $moduleName -ListAvailable |\r + Where {(!$minimumVersion -or ($_.Version -ge $minimumVersion)) -and (!$maximumVersion -or ($_.Version -le $maximumVersion))} |\r + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} |\r + Select -First 1\r +}\r +\r +function GetHighestInstallableModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true, Position = 0)]\r + [string] $moduleName\r + )\r +\r + try {\r + InstallPowerShellGet\r + Find-Module SqlChangeAutomation -AllVersions |\r + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} |\r + Select -First 1\r + }\r + catch {\r + Write-Warning \"Could not find any suitable versions of $moduleName from any registered PSRepository\"\r + }\r +}\r +\r +function GetInstalledSqlChangeAutomationVersion {\r + $scaModule = (Get-Module $SqlChangeAutomationModuleName)\r +\r + if ($scaModule -ne $null) {\r + return $scaModule.Version\r + }\r +\r + $dlmaModule = (Get-Module $DlmAutomationModuleName)\r +\r + if ($dlmaModule -ne $null) {\r + return $dlmaModule.Version\r + }\r +\r + return $null\r +}\r +\r +function ConfigureProxyIfVariableSet\r +{\r + if ([string]::IsNullOrWhiteSpace($DLMAutomationProxyUrl) -eq $false)\r + {\r + Write-Debug \"Setting DefaultWebProxy to $proxyUrl\"\r +\r + [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy($DLMAutomationProxyUrl)\r + [System.Net.WebRequest]::DefaultWebProxy.credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials\r + [System.Net.WebRequest]::DefaultWebProxy.BypassProxyOnLocal = $True\r + }\r +}\r +\r +\r +$ErrorActionPreference = \u0027Stop\u0027\r +$VerbosePreference = \u0027Continue\u0027\r +\r +# Set process level FUR environment\r +$env:REDGATE_FUR_ENVIRONMENT = \"Octopus Step Templates\"\r +\r +#Helper functions for paramter handling\r +function Required() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + if ([string]::IsNullOrWhiteSpace($Parameter)) { throw \"You must enter a value for \u0027$Name\u0027\" }\r +}\r +function Optional() {\r + #Default is untyped here - if we specify [string] powershell will convert nulls into empty string\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $false)]$Default\r + )\r + if ([string]::IsNullOrWhiteSpace($Parameter)) {\r + $Default\r + } else {\r + $Parameter\r + }\r +}\r +function RequireBool() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + $Result = $False\r + if (![bool]::TryParse($Parameter , [ref]$Result )) { throw \"\u0027$Name\u0027 must be a boolean value.\" }\r + $Result\r +}\r +function RequirePositiveNumber() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + $Result = 0\r + if (![int32]::TryParse($Parameter , [ref]$Result )) { throw \"\u0027$Name\u0027 must be a numerical value.\" }\r + if ($Result -lt 0) { throw \"\u0027$Name\u0027 must be \u003e= 0.\" }\r + $Result\r +}\r +\r +$SpecificModuleVersion = Optional -Parameter $SpecificModuleVersion\r +$UseInstalledModuleVersion = Optional -Parameter $UseInstalledModuleVersion -Default \u0027False\u0027\r +$UseInstalledVersionSwitch = [bool]::Parse($UseInstalledModuleVersion)\r +InstallCorrectSqlChangeAutomation -requiredVersion $SpecificModuleVersion -useInstalledVersion $UseInstalledVersionSwitch\r +\r +# Check if SQL Change Automation is installed.\r +$powershellModule = Get-Module -Name SqlChangeAutomation\r +if ($powershellModule -eq $null) {\r + throw \"Cannot find SQL Change Automation on your Octopus Tentacle. If SQL Change Automation is installed, try restarting the Tentacle service for it to be detected.\"\r +}\r +\r +$currentVersion = $powershellModule.Version\r +$minimumRequiredVersion = [version] \u00273.0.3\u0027\r +if ($currentVersion -lt $minimumRequiredVersion) {\r + throw \"This step requires SQL Change Automation version $minimumRequiredVersion or later. The current version is $currentVersion. The latest version can be found at http://www.red-gate.com/sca/productpage\"\r +}\r +\r +$minimumRequiredVersionDataCompareOptions = [version] \u00273.3.0\u0027\r +$minimumRequiredVersionTrustServerCertificate = [version]\u00274.3.20267\u0027\r +\r +function AreConnectionOptionsHandled($encryptConnection, $trustServerCertificate)\r +{\r + if ([string]::IsNullOrWhiteSpace($currentVersion) -or $currentVersion -ge $minimumRequiredVersionTrustServerCertificate)\r + {\r + return $true\r + }\r + elseif($encryptConnection -or $trustServerCertificate)\r + {\r + Write-Warning \"Encrypt and TrustServerCertificate options require SQL Change Automation version $minimumRequiredVersionTrustServerCertificate or later. The current version is $currentVersion.\"\r + return $false\r + }\r +}\r +\r +# Check the parameters.\r +Required -Parameter $DLMAutomationDeploymentResourcesPath -Name \u0027Export Path\u0027\r +Required -Parameter $DLMAutomationDeleteExistingFiles -Name \u0027Delete files in export folder\u0027\r +Required -Parameter $DLMAutomationNuGetDbPackageDownloadStepName -Name \u0027Database package step\u0027\r +Required -Parameter $DLMAutomationDatabaseServer -Name \u0027Target SQL Server instance\u0027\r +Required -Parameter $DLMAutomationDatabaseName -Name \u0027Target database name\u0027\r +$DLMAutomationDatabaseUsername = Optional -Parameter $DLMAutomationDatabaseUsername\r +$DLMAutomationDatabasePassword = Optional -Parameter $DLMAutomationDatabasePassword\r +$DLMAutomationEncrypt = Optional -Parameter $DLMAutomationEncrypt\r +$DLMAutomationTrustServerCertificate = Optional -Parameter $DLMAutomationTrustServerCertificate\r +$DLMAutomationFilterPath = Optional -Parameter $DLMAutomationFilterPath\r +$DLMAutomationCompareOptions = Optional -Parameter $DLMAutomationCompareOptions\r +$DLMAutomationDataCompareOptions = Optional -Parameter $DLMAutomationDataCompareOptions\r +$DLMAutomationTransactionIsolationLevel = Optional -Parameter $DLMAutomationTransactionIsolationLevel -Default \u0027Serializable\u0027\r +$DLMAutomationIgnoreStaticData = Optional -Parameter $DLMAutomationIgnoreStaticData -Default \u0027False\u0027\r +$DLMAutomationIncludeIdenticalsInReport = Optional -Parameter $DLMAutomationIncludeIdenticalsInReport -Default \u0027False\u0027\r +$DLMAutomationModuleInstallationFolder = Optional -Parameter $DLMAutomationModuleInstallationFolder\r +$DLMAutomationProxyUrl = Optional -Parameter $DLMAutomationProxyUrl\r +\r +# Get the NuGet package installation directory path.\r +$packagePath = $OctopusParameters[\"Octopus.Action[$DLMAutomationNuGetDbPackageDownloadStepName].Output.Package.InstallationDirectoryPath\"]\r +if($packagePath -eq $null) {\r + throw \"The \u0027Database package download step\u0027 is not a \u0027Deploy a NuGet package\u0027 step: \u0027$DLMAutomationNuGetDbPackageDownloadStepName\u0027\"\r +}\r +\r +# Constructing the unique export path.\r +$projectId = $OctopusParameters[\"Octopus.Project.Id\"]\r +$releaseNumber = $OctopusParameters[\"Octopus.Release.Number\"]\r +$nugetPackageId = $OctopusParameters[\"Octopus.Action[$DLMAutomationNuGetDbPackageDownloadStepName].Package.NuGetPackageId\"]\r +$exportPath = Join-Path (Join-Path (Join-Path $DLMAutomationDeploymentResourcesPath $projectId) $releaseNumber) $nugetPackageId\r +\r +# Make sure the directory we\u0027re about to create doesn\u0027t already exist, and delete any files if requested.\r +if ((Test-Path $exportPath) -AND ((Get-ChildItem $exportPath | Measure-Object).Count -ne 0)) {\r + if ($DLMAutomationDeleteExistingFiles -eq \u0027True\u0027) {\r + Write-Host \"Deleting all files in $exportPath\"\r + rmdir $exportPath -Recurse -Force\r + } else {\r + throw \"The export path is not empty: $exportPath. Select the \u0027Delete files in export folder\u0027 option to overwrite the existing folder contents.\"\r + }\r +}\r +\r +$connectionOptions = @{ }\r +\r +if(AreConnectionOptionsHandled([bool]::Parse($DLMAutomationEncrypt), [bool]::Parse($DLMAutomationTrustServerCertificate))) {\r + $connectionOptions += @{ \u0027Encrypt\u0027 = [bool]::Parse($DLMAutomationEncrypt) }\r + $connectionOptions += @{ \u0027TrustServerCertificate\u0027 = [bool]::Parse($DLMAutomationTrustServerCertificate) }\r +}\r +\r +# Determine whether or not to include identical objects in the report.\r +$targetDB = New-DatabaseConnection @connectionOptions `\r + -ServerInstance $DLMAutomationDatabaseServer `\r + -Database $DLMAutomationDatabaseName `\r + -Username $DLMAutomationDatabaseUsername `\r + -Password $DLMAutomationDatabasePassword | Test-DatabaseConnection\r +\r +$importedBuildArtifact = Import-DatabaseBuildArtifact -Path $packagePath\r +\r +# Only allow sqlcmd variables that don\u0027t have special characters like spaces, colon or dashes\r +$regex = \u0027^[a-zA-Z_][a-zA-Z0-9_]+$\u0027\r +$sqlCmdVariables = @{}\r +$OctopusParameters.Keys | Where { $_ -match $regex } | ForEach {\r +\t$sqlCmdVariables[$_] = $OctopusParameters[$_]\r +}\r +\r +# Create the deployment resources from the database to the NuGet package\r +$releaseParams = @{\r + Target = $targetDB\r + Source = $importedBuildArtifact\r + TransactionIsolationLevel = $DLMAutomationTransactionIsolationLevel\r + IgnoreStaticData = [bool]::Parse($DLMAutomationIgnoreStaticData)\r + FilterPath = $DLMAutomationFilterPath\r + SQLCompareOptions = $DLMAutomationCompareOptions\r + IncludeIdenticalsInReport = [bool]::Parse($DLMAutomationIncludeIdenticalsInReport)\r + SqlCmdVariables = $sqlCmdVariables\r +}\r +\r +if($currentVersion -ge $minimumRequiredVersionDataCompareOptions) {\r + $releaseParams.SQLDataCompareOptions = $DLMAutomationDataCompareOptions\r +} elseif(-not [string]::IsNullOrWhiteSpace($DLMAutomationDataCompareOptions)) {\r + Write-Warning \"SQL Data Compare options requires SQL Change Automation version $minimumRequiredVersionDataCompareOptions or later. The current version is $currentVersion.\"\r +}\r +\r +$release = New-DatabaseReleaseArtifact @releaseParams\r +\r +# Export the deployment resources to disk\r +$release | Export-DatabaseReleaseArtifact -Path $exportPath\r +\r +# Import the changes summary, deployment warnings, and update script as Octopus artifacts, so you can review them.\r +function UploadIfExists() {\r + Param(\r + [Parameter(Mandatory = $true)]\r + [string]$ArtifactPath,\r + [Parameter(Mandatory = $true)]\r + [string]$Name\r + )\r + if (Test-Path $ArtifactPath) {\r + New-OctopusArtifact $ArtifactPath -Name $Name\r + }\r +}\r +\r +UploadIfExists -ArtifactPath \"$exportPath\\Reports\\Changes.html\" -Name \"Changes-$DLMAutomationDatabaseName.html\"\r +UploadIfExists -ArtifactPath \"$exportPath\\Reports\\Drift.html\" -Name \"Drift-$DLMAutomationDatabaseName.html\"\r +UploadIfExists -ArtifactPath \"$exportPath\\Reports\\Warnings.xml\" -Name \"Warnings-$DLMAutomationDatabaseName.xml\"\r +UploadIfExists -ArtifactPath \"$exportPath\\Update.sql\" -Name \"Update-$DLMAutomationDatabaseName.sql\"\r +UploadIfExists -ArtifactPath \"$exportPath\\TargetedDeploymentScript.sql\" -Name \"TargetedDeploymentScript-$DLMAutomationDatabaseName.sql\"\r +UploadIfExists -ArtifactPath \"$exportPath\\DriftRevertScript.sql\" -Name \"DriftRevertScript-$DLMAutomationDatabaseName.sql\"\r +\r +# Sets a variable if there are changes to deploy. Useful if you want to have steps run only when this is set\r +if ($release.UpdateSQL -notlike \u0027*This script is empty because the Target and Source schemas are equivalent*\u0027)\r +{\r + Set-OctopusVariable -name \"ChangesToDeploy\" -value \"True\"\r +}\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": { + + }, + "Parameters": [ + { + "Name": "DLMAutomationDeploymentResourcesPath", + "Label": "Export path", + "HelpText": "The path that the database deployment resources will be exported to. + +This path is used in the \"Redgate - Deploy from Database Release\" step, and must be accessible to all tentacles used in database deployment steps.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationDeleteExistingFiles", + "Label": "Delete files in export folder", + "HelpText": "If the folder that the deployment resources are exported to isn\u0027t empty, this step will fail. Select this option to delete any files in the folder.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationNuGetDbPackageDownloadStepName", + "Label": "Database package step", + "HelpText": "Select the step in this project which downloads the database package.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Name": "DLMAutomationDatabaseServer", + "Label": "Target SQL Server instance", + "HelpText": "The fully qualified SQL Server instance name for the target database.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationDatabaseName", + "Label": "Target database name", + "HelpText": "The name of the database that the source schema (the database package) will be compared with to generate the deployment resources. This must be an existing database.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationDatabaseUsername", + "Label": "Username (optional)", + "HelpText": "The SQL Server username used to connect to the database. If you leave this field and \u0027Password\u0027 blank, Windows authentication will be used to connect instead, using the account that runs the Tentacle service.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationDatabasePassword", + "Label": "Password (optional)", + "HelpText": "You must enter a password if you entered a username.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "DLMAutomationEncrypt", + "Label": "Encrypt", + "HelpText": "Specify whether SSL encryption is used by SQL Server when a certificate is installed.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationTrustServerCertificate", + "Label": "Trust Server Certificate", + "HelpText": "Specify whether to force SQL Server to skip certificate validation.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationFilterPath", + "Label": "Filter path (optional)", + "HelpText": "Specify the location of a SQL Compare filter file (.scpf), which defines objects to include/exclude in the schema comparison. Filter files are generated by SQL Source Control. + +For more help see [Using SQL Compare filters in SQL Change Automation](http://www.red-gate.com/sca/ps/help/filters).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationCompareOptions", + "Label": "SQL Compare options (optional)", + "HelpText": "Enter SQL Compare options to apply when generating the update script. Use a comma-separated list to enter multiple values. For more help see [Using SQL Compare options in SQL Change Automation](http://www.red-gate.com/sca/add-ons/compare-options).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationDataCompareOptions", + "Label": "SQL Data Compare options (optional)", + "HelpText": "Enter SQL Data Compare options to apply when generating the update script. Use a comma-separated list to enter multiple values. For more help see [Using SQL Data Compare options in SQL Change Automation](http://www.red-gate.com/sca/ps/help/datacompareoptions).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationTransactionIsolationLevel", + "Label": "Transaction isolation level (optional)", + "HelpText": "Select the transaction isolation level to be used in deployment scripts.", + "DefaultValue": "Serializable", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Serializable +Snapshot +RepeatableRead +ReadCommitted +ReadUncommitted" + } + }, + { + "Name": "DLMAutomationIgnoreStaticData", + "Label": "Ignore static data", + "HelpText": "Exclude changes to static data when generating the deployment resources.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationIncludeIdenticalsInReport", + "Label": "Include identical objects in the change report", + "HelpText": "By default, the change report only includes added, modified and removed objects. Choose this option to also include identical objects.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "SpecificModuleVersion", + "Label": "SQL Change Automation version (optional)", + "HelpText": "If you wish to use a specific version of SQL Change Automation rather than the latest, enter the version number here.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "UseInstalledModuleVersion", + "Label": "Only use a version of SQL Change Automation that is already installed", + "HelpText": "This prevents attempting to access PowerShell Gallery, which can be helpful when the build agent does not have access to the internet", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationModuleInstallationFolder", + "Label": "Module Installation Folder (optional)", + "HelpText": "By default, module folders do not persist between steps. Setting this field to a specific folder will ensure that modules persist, and do not have to be downloaded again.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationProxyUrl", + "Label": "Proxy URL (optional)", + "HelpText": "By default, no proxy is used when connecting to Powershell Gallery. Alternatively, a proxy URL can be specified here that can be used for Powershell Gallery.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-03-19T10:01:37.110+00:00", + "LastModifiedBy": "support@red-gate.com", + "$Meta": { + "ExportedAt": "2015-07-17T11:04:21.348Z", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "redgate" +} diff --git a/step-templates/redgate-deploy-database-release-worker-friendly.json.human b/step-templates/redgate-deploy-database-release-worker-friendly.json.human new file mode 100644 index 000000000..eb601fae0 --- /dev/null +++ b/step-templates/redgate-deploy-database-release-worker-friendly.json.human @@ -0,0 +1,446 @@ +{ + "Id": "adf9a009-8bbb-4b82-8f3b-6fb12ef4ba18", + "Name": "Redgate - Deploy from Database Release (Worker Friendly)", + "Description": "Uses the deployment resources from the 'Redgate - Create Database Release' step to deploy the database changes using Redgate's [SQL Change Automation](http://www.red-gate.com/sca/productpage). + +Requires SQL Change Automation version 3.0.2 or later. + +*Version date: 2019-07-26* + +This step template is worker friendly, you can pass in a package reference rather than having to reference a previous step which downloaded the package. This step requires Octopus Deploy **2019.10.0** or higher.", + "ActionType": "Octopus.Script", + "Version": 4, + "Author": "octobob", + "Packages": [ + { + "Id": "cbac673c-43fb-4f6f-8204-31597bb57077", + "Name": "DLMAutomationPackageName", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "DLMAutomationPackageName" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptBody": "$DlmAutomationModuleName = \"DLMAutomation\" +$SqlChangeAutomationModuleName = \"SqlChangeAutomation\" +$ModulesFolder = \"$Home\\Documents\\WindowsPowerShell\\Modules\" + +if ([string]::IsNullOrWhiteSpace($DLMModuleInstallLocation) -eq $false) +{ +\tif ((Test-Path $DLMModuleInstallLocation -IsValid) -eq $false) + { + \tWrite-Error \"The path $DLMModuleInstallLocation is not valid, please use a relative or absolute path.\" + exit 1 + } + + $ModulesFolder = [System.IO.Path]::GetFullPath($DLMModuleInstallLocation) +} + +Write-Host \"Modules will be installed into $ModulesFolder\" + +$LocalModules = (New-Item \"$ModulesFolder\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\" + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +function IsScaAvailable +{ + if ((Get-Module $SqlChangeAutomationModuleName) -ne $null) { + return $true + } + + return $false +} + +function InstallCorrectSqlChangeAutomation +{ + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Version]$requiredVersion + ) + + $moduleName = $SqlChangeAutomationModuleName + + # this will be null if $requiredVersion is not specified - which is exactly what we want + $maximumVersion = $requiredVersion + + if ($requiredVersion) { + if ($requiredVersion.Revision -eq -1) { + #If provided with a 3 part version number (the 4th part, revision, == -1), we should allow any value for the revision + $maximumVersion = [Version]\"$requiredVersion.$([System.Int32]::MaxValue)\" + } + + if ($requiredVersion.Major -lt 3) { + # If the specified version is below V3 then the user is requesting a version of DLMA. We should look for that module name instead + $moduleName = $DlmAutomationModuleName + } + } + + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion + + if (!$installedModule) { + #Either SCA isn't installed at all or $requiredVersion is specified but that version of SCA isn't installed + Write-Verbose \"$moduleName $requiredVersion not available - attempting to download from gallery\" + InstallLocalModule -moduleName $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion + } + elseif (!$requiredVersion) { + #We've got a version of SCA installed, but $requiredVersion isn't specified so we might be able to upgrade + $newest = GetHighestInstallableModule $moduleName + if ($newest -and ($installedModule.Version -lt $newest.Version)) { + Write-Verbose \"Updating $moduleName to version $($newest.Version)\" + InstallLocalModule -moduleName $moduleName -minimumVersion $newest.Version + } + } + + # Now we're done with install/upgrade, try to import the highest available module that matches our version requirements + + # We can't just use -minimumVersion and -maximumVersion arguments on Import-Module because PowerShell 3 doesn't have them, + # so we have to find the precise matching installed version using our code, then import that specifically. Note that + # $requiredVersion and $maximumVersion might be null when there's no specific version we need. + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion + + if (!$installedModule -and !$requiredVersion) { + #Did not find SCA, and we don't have a required version so we might be able to use an installed DLMA instead. + Write-Verbose \"$moduleName is not installed - trying to fall back to $DlmAutomationModuleName\" + $installedModule = GetHighestInstalledModule $DlmAutomationModuleName + } + + if ($installedModule) { + Write-Verbose \"Importing installed $($installedModule.Name) version $($installedModule.Version)\" + Import-Module $installedModule -Force + } + else { + throw \"$moduleName $requiredVersion is not installed, and could not be downloaded from the PowerShell gallery\" + } +} + +function InstallPowerShellGet { + [CmdletBinding()] + Param() + $psget = GetHighestInstalledModule PowerShellGet + if (!$psget) + { + Write-Warning @\" +Cannot access the PowerShell Gallery because PowerShellGet is not installed. +To install PowerShellGet, either upgrade to PowerShell 5 or install the PackageManagement MSI. +See https://docs.microsoft.com/en-us/powershell/gallery/installing-psget for more details. +\"@ + throw \"PowerShellGet is not available\" + } + + if ($psget.Version -lt [Version]'1.6') { + #Bootstrap the NuGet package provider, which updates NuGet without requiring admin rights + Write-Debug \"Installing NuGet package provider\" + Get-PackageProvider NuGet -ForceBootstrap | Out-Null + + #Use the currently-installed version of PowerShellGet + Import-PackageProvider PowerShellGet + + #Download the version of PowerShellGet that we actually need + Write-Debug \"Installing PowershellGet\" + Save-Module -Name PowerShellGet -Path $LocalModules -MinimumVersion 1.6 -Force -ErrorAction SilentlyContinue + } + + Write-Debug \"Importing PowershellGet\" + Import-Module PowerShellGet -MinimumVersion 1.6 -Force + #Make sure we're actually using the package provider from the imported version of PowerShellGet + Import-PackageProvider ((Get-Module PowerShellGet).Path) | Out-Null +} + +function InstallLocalModule { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$moduleName, + [Parameter(Mandatory = $false)] + [Version]$minimumVersion, + [Parameter(Mandatory = $false)] + [Version]$maximumVersion + ) + try { + InstallPowerShellGet + + Write-Debug \"Install $moduleName $requiredVersion\" + Save-Module -Name $moduleName -Path $LocalModules -Force -AcceptLicense -MinimumVersion $minimumVersion -MaximumVersion $maximumVersion -ErrorAction Stop + } + catch { + Write-Warning \"Could not install $moduleName $requiredVersion from any registered PSRepository\" + } +} + +function GetHighestInstalledModule { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [string] $moduleName, + + [Parameter(Mandatory = $false)] + [Version]$minimumVersion, + [Parameter(Mandatory = $false)] + [Version]$maximumVersion + ) + + return Get-Module $moduleName -ListAvailable | + Where {(!$minimumVersion -or ($_.Version -ge $minimumVersion)) -and (!$maximumVersion -or ($_.Version -le $maximumVersion))} | + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} | + Select -First 1 +} + +function GetHighestInstallableModule { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [string] $moduleName + ) + + try { + InstallPowerShellGet + Find-Module SqlChangeAutomation -AllVersions | + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} | + Select -First 1 + } + catch { + Write-Warning \"Could not find any suitable versions of $moduleName from any registered PSRepository\" + } +} + +function GetInstalledSqlChangeAutomationVersion { + $scaModule = (Get-Module $SqlChangeAutomationModuleName) + + if ($scaModule -ne $null) { + return $scaModule.Version + } + + $dlmaModule = (Get-Module $DlmAutomationModuleName) + + if ($dlmaModule -ne $null) { + return $dlmaModule.Version + } + + return $null +} + + +$ErrorActionPreference = 'Stop' +$VerbosePreference = 'Continue' + +# Set process level FUR environment +$env:REDGATE_FUR_ENVIRONMENT = \"Octopus Step Templates\" + +#Helper functions for paramter handling +function Required() { + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $true)][string]$Name + ) + if ([string]::IsNullOrWhiteSpace($Parameter)) { throw \"You must enter a value for '$Name'\" } +} +function Optional() { + #Default is untyped here - if we specify [string] powershell will convert nulls into empty string + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $false)]$Default + ) + if ([string]::IsNullOrWhiteSpace($Parameter)) { + $Default + } else { + $Parameter + } +} +function RequireBool() { + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $true)][string]$Name + ) + $Result = $False + if (![bool]::TryParse($Parameter , [ref]$Result )) { throw \"'$Name' must be a boolean value.\" } + $Result +} +function RequirePositiveNumber() { + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $true)][string]$Name + ) + $Result = 0 + if (![int32]::TryParse($Parameter , [ref]$Result )) { throw \"'$Name' must be a numerical value.\" } + if ($Result -lt 0) { throw \"'$Name' must be >= 0.\" } + $Result +} + +$SpecificModuleVersion = Optional -Parameter $SpecificModuleVersion +InstallCorrectSqlChangeAutomation -requiredVersion $SpecificModuleVersion + +# Check if SQL Change Automation is installed.\t +$powershellModule = Get-Module -Name SqlChangeAutomation\t +if ($powershellModule -eq $null) { \t + throw \"Cannot find SQL Change Automation on your Octopus Tentacle. If SQL Change Automation is installed, try restarting the Tentacle service for it to be detected.\"\t +} + +$currentVersion = $powershellModule.Version\t +$minimumRequiredVersion = [version] '3.0.3'\t +if ($currentVersion -lt $minimumRequiredVersion) { \t + throw \"This step requires SQL Change Automation version $minimumRequiredVersion or later. The current version is $currentVersion. The latest version can be found at http://www.red-gate.com/sca/productpage\"\t +} + +$minimumRequiredVersionDataCompareOptions = [version] '3.3.0' + +# Check the parameters. +Required -Parameter $DLMAutomationDeploymentResourcesPath -Name 'Export path' +Required -Parameter $DLMAutomationDatabaseServer -Name 'Target SQL Server instance' +Required -Parameter $DLMAutomationDatabaseName -Name 'Target database name' +$DLMAutomationDatabaseUsername = Optional -Parameter $DLMAutomationDatabaseUsername +$DLMAutomationDatabasePassword = Optional -Parameter $DLMAutomationDatabasePassword +$DLMAutomationSkipPostUpdateSchemaCheck = Optional -Parameter $DLMAutomationSkipPostUpdateSchemaCheck -Default \"False\" +$DLMAutomationQueryBatchTimeout = Optional -Parameter $DLMAutomationQueryBatchTimeout -Default '30' + +$skipPostUpdateSchemaCheck = RequireBool -Parameter $DLMAutomationSkipPostUpdateSchemaCheck -Name 'Skip post update schema check' +$queryBatchTimeout = RequirePositiveNumber -Parameter $DLMAutomationQueryBatchTimeout -Name 'Query Batch Timeout' + +# Check whether database deployment resources export path exists and is a valid directory path +if((Test-Path $DLMAutomationDeploymentResourcesPath) -eq $true) { + if((Get-Item $DLMAutomationDeploymentResourcesPath) -isnot [System.IO.DirectoryInfo]) { + throw \"The export path is not a valid folder: $DLMAutomationDeploymentResourcesPath\" + } +} else { + throw \"The export path folder doesn't exist, or the current Windows account can't access it: $DLMAutomationDeploymentResourcesPath\" +} + +# Constructing the unique export path. +$nugetPackageId = $OctopusParameters[\"Octopus.Action.Package[DLMAutomationPackageName].PackageId\"] +$projectId = $OctopusParameters['Octopus.Project.Id'] +$releaseNumber = $OctopusParameters['Octopus.Release.Number'] +$exportPath = Join-Path (Join-Path (Join-Path $DLMAutomationDeploymentResourcesPath $projectId) $releaseNumber) $nugetPackageId + +# Create and test connection to the database. +$databaseConnection = New-DatabaseConnection -ServerInstance $DLMAutomationDatabaseServer ` + -Database $DLMAutomationDatabaseName ` + -Username $DLMAutomationDatabaseUsername ` + -Password $DLMAutomationDatabasePassword | Test-DatabaseConnection + +$releaseUrl = $OctopusParameters['Octopus.Web.ServerUri'] + $OctopusParameters['Octopus.Web.DeploymentLink']; +# Import and deploy the release. +Import-DatabaseReleaseArtifact $exportPath | Use-DatabaseReleaseArtifact -DeployTo $databaseConnection -QueryBatchTimeout $queryBatchTimeout -ReleaseUrl $releaseUrl -SkipPostUpdateSchemaCheck:$skipPostUpdateSchemaCheck", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "57b50569-40cb-42b2-80a0-d607fff366ec", + "Name": "DLMAutomationDeploymentResourcesPath", + "Label": "Export path", + "HelpText": "The path the database deployment resources were exported to. + +This should be the same path specified in the \"Redgate - Create Database Release\" step, and must be accessible to all tentacles used in database deployment steps.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "30a84de3-af9a-4c00-b9d4-ad9a96c59df6", + "Name": "DLMAutomationDatabaseServer", + "Label": "Target SQL Server instance", + "HelpText": "The fully qualified SQL Server instance name for the target database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9bd39d00-e163-4051-bce5-635cbab28068", + "Name": "DLMAutomationDatabaseName", + "Label": "Target database name", + "HelpText": "The name of the database to deploy changes to. This must be an existing database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "91c79e89-f988-4ec1-90ec-7ba64e3b7be7", + "Name": "DLMAutomationDatabaseUsername", + "Label": "Username (optional)", + "HelpText": "The SQL Server username used to connect to the database. If you leave this field and 'Password' blank, Windows authentication will be used to connect instead, using the account that runs the Tentacle service.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2074e5f7-9987-411a-bbfe-87ad28c4d3ab", + "Name": "DLMAutomationDatabasePassword", + "Label": "Password (optional)", + "HelpText": "You must enter a password if you entered a username.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "da1aa9b7-3e11-4982-b027-274d6b6c7561", + "Name": "DLMAutomationQueryBatchTimeout", + "Label": "Query batch timeout (in seconds)", + "HelpText": "The execution timeout, in seconds, for each batch of queries in the update script. The default value is 30 seconds. A value of zero indicates no execution timeout.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "411b3ad1-4968-4cdb-b47b-3ddb4eab0468", + "Name": "DLMAutomationSkipPostUpdateSchemaCheck", + "Label": "Skip post update schema check", + "HelpText": "Don't check that the target database has the correct schema after the update has run.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "e824b03b-802c-45c9-ba1e-c1540888789a", + "Name": "SpecificModuleVersion", + "Label": "SQL Change Automation version (optional)", + "HelpText": "If you wish to use a specific version of SQL Change Automation rather than the latest, enter the version number here.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "25a16ceb-d668-4ea9-a645-fbf2001c1615", + "Name": "DLMAutomationPackageName", + "Label": "Package", + "HelpText": "The package which is being deployed", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "61adc6ec-4216-41c8-ab30-dba6cfcd37d0", + "Name": "DLMModuleInstallLocation", + "Label": "SQL Change Automation Install Location (optional)", + "HelpText": "The SQL Change Automation cmdlets will be downloaded from the [PowerShell gallery](https://www.powershellgallery.com/packages/SqlChangeAutomation). Please specify the folder folder where those packages will be saved to. It can be relative or absolute. + + +If this is empty it will default `$Home\\Documents\\WindowsPowerShell\\Modules` which is the [recommended location](https://docs.microsoft.com/en-us/powershell/scripting/developer/module/installing-a-powershell-module?view=powershell-7#where-to-install-modules) from Microsoft.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2020-05-01T15:21:38.717Z", + "OctopusVersion": "2020.1.10", + "Type": "ActionTemplate" + }, + "Category": "redgate" + } diff --git a/step-templates/redgate-deploy-from-database-release.json.human b/step-templates/redgate-deploy-from-database-release.json.human new file mode 100644 index 000000000..7f201e531 --- /dev/null +++ b/step-templates/redgate-deploy-from-database-release.json.human @@ -0,0 +1,517 @@ +{ + "Id": "7d18aeb8-5e69-4c91-aca4-0d71022944e8", + "Name": "Redgate - Deploy from Database Release", + "Description": "Uses the deployment resources from the \u0027Redgate - Create Database Release\u0027 step to deploy the database changes using Redgate\u0027s [SQL Change Automation](http://www.red-gate.com/sca/productpage).\r +\r +Requires SQL Change Automation version 3.0.2 or later.\r +\r +*Version date: 2020-12-21*", + "ActionType": "Octopus.Script", + "Version": 18, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function GetModuleInstallationFolder\r +{\r + if (ModuleInstallationFolderIsValid)\r + {\r + return [System.IO.Path]::GetFullPath($DLMAutomationModuleInstallationFolder)\r + }\r +\r + return \"$PSScriptRoot\\Modules\"\r +}\r +\r +function ModuleInstallationFolderIsValid\r +{\r + if ([string]::IsNullOrWhiteSpace($DLMAutomationModuleInstallationFolder))\r + {\r + return $false\r + }\r +\r + return (Test-Path $DLMAutomationModuleInstallationFolder -IsValid) -eq $true;\r +}\r +\r +$DlmAutomationModuleName = \"DLMAutomation\"\r +$SqlChangeAutomationModuleName = \"SqlChangeAutomation\"\r +$ModulesFolder = GetModuleInstallationFolder\r +$LocalModules = (New-Item \"$ModulesFolder\" -ItemType Directory -Force).FullName\r +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\"\r +\r +function IsScaAvailable\r +{\r + if ((Get-Module $SqlChangeAutomationModuleName) -ne $null) {\r + return $true\r + }\r +\r + return $false\r +}\r +\r +function InstallCorrectSqlChangeAutomation\r +{\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $false)]\r + [Version]$requiredVersion,\r + [Parameter(Mandatory = $false)]\r + [bool]$useInstalledVersion\r + )\r +\r + $moduleName = $SqlChangeAutomationModuleName\r +\r + # this will be null if $requiredVersion is not specified - which is exactly what we want\r + $maximumVersion = $requiredVersion\r +\r + if ($requiredVersion) {\r + if ($requiredVersion.Revision -eq -1) {\r + #If provided with a 3 part version number (the 4th part, revision, == -1), we should allow any value for the revision\r + $maximumVersion = [Version]\"$requiredVersion.$([System.Int32]::MaxValue)\"\r + }\r +\r + if ($requiredVersion.Major -lt 3) {\r + # If the specified version is below V3 then the user is requesting a version of DLMA. We should look for that module name instead\r + $moduleName = $DlmAutomationModuleName\r + }\r + }\r +\r + if ($useInstalledVersion) {\r + Write-Verbose \"Option to use installed version is selected. Skipping update/install using PowerShellGet.\"\r + }\r + else {\r + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r +\r + if (!$installedModule) {\r + #Either SCA isn\u0027t installed at all or $requiredVersion is specified but that version of SCA isn\u0027t installed\r + Write-Verbose \"$moduleName $requiredVersion not available - attempting to download from gallery\"\r + InstallLocalModule -moduleName $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r + }\r + elseif (!$requiredVersion) {\r + #We\u0027ve got a version of SCA installed, but $requiredVersion isn\u0027t specified so we might be able to upgrade\r + $newest = GetHighestInstallableModule $moduleName\r + if ($newest -and ($installedModule.Version -lt $newest.Version)) {\r + Write-Verbose \"Updating $moduleName to version $($newest.Version)\"\r + InstallLocalModule -moduleName $moduleName -minimumVersion $newest.Version\r + }\r + }\r + }\r +\r + # Now we\u0027re done with install/upgrade, try to import the highest available module that matches our version requirements\r +\r + # We can\u0027t just use -minimumVersion and -maximumVersion arguments on Import-Module because PowerShell 3 doesn\u0027t have them,\r + # so we have to find the precise matching installed version using our code, then import that specifically. Note that\r + # $requiredVersion and $maximumVersion might be null when there\u0027s no specific version we need.\r + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r +\r + if (!$installedModule -and !$requiredVersion) {\r + #Did not find SCA, and we don\u0027t have a required version so we might be able to use an installed DLMA instead.\r + Write-Verbose \"$moduleName is not installed - trying to fall back to $DlmAutomationModuleName\"\r + $installedModule = GetHighestInstalledModule $DlmAutomationModuleName\r + }\r +\r + if ($installedModule) {\r + Write-Verbose \"Importing installed $($installedModule.Name) version $($installedModule.Version)\"\r + Import-Module $installedModule -Force\r + }\r + else {\r + throw \"$moduleName $requiredVersion is not installed, and could not be downloaded from the PowerShell gallery\"\r + }\r +}\r +\r +function InstallPowerShellGet {\r + [CmdletBinding()]\r + Param()\r +\r + ConfigureProxyIfVariableSet\r + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12\r +\r + $psget = GetHighestInstalledModule PowerShellGet\r + if (!$psget)\r + {\r + Write-Warning @\"\r +Cannot access the PowerShell Gallery because PowerShellGet is not installed.\r +To install PowerShellGet, either upgrade to PowerShell 5 or install the PackageManagement MSI.\r +See https://docs.microsoft.com/en-us/powershell/gallery/installing-psget for more details.\r +\"@\r + throw \"PowerShellGet is not available\"\r + }\r +\r + if ($psget.Version -lt [Version]\u00271.6\u0027) {\r + #Bootstrap the NuGet package provider, which updates NuGet without requiring admin rights\r + Write-Debug \"Installing NuGet package provider\"\r + Get-PackageProvider NuGet -ForceBootstrap | Out-Null\r +\r + #Use the currently-installed version of PowerShellGet\r + Import-PackageProvider PowerShellGet\r +\r + #Download the version of PowerShellGet that we actually need\r + Write-Debug \"Installing PowershellGet\"\r + Save-Module -Name PowerShellGet -Path $LocalModules -MinimumVersion 1.6 -Force -ErrorAction SilentlyContinue\r + }\r +\r + Write-Debug \"Importing PowershellGet\"\r + Import-Module PowerShellGet -MinimumVersion 1.6 -Force\r + #Make sure we\u0027re actually using the package provider from the imported version of PowerShellGet\r + Import-PackageProvider ((Get-Module PowerShellGet).Path) | Out-Null\r +}\r +\r +function InstallLocalModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true)]\r + [string]$moduleName,\r + [Parameter(Mandatory = $false)]\r + [Version]$minimumVersion,\r + [Parameter(Mandatory = $false)]\r + [Version]$maximumVersion\r + )\r + try {\r + InstallPowerShellGet\r +\r + Write-Debug \"Install $moduleName $requiredVersion\"\r + Save-Module -Name $moduleName -Path $LocalModules -Force -AcceptLicense -MinimumVersion $minimumVersion -MaximumVersion $maximumVersion -ErrorAction Stop\r + }\r + catch {\r + Write-Warning \"Could not install $moduleName $requiredVersion from any registered PSRepository\"\r + }\r +}\r +\r +function GetHighestInstalledModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true, Position = 0)]\r + [string] $moduleName,\r +\r + [Parameter(Mandatory = $false)]\r + [Version]$minimumVersion,\r + [Parameter(Mandatory = $false)]\r + [Version]$maximumVersion\r + )\r +\r + return Get-Module $moduleName -ListAvailable |\r + Where {(!$minimumVersion -or ($_.Version -ge $minimumVersion)) -and (!$maximumVersion -or ($_.Version -le $maximumVersion))} |\r + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} |\r + Select -First 1\r +}\r +\r +function GetHighestInstallableModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true, Position = 0)]\r + [string] $moduleName\r + )\r +\r + try {\r + InstallPowerShellGet\r + Find-Module SqlChangeAutomation -AllVersions |\r + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} |\r + Select -First 1\r + }\r + catch {\r + Write-Warning \"Could not find any suitable versions of $moduleName from any registered PSRepository\"\r + }\r +}\r +\r +function GetInstalledSqlChangeAutomationVersion {\r + $scaModule = (Get-Module $SqlChangeAutomationModuleName)\r +\r + if ($scaModule -ne $null) {\r + return $scaModule.Version\r + }\r +\r + $dlmaModule = (Get-Module $DlmAutomationModuleName)\r +\r + if ($dlmaModule -ne $null) {\r + return $dlmaModule.Version\r + }\r +\r + return $null\r +}\r +\r +function ConfigureProxyIfVariableSet\r +{\r + if ([string]::IsNullOrWhiteSpace($DLMAutomationProxyUrl) -eq $false)\r + {\r + Write-Debug \"Setting DefaultWebProxy to $proxyUrl\"\r +\r + [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy($DLMAutomationProxyUrl)\r + [System.Net.WebRequest]::DefaultWebProxy.credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials\r + [System.Net.WebRequest]::DefaultWebProxy.BypassProxyOnLocal = $True\r + }\r +}\r +\r +\r +$ErrorActionPreference = \u0027Stop\u0027\r +$VerbosePreference = \u0027Continue\u0027\r +\r +# Set process level FUR environment\r +$env:REDGATE_FUR_ENVIRONMENT = \"Octopus Step Templates\"\r +\r +#Helper functions for paramter handling\r +function Required() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + if ([string]::IsNullOrWhiteSpace($Parameter)) { throw \"You must enter a value for \u0027$Name\u0027\" }\r +}\r +function Optional() {\r + #Default is untyped here - if we specify [string] powershell will convert nulls into empty string\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $false)]$Default\r + )\r + if ([string]::IsNullOrWhiteSpace($Parameter)) {\r + $Default\r + } else {\r + $Parameter\r + }\r +}\r +function RequireBool() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + $Result = $False\r + if (![bool]::TryParse($Parameter , [ref]$Result )) { throw \"\u0027$Name\u0027 must be a boolean value.\" }\r + $Result\r +}\r +function RequirePositiveNumber() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + $Result = 0\r + if (![int32]::TryParse($Parameter , [ref]$Result )) { throw \"\u0027$Name\u0027 must be a numerical value.\" }\r + if ($Result -lt 0) { throw \"\u0027$Name\u0027 must be \u003e= 0.\" }\r + $Result\r +}\r +\r +$SpecificModuleVersion = Optional -Parameter $SpecificModuleVersion\r +$UseInstalledModuleVersion = Optional -Parameter $UseInstalledModuleVersion -Default \u0027False\u0027\r +$UseInstalledVersionSwitch = [bool]::Parse($UseInstalledModuleVersion)\r +InstallCorrectSqlChangeAutomation -requiredVersion $SpecificModuleVersion -useInstalledVersion $UseInstalledVersionSwitch\r +\r +# Check if SQL Change Automation is installed.\r +$powershellModule = Get-Module -Name SqlChangeAutomation\r +if ($powershellModule -eq $null) {\r + throw \"Cannot find SQL Change Automation on your Octopus Tentacle. If SQL Change Automation is installed, try restarting the Tentacle service for it to be detected.\"\r +}\r +\r +$currentVersion = $powershellModule.Version\r +$minimumRequiredVersion = [version] \u00273.0.3\u0027\r +if ($currentVersion -lt $minimumRequiredVersion) {\r + throw \"This step requires SQL Change Automation version $minimumRequiredVersion or later. The current version is $currentVersion. The latest version can be found at http://www.red-gate.com/sca/productpage\"\r +}\r +\r +$minimumRequiredVersionDataCompareOptions = [version] \u00273.3.0\u0027\r +$minimumRequiredVersionTrustServerCertificate = [version]\u00274.3.20267\u0027\r +\r +function AreConnectionOptionsHandled($encryptConnection, $trustServerCertificate)\r +{\r + if ([string]::IsNullOrWhiteSpace($currentVersion) -or $currentVersion -ge $minimumRequiredVersionTrustServerCertificate)\r + {\r + return $true\r + }\r + elseif($encryptConnection -or $trustServerCertificate)\r + {\r + Write-Warning \"Encrypt and TrustServerCertificate options require SQL Change Automation version $minimumRequiredVersionTrustServerCertificate or later. The current version is $currentVersion.\"\r + return $false\r + }\r +}\r +\r +# Check the parameters.\r +Required -Parameter $DLMAutomationDeploymentResourcesPath -Name \u0027Export path\u0027\r +Required -Parameter $DLMAutomationNuGetDbPackageDownloadStepName -Name \u0027Database package step\u0027\r +Required -Parameter $DLMAutomationDatabaseServer -Name \u0027Target SQL Server instance\u0027\r +Required -Parameter $DLMAutomationDatabaseName -Name \u0027Target database name\u0027\r +$DLMAutomationDatabaseUsername = Optional -Parameter $DLMAutomationDatabaseUsername\r +$DLMAutomationDatabasePassword = Optional -Parameter $DLMAutomationDatabasePassword\r +$DLMAutomationTrustServerCertificate = Optional -Parameter $DLMAutomationTrustServerCertificate\r +$DLMAutomationEncrypt = Optional -Parameter $DLMAutomationEncrypt\r +$DLMAutomationSkipPostUpdateSchemaCheck = Optional -Parameter $DLMAutomationSkipPostUpdateSchemaCheck -Default \"False\"\r +$DLMAutomationQueryBatchTimeout = Optional -Parameter $DLMAutomationQueryBatchTimeout -Default \u002730\u0027\r +$DLMAutomationModuleInstallationFolder = Optional -Parameter $DLMAutomationModuleInstallationFolder\r +$DLMAutomationProxyUrl = Optional -Parameter $DLMAutomationProxyUrl\r +\r +$skipPostUpdateSchemaCheck = RequireBool -Parameter $DLMAutomationSkipPostUpdateSchemaCheck -Name \u0027Skip post update schema check\u0027\r +$queryBatchTimeout = RequirePositiveNumber -Parameter $DLMAutomationQueryBatchTimeout -Name \u0027Query Batch Timeout\u0027\r +\r +# Check whether database deployment resources export path exists and is a valid directory path\r +if((Test-Path $DLMAutomationDeploymentResourcesPath) -eq $true) {\r + if((Get-Item $DLMAutomationDeploymentResourcesPath) -isnot [System.IO.DirectoryInfo]) {\r + throw \"The export path is not a valid folder: $DLMAutomationDeploymentResourcesPath\"\r + }\r +} else {\r + throw \"The export path folder doesn\u0027t exist, or the current Windows account can\u0027t access it: $DLMAutomationDeploymentResourcesPath\"\r +}\r +\r +# Get the NuGet package ID and validate the step name.\r +$nugetPackageId = $OctopusParameters[\"Octopus.Action[$DLMAutomationNuGetDbPackageDownloadStepName].Package.NuGetPackageId\"]\r +if ($nugetPackageId -eq $null) {\r + throw \"The \u0027Database package download step\u0027 is not a \u0027Deploy a NuGet package\u0027 step: \u0027$DLMAutomationNuGetDbPackageDownloadStepName\u0027\"\r +}\r +\r +# Constructing the unique export path.\r +$projectId = $OctopusParameters[\u0027Octopus.Project.Id\u0027]\r +$releaseNumber = $OctopusParameters[\u0027Octopus.Release.Number\u0027]\r +$exportPath = Join-Path (Join-Path (Join-Path $DLMAutomationDeploymentResourcesPath $projectId) $releaseNumber) $nugetPackageId\r +\r +$connectionOptions = @{ }\r +\r +if(AreConnectionOptionsHandled([bool]::Parse($DLMAutomationEncrypt), [bool]::Parse($DLMAutomationTrustServerCertificate))) {\r + $connectionOptions += @{ \u0027Encrypt\u0027 = [bool]::Parse($DLMAutomationEncrypt) }\r + $connectionOptions += @{ \u0027TrustServerCertificate\u0027 = [bool]::Parse($DLMAutomationTrustServerCertificate) }\r +}\r +\r +# Create and test connection to the database.\r +$databaseConnection = New-DatabaseConnection @connectionOptions `\r + -ServerInstance $DLMAutomationDatabaseServer `\r + -Database $DLMAutomationDatabaseName `\r + -Username $DLMAutomationDatabaseUsername `\r + -Password $DLMAutomationDatabasePassword | Test-DatabaseConnection\r +\r +$releaseUrl = $OctopusParameters[\u0027#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}\u0027] + $OctopusParameters[\u0027Octopus.Web.DeploymentLink\u0027];\r +# Import and deploy the release.\r +Import-DatabaseReleaseArtifact $exportPath | Use-DatabaseReleaseArtifact -DeployTo $databaseConnection -QueryBatchTimeout $queryBatchTimeout -ReleaseUrl $releaseUrl -SkipPostUpdateSchemaCheck:$skipPostUpdateSchemaCheck\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": { + + }, + "Parameters": [ + { + "Name": "DLMAutomationDeploymentResourcesPath", + "Label": "Export path", + "HelpText": "The path the database deployment resources were exported to. + +This should be the same path specified in the \"Redgate - Create Database Release\" step, and must be accessible to all tentacles used in database deployment steps.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationNuGetDbPackageDownloadStepName", + "Label": "Database package step", + "HelpText": "Select the step in this project which downloads the database package.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Name": "DLMAutomationDatabaseServer", + "Label": "Target SQL Server instance", + "HelpText": "The fully qualified SQL Server instance name for the target database.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationDatabaseName", + "Label": "Target database name", + "HelpText": "The name of the database to deploy changes to. This must be an existing database.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationDatabaseUsername", + "Label": "Username (optional)", + "HelpText": "The SQL Server username used to connect to the database. If you leave this field and \u0027Password\u0027 blank, Windows authentication will be used to connect instead, using the account that runs the Tentacle service.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationDatabasePassword", + "Label": "Password (optional)", + "HelpText": "You must enter a password if you entered a username.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "DLMAutomationEncrypt", + "Label": "Encrypt", + "HelpText": "Specify whether SSL encryption is used by SQL Server when a certificate is installed.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationTrustServerCertificate", + "Label": "Trust Server Certificate", + "HelpText": "Specify whether to force SQL Server to skip certificate validation.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationQueryBatchTimeout", + "Label": "Query batch timeout (in seconds)", + "HelpText": "The execution timeout, in seconds, for each batch of queries in the update script. The default value is 30 seconds. A value of zero indicates no execution timeout.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationSkipPostUpdateSchemaCheck", + "Label": "Skip post update schema check", + "HelpText": "Don\u0027t check that the target database has the correct schema after the update has run.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "SpecificModuleVersion", + "Label": "SQL Change Automation version (optional)", + "HelpText": "If you wish to use a specific version of SQL Change Automation rather than the latest, enter the version number here.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "UseInstalledModuleVersion", + "Label": "Only use a version of SQL Change Automation that is already installed", + "HelpText": "This prevents attempting to access PowerShell Gallery, which can be helpful when the build agent does not have access to the internet", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationModuleInstallationFolder", + "Label": "Module Installation Folder (optional)", + "HelpText": "By default, module folders do not persist between steps. Setting this field to a specific folder will ensure that modules persist, and do not have to be downloaded again.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationProxyUrl", + "Label": "Proxy URL (optional)", + "HelpText": "By default, no proxy is used when connecting to Powershell Gallery. Alternatively, a proxy URL can be specified here that can be used for Powershell Gallery.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-03-19T10:01:37.125+00:00", + "LastModifiedBy": "benimac93", + "$Meta": { + "ExportedAt": "2021-08-23T12:40:10.975Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "Category": "redgate" +} diff --git a/step-templates/redgate-deploy-from-database.json.human b/step-templates/redgate-deploy-from-database.json.human new file mode 100644 index 000000000..222545ac3 --- /dev/null +++ b/step-templates/redgate-deploy-from-database.json.human @@ -0,0 +1,609 @@ +{ + "Id": "548a661f-0c77-479d-acbd-da1e0875df1d", + "Name": "Redgate - Deploy from Database", + "Description": "Uses Redgate\u0027s [SQL Change Automation](http://www.red-gate.com/sca/productpage) to deploy a source schema to a SQL Server database without a review step.\r +\r +Requires SQL Change Automation version 3.0.2 or later.\r +\r +*Version date: 2020-12-21*", + "ActionType": "Octopus.Script", + "Version": 37, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function GetModuleInstallationFolder\r +{\r + if (ModuleInstallationFolderIsValid)\r + {\r + return [System.IO.Path]::GetFullPath($DLMAutomationModuleInstallationFolder)\r + }\r +\r + return \"$PSScriptRoot\\Modules\"\r +}\r +\r +function ModuleInstallationFolderIsValid\r +{\r + if ([string]::IsNullOrWhiteSpace($DLMAutomationModuleInstallationFolder))\r + {\r + return $false\r + }\r +\r + return (Test-Path $DLMAutomationModuleInstallationFolder -IsValid) -eq $true;\r +}\r +\r +$DlmAutomationModuleName = \"DLMAutomation\"\r +$SqlChangeAutomationModuleName = \"SqlChangeAutomation\"\r +$ModulesFolder = GetModuleInstallationFolder\r +$LocalModules = (New-Item \"$ModulesFolder\" -ItemType Directory -Force).FullName\r +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\"\r +\r +function IsScaAvailable\r +{\r + if ((Get-Module $SqlChangeAutomationModuleName) -ne $null) {\r + return $true\r + }\r +\r + return $false\r +}\r +\r +function InstallCorrectSqlChangeAutomation\r +{\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $false)]\r + [Version]$requiredVersion,\r + [Parameter(Mandatory = $false)]\r + [bool]$useInstalledVersion\r + )\r +\r + $moduleName = $SqlChangeAutomationModuleName\r +\r + # this will be null if $requiredVersion is not specified - which is exactly what we want\r + $maximumVersion = $requiredVersion\r +\r + if ($requiredVersion) {\r + if ($requiredVersion.Revision -eq -1) {\r + #If provided with a 3 part version number (the 4th part, revision, == -1), we should allow any value for the revision\r + $maximumVersion = [Version]\"$requiredVersion.$([System.Int32]::MaxValue)\"\r + }\r +\r + if ($requiredVersion.Major -lt 3) {\r + # If the specified version is below V3 then the user is requesting a version of DLMA. We should look for that module name instead\r + $moduleName = $DlmAutomationModuleName\r + }\r + }\r +\r + if ($useInstalledVersion) {\r + Write-Verbose \"Option to use installed version is selected. Skipping update/install using PowerShellGet.\"\r + }\r + else {\r + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r +\r + if (!$installedModule) {\r + #Either SCA isn\u0027t installed at all or $requiredVersion is specified but that version of SCA isn\u0027t installed\r + Write-Verbose \"$moduleName $requiredVersion not available - attempting to download from gallery\"\r + InstallLocalModule -moduleName $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r + }\r + elseif (!$requiredVersion) {\r + #We\u0027ve got a version of SCA installed, but $requiredVersion isn\u0027t specified so we might be able to upgrade\r + $newest = GetHighestInstallableModule $moduleName\r + if ($newest -and ($installedModule.Version -lt $newest.Version)) {\r + Write-Verbose \"Updating $moduleName to version $($newest.Version)\"\r + InstallLocalModule -moduleName $moduleName -minimumVersion $newest.Version\r + }\r + }\r + }\r +\r + # Now we\u0027re done with install/upgrade, try to import the highest available module that matches our version requirements\r +\r + # We can\u0027t just use -minimumVersion and -maximumVersion arguments on Import-Module because PowerShell 3 doesn\u0027t have them,\r + # so we have to find the precise matching installed version using our code, then import that specifically. Note that\r + # $requiredVersion and $maximumVersion might be null when there\u0027s no specific version we need.\r + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r +\r + if (!$installedModule -and !$requiredVersion) {\r + #Did not find SCA, and we don\u0027t have a required version so we might be able to use an installed DLMA instead.\r + Write-Verbose \"$moduleName is not installed - trying to fall back to $DlmAutomationModuleName\"\r + $installedModule = GetHighestInstalledModule $DlmAutomationModuleName\r + }\r +\r + if ($installedModule) {\r + Write-Verbose \"Importing installed $($installedModule.Name) version $($installedModule.Version)\"\r + Import-Module $installedModule -Force\r + }\r + else {\r + throw \"$moduleName $requiredVersion is not installed, and could not be downloaded from the PowerShell gallery\"\r + }\r +}\r +\r +function InstallPowerShellGet {\r + [CmdletBinding()]\r + Param()\r +\r + ConfigureProxyIfVariableSet\r + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12\r +\r + $psget = GetHighestInstalledModule PowerShellGet\r + if (!$psget)\r + {\r + Write-Warning @\"\r +Cannot access the PowerShell Gallery because PowerShellGet is not installed.\r +To install PowerShellGet, either upgrade to PowerShell 5 or install the PackageManagement MSI.\r +See https://docs.microsoft.com/en-us/powershell/gallery/installing-psget for more details.\r +\"@\r + throw \"PowerShellGet is not available\"\r + }\r +\r + if ($psget.Version -lt [Version]\u00271.6\u0027) {\r + #Bootstrap the NuGet package provider, which updates NuGet without requiring admin rights\r + Write-Debug \"Installing NuGet package provider\"\r + Get-PackageProvider NuGet -ForceBootstrap | Out-Null\r +\r + #Use the currently-installed version of PowerShellGet\r + Import-PackageProvider PowerShellGet\r +\r + #Download the version of PowerShellGet that we actually need\r + Write-Debug \"Installing PowershellGet\"\r + Save-Module -Name PowerShellGet -Path $LocalModules -MinimumVersion 1.6 -Force -ErrorAction SilentlyContinue\r + }\r +\r + Write-Debug \"Importing PowershellGet\"\r + Import-Module PowerShellGet -MinimumVersion 1.6 -Force\r + #Make sure we\u0027re actually using the package provider from the imported version of PowerShellGet\r + Import-PackageProvider ((Get-Module PowerShellGet).Path) | Out-Null\r +}\r +\r +function InstallLocalModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true)]\r + [string]$moduleName,\r + [Parameter(Mandatory = $false)]\r + [Version]$minimumVersion,\r + [Parameter(Mandatory = $false)]\r + [Version]$maximumVersion\r + )\r + try {\r + InstallPowerShellGet\r +\r + Write-Debug \"Install $moduleName $requiredVersion\"\r + Save-Module -Name $moduleName -Path $LocalModules -Force -AcceptLicense -MinimumVersion $minimumVersion -MaximumVersion $maximumVersion -ErrorAction Stop\r + }\r + catch {\r + Write-Warning \"Could not install $moduleName $requiredVersion from any registered PSRepository\"\r + }\r +}\r +\r +function GetHighestInstalledModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true, Position = 0)]\r + [string] $moduleName,\r +\r + [Parameter(Mandatory = $false)]\r + [Version]$minimumVersion,\r + [Parameter(Mandatory = $false)]\r + [Version]$maximumVersion\r + )\r +\r + return Get-Module $moduleName -ListAvailable |\r + Where {(!$minimumVersion -or ($_.Version -ge $minimumVersion)) -and (!$maximumVersion -or ($_.Version -le $maximumVersion))} |\r + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} |\r + Select -First 1\r +}\r +\r +function GetHighestInstallableModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true, Position = 0)]\r + [string] $moduleName\r + )\r +\r + try {\r + InstallPowerShellGet\r + Find-Module SqlChangeAutomation -AllVersions |\r + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} |\r + Select -First 1\r + }\r + catch {\r + Write-Warning \"Could not find any suitable versions of $moduleName from any registered PSRepository\"\r + }\r +}\r +\r +function GetInstalledSqlChangeAutomationVersion {\r + $scaModule = (Get-Module $SqlChangeAutomationModuleName)\r +\r + if ($scaModule -ne $null) {\r + return $scaModule.Version\r + }\r +\r + $dlmaModule = (Get-Module $DlmAutomationModuleName)\r +\r + if ($dlmaModule -ne $null) {\r + return $dlmaModule.Version\r + }\r +\r + return $null\r +}\r +\r +function ConfigureProxyIfVariableSet\r +{\r + if ([string]::IsNullOrWhiteSpace($DLMAutomationProxyUrl) -eq $false)\r + {\r + Write-Debug \"Setting DefaultWebProxy to $proxyUrl\"\r +\r + [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy($DLMAutomationProxyUrl)\r + [System.Net.WebRequest]::DefaultWebProxy.credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials\r + [System.Net.WebRequest]::DefaultWebProxy.BypassProxyOnLocal = $True\r + }\r +}\r +\r +\r +$ErrorActionPreference = \u0027Stop\u0027\r +$VerbosePreference = \u0027Continue\u0027\r +\r +# Set process level FUR environment\r +$env:REDGATE_FUR_ENVIRONMENT = \"Octopus Step Templates\"\r +\r +#Helper functions for paramter handling\r +function Required() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + if ([string]::IsNullOrWhiteSpace($Parameter)) { throw \"You must enter a value for \u0027$Name\u0027\" }\r +}\r +function Optional() {\r + #Default is untyped here - if we specify [string] powershell will convert nulls into empty string\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $false)]$Default\r + )\r + if ([string]::IsNullOrWhiteSpace($Parameter)) {\r + $Default\r + } else {\r + $Parameter\r + }\r +}\r +function RequireBool() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + $Result = $False\r + if (![bool]::TryParse($Parameter , [ref]$Result )) { throw \"\u0027$Name\u0027 must be a boolean value.\" }\r + $Result\r +}\r +function RequirePositiveNumber() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + $Result = 0\r + if (![int32]::TryParse($Parameter , [ref]$Result )) { throw \"\u0027$Name\u0027 must be a numerical value.\" }\r + if ($Result -lt 0) { throw \"\u0027$Name\u0027 must be \u003e= 0.\" }\r + $Result\r +}\r +\r +$SpecificModuleVersion = Optional -Parameter $SpecificModuleVersion\r +$UseInstalledModuleVersion = Optional -Parameter $UseInstalledModuleVersion -Default \u0027False\u0027\r +$UseInstalledVersionSwitch = [bool]::Parse($UseInstalledModuleVersion)\r +InstallCorrectSqlChangeAutomation -requiredVersion $SpecificModuleVersion -useInstalledVersion $UseInstalledVersionSwitch\r +\r +# Check if SQL Change Automation is installed.\r +$powershellModule = Get-Module -Name SqlChangeAutomation\r +if ($powershellModule -eq $null) {\r + throw \"Cannot find SQL Change Automation on your Octopus Tentacle. If SQL Change Automation is installed, try restarting the Tentacle service for it to be detected.\"\r +}\r +\r +$currentVersion = $powershellModule.Version\r +$minimumRequiredVersion = [version] \u00273.0.3\u0027\r +if ($currentVersion -lt $minimumRequiredVersion) {\r + throw \"This step requires SQL Change Automation version $minimumRequiredVersion or later. The current version is $currentVersion. The latest version can be found at http://www.red-gate.com/sca/productpage\"\r +}\r +\r +$minimumRequiredVersionDataCompareOptions = [version] \u00273.3.0\u0027\r +$minimumRequiredVersionTrustServerCertificate = [version]\u00274.3.20267\u0027\r +\r +function AreConnectionOptionsHandled($encryptConnection, $trustServerCertificate)\r +{\r + if ([string]::IsNullOrWhiteSpace($currentVersion) -or $currentVersion -ge $minimumRequiredVersionTrustServerCertificate)\r + {\r + return $true\r + }\r + elseif($encryptConnection -or $trustServerCertificate)\r + {\r + Write-Warning \"Encrypt and TrustServerCertificate options require SQL Change Automation version $minimumRequiredVersionTrustServerCertificate or later. The current version is $currentVersion.\"\r + return $false\r + }\r +}\r +\r +# Check the parameters.\r +Required -Parameter $DLMAutomationSourceDatabaseServer -Name \u0027Source SQL Server instance\u0027\r +Required -Parameter $DLMAutomationSourceDatabaseName -Name \u0027Source database name\u0027\r +$DLMAutomationSourceUsername = Optional -Parameter $DLMAutomationSourceUsername\r +$DLMAutomationSourcePassword = Optional -Parameter $DLMAutomationSourcePassword\r +$DLMAutomationSourceTrustServerCertificate = Optional -Parameter $DLMAutomationSourceTrustServerCertificate\r +$DLMAutomationSourceEncrypt = Optional -Parameter $DLMAutomationSourceEncrypt\r +Required -Parameter $DLMAutomationTargetDatabaseServer -Name \u0027Target SQL Server instance\u0027\r +Required -Parameter $DLMAutomationTargetDatabaseName -Name \u0027Target database name\u0027\r +$DLMAutomationTargetUsername = Optional -Parameter $DLMAutomationTargetUsername\r +$DLMAutomationTargetPassword = Optional -Parameter $DLMAutomationTargetPassword\r +$DLMAutomationTargetTrustServerCertificate = Optional -Parameter $DLMAutomationTargetTrustServerCertificate\r +$DLMAutomationTargetEncrypt = Optional -Parameter $DLMAutomationTargetEncrypt\r +$DLMAutomationFilterPath = Optional -Parameter $DLMAutomationFilterPath\r +$DLMAutomationCompareOptions = Optional -Parameter $DLMAutomationCompareOptions\r +$DLMAutomationDataCompareOptions = Optional -Parameter $DLMAutomationDataCompareOptions\r +$DLMAutomationTransactionIsolationLevel = Optional -Parameter $DLMAutomationTransactionIsolationLevel -Default \u0027Serializable\u0027\r +$DLMAutomationSkipPostUpdateSchemaCheck = Optional -Parameter $DLMAutomationSkipPostUpdateSchemaCheck -Default \"False\"\r +$DLMAutomationQueryBatchTimeout = Optional -Parameter $DLMAutomationQueryBatchTimeout -Default \u002730\u0027\r +$DLMAutomationModuleInstallationFolder = Optional -Parameter $DLMAutomationModuleInstallationFolder\r +$DLMAutomationProxyUrl = Optional -Parameter $DLMAutomationProxyUrl\r +\r +$skipPostUpdateSchemaCheck = RequireBool -Parameter $DLMAutomationSkipPostUpdateSchemaCheck -Name \u0027Skip post update schema check\u0027\r +$queryBatchTimeout = RequirePositiveNumber -Parameter $DLMAutomationQueryBatchTimeout -Name \u0027Query Batch Timeout\u0027\r +\r +\r +$targetConnectionOptions = @{ }\r +$sourceConnectionOptions = @{ }\r +\r +if(AreConnectionOptionsHandled([bool]::Parse($DLMAutomationTargetEncrypt) -or [bool]::Parse($DLMAutomationSourceEncrypt), `\r + [bool]::Parse($DLMAutomationTargetTrustServerCertificate) -or [bool]::Parse($DLMAutomationSourceTrustServerCertificate))) {\r + $targetConnectionOptions += @{ \u0027Encrypt\u0027 = [bool]::Parse($DLMAutomationTargetEncrypt) }\r + $targetConnectionOptions += @{ \u0027TrustServerCertificate\u0027 = [bool]::Parse($DLMAutomationTargetTrustServerCertificate) }\r +\t$sourceConnectionOptions += @{ \u0027Encrypt\u0027 = [bool]::Parse($DLMAutomationSourceEncrypt) }\r + $sourceConnectionOptions += @{ \u0027TrustServerCertificate\u0027 = [bool]::Parse($DLMAutomationSourceTrustServerCertificate) }\r +}\r +\r +$targetDB = New-DatabaseConnection @targetConnectionOptions `\r + -ServerInstance $DLMAutomationTargetDatabaseServer `\r + -Database $DLMAutomationTargetDatabaseName `\r + -Username $DLMAutomationTargetUsername `\r + -Password $DLMAutomationTargetPassword | Test-DatabaseConnection\r +\r +$sourceDB = New-DatabaseConnection @sourceConnectionOptions `\r +\t\t\t\t\t\t\t\t -ServerInstance $DLMAutomationSourceDatabaseServer `\r + -Database $DLMAutomationSourceDatabaseName `\r + -Username $DLMAutomationSourceUsername `\r + -Password $DLMAutomationSourcePassword | Test-DatabaseConnection\r +\r +# Create the deployment resources, only adding the arguments that are not null or empty.\r +$releaseParams = @{\r + Target = $targetDB\r + Source = $sourceDB\r + TransactionIsolationLevel = $DLMAutomationTransactionIsolationLevel\r + FilterPath = $DLMAutomationFilterPath\r + SQLCompareOptions = $DLMAutomationCompareOptions\r +}\r +\r +if($currentVersion -ge $minimumRequiredVersionDataCompareOptions) {\r + $releaseParams.SQLDataCompareOptions = $DLMAutomationDataCompareOptions\r +} elseif(-not [string]::IsNullOrWhiteSpace($DLMAutomationDataCompareOptions)) {\r + Write-Warning \"SQL Data Compare options requires SQL Change Automation version $minimumRequiredVersionDataCompareOptions or later. The current version is $currentVersion.\"\r +}\r +\r +$release = New-DatabaseReleaseArtifact @releaseParams\r +\r +$releaseUrl = $OctopusParameters[\u0027#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}\u0027] + $OctopusParameters[\u0027Octopus.Web.DeploymentLink\u0027];\r +# Deploy the source schema to the target database.\r +$release | Use-DatabaseReleaseArtifact -DeployTo $targetDB -SkipPreUpdateSchemaCheck -QueryBatchTimeout $queryBatchTimeout -ReleaseUrl $releaseUrl -SkipPostUpdateSchemaCheck:$skipPostUpdateSchemaCheck\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": { + + }, + "Parameters": [ + { + "Name": "DLMAutomationSourceDatabaseServer", + "Label": "Source SQL Server instance", + "HelpText": "The fully qualified instance name of the SQL Server that hosts the source database.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationSourceDatabaseName", + "Label": "Source database name", + "HelpText": "The name of the database with the source schema (the schema that will be deployed).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationSourceUsername", + "Label": "Username (optional)", + "HelpText": "The SQL Server username used to connect to the source database. If you leave this field and \u0027Password\u0027 blank, Windows authentication will be used to connect instead, using the account that runs the Tentacle service.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationSourcePassword", + "Label": "Password (optional)", + "HelpText": "You must enter a password if you entered a username for source database.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "DLMAutomationSourceEncrypt", + "Label": "Encrypt", + "HelpText": "Specify whether SSL encryption is used by SQL Server when a certificate is installed.", + "DefaultValue": "true", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationSourceTrustServerCertificate", + "Label": "Trust Server Certificate", + "HelpText": "Specify whether to force SQL Server to skip certificate validation.", + "DefaultValue": "true", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationTargetDatabaseServer", + "Label": "Target SQL Server instance", + "HelpText": "The fully qualified SQL Server instance name for the target database.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationTargetDatabaseName", + "Label": "Target database name", + "HelpText": "The name of the database to deploy changes to. This must be an existing database.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationTargetUsername", + "Label": "Username (optional)", + "HelpText": "The SQL Server username used to connect to the target database. If you leave this field and \u0027Password\u0027 blank, Windows authentication will be used to connect instead, using the account that runs the Tentacle service.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationTargetPassword", + "Label": "Password (optional)", + "HelpText": "You must enter a password if you entered a username for target database.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "DLMAutomationTargetEncrypt", + "Label": "Encrypt", + "HelpText": "Specify whether SSL encryption is used by SQL Server when a certificate is installed.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationTargetTrustServerCertificate", + "Label": "Trust Server Certificate", + "HelpText": "Specify whether to force SQL Server to skip certificate validation.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationFilterPath", + "Label": "Filter path (optional)", + "HelpText": "Specify the location of a SQL Compare filter file (.scpf), which defines objects to include/exclude in the schema comparison. Filter files are generated by SQL Source Control. + +For more help see [Using SQL Compare filters in SQL Change Automation](http://www.red-gate.com/sca/ps/help/filters).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationCompareOptions", + "Label": "SQL Compare options (optional)", + "HelpText": "Enter SQL Compare options to apply when generating the update script. Use a comma-separated list to enter multiple values. For more help see [Using SQL Compare options in SQL Change Automation](http://www.red-gate.com/sca/add-ons/compare-options).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationDataCompareOptions", + "Label": "SQL Data Compare options (optional)", + "HelpText": "Enter SQL Data Compare options to apply when generating the update script. Use a comma-separated list to enter multiple values. For more help see [Using SQL Data Compare options in SQL Change Automation](http://www.red-gate.com/sca/ps/help/datacompareoptions).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationTransactionIsolationLevel", + "Label": "Transaction isolation level (optional)", + "HelpText": "Select the transaction isolation level to be used in deployment scripts.", + "DefaultValue": "Serializable", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Serializable +Snapshot +RepeatableRead +ReadCommitted +ReadUncommitted" + } + }, + { + "Name": "DLMAutomationQueryBatchTimeout", + "Label": "Query batch timeout (in seconds)", + "HelpText": "The execution timeout, in seconds, for each batch of queries in the update script. The default value is 30 seconds. A value of zero indicates no execution timeout.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationSkipPostUpdateSchemaCheck", + "Label": "Skip post update schema check", + "HelpText": "Don\u0027t check that the target database has the correct schema after the update has run.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "SpecificModuleVersion", + "Label": "SQL Change Automation version (optional)", + "HelpText": "If you wish to use a specific version of SQL Change Automation rather than the latest, enter the version number here.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "UseInstalledModuleVersion", + "Label": "Only use a version of SQL Change Automation that is already installed", + "HelpText": "This prevents attempting to access PowerShell Gallery, which can be helpful when the build agent does not have access to the internet", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationModuleInstallationFolder", + "Label": "Module Installation Folder (optional)", + "HelpText": "By default, module folders do not persist between steps. Setting this field to a specific folder will ensure that modules persist, and do not have to be downloaded again.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationProxyUrl", + "Label": "Proxy URL (optional)", + "HelpText": "By default, no proxy is used when connecting to Powershell Gallery. Alternatively, a proxy URL can be specified here that can be used for Powershell Gallery.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-03-19T10:01:37.141+00:00", + "LastModifiedBy": "benjimac93", + "$Meta": { + "ExportedAt": "2021-08-23T12:40:10.975Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "Category": "redgate" +} diff --git a/step-templates/redgate-deploy-from-package-worker-friendly.json.human b/step-templates/redgate-deploy-from-package-worker-friendly.json.human new file mode 100644 index 000000000..5911dca9b --- /dev/null +++ b/step-templates/redgate-deploy-from-package-worker-friendly.json.human @@ -0,0 +1,588 @@ +{ + "Id": "02b5b305-92ca-4612-a632-92eb840bd2a8", + "Name": "Redgate - Deploy from Package (Worker Friendly)", + "Description": "Uses Redgate's [SQL Change Automation](http://www.red-gate.com/sca/productpage) to deploy a package containing a database schema to a SQL Server database, without a review step. + +Requires SQL Change Automation version 3.0.2 or later. + +*Version date: 2022-01-24* + +This step template is worker friendly, you can pass in a package reference rather than having to reference a previous step which downloaded the package. This step requires Octopus Deploy **2019.10.0** or higher. + +**NOTE**: This template requires the SQLCMD utility, if not found, the template will install the following: + - Visual Studio 2017 C++ Redistributable + - SQL Server 2017 ODBC driver + - SQLCMD utility", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "8ec6200a-24c9-4273-bdcc-5ff46ce3111f", + "Name": "DLMAutomation.Package.Name", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "DLMAutomationPackageName" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptBody": "$DlmAutomationModuleName = \"DLMAutomation\" +$SqlChangeAutomationModuleName = \"SqlChangeAutomation\" +$ModulesFolder = \"$Home\\Documents\\WindowsPowerShell\\Modules\" + + +if ([string]::IsNullOrWhiteSpace($DLMModuleInstallLocation) -eq $false) +{ +\tif ((Test-Path $DLMModuleInstallLocation -IsValid) -eq $false) + { + \tWrite-Error \"The path $DLMModuleInstallLocation is not valid, please use a relative or absolute path.\" + exit 1 + } + + $ModulesFolder = [System.IO.Path]::GetFullPath($DLMModuleInstallLocation) +} + +Write-Host \"Modules will be installed into $ModulesFolder\" + +$LocalModules = (New-Item \"$ModulesFolder\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\" + +function IsScaAvailable +{ + if ((Get-Module $SqlChangeAutomationModuleName) -ne $null) { + return $true + } + + return $false +} + +function InstallCorrectSqlChangeAutomation +{ + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Version]$requiredVersion + ) + + $moduleName = $SqlChangeAutomationModuleName + + # this will be null if $requiredVersion is not specified - which is exactly what we want + $maximumVersion = $requiredVersion + + if ($requiredVersion) { + if ($requiredVersion.Revision -eq -1) { + #If provided with a 3 part version number (the 4th part, revision, == -1), we should allow any value for the revision + $maximumVersion = [Version]\"$requiredVersion.$([System.Int32]::MaxValue)\" + } + + if ($requiredVersion.Major -lt 3) { + # If the specified version is below V3 then the user is requesting a version of DLMA. We should look for that module name instead + $moduleName = $DlmAutomationModuleName + } + } + + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion + + if (!$installedModule) { + #Either SCA isn't installed at all or $requiredVersion is specified but that version of SCA isn't installed + Write-Verbose \"$moduleName $requiredVersion not available - attempting to download from gallery\" + InstallLocalModule -moduleName $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion + } + elseif (!$requiredVersion) { + #We've got a version of SCA installed, but $requiredVersion isn't specified so we might be able to upgrade + $newest = GetHighestInstallableModule $moduleName + if ($newest -and ($installedModule.Version -lt $newest.Version)) { + Write-Verbose \"Updating $moduleName to version $($newest.Version)\" + InstallLocalModule -moduleName $moduleName -minimumVersion $newest.Version + } + } + + # Now we're done with install/upgrade, try to import the highest available module that matches our version requirements + + # We can't just use -minimumVersion and -maximumVersion arguments on Import-Module because PowerShell 3 doesn't have them, + # so we have to find the precise matching installed version using our code, then import that specifically. Note that + # $requiredVersion and $maximumVersion might be null when there's no specific version we need. + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion + + if (!$installedModule -and !$requiredVersion) { + #Did not find SCA, and we don't have a required version so we might be able to use an installed DLMA instead. + Write-Verbose \"$moduleName is not installed - trying to fall back to $DlmAutomationModuleName\" + $installedModule = GetHighestInstalledModule $DlmAutomationModuleName + } + + if ($installedModule) { + Write-Verbose \"Importing installed $($installedModule.Name) version $($installedModule.Version)\" + Import-Module $installedModule -Force + } + else { + throw \"$moduleName $requiredVersion is not installed, and could not be downloaded from the PowerShell gallery\" + } +} + +function InstallPowerShellGet { + [CmdletBinding()] + Param() + + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + $psget = GetHighestInstalledModule PowerShellGet + if (!$psget) + { + Write-Warning @\" +Cannot access the PowerShell Gallery because PowerShellGet is not installed. +To install PowerShellGet, either upgrade to PowerShell 5 or install the PackageManagement MSI. +See https://docs.microsoft.com/en-us/powershell/gallery/installing-psget for more details. +\"@ + throw \"PowerShellGet is not available\" + } + + if ($psget.Version -lt [Version]'1.6') { + #Bootstrap the NuGet package provider, which updates NuGet without requiring admin rights + Write-Debug \"Installing NuGet package provider\" + Get-PackageProvider NuGet -ForceBootstrap | Out-Null + + #Use the currently-installed version of PowerShellGet + Import-PackageProvider PowerShellGet + + #Download the version of PowerShellGet that we actually need + Write-Debug \"Installing PowershellGet\" + Save-Module -Name PowerShellGet -Path $LocalModules -MinimumVersion 1.6 -Force -ErrorAction SilentlyContinue + } + + Write-Debug \"Importing PowershellGet\" + Import-Module PowerShellGet -MinimumVersion 1.6 -Force + #Make sure we're actually using the package provider from the imported version of PowerShellGet + Import-PackageProvider ((Get-Module PowerShellGet).Path) | Out-Null +} + +function InstallLocalModule { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$moduleName, + [Parameter(Mandatory = $false)] + [Version]$minimumVersion, + [Parameter(Mandatory = $false)] + [Version]$maximumVersion + ) + try { + InstallPowerShellGet + + Write-Debug \"Install $moduleName $requiredVersion\" + Save-Module -Name $moduleName -Path $LocalModules -Force -AcceptLicense -MinimumVersion $minimumVersion -MaximumVersion $maximumVersion -ErrorAction Stop + } + catch { + Write-Warning \"Could not install $moduleName $requiredVersion from any registered PSRepository\" + } +} + +function GetHighestInstalledModule { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [string] $moduleName, + + [Parameter(Mandatory = $false)] + [Version]$minimumVersion, + [Parameter(Mandatory = $false)] + [Version]$maximumVersion + ) + + return Get-Module $moduleName -ListAvailable | + Where {(!$minimumVersion -or ($_.Version -ge $minimumVersion)) -and (!$maximumVersion -or ($_.Version -le $maximumVersion))} | + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} | + Select -First 1 +} + +function GetHighestInstallableModule { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [string] $moduleName + ) + + try { + InstallPowerShellGet + Find-Module SqlChangeAutomation -AllVersions | + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} | + Select -First 1 + } + catch { + Write-Warning \"Could not find any suitable versions of $moduleName from any registered PSRepository\" + } +} + +function GetInstalledSqlChangeAutomationVersion { + $scaModule = (Get-Module $SqlChangeAutomationModuleName) + + if ($scaModule -ne $null) { + return $scaModule.Version + } + + $dlmaModule = (Get-Module $DlmAutomationModuleName) + + if ($dlmaModule -ne $null) { + return $dlmaModule.Version + } + + return $null +} + +$ErrorActionPreference = 'Stop' +$VerbosePreference = 'Continue' + +# Set process level FUR environment +$env:REDGATE_FUR_ENVIRONMENT = \"Octopus Step Templates\" + +#Helper functions for paramter handling +function Required() { + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $true)][string]$Name + ) + if ([string]::IsNullOrWhiteSpace($Parameter)) { throw \"You must enter a value for '$Name'\" } +} +function Optional() { + #Default is untyped here - if we specify [string] powershell will convert nulls into empty string + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $false)]$Default + ) + if ([string]::IsNullOrWhiteSpace($Parameter)) { + $Default + } else { + $Parameter + } +} +function RequireBool() { + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $true)][string]$Name + ) + $Result = $False + if (![bool]::TryParse($Parameter , [ref]$Result )) { throw \"'$Name' must be a boolean value.\" } + $Result +} +function RequirePositiveNumber() { + Param( + [Parameter(Mandatory = $false)][string]$Parameter, + [Parameter(Mandatory = $true)][string]$Name + ) + $Result = 0 + if (![int32]::TryParse($Parameter , [ref]$Result )) { throw \"'$Name' must be a numerical value.\" } + if ($Result -lt 0) { throw \"'$Name' must be >= 0.\" } + $Result +} + +function Get-SqlcmdInstalled +{ +\t# Define variables + $searchPaths = @(\"c:\\program files\\microsoft sql server\", \"c:\\program files (x86)\\microsoft sql server\") + + # Loop through search paths + foreach ($searchPath in $searchPaths) + { + \t# Ensure folder exists + if (Test-Path -Path $searchPath) + { + \t# Search the path + return ($null -ne (Get-ChildItem -Path $searchPath -Recurse | Where-Object {$_.Name -eq \"sqlcmd.exe\"})) + } + } + + # Not found + return $false +} + +$SpecificModuleVersion = Optional -Parameter $SpecificModuleVersion +InstallCorrectSqlChangeAutomation -requiredVersion $SpecificModuleVersion + +# Check if SQL Change Automation is installed.\t +$powershellModule = Get-Module -Name SqlChangeAutomation\t +if ($powershellModule -eq $null) { \t + throw \"Cannot find SQL Change Automation on your Octopus Tentacle. If SQL Change Automation is installed, try restarting the Tentacle service for it to be detected.\"\t +} + +# Check to for sqlcmd +$sqlCmdExists = Get-SqlCmdInstalled + +if ($sqlCmdExists -eq $false) +{ +\tWrite-Verbose \"This template requires the sqlcmd utility, downloading ...\" +\t$tempPath = (New-Item \"$PSScriptRoot\\sqlcmd\" -ItemType Directory -Force).FullName + +\t$sqlCmdUrl = \"\" + $odbcUrl = \"\" + $redistributableUrl = \"\" + + switch ($Env:PROCESSOR_ARCHITECTURE) + { + \t\"AMD64\" + { + \t$sqlCmdUrl = \"https://go.microsoft.com/fwlink/?linkid=2142258\" + $odbcUrl = \"https://go.microsoft.com/fwlink/?linkid=2168524\" + $redistributableUrl = \"https://aka.ms/vs/17/release/vc_redist.x64.exe\" + break + } + \"x86\" + { + \t$sqlCmdUrl = \"https://go.microsoft.com/fwlink/?linkid=2142257\" + $odbcUrl = \"https://go.microsoft.com/fwlink/?linkid=2168713\" + $redistributableUrl = \"https://aka.ms/vs/17/release/vc_redist.x86.exe\" + break + } + } + + Invoke-WebRequest -Uri $sqlCmdUrl -OutFile \"$tempPath\\sqlcmd.msi\" -UseBasicParsing +\tInvoke-WebRequest -Uri $odbcUrl -OutFile \"$tempPath\\msodbc.msi\" -UseBasicParsing +\tInvoke-WebRequest -Uri $redistributableUrl -Outfile \"$tempPath\\vc_redist.exe\" -UseBasicParsing + +\tWrite-Verbose \"Installing Visual Studio 2017 C++ redistrutable prequisite ...\" +\tStart-Process -FilePath \"$tempPath\\vc_redist.exe\" -ArgumentList @(\"/install\", \"/passive\", \"/norestart\") -NoNewWindow -Wait + Write-Verbose \"Installing SQL Server 2017 ODBC driver prequisite ...\" +\tStart-Process -FilePath \"msiexec.exe\" -ArgumentList @(\"/i\", \"$tempPath\\msodbc.msi\", \"IACCEPTMSODBCSQLLICENSETERMS=YES\", \"/qn\") -NoNewWindow -Wait + Write-Verbose \"Installing SQLCMD utility ...\" +\tStart-Process -FilePath \"msiexec.exe\" -ArgumentList @(\"/i\", \"$tempPath\\sqlcmd.msi\", \"IACCEPTMSSQLCMDLNUTILSLICENSETERMS=YES\", \"/qn\") -NoNewWindow -Wait + +\tWrite-Verbose \"Sqlcmd Installation complete!\" +} + +$currentVersion = $powershellModule.Version\t +$minimumRequiredVersion = [version] '3.0.3'\t +if ($currentVersion -lt $minimumRequiredVersion) { \t + throw \"This step requires SQL Change Automation version $minimumRequiredVersion or later. The current version is $currentVersion. The latest version can be found at http://www.red-gate.com/sca/productpage\"\t +} + +$minimumRequiredVersionDataCompareOptions = [version] '3.3.0' + +# Check the parameters. +Required -Parameter $DLMAutomationTargetDatabaseServer -Name 'Target SQL Server instance' +Required -Parameter $DLMAutomationTargetDatabaseName -Name 'Target database name' +$DLMAutomationTargetUsername = Optional -Parameter $DLMAutomationTargetUsername +$DLMAutomationTargetPassword = Optional -Parameter $DLMAutomationTargetPassword +$DLMAutomationFilterPath = Optional -Parameter $DLMAutomationFilterPath +$DLMAutomationCompareOptions = Optional -Parameter $DLMAutomationCompareOptions +$DLMAutomationDataCompareOptions = Optional -Parameter $DLMAutomationDataCompareOptions +$DLMAutomationTransactionIsolationLevel = Optional -Parameter $DLMAutomationTransactionIsolationLevel -Default \"Serializable\" +$DLMAutomationIgnoreStaticData = Optional -Parameter $DLMAutomationIgnoreStaticData -Default 'False' +$DLMAutomationSkipPostUpdateSchemaCheck = Optional -Parameter $DLMAutomationSkipPostUpdateSchemaCheck -Default \"False\" +$DLMAutomationQueryBatchTimeout = Optional -Parameter $DLMAutomationQueryBatchTimeout -Default '30' +$DLMAutomationTrustServerCertificate = [Convert]::ToBoolean($OctopusParameters[\"DLMAutomationTrustServerCertificate\"]) + +$skipPostUpdateSchemaCheck = RequireBool -Parameter $DLMAutomationSkipPostUpdateSchemaCheck -Name 'Skip post update schema check' +$queryBatchTimeout = RequirePositiveNumber -Parameter $DLMAutomationQueryBatchTimeout -Name 'Query Batch Timeout' + +$targetDB = New-DatabaseConnection -ServerInstance $DLMAutomationTargetDatabaseServer -Database $DLMAutomationTargetDatabaseName -Username $DLMAutomationTargetUsername -Password $DLMAutomationTargetPassword -TrustServerCertificate $DLMAutomationTrustServerCertificate | Test-DatabaseConnection + +$packageExtractPath = $OctopusParameters[\"Octopus.Action.Package[DLMAutomation.Package.Name].ExtractedPath\"] +$importedBuildArtifact = Import-DatabaseBuildArtifact -Path $packageExtractPath + +# Only allow sqlcmd variables that don't have special characters like spaces, colon or dashes +$regex = '^[a-zA-Z_][a-zA-Z0-9_]+$' +$sqlCmdVariables = @{} +$OctopusParameters.Keys | Where { $_ -match $regex } | ForEach { +\t$sqlCmdVariables[$_] = $OctopusParameters[$_] +} + +# Create database deployment resources from the NuGet package to the database +$releaseParams = @{ + Target = $targetDB + Source = $importedBuildArtifact + TransactionIsolationLevel = $DLMAutomationTransactionIsolationLevel + IgnoreStaticData = [bool]::Parse($DLMAutomationIgnoreStaticData) + FilterPath = $DLMAutomationFilterPath + SQLCompareOptions = $DLMAutomationCompareOptions + SqlCmdVariables = $sqlCmdVariables +} + +if($currentVersion -ge $minimumRequiredVersionDataCompareOptions) { + $releaseParams.SQLDataCompareOptions = $DLMAutomationDataCompareOptions +} elseif(-not [string]::IsNullOrWhiteSpace($DLMAutomationDataCompareOptions)) { + Write-Warning \"SQL Data Compare options requires SQL Change Automation version $minimumRequiredVersionDataCompareOptions or later. The current version is $currentVersion.\" +} + +$release = New-DatabaseReleaseArtifact @releaseParams + +# Deploy the source schema to the target database. +Write-Host \"Timeout = $queryBatchTimeout\" +$releaseUrl = $OctopusParameters['Octopus.Web.ServerUri'] + $OctopusParameters['Octopus.Web.DeploymentLink']; +$release | Use-DatabaseReleaseArtifact -DeployTo $targetDB -SkipPreUpdateSchemaCheck -QueryBatchTimeout $queryBatchTimeout -ReleaseUrl $releaseUrl -SkipPostUpdateSchemaCheck:$skipPostUpdateSchemaCheck + +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "3aa8db8b-94e7-4a13-8ac3-054ad5456f3f", + "Name": "DLMAutomationPackageName", + "Label": "Package", + "HelpText": "The name of the package to extract and deploy", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "529e5641-44a7-42fe-9a55-72c3c08d2b7d", + "Name": "DLMAutomationTargetDatabaseServer", + "Label": "Target SQL Server instance", + "HelpText": "The fully qualified SQL Server instance name for the target database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "29c48d57-c4f2-4dda-9866-b97a20f84c60", + "Name": "DLMAutomationTargetDatabaseName", + "Label": "Target database name", + "HelpText": "The name of the database to deploy changes to. This must be an existing database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "96c56aaf-ed86-4728-a68c-bac95e9e1040", + "Name": "DLMAutomationTargetUsername", + "Label": "Username (optional)", + "HelpText": "The SQL Server username used to connect to the database. If you leave this field and 'Password' blank, Windows authentication will be used to connect instead, using the account that runs the Tentacle service.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "515c98af-ff2b-488c-bebd-81bff7b82761", + "Name": "DLMAutomationTargetPassword", + "Label": "Password (optional)", + "HelpText": "You must enter a password if you entered a username.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "cfebb7c8-497b-4406-8d64-34973c12711c", + "Name": "DLMAutomationFilterPath", + "Label": "Filter path (optional)", + "HelpText": "Specify the location of a SQL Compare filter file (.scpf), which defines objects to include/exclude in the schema comparison. Filter files are generated by SQL Source Control. + +For more help see [Using SQL Compare filters in SQL Change Automation](http://www.red-gate.com/sca/ps/help/filters).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a305bef9-af25-44f6-a78b-d641d7b6609a", + "Name": "DLMAutomationCompareOptions", + "Label": "SQL Compare options (optional)", + "HelpText": "Enter SQL Compare options to apply when generating the update script. Use a comma-separated list to enter multiple values. For more help see [Using SQL Compare options in SQL Change Automation](http://www.red-gate.com/sca/add-ons/compare-options).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1bbdec30-e207-4e5e-b08c-26bae87f78b3", + "Name": "DLMAutomationDataCompareOptions", + "Label": "SQL Data Compare options (optional)", + "HelpText": "Enter SQL Data Compare options to apply when generating the update script. Use a comma-separated list to enter multiple values. For more help see [Using SQL Data Compare options in SQL Change Automation](http://www.red-gate.com/sca/ps/help/datacompareoptions).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "367a4913-6c79-451a-bfe6-269462f784f8", + "Name": "DLMAutomationTransactionIsolationLevel", + "Label": "Transaction isolation level (optional)", + "HelpText": "Select the transaction isolation level to be used in deployment scripts.", + "DefaultValue": "Serializable", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Serializable +Snapshot +RepeatableRead +ReadCommitted +ReadUncommitted" + } + }, + { + "Id": "b5e1544c-45c9-455c-9c5c-ab5c3ec37c8a", + "Name": "DLMAutomationIgnoreStaticData", + "Label": "Ignore static data", + "HelpText": "Exclude changes to static data when generating the deployment resources.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "56644eed-84d2-4efb-a645-8a690770af01", + "Name": "DLMAutomationQueryBatchTimeout", + "Label": "Query batch timeout (in seconds)", + "HelpText": "The execution timeout, in seconds, for each batch of queries in the update script. The default value is 30 seconds. A value of zero indicates no execution timeout.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a5d870d2-bef0-43e2-888a-265a8c7bccf3", + "Name": "DLMAutomationSkipPostUpdateSchemaCheck", + "Label": "Skip post update schema check", + "HelpText": "Don't check that the target database has the correct schema after the update has run.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "66a03c11-eb4c-4c59-81cf-e1f09c31aee7", + "Name": "SpecificModuleVersion", + "Label": "SQL Change Automation version (optional)", + "HelpText": "If you wish to use a specific version of SQL Change Automation rather than the latest, enter the version number here.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2d38d44f-409f-407b-9cc2-401a95f07d01", + "Name": "DLMModuleInstallLocation", + "Label": "SQL Change Automation Install Location (optional)", + "HelpText": "The SQL Change Automation cmdlets will be downloaded from the [PowerShell gallery](https://www.powershellgallery.com/packages/SqlChangeAutomation). Please specify the folder folder where those packages will be saved to. It can be relative or absolute. + + +If this is empty it will default `$Home\\Documents\\WindowsPowerShell\\Modules` which is the [recommended location](https://docs.microsoft.com/en-us/powershell/scripting/developer/module/installing-a-powershell-module?view=powershell-7#where-to-install-modules) from Microsoft.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4daa7ac9-65b8-47ff-b00a-616f94896664", + "Name": "DLMAutomationTrustServerCertificate", + "Label": "Trust server certificate", + "HelpText": "Check this box to trust the SQL Server certificate.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2022-01-25T16:00:25.269Z", + "OctopusVersion": "2022.1.80", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "redgate" +} diff --git a/step-templates/redgate-deploy-from-package.json.human b/step-templates/redgate-deploy-from-package.json.human new file mode 100644 index 000000000..d66cd42f0 --- /dev/null +++ b/step-templates/redgate-deploy-from-package.json.human @@ -0,0 +1,577 @@ +{ + "Id": "19f750fb-2ce8-4361-859e-2dfcdf08a952", + "Name": "Redgate - Deploy from Package", + "Description": "Uses Redgate\u0027s [SQL Change Automation](http://www.red-gate.com/sca/productpage) to deploy a package containing a database schema to a SQL Server database, without a review step.\r +\r +Requires SQL Change Automation version 3.0.2 or later.\r +\r +*Version date: 2020-12-21*", + "ActionType": "Octopus.Script", + "Version": 18, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function GetModuleInstallationFolder\r +{\r + if (ModuleInstallationFolderIsValid)\r + {\r + return [System.IO.Path]::GetFullPath($DLMAutomationModuleInstallationFolder)\r + }\r +\r + return \"$PSScriptRoot\\Modules\"\r +}\r +\r +function ModuleInstallationFolderIsValid\r +{\r + if ([string]::IsNullOrWhiteSpace($DLMAutomationModuleInstallationFolder))\r + {\r + return $false\r + }\r +\r + return (Test-Path $DLMAutomationModuleInstallationFolder -IsValid) -eq $true;\r +}\r +\r +$DlmAutomationModuleName = \"DLMAutomation\"\r +$SqlChangeAutomationModuleName = \"SqlChangeAutomation\"\r +$ModulesFolder = GetModuleInstallationFolder\r +$LocalModules = (New-Item \"$ModulesFolder\" -ItemType Directory -Force).FullName\r +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\"\r +\r +function IsScaAvailable\r +{\r + if ((Get-Module $SqlChangeAutomationModuleName) -ne $null) {\r + return $true\r + }\r +\r + return $false\r +}\r +\r +function InstallCorrectSqlChangeAutomation\r +{\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $false)]\r + [Version]$requiredVersion,\r + [Parameter(Mandatory = $false)]\r + [bool]$useInstalledVersion\r + )\r +\r + $moduleName = $SqlChangeAutomationModuleName\r +\r + # this will be null if $requiredVersion is not specified - which is exactly what we want\r + $maximumVersion = $requiredVersion\r +\r + if ($requiredVersion) {\r + if ($requiredVersion.Revision -eq -1) {\r + #If provided with a 3 part version number (the 4th part, revision, == -1), we should allow any value for the revision\r + $maximumVersion = [Version]\"$requiredVersion.$([System.Int32]::MaxValue)\"\r + }\r +\r + if ($requiredVersion.Major -lt 3) {\r + # If the specified version is below V3 then the user is requesting a version of DLMA. We should look for that module name instead\r + $moduleName = $DlmAutomationModuleName\r + }\r + }\r +\r + if ($useInstalledVersion) {\r + Write-Verbose \"Option to use installed version is selected. Skipping update/install using PowerShellGet.\"\r + }\r + else {\r + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r +\r + if (!$installedModule) {\r + #Either SCA isn\u0027t installed at all or $requiredVersion is specified but that version of SCA isn\u0027t installed\r + Write-Verbose \"$moduleName $requiredVersion not available - attempting to download from gallery\"\r + InstallLocalModule -moduleName $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r + }\r + elseif (!$requiredVersion) {\r + #We\u0027ve got a version of SCA installed, but $requiredVersion isn\u0027t specified so we might be able to upgrade\r + $newest = GetHighestInstallableModule $moduleName\r + if ($newest -and ($installedModule.Version -lt $newest.Version)) {\r + Write-Verbose \"Updating $moduleName to version $($newest.Version)\"\r + InstallLocalModule -moduleName $moduleName -minimumVersion $newest.Version\r + }\r + }\r + }\r +\r + # Now we\u0027re done with install/upgrade, try to import the highest available module that matches our version requirements\r +\r + # We can\u0027t just use -minimumVersion and -maximumVersion arguments on Import-Module because PowerShell 3 doesn\u0027t have them,\r + # so we have to find the precise matching installed version using our code, then import that specifically. Note that\r + # $requiredVersion and $maximumVersion might be null when there\u0027s no specific version we need.\r + $installedModule = GetHighestInstalledModule $moduleName -minimumVersion $requiredVersion -maximumVersion $maximumVersion\r +\r + if (!$installedModule -and !$requiredVersion) {\r + #Did not find SCA, and we don\u0027t have a required version so we might be able to use an installed DLMA instead.\r + Write-Verbose \"$moduleName is not installed - trying to fall back to $DlmAutomationModuleName\"\r + $installedModule = GetHighestInstalledModule $DlmAutomationModuleName\r + }\r +\r + if ($installedModule) {\r + Write-Verbose \"Importing installed $($installedModule.Name) version $($installedModule.Version)\"\r + Import-Module $installedModule -Force\r + }\r + else {\r + throw \"$moduleName $requiredVersion is not installed, and could not be downloaded from the PowerShell gallery\"\r + }\r +}\r +\r +function InstallPowerShellGet {\r + [CmdletBinding()]\r + Param()\r +\r + ConfigureProxyIfVariableSet\r + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12\r +\r + $psget = GetHighestInstalledModule PowerShellGet\r + if (!$psget)\r + {\r + Write-Warning @\"\r +Cannot access the PowerShell Gallery because PowerShellGet is not installed.\r +To install PowerShellGet, either upgrade to PowerShell 5 or install the PackageManagement MSI.\r +See https://docs.microsoft.com/en-us/powershell/gallery/installing-psget for more details.\r +\"@\r + throw \"PowerShellGet is not available\"\r + }\r +\r + if ($psget.Version -lt [Version]\u00271.6\u0027) {\r + #Bootstrap the NuGet package provider, which updates NuGet without requiring admin rights\r + Write-Debug \"Installing NuGet package provider\"\r + Get-PackageProvider NuGet -ForceBootstrap | Out-Null\r +\r + #Use the currently-installed version of PowerShellGet\r + Import-PackageProvider PowerShellGet\r +\r + #Download the version of PowerShellGet that we actually need\r + Write-Debug \"Installing PowershellGet\"\r + Save-Module -Name PowerShellGet -Path $LocalModules -MinimumVersion 1.6 -Force -ErrorAction SilentlyContinue\r + }\r +\r + Write-Debug \"Importing PowershellGet\"\r + Import-Module PowerShellGet -MinimumVersion 1.6 -Force\r + #Make sure we\u0027re actually using the package provider from the imported version of PowerShellGet\r + Import-PackageProvider ((Get-Module PowerShellGet).Path) | Out-Null\r +}\r +\r +function InstallLocalModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true)]\r + [string]$moduleName,\r + [Parameter(Mandatory = $false)]\r + [Version]$minimumVersion,\r + [Parameter(Mandatory = $false)]\r + [Version]$maximumVersion\r + )\r + try {\r + InstallPowerShellGet\r +\r + Write-Debug \"Install $moduleName $requiredVersion\"\r + Save-Module -Name $moduleName -Path $LocalModules -Force -AcceptLicense -MinimumVersion $minimumVersion -MaximumVersion $maximumVersion -ErrorAction Stop\r + }\r + catch {\r + Write-Warning \"Could not install $moduleName $requiredVersion from any registered PSRepository\"\r + }\r +}\r +\r +function GetHighestInstalledModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true, Position = 0)]\r + [string] $moduleName,\r +\r + [Parameter(Mandatory = $false)]\r + [Version]$minimumVersion,\r + [Parameter(Mandatory = $false)]\r + [Version]$maximumVersion\r + )\r +\r + return Get-Module $moduleName -ListAvailable |\r + Where {(!$minimumVersion -or ($_.Version -ge $minimumVersion)) -and (!$maximumVersion -or ($_.Version -le $maximumVersion))} |\r + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} |\r + Select -First 1\r +}\r +\r +function GetHighestInstallableModule {\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory = $true, Position = 0)]\r + [string] $moduleName\r + )\r +\r + try {\r + InstallPowerShellGet\r + Find-Module SqlChangeAutomation -AllVersions |\r + Sort -Property @{Expression = {[System.Version]($_.Version)}; Descending = $True} |\r + Select -First 1\r + }\r + catch {\r + Write-Warning \"Could not find any suitable versions of $moduleName from any registered PSRepository\"\r + }\r +}\r +\r +function GetInstalledSqlChangeAutomationVersion {\r + $scaModule = (Get-Module $SqlChangeAutomationModuleName)\r +\r + if ($scaModule -ne $null) {\r + return $scaModule.Version\r + }\r +\r + $dlmaModule = (Get-Module $DlmAutomationModuleName)\r +\r + if ($dlmaModule -ne $null) {\r + return $dlmaModule.Version\r + }\r +\r + return $null\r +}\r +\r +function ConfigureProxyIfVariableSet\r +{\r + if ([string]::IsNullOrWhiteSpace($DLMAutomationProxyUrl) -eq $false)\r + {\r + Write-Debug \"Setting DefaultWebProxy to $proxyUrl\"\r +\r + [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy($DLMAutomationProxyUrl)\r + [System.Net.WebRequest]::DefaultWebProxy.credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials\r + [System.Net.WebRequest]::DefaultWebProxy.BypassProxyOnLocal = $True\r + }\r +}\r +\r +\r +$ErrorActionPreference = \u0027Stop\u0027\r +$VerbosePreference = \u0027Continue\u0027\r +\r +# Set process level FUR environment\r +$env:REDGATE_FUR_ENVIRONMENT = \"Octopus Step Templates\"\r +\r +#Helper functions for paramter handling\r +function Required() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + if ([string]::IsNullOrWhiteSpace($Parameter)) { throw \"You must enter a value for \u0027$Name\u0027\" }\r +}\r +function Optional() {\r + #Default is untyped here - if we specify [string] powershell will convert nulls into empty string\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $false)]$Default\r + )\r + if ([string]::IsNullOrWhiteSpace($Parameter)) {\r + $Default\r + } else {\r + $Parameter\r + }\r +}\r +function RequireBool() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + $Result = $False\r + if (![bool]::TryParse($Parameter , [ref]$Result )) { throw \"\u0027$Name\u0027 must be a boolean value.\" }\r + $Result\r +}\r +function RequirePositiveNumber() {\r + Param(\r + [Parameter(Mandatory = $false)][string]$Parameter,\r + [Parameter(Mandatory = $true)][string]$Name\r + )\r + $Result = 0\r + if (![int32]::TryParse($Parameter , [ref]$Result )) { throw \"\u0027$Name\u0027 must be a numerical value.\" }\r + if ($Result -lt 0) { throw \"\u0027$Name\u0027 must be \u003e= 0.\" }\r + $Result\r +}\r +\r +$SpecificModuleVersion = Optional -Parameter $SpecificModuleVersion\r +$UseInstalledModuleVersion = Optional -Parameter $UseInstalledModuleVersion -Default \u0027False\u0027\r +$UseInstalledVersionSwitch = [bool]::Parse($UseInstalledModuleVersion)\r +InstallCorrectSqlChangeAutomation -requiredVersion $SpecificModuleVersion -useInstalledVersion $UseInstalledVersionSwitch\r +\r +# Check if SQL Change Automation is installed.\r +$powershellModule = Get-Module -Name SqlChangeAutomation\r +if ($powershellModule -eq $null) {\r + throw \"Cannot find SQL Change Automation on your Octopus Tentacle. If SQL Change Automation is installed, try restarting the Tentacle service for it to be detected.\"\r +}\r +\r +$currentVersion = $powershellModule.Version\r +$minimumRequiredVersion = [version] \u00273.0.3\u0027\r +if ($currentVersion -lt $minimumRequiredVersion) {\r + throw \"This step requires SQL Change Automation version $minimumRequiredVersion or later. The current version is $currentVersion. The latest version can be found at http://www.red-gate.com/sca/productpage\"\r +}\r +\r +$minimumRequiredVersionDataCompareOptions = [version] \u00273.3.0\u0027\r +$minimumRequiredVersionTrustServerCertificate = [version]\u00274.3.20267\u0027\r +\r +function AreConnectionOptionsHandled($encryptConnection, $trustServerCertificate)\r +{\r + if ([string]::IsNullOrWhiteSpace($currentVersion) -or $currentVersion -ge $minimumRequiredVersionTrustServerCertificate)\r + {\r + return $true\r + }\r + elseif($encryptConnection -or $trustServerCertificate)\r + {\r + Write-Warning \"Encrypt and TrustServerCertificate options require SQL Change Automation version $minimumRequiredVersionTrustServerCertificate or later. The current version is $currentVersion.\"\r + return $false\r + }\r +}\r +\r +# Check the parameters.\r +Required -Parameter $DLMAutomationNuGetDbPackageDownloadStepName -Name \u0027Database package step\u0027\r +Required -Parameter $DLMAutomationTargetDatabaseServer -Name \u0027Target SQL Server instance\u0027\r +Required -Parameter $DLMAutomationTargetDatabaseName -Name \u0027Target database name\u0027\r +$DLMAutomationTargetTrustServerCertificate = Optional -Parameter $DLMAutomationTargetTrustServerCertificate\r +$DLMAutomationTargetEncrypt = Optional -Parameter $DLMAutomationTargetEncrypt\r +$DLMAutomationTargetUsername = Optional -Parameter $DLMAutomationTargetUsername\r +$DLMAutomationTargetPassword = Optional -Parameter $DLMAutomationTargetPassword\r +$DLMAutomationFilterPath = Optional -Parameter $DLMAutomationFilterPath\r +$DLMAutomationCompareOptions = Optional -Parameter $DLMAutomationCompareOptions\r +$DLMAutomationDataCompareOptions = Optional -Parameter $DLMAutomationDataCompareOptions\r +$DLMAutomationTransactionIsolationLevel = Optional -Parameter $DLMAutomationTransactionIsolationLevel -Default \"Serializable\"\r +$DLMAutomationIgnoreStaticData = Optional -Parameter $DLMAutomationIgnoreStaticData -Default \u0027False\u0027\r +$DLMAutomationSkipPostUpdateSchemaCheck = Optional -Parameter $DLMAutomationSkipPostUpdateSchemaCheck -Default \"False\"\r +$DLMAutomationQueryBatchTimeout = Optional -Parameter $DLMAutomationQueryBatchTimeout -Default \u002730\u0027\r +$DLMAutomationModuleInstallationFolder = Optional -Parameter $DLMAutomationModuleInstallationFolder\r +$DLMAutomationProxyUrl = Optional -Parameter $DLMAutomationProxyUrl\r +\r +$skipPostUpdateSchemaCheck = RequireBool -Parameter $DLMAutomationSkipPostUpdateSchemaCheck -Name \u0027Skip post update schema check\u0027\r +$queryBatchTimeout = RequirePositiveNumber -Parameter $DLMAutomationQueryBatchTimeout -Name \u0027Query Batch Timeout\u0027\r +\r +# Get the NuGet package installation directory path.\r +$packageExtractPath = $OctopusParameters[\"Octopus.Action[$DLMAutomationNuGetDbPackageDownloadStepName].Output.Package.InstallationDirectoryPath\"]\r +if($packageExtractPath -eq $null) {\r + throw \"The \u0027Database package download step\u0027 is not a \u0027Deploy a NuGet package\u0027 step: \u0027$DLMAutomationNuGetDbPackageDownloadStepName\u0027\"\r +}\r +\r +$targetConnectionOptions = @{ }\r +\r +if(AreConnectionOptionsHandled([bool]::Parse($DLMAutomationTargetEncrypt), [bool]::Parse($DLMAutomationTargetTrustServerCertificate))) {\r + $targetConnectionOptions += @{ \u0027Encrypt\u0027 = [bool]::Parse($DLMAutomationTargetEncrypt) }\r + $targetConnectionOptions += @{ \u0027TrustServerCertificate\u0027 = [bool]::Parse($DLMAutomationTargetTrustServerCertificate) }\r +}\r +\r +$targetDB = New-DatabaseConnection @targetConnectionOptions `\r + -ServerInstance $DLMAutomationTargetDatabaseServer `\r + -Database $DLMAutomationTargetDatabaseName `\r + -Username $DLMAutomationTargetUsername `\r + -Password $DLMAutomationTargetPassword\r +\r +$importedBuildArtifact = Import-DatabaseBuildArtifact -Path $packageExtractPath\r +\r +# Only allow sqlcmd variables that don\u0027t have special characters like spaces, colon or dashes\r +$regex = \u0027^[a-zA-Z_][a-zA-Z0-9_]+$\u0027\r +$sqlCmdVariables = @{}\r +$OctopusParameters.Keys | Where { $_ -match $regex } | ForEach {\r +\t$sqlCmdVariables[$_] = $OctopusParameters[$_]\r +}\r +\r +# Create database deployment resources from the NuGet package to the database\r +$releaseParams = @{\r + Target = $targetDB\r + Source = $importedBuildArtifact\r + TransactionIsolationLevel = $DLMAutomationTransactionIsolationLevel\r + IgnoreStaticData = [bool]::Parse($DLMAutomationIgnoreStaticData)\r + FilterPath = $DLMAutomationFilterPath\r + SQLCompareOptions = $DLMAutomationCompareOptions\r + SqlCmdVariables = $sqlCmdVariables\r +}\r +\r +if($currentVersion -ge $minimumRequiredVersionDataCompareOptions) {\r + $releaseParams.SQLDataCompareOptions = $DLMAutomationDataCompareOptions\r +} elseif(-not [string]::IsNullOrWhiteSpace($DLMAutomationDataCompareOptions)) {\r + Write-Warning \"SQL Data Compare options requires SQL Change Automation version $minimumRequiredVersionDataCompareOptions or later. The current version is $currentVersion.\"\r +}\r +\r +$release = New-DatabaseReleaseArtifact @releaseParams\r +\r +# Deploy the source schema to the target database.\r +Write-Host \"Timeout = $queryBatchTimeout\"\r +$releaseUrl = $OctopusParameters[\u0027#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}\u0027] + $OctopusParameters[\u0027Octopus.Web.DeploymentLink\u0027];\r +$release | Use-DatabaseReleaseArtifact -DeployTo $targetDB -SkipPreUpdateSchemaCheck -QueryBatchTimeout $queryBatchTimeout -ReleaseUrl $releaseUrl -SkipPostUpdateSchemaCheck:$skipPostUpdateSchemaCheck\r +\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": { + + }, + "Parameters": [ + { + "Name": "DLMAutomationNuGetDbPackageDownloadStepName", + "Label": "Database package step", + "HelpText": "Select the step in this project which downloads the database package.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Name": "DLMAutomationTargetDatabaseServer", + "Label": "Target SQL Server instance", + "HelpText": "The fully qualified SQL Server instance name for the target database.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationTargetDatabaseName", + "Label": "Target database name", + "HelpText": "The name of the database to deploy changes to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationTargetUsername", + "Label": "Username (optional)", + "HelpText": "The SQL Server username used to connect to the database. If you leave this field and \u0027Password\u0027 blank, Windows authentication will be used to connect instead, using the account that runs the Tentacle service.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationTargetPassword", + "Label": "Password (optional)", + "HelpText": "You must enter a password if you entered a username.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "DLMAutomationTargetEncrypt", + "Label": "Encrypt", + "HelpText": "Specify whether SSL encryption is used by SQL Server when a certificate is installed.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationTargetTrustServerCertificate", + "Label": "Trust Server Certificate", + "HelpText": "Specify whether to force SQL Server to skip certificate validation.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationFilterPath", + "Label": "Filter path (optional)", + "HelpText": "Specify the location of a SQL Compare filter file (.scpf), which defines objects to include/exclude in the schema comparison. Filter files are generated by SQL Source Control. + +For more help see [Using SQL Compare filters in SQL Change Automation](http://www.red-gate.com/sca/ps/help/filters).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationCompareOptions", + "Label": "SQL Compare options (optional)", + "HelpText": "Enter SQL Compare options to apply when generating the update script. Use a comma-separated list to enter multiple values. For more help see [Using SQL Compare options in SQL Change Automation](http://www.red-gate.com/sca/add-ons/compare-options).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationDataCompareOptions", + "Label": "SQL Data Compare options (optional)", + "HelpText": "Enter SQL Data Compare options to apply when generating the update script. Use a comma-separated list to enter multiple values. For more help see [Using SQL Data Compare options in SQL Change Automation](http://www.red-gate.com/sca/ps/help/datacompareoptions).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationTransactionIsolationLevel", + "Label": "Transaction isolation level (optional)", + "HelpText": "Select the transaction isolation level to be used in deployment scripts.", + "DefaultValue": "Serializable", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Serializable +Snapshot +RepeatableRead +ReadCommitted +ReadUncommitted" + } + }, + { + "Name": "DLMAutomationIgnoreStaticData", + "Label": "Ignore static data", + "HelpText": "Exclude changes to static data when generating the deployment resources.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationQueryBatchTimeout", + "Label": "Query batch timeout (in seconds)", + "HelpText": "The execution timeout, in seconds, for each batch of queries in the update script. The default value is 30 seconds. A value of zero indicates no execution timeout.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationSkipPostUpdateSchemaCheck", + "Label": "Skip post update schema check", + "HelpText": "Don\u0027t check that the target database has the correct schema after the update has run.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "SpecificModuleVersion", + "Label": "SQL Change Automation version (optional)", + "HelpText": "If you wish to use a specific version of SQL Change Automation rather than the latest, enter the version number here.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "UseInstalledModuleVersion", + "Label": "Only use a version of SQL Change Automation that is already installed", + "HelpText": "This prevents attempting to access PowerShell Gallery, which can be helpful when the build agent does not have access to the internet", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "DLMAutomationModuleInstallationFolder", + "Label": "Module Installation Folder (optional)", + "HelpText": "By default, module folders do not persist between steps. Setting this field to a specific folder will ensure that modules persist, and do not have to be downloaded again.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DLMAutomationProxyUrl", + "Label": "Proxy URL (optional)", + "HelpText": "By default, no proxy is used when connecting to Powershell Gallery. Alternatively, a proxy URL can be specified here that can be used for Powershell Gallery.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-03-19T10:01:37.152+00:00", + "LastModifiedBy": "benjimac93", + "$Meta": { + "ExportedAt": "2021-08-23T12:40:10.975Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "Category": "redgate" +} diff --git a/step-templates/redgate-oracle-create-release.json.human b/step-templates/redgate-oracle-create-release.json.human new file mode 100644 index 000000000..428a218e0 --- /dev/null +++ b/step-templates/redgate-oracle-create-release.json.human @@ -0,0 +1,336 @@ +{ + "Id": "0aa40fba-949c-4065-a438-010349c3fd0c", + "Name": "Redgate - Create Oracle Release", + "Description": "This step will use the [Redgate Oracle Deployment Suite](https://www.red-gate.com/products/oracle-development/deployment-suite-for-oracle/). It will create a delta sql script and report so you can review it prior to being deployed to the Oracle Server. This step will download a package containing the scripts generated by Redgate Source Control for Oracle. + +Please note: the Redgate Oracle Deployment Suite must be installed on the target machine for this to work. The target machine must also have the ability to connect to the Oracle database.", + "ActionType": "Octopus.TentaclePackage", + "Version": 8, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "c02bc919-e5e4-4d3e-b7c5-1f2489201850", + "Name": "", + "PackageId": "#{Redgate.Oracle.PackageName}", + "FeedId": "#{Redgate.Oracle.PackageFeed}", + "AcquisitionLocation": "Server", + "Properties": {} + } + ], + "Properties": { + "Octopus.Action.Package.AutomaticallyRunConfigurationTransformationFiles": "True", + "Octopus.Action.Package.AutomaticallyUpdateAppSettingsAndConnectionStrings": "True", + "Octopus.Action.EnabledFeatures": "Octopus.Features.CustomScripts", + "Octopus.Action.CustomScripts.PostDeploy.ps1": "$exportPath = $OctopusParameters[\"Redgate.Oracle.ExportPath\"] +$server = $OctopusParameters[\"Redgate.Oracle.Server\"] +$user = $OctopusParameters[\"Redgate.Oracle.Username\"] +$password = $OctopusParameters[\"Redgate.Oracle.Password\"] +$oracleTools = $OctopusParameters[\"Redgate.Oracle.InstallPath\"] +$deploymentSchema = $OctopusParameters[\"Redgate.Oracle.DeploymentSchema\"] +$sourceSchema = $OctopusParameters[\"Redgate.Oracle.SourceSchema\"] +$packageInstallDirectory = $OctopusParameters[\"Octopus.Action.Package.InstallationDirectoryPath\"] +$excludeOptions = $OctopusParameters[\"Redgate.Oracle.ExcludeOptions\"] +$behaviorOptions = $OctopusParameters[\"Redgate.Oracle.BehaviorOptions\"] +$ignoreOptions = $OctopusParameters[\"Redgate.Oracle.IgnoreOptions\"] +$storageOptions = $OctopusParameters[\"Redgate.Oracle.StorageOptions\"] +$excludeDependencies = $OctopusParameters[\"Redgate.Oracle.ExcludeDependencies\"] +$filterPath = $OctopusParameters[\"Redgate.Oracle.FilterPath\"] +$includeIdentical = $OctopusParameters[\"Redgate.Oracle.IncludeIdentical\"] + +Write-Host \"Export Path: $exportPath\" +Write-Host \"Oracle Server: $server\" +Write-Host \"Oracle Username: $user\" +Write-Host \"Oracle Password not shown\" +Write-Host \"Oracle Source Schema: $sourceSchema\" +Write-Host \"Oracle Deployment Schema: $deploymentSchema\" +Write-Host \"Oracle Toolbelt Install Path: $oracleTools\" +Write-Host \"Package Install Directory: $packageInstallDirectory\" +Write-Host \"Filter Path: $filterPath\" +Write-Host \"Behavior Options: $behaviorOptions\" +Write-Host \"Exclude Options: $excludeOptions\" +Write-Host \"Exclude Dependencies: $excludeDependencies\" +Write-Host \"Ignore Options: $ignoreOptions\" +Write-Host \"Storage Options: $storageOptions\" +Write-Host \"Include Identical: $includeIdentical\" + +$sourceFolder = \"$packageInstallDirectory\\{$sourceSchema}\" +Write-Host \"Source Folder: $sourceFolder\" + +$maskedConnectionString = \"$user/*****@$server{$deploymentSchema}\" +$unmaskedConnectionString = \"$user/$password@$server{$deploymentSchema}\" +Write-Host \"Creating a delta script by connecting to: $maskedConnectionString\" + +$oracleToolsSearchPath = \"$oracleTools\\**\\SCO.exe\" +$scoexeOptions = Get-ChildItem -Path $oracleToolsSearchPath | Sort-Object [Version] -Descending + +if ($scoexeOptions -eq $null){ + Write-Error \"Unable to find Oracle Schema Compare, please verify it is installed in the directory specified\" +} + +$schemaCompareExe = $scoexeOptions[0] +Write-Host \"Running the exe $schemaCompareExe\" + +$deltaReportPath = \"$exportPath\\Changes.html\" +Write-Host \"Creating the delta report $deltaReportPath\" + +$changeScript = \"$exportPath\\Update.sql\" +Write-Host \"The change script is set to $changeScript\" + +$AllArgs = @( +\t\"/source:`\"$sourceFolder`\"\", + \"/target:`\"$unmaskedConnectionString`\"\", + \"/scriptfile:`\"$changeScript`\"\", + \"/report:`\"$deltaReportPath`\"\", + \"/reporttype:Simple\") + +if ([string]::IsNullOrWhiteSpace($behaviorOptions) -eq $false){ +\tWrite-Host \"Behavior Options specified, adding them to the command line\" +\t$AllArgs += \"/b:$behaviorOptions\" +} + +if ([string]::IsNullOrWhiteSpace($excludeOptions) -eq $false){ +\tWrite-Host \"Exclude Options specified, adding them to the command line\" +\t$AllArgs += \"/exc:$excludeOptions\" +} + +if ([string]::IsNullOrWhiteSpace($ignoreOptions) -eq $false){ +\tWrite-Host \"Ignore Options specified, adding them to the command line\" +\t$AllArgs += \"/i:$ignoreOptions\" +} + +if ([string]::IsNullOrWhiteSpace($storageOptions) -eq $false){ +\tWrite-Host \"Storage Options specified, adding them to the command line\" +\t$AllArgs += \"/g:$storageOptions\" +} + +if ($excludeDependencies -eq \"True\"){ +\tWrite-Host \"Exclude Dependencies set to true, adding them to the command line\" +\t$AllArgs += \"/excludedependencies\" +} + +if ($includeIdentical -eq \"True\"){ +\tWrite-Host \"Include identical set to true, adding that to the command line\" + $AllArgs += \"/includeidentical\" +} + +if ([string]::IsNullOrWhiteSpace($filterPath) -eq $false){ +\tWrite-Host \"Custom Filter Path specified, adding them to the command line\" +\t$AllArgs += \"/f:`\"$sourceFolder\\$filterPath`\"\" +} + +& \"$schemaCompareExe\" $AllArgs + +$successful = $false +$upload = $false +if ($lastExitCode -eq 61){ +\tWrite-Highlight \"Changes found, the delta script location is: $changeScript\" + Write-Highlight \"Uploading script and report as artifacts\" + +\t$successful = $true + $upload = $true +} + +if ($lastExitCode -eq 0){ +\tWrite-Highlight \"No changes were detected\" +\t$successful = $true +} + +Set-OctopusVariable -name \"OracleRedgateCreateReleaseChangesFound\" -value $upload + +if ($upload){ + $environmentName = $OctopusParameters[\"Octopus.Environment.Name\"] + $artifactName = \"$environmentName\" + \"Changes.html\" + New-OctopusArtifact -Path \"$deltaReportPath\" -Name \"$artifactName\" + + $scriptArtifactName = \"$environmentName\" + \"Update.sql\" + New-OctopusArtifact -Path \"$changeScript\" -Name \"$scriptArtifactName\" +} + +Set-OctopusVariable -name \"DatabaseChangesFound\" -value $upload + +if ($successful){ + exit 0 +}", + "Octopus.Action.Package.PackageId": "#{Redgate.Oracle.PackageName}", + "Octopus.Action.Package.FeedId": "#{Redgate.Oracle.PackageFeed}", + "Octopus.Action.Package.DownloadOnTentacle": "False" + }, + "Parameters": [ + { + "Id": "fed1466b-b5d2-4815-9b54-7b7e696a97aa", + "Name": "Redgate.Oracle.InstallPath", + "Label": "Deployment Suite for Oracle Install Path", + "HelpText": "The location where the Deployment Suite for Oracle is installed to", + "DefaultValue": "C:\\Program Files\\Red Gate\\", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bdeab5b9-c12c-43d8-9919-10e7c4a0e742", + "Name": "Redgate.Oracle.PackageFeed", + "Label": "Package Feed Id", + "HelpText": "The id of the package feed to use. This will use the default feed id. If you need to use another feed id you can go to library -> external feeds and select the feed you want to use. If you look in the url, that is the feed id.", + "DefaultValue": "feeds-builtin", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "67da3f13-b1ca-48bb-a81a-dc3efe53dea3", + "Name": "Redgate.Oracle.PackageName", + "Label": "Package Name", + "HelpText": "The name of the package to deploy", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e12c322b-7e60-4b0e-8d80-2dbc2abca58f", + "Name": "Redgate.Oracle.ExportPath", + "Label": "Export Path", + "HelpText": "The path where the scripts and reports will be exported to", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ee435c96-3542-4b38-ad12-c38fc607c23d", + "Name": "Redgate.Oracle.Server", + "Label": "TNS Name", + "HelpText": "The TNS Name entry in tnsnames.ora containing the necessary connection information", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8d19f141-b250-4fc5-999d-457549cd041f", + "Name": "Redgate.Oracle.Username", + "Label": "Oracle Username", + "HelpText": "The username of the user to sign with", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "138eff39-87e6-462d-808c-70259fd251cc", + "Name": "Redgate.Oracle.Password", + "Label": "Oracle User Password", + "HelpText": "The password of the user to login with", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "3e164f6c-1bcd-4318-a72c-65bf21d64c9a", + "Name": "Redgate.Oracle.SourceSchema", + "Label": "Source Schema", + "HelpText": "The source schema you wish to deploy from", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "49225f70-591c-4626-abc2-e351065b7510", + "Name": "Redgate.Oracle.DeploymentSchema", + "Label": "Deployment Schema", + "HelpText": "The schema being deployed", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "243fd30b-333d-4d93-936a-da64e167f44c", + "Name": "Redgate.Oracle.FilterPath", + "Label": "Custom Filter Path (optional)", + "HelpText": "By default, the filter.scpf will be used, if present for the deployment. Provide a relative path to the filter file you wish to use. It will be the path relative to the schema folder provided.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "21650183-f187-424a-bb62-2ade2805bbe7", + "Name": "Redgate.Oracle.BehaviorOptions", + "Label": "Behavior Options (optional)", + "HelpText": "Specify the behavior options as listed out in the [documentation for Schema Compare for Oracle](https://documentation.red-gate.com/sco5/using-the-command-line/command-line-switches#Commandlineswitches-/behavior:%3Cvalue%3E). + +Default is \"hdr\" (scriptheader, defineoff, and detectcolumnrenames)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a7404e1d-dc7a-4c2a-98a3-2e2298bc3763", + "Name": "Redgate.Oracle.ExcludeOptions", + "Label": "Exclude Options (optional)", + "HelpText": "Specify the exclude options as listed out in the [documentation for Schema Compare for Oracle](https://documentation.red-gate.com/sco5/using-the-command-line/command-line-switches#Commandlineswitches-/exclude:%3Cbehavior%3E). + +Default is no exclusions", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6db5c28c-eb83-43d9-a839-904c8e66e83f", + "Name": "Redgate.Oracle.ExcludeDependencies", + "Label": "Exclude Dependencies", + "HelpText": "Excludes dependent (referenced) objects from deployment. Default is false.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "5de8b830-695b-458f-9774-9da89c712094", + "Name": "Redgate.Oracle.IgnoreOptions", + "Label": "Ignore Options (optional)", + "HelpText": "Specify the ignore options as listed out in the [documentation for Schema Compare for Oracle](https://documentation.red-gate.com/sco5/using-the-command-line/command-line-switches#Commandlineswitches-/ignore:%3Cvalue%3E). + +Default is \"sdwqgva\" (slowdependencies, dependentobjects, whitespace, doublequotes, storage, sequencevalue, and mviewvalue)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "75bad918-a431-4256-a5d6-94b4a79e3d13", + "Name": "Redgate.Oracle.StorageOptions", + "Label": "Storage Options (optional)", + "HelpText": "Specify the storage options as listed out in the [documentation for Schema Compare for Oracle](https://documentation.red-gate.com/sco5/using-the-command-line/command-line-switches#Commandlineswitches-/storage:%3Cvalue%3E). + +The default is \"none\"", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "dc806786-d26e-474b-bc14-7cc066c7a421", + "Name": "Redgate.Oracle.IncludeIdentical", + "Label": "Include Identical", + "HelpText": "Include Identical options in the generated HTML report", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2019-04-16T14:34Z", + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2019-03-26T18:54:49.750Z", + "OctopusVersion": "2019.2.4", + "Type": "ActionTemplate" + }, + "Category": "redgate" +} diff --git a/step-templates/redgate-sql-clone-create-clone.json.human b/step-templates/redgate-sql-clone-create-clone.json.human new file mode 100644 index 000000000..f88c81005 --- /dev/null +++ b/step-templates/redgate-sql-clone-create-clone.json.human @@ -0,0 +1,210 @@ +{ + "Id": "96d88bbf-2e0a-4630-b4b6-bd179effedd7", + "Name": "Redgate - SQL Clone, Create Clone", + "Description": "Creates a database clone with [Redgate SQL Clone](https://www.red-gate.com/products/dba/sql-clone/index). + +Requires SQL Clone. + +*Version date: 16th May 2019*", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +# The code for this step template is largely a copy/paste job from the +# Azure DevOps Services step template which is maintained by Redgate: +# https://github.com/red-gate/SqlCloneVSTSExtension/blob/master/ImageTask/SQLCloneCloneTask.ps1 +# The code was copied and adapted on 16th May 2019. + +Write-Verbose \"cloneServer is $cloneServer\" +Write-Verbose \"cloneUser is $cloneUser\" +Write-Verbose \"clonePassword is $clonePassword\" +Write-Verbose \"imageNameForClone is $imageNameForClone\" +Write-Verbose \"templateName is $templateName\" +Write-Verbose \"cloneSqlServer is $cloneSqlServer\" +Write-Verbose \"cloneName is $cloneName\" +Write-Verbose \"deleteClone is $deleteClone\" + +Write-Debug \"Entering script SQLCloneCloneTask.ps1\" + +# This line is broken: Import-Module \"$PSScriptRoot\\Modules\\RedGate.SQLClone.PowerShell.dll\" + +if($cloneUser){ + $password = ConvertTo-SecureString -String $clonePassword -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $cloneUser,$password + +} +Connect-SqlClone -ServerUrl $cloneServer -Credential $credential +Write-Output \"Connected to SQL Clone server\" + + $sqlServerParts = $cloneSqlServer.Split('\\', [System.StringSplitOptions]::RemoveEmptyEntries) + if ($sqlServerParts.Count -ge 3) + { + write-error 'SQL Server instance ' + $cloneSqlServer + ' has not been recognised, if specifying a named instance please use \"machine\\instance\"' + exit 1 + } + $cloneSqlServerHost = $sqlServerParts[0] + $instanceName = '' + if ($sqlServerParts.Count -ge 2) + { + $instanceName = $sqlServerParts[1] + } + + try + { + $instance = Get-SqlCloneSqlServerInstance -MachineName $cloneSqlServerHost -InstanceName $instanceName + Write-Output \"Found SQL Server instance\" + } + catch + { + $instances = Get-SqlCloneSqlServerInstance + $instanceNames = \"`n\" + Foreach ($cInstance in $instances) + { + $instanceNames += $cInstance.Name + \"`n\" + } + $message = 'SQL Server instance \"' + $cloneSqlServer + '\" has not been added to SQL Clone, available instances:' + $instanceNames + write-error $message + exit 1 + } + + try + { + $image = Get-SqlCloneImage -Name $imageNameForClone + Write-Output \"Found image\" + } + catch + { + $images = Get-SqlCloneImage + $imageNames = \"`n\" + Foreach ($cImage in $images) + { + $imageNames += $cImage.Name + \"`n\" + } + $message = 'SQL Clone image \"' + $imageNameForClone + '\" has not been added to SQL Clone, available images:' + $imageNames + write-error $message + exit 1 + } + + if($deleteClone) + { + try + { + $clone = Get-SqlClone -Name $cloneName -Location $instance + Write-Output \"Deleting existing clone\" + Remove-SqlClone -Clone $clone | Wait-SqlCloneOperation + } + catch + { + # Clone didn't exist so nothing to do + } + } + if($templateName) + { + Write-Output \"Creating clone with template:\" + $templateName + $image | New-SqlClone -Name $cloneName -Location $instance -Template $templateName | Wait-SqlCloneOperation + } + else + { + Write-Output \"Creating clone\" + $image | New-SqlClone -Name $cloneName -Location $instance | Wait-SqlCloneOperation + } + Write-Output \"Finished creating clone\" + +Write-Debug \"Leaving script SQLCloneCloneTask.ps1\"" + }, + "Parameters": [ + { + "Id": "8c140a4c-65a2-4341-a604-73d14775b3a0", + "Name": "cloneServer", + "Label": "SQL Clone Server (required)", + "HelpText": "The URL for your SQL Clone server (e.g. http://sql-clone.example.com:14145)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e5ac1d04-b8a5-440e-ba69-a5d66a53abba", + "Name": "cloneUser", + "Label": "SQL Clone User (optional)", + "HelpText": "User account to access SQL Clone. (If left blank Octopus tentacle account will be used.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5f6288f2-57c9-4a11-91f2-b0c2e3cb9ccd", + "Name": "clonePassword", + "Label": "SQL Clone Password (optional)", + "HelpText": "User account to access SQL Clone. (If left blank Octopus tentacle account will be used.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ee5286d3-f233-410c-92ab-36743f8743e7", + "Name": "imageNameForClone", + "Label": "Image Name For Clone (required)", + "HelpText": "The name of the database image from which to create database clone.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0edb1e1f-0c27-428b-9ff0-7c76faf1369e", + "Name": "templateName", + "Label": "Template Name (optional)", + "HelpText": "A template to modify this clone (optional).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5e9f4d95-4776-4a0e-8245-05d645bd0997", + "Name": "cloneSqlServer", + "Label": "SQL Server (required)", + "HelpText": "The target SQL Server to create the clone on. This SQL Server instance must have already been added to the SQL Clone Server specified above.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "62da0567-35f5-4c3f-ac9e-61b75a399053", + "Name": "cloneName", + "Label": "Clone Name (required)", + "HelpText": "The name of the clone, which will also be the database name.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ab892024-c4b5-46c2-9434-2ad150e3e014", + "Name": "deleteClone", + "Label": "Delete clone if exists", + "HelpText": "Delete any existing clone with the same name prior to creating this clone.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2019-05-16T11:37:47.360Z", + "LastModifiedBy": "alex-yates", + "$Meta": { + "ExportedAt": "2019-05-16T11:37:47.360Z", + "OctopusVersion": "2019.2.7", + "Type": "ActionTemplate" + }, + "Category": "redgate" +} diff --git a/step-templates/redgate-sql-clone-create-image.json.human b/step-templates/redgate-sql-clone-create-image.json.human new file mode 100644 index 000000000..e68724361 --- /dev/null +++ b/step-templates/redgate-sql-clone-create-image.json.human @@ -0,0 +1,264 @@ +{ + "Id": "4ff62eff-f615-453e-9a14-ca7bf67cb586", + "Name": "Redgate - SQL Clone, Create Image", + "Description": "Creates a database image with [Redgate SQL Clone](https://www.red-gate.com/products/dba/sql-clone/index). + +Requires SQL Clone. + +*Version date: 16th May 2019*", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +# The code for this step template is largely a copy/paste job from the +# Azure DevOps Services step template which is maintained by Redgate: +# https://github.com/red-gate/SqlCloneVSTSExtension/blob/master/ImageTask/SQLCloneImageTask.ps1 +# The code was copied and adapted on 16th May 2019. + +Write-Verbose \"cloneServer is $cloneServer\" +Write-Verbose \"cloneUser is $cloneUser\" +Write-Verbose \"clonePassword is $clonePassword\" +Write-Verbose \"sourceType is $sourceType\" +Write-Verbose \"imageName is $imageName\" +Write-Verbose \"imageLocation is $imageLocation\" +Write-Verbose \"sourceInstance is $sourceInstance\" +Write-Verbose \"sourceDatabase is $sourceDatabase\" +Write-Verbose \"sourceFileNames is $sourceFileNames\" +Write-Verbose \"sourceFilePassword is $sourceFilePassword\" +Write-Verbose \"modificationScriptFiles is $modificationScriptFiles\" + +Write-Debug \"Entering script SQLCloneImageTask.ps1\" + +# This line is broken: Import-Module \"$PSScriptRoot\\Modules\\RedGate.SQLClone.PowerShell.dll\" + +if($cloneUser){ + $password = ConvertTo-SecureString -String $clonePassword -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $cloneUser,$password + +} +Connect-SqlClone -ServerUrl $cloneServer -Credential $credential +Write-Output \"Connected to SQL Clone server\" + + try + { + $cloneImageLocation = Get-SqlCloneImageLocation $imageLocation + Write-Output \"Found image location\" + } + catch + { + $imageLocations = Get-SqlCloneImageLocation + $imageLocationNames = \"`n\" + Foreach ($cImageLocation in $imageLocations) + { + $imageLocationNames += $cImageLocation.Path + \"`n\" + } + $message = 'SQL Clone image location \"' + $imageLocation + '\" has not been added to SQL Clone, available locations:' + $imageLocationNames + write-error $message + exit 1 + } + + $sqlServerParts = $sourceInstance.Split('\\', [System.StringSplitOptions]::RemoveEmptyEntries) + if ($sqlServerParts.Count -ge 3) + { + write-error 'SQL Server instance ' + $sourceInstance + ' has not been recognised, if specifying a named instance please use \"machine\\instance\"' + exit 1 + } + $cloneSqlServerHost = $sqlServerParts[0] + $instanceName = '' + if ($sqlServerParts.Count -ge 2) + { + $instanceName = $sqlServerParts[1] + } + + try + { + $instance = Get-SqlCloneSqlServerInstance -MachineName $cloneSqlServerHost -InstanceName $instanceName + Write-Output \"Found SQL Server instance\" + } + catch + { + $instances = Get-SqlCloneSqlServerInstance + $instanceNames = \"`n\" + Foreach ($cInstance in $instances) + { + $instanceNames += $cInstance.Name + \"`n\" + } + $message = 'SQL Server instance \"' + $sourceInstance + '\" has not been added to SQL Clone, available instances:' + $instanceNames + write-error $message + exit 1 + } + + $modificationScripts = @() + if($modificationScriptFiles){ + $modificationFiles = $modificationScriptFiles.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries) + + Foreach ($modificationScriptFile in $modificationFiles) + { + if ($modificationScriptFile -Like \"*.sql\") + { + $modificationScripts += New-SqlCloneSqlScript -Path $modificationScriptFile + } + + if ($modificationScriptFile -Like \"*.dmsmaskset\") + { + $modificationScripts += New-SqlCloneMask -Path $modificationScriptFile + } + } + } + + if ($sourceType -eq 'database') + { + Write-Output \"Source type = database\" + Write-Output \"Creating image\" + $NewImage = New-SqlCloneImage -Name $imageName -SqlServerInstance $instance -DatabaseName $sourceDatabase -Destination $cloneImageLocation -Modifications $modificationScripts | Wait-SqlCloneOperation + Write-Output \"Finished creating image\" + } + else + { + Write-Output \"Source type = backup\" + $backupFiles = $sourceFileNames.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries) + Write-Output \"Creating image from backup\" + if($sourceFilePassword) + { + $NewImage = New-SqlCloneImage -Name $imageName -SqlServerInstance $instance -BackupFileName $backupFiles -BackupPassword $sourceFilePassword -Destination $cloneImageLocation -Modifications $modificationScripts | Wait-SqlCloneOperation + } + else + { + $NewImage = New-SqlCloneImage -Name $imageName -SqlServerInstance $instance -BackupFileName $backupFiles -Destination $cloneImageLocation -Modifications $modificationScripts | Wait-SqlCloneOperation + } + Write-Output \"Finished creating image from backup\" + } + + + +Write-Debug \"Leaving script SQLCloneImageTask.ps1\" +" + }, + "Parameters": [ + { + "Id": "8c140a4c-65a2-4341-a604-73d14775b3a0", + "Name": "cloneServer", + "Label": "SQL Clone Server (required)", + "HelpText": "The URL for your SQL Clone server (e.g. http://sql-clone.example.com:14145)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e5ac1d04-b8a5-440e-ba69-a5d66a53abba", + "Name": "cloneUser", + "Label": "SQL Clone User (optional)", + "HelpText": "User account to access SQL Clone. (If left blank Octopus tentacle account will be used.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5f6288f2-57c9-4a11-91f2-b0c2e3cb9ccd", + "Name": "clonePassword", + "Label": "SQL Clone Password (optional)", + "HelpText": "User account to access SQL Clone. (If left blank Octopus tentacle account will be used.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ee5286d3-f233-410c-92ab-36743f8743e7", + "Name": "imageName", + "Label": "Image Name (required)", + "HelpText": "The name of your database image.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0edb1e1f-0c27-428b-9ff0-7c76faf1369e", + "Name": "imageLocation", + "Label": "Image Location (required)", + "HelpText": "Images need to have a UNC (network share) path destination in order to be shared with other machines. For example: \\\\servername\\fileshare\\images", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "aa803a9d-ab55-4e36-969c-3b0e3637d36e", + "Name": "sourceInstance", + "Label": "Source SQL Server Instance (required)", + "HelpText": "For example: MYSERVER\\MYINSTANCE", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "39c29a4a-f483-4b96-bf08-238bb677835d", + "Name": "sourceType", + "Label": "Image Source Type (required)", + "HelpText": "Images can be created from a backup file (.bak or .sqb) or from a SQL Server database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "database|SQL Server Database +backup|Backup file(s)" + } + }, + { + "Id": "5e9f4d95-4776-4a0e-8245-05d645bd0997", + "Name": "sourceDatabase", + "Label": "Source Database (required if Source Type is SQL Server Database)", + "HelpText": "The database from which to create the image.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "62da0567-35f5-4c3f-ac9e-61b75a399053", + "Name": "sourceFileNames", + "Label": "Source File Names (required if Source Type is Backup File(s))", + "HelpText": "For striped and/or differential backups, separate multiple file paths with ;", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ab892024-c4b5-46c2-9434-2ad150e3e014", + "Name": "sourceFilePassword", + "Label": "Source File Password (optional)", + "HelpText": "Source backup file password.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "a758e9de-05c6-4879-8541-d7f73215fe87", + "Name": "modificationScriptFiles", + "Label": "Modification Script Files (optional)", + "HelpText": "SQL scripts and/or Data Masker masking set files (Data Masker must be installed on the clone agent machine), separate multiple file paths with ;", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2019-05-16T11:37:47.360Z", + "LastModifiedBy": "alex-yates", + "$Meta": { + "ExportedAt": "2019-05-16T11:37:47.360Z", + "OctopusVersion": "2019.2.7", + "Type": "ActionTemplate" + }, + "Category": "redgate" +} diff --git a/step-templates/redgate-sql-clone-delete-clone.json.human b/step-templates/redgate-sql-clone-delete-clone.json.human new file mode 100644 index 000000000..192f5f92c --- /dev/null +++ b/step-templates/redgate-sql-clone-delete-clone.json.human @@ -0,0 +1,155 @@ +{ + "Id": "b13ba90b-3e67-4175-aad4-9531783c4c11", + "Name": "Redgate - SQL Clone, Delete Clone", + "Description": "Deletes a database clone with [Redgate SQL Clone](https://www.red-gate.com/products/dba/sql-clone/index). + +Requires SQL Clone. + +*Version date: 16th May 2019*", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +Write-Debug \"Entering script SQLCloneDeleteTask.ps1\" + +# The code for this step template is largely a copy/paste job from the +# Azure DevOps Services step template which is maintained by Redgate: +# https://github.com/red-gate/SqlCloneVSTSExtension/blob/master/ImageTask/SQLCloneImageTask.ps1 +# The code was copied and adapted on 16th May 2019. + +Write-Verbose \"cloneServer is $cloneServer\" +Write-Verbose \"cloneUser is $cloneUser\" +Write-Verbose \"clonePassword is $clonePassword\" +Write-Verbose \"cloneSqlServer is $cloneSqlServer\" +Write-Verbose \"cloneName is $cloneName\" + +# This line is broken: Import-Module \"$PSScriptRoot\\Modules\\RedGate.SQLClone.PowerShell.dll\" + +if($cloneUser){ + $password = ConvertTo-SecureString -String $clonePassword -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $cloneUser,$password + +} +Connect-SqlClone -ServerUrl $cloneServer -Credential $credential +Write-Output \"Connected to SQL Clone server\" + + $sqlServerParts = $cloneSqlServer.Split('\\', [System.StringSplitOptions]::RemoveEmptyEntries) + if ($sqlServerParts.Count -ge 3) + { + write-error 'SQL Server instance \"' + $cloneSqlServer + '\" has not been recognised, if specifying a named instance please use \"machine\\instance\"' + exit 1 + } + $cloneSqlServerHost = $sqlServerParts[0] + $instanceName = '' + if ($sqlServerParts.Count -ge 2) + { + $instanceName = $sqlServerParts[1] + } + try + { + $instance = Get-SqlCloneSqlServerInstance -MachineName $cloneSqlServerHost -InstanceName $instanceName + Write-Output \"Found SQL Server instance\" + } + catch + { + $instances = Get-SqlCloneSqlServerInstance + $instanceNames = \"`n\" + Foreach ($cInstance in $instances) + { + $instanceNames += $cInstance.Name + \"`n\" + } + $message = 'SQL Server instance \"' + $cloneSqlServer + '\" has not been added to SQL Clone, available instances:' + $instanceNames + write-error $message + exit 1 + } + + try + { + $clone = Get-SqlClone -Name $cloneName -Location $instance + Write-Output \"Found clone\" + } + catch + { + $clones = Get-SqlClone -Location $instance + $cloneNames = \"`n\" + Foreach ($cClone in $clones) + { + $cloneNames += $cClone.Name + \"`n\" + } + $message = 'SQL Clone clone ' + $cloneName + ' does not exist, available clones on SQL instance \"' + $cloneSqlServer + '\":' + $cloneNames + write-error $message + exit 1 + } + + Write-Output \"Deleting clone\" + Remove-SqlClone -Clone $clone | Wait-SqlCloneOperation + Write-Output \"Finished deleting clone\" + +Write-Debug \"Leaving script SQLCloneDeleteTask.ps1\"" + }, + "Parameters": [ + { + "Id": "8c140a4c-65a2-4341-a604-73d14775b3a0", + "Name": "cloneServer", + "Label": "SQL Clone Server (required)", + "HelpText": "The URL for your SQL Clone server (e.g. http://sql-clone.example.com:14145)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e5ac1d04-b8a5-440e-ba69-a5d66a53abba", + "Name": "cloneUser", + "Label": "SQL Clone User (optional)", + "HelpText": "User account to access SQL Clone. (If left blank Octopus tentacle account will be used.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5f6288f2-57c9-4a11-91f2-b0c2e3cb9ccd", + "Name": "clonePassword", + "Label": "SQL Clone Password (optional)", + "HelpText": "User account to access SQL Clone. (If left blank Octopus tentacle account will be used.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ab892024-c4b5-46c2-9434-2ad150e3e014", + "Name": "cloneSqlServer", + "Label": "SQL Server Instance (required)", + "HelpText": "The SQL Server instance the clone exists on. This SQL Server instance must have already been added to the SQL Clone Server specified above.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a758e9de-05c6-4879-8541-d7f73215fe87", + "Name": "cloneName", + "Label": "Clone Name (required)", + "HelpText": "The name of the clone to delete.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2019-05-16T11:37:47.360Z", + "LastModifiedBy": "alex-yates", + "$Meta": { + "ExportedAt": "2019-05-16T11:37:47.360Z", + "OctopusVersion": "2019.2.7", + "Type": "ActionTemplate" + }, + "Category": "redgate" +} diff --git a/step-templates/redgate-sql-clone-delete-image.json.human b/step-templates/redgate-sql-clone-delete-image.json.human new file mode 100644 index 000000000..53b3fe380 --- /dev/null +++ b/step-templates/redgate-sql-clone-delete-image.json.human @@ -0,0 +1,113 @@ +{ + "Id": "5ba6d0f2-04f1-4b52-adbf-9cf23b12ee58", + "Name": "Redgate - SQL Clone, Delete Image", + "Description": "Deletes a database image with [Redgate SQL Clone](https://www.red-gate.com/products/dba/sql-clone/index). + +Requires SQL Clone. + +*Version date: 16th May 2019*", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +# The code for this step template is largely a copy/paste job from the +# Azure DevOps Services step template which is maintained by Redgate: +# https://github.com/red-gate/SqlCloneVSTSExtension/blob/master/DeleteImageTask/SQLCloneDeleteImageTask.ps1 +# The code was copied and adapted on 16th May 2019. + +Write-Verbose \"cloneServer is $cloneServer\" +Write-Verbose \"cloneUser is $cloneUser\" +Write-Verbose \"clonePassword is $clonePassword\" +Write-Verbose \"imageName is $imageName\" + +# This line is broken: Import-Module \"$PSScriptRoot\\Modules\\RedGate.SQLClone.PowerShell.dll\" + +if($cloneUser){ + $password = ConvertTo-SecureString -String $clonePassword -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $cloneUser,$password + +} +Connect-SqlClone -ServerUrl $cloneServer -Credential $credential +Write-Output \"Connected to SQL Clone server\" + + try + { + $image = Get-SqlCloneImage -Name $imageName + Write-Output \"Found image\" + } + catch + { + $images = Get-SqlCloneImage + $imageNames = \"`n\" + Foreach ($cImage in $images) + { + $imageNames += $cImage.Name + \"`n\" + } + $message = 'SQL Clone image ' + $imageName + ' does not exist, available images: ' + $imageNames + write-error $message + exit 1 + } + + Write-Output \"Deleting image\" + Remove-SqlCloneImage -Image $image | Wait-SqlCloneOperation + Write-Output \"Finished deleting image\" + +Write-Debug \"Leaving script SQLCloneDeleteImageTask.ps1\" +" + }, + "Parameters": [ + { + "Id": "8c140a4c-65a2-4341-a604-73d14775b3a0", + "Name": "cloneServer", + "Label": "SQL Clone Server (required)", + "HelpText": "The URL for your SQL Clone server (e.g. http://sql-clone.example.com:14145)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e5ac1d04-b8a5-440e-ba69-a5d66a53abba", + "Name": "cloneUser", + "Label": "SQL Clone User (optional)", + "HelpText": "User account to access SQL Clone. (If left blank Octopus tentacle account will be used.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5f6288f2-57c9-4a11-91f2-b0c2e3cb9ccd", + "Name": "clonePassword", + "Label": "SQL Clone Password (optional)", + "HelpText": "User account to access SQL Clone. (If left blank Octopus tentacle account will be used.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ee5286d3-f233-410c-92ab-36743f8743e7", + "Name": "imageName", + "Label": "Image Name (required)", + "HelpText": "The name of your database image.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2019-05-16T11:37:47.360Z", + "LastModifiedBy": "alex-yates", + "$Meta": { + "ExportedAt": "2019-05-16T11:37:47.360Z", + "OctopusVersion": "2019.2.7", + "Type": "ActionTemplate" + }, + "Category": "redgate" +} diff --git a/step-templates/redis-install.json.human b/step-templates/redis-install.json.human new file mode 100644 index 000000000..eea6b5d13 --- /dev/null +++ b/step-templates/redis-install.json.human @@ -0,0 +1,140 @@ +{ + "Id": "3cf1dadf-169d-4fd2-b95b-f5f07343b911", + "Name": "Redis - Install service", + "Description": "Script will make sure that Redis server is installed as a Windows service + +- It will start Redis inside package directory +- To provide custom config you can prepare your own package (e.g. my-redis-64) or improve this template", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$step = $OctopusParameters['ris_UnpackageStep']\r +$force = $OctopusParameters['ris_ForceReinstall']\r +$name = $OctopusParameters['ris_ServiceName']\r +$port = $OctopusParameters['ris_Port']\r +\r +$outputPath = $OctopusParameters[\"Octopus.Action[$step].Package.CustomInstallationDirectory\"]\r +if(!$outputPath) \r +{\r + $outputPath = $OctopusParameters[\"Octopus.Action[$step].Output.Package.InstallationDirectoryPath\"]\r +}\r +if(!$outputPath) \r +{\r + Throw \"Unable to find output path for step $step. Make sure you've selected the correct step for your package.\"\r +}\r +\r +$path = Join-Path $outputPath '\\tools\\redis-server.exe'\r +if (-not (Test-Path $path) )\r +{\r + Throw \"$path was not found\"\r +}\r +\r +$service = Get-Service -Name $name -ErrorAction SilentlyContinue\r +if ($service) {\r +\r + if (-not $force) {\r + Write-Host \"Service already installed. Skipping this time.\"\r + return\r + }\r +\r + Write-Host \">>> Uninstalling with: sc.exe\"\r + if ($service.Status -eq 'Running') {\r + &\"sc.exe\" stop $name | Write-Host\r + }\r + &\"sc.exe\" delete $name | Write-Host\r +\r + $limit = 15\r + while (Get-Service -Name $name -ErrorAction SilentlyContinue) {\r + Start-Sleep -s 1\r + \r + $limit = $limit - 1\r + if ($limit -eq 0) {\r + Throw \"Unable to stop Redis service within 15s\"\r + }\r + }\r +}\r +\r +Write-Host \">>> Installing with: $path\"\r +\r +Set-Location $outputPath\r +\r +& $path --service-install --service-name $name --port $port | echo\r +& $path --service-start --service-name $name | echo\r +\r +Write-Host \">>> Verification: Expecting the service with 'Running' status\"\r +\r +$limit = 15\r +do {\r + Start-Sleep -s 1\r +\r + $limit = $limit - 1\r + if ($limit -eq 0) {\r + Throw \"Redis service did not start within 15s\"\r + }\r +\r + $service = Get-Service -Name $name -ErrorAction SilentlyContinue\r +\r +} until ($service -and $service.Status -eq 'Running')", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "2fdb42bf-b1f0-4187-b925-bdf4975090e4", + "Name": "ris_UnpackageStep", + "Label": "Unpackage step", + "HelpText": "The step where you unpack the topshelf service", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + }, + "Links": {} + }, + { + "Id": "58802353-e686-421c-b71c-8866759eca42", + "Name": "ris_ServiceName", + "Label": "Service name", + "HelpText": null, + "DefaultValue": "Redis", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "0f7fdded-881d-4bb8-94ee-5d6d71d38575", + "Name": "ris_Port", + "Label": "Port", + "HelpText": null, + "DefaultValue": "6379", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "e7411860-8679-4519-b871-dd9724ee357e", + "Name": "ris_ForceReinstall", + "Label": "Force reinstall", + "HelpText": "Service with the same name will be removed before installation", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "LastModifiedBy": "J-Sek", + "$Meta": { + "ExportedAt": "2017-08-31T23:11:05.000Z", + "OctopusVersion": "3.16.6", + "Type": "ActionTemplate" + }, + "Category": "redis" +} + diff --git a/step-templates/redis-uninstall.json.human b/step-templates/redis-uninstall.json.human new file mode 100644 index 000000000..ffb2e6889 --- /dev/null +++ b/step-templates/redis-uninstall.json.human @@ -0,0 +1,60 @@ +{ + "Id": "959fbb4a-8e2e-4e21-bc55-863837b3cbed", + "Name": "Redis - Uninstall service", + "Description": "This step can be used before unpacking a package with Redis service to stop and remove the previous installation, if there is one.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$name = $OctopusParameters['rus_ServiceName']\r +\r +$service = Get-Service -Name $name -ErrorAction SilentlyContinue\r +\r +if (-not $service) {\r + Write-Host \">>> $name service not found. Skipping this time.\"\r + return\r +}\r +\r +Write-Host \">>> Uninstalling with: sc.exe\"\r +if ($service.Status -eq 'Running') {\r + &\"sc.exe\" stop $name | Write-Host\r +}\r +&\"sc.exe\" delete $name | Write-Host\r +\r +$limit = 15\r +while (Get-Service -Name $name -ErrorAction SilentlyContinue) {\r + Start-Sleep -s 1\r + \r + $limit = $limit - 1\r + if ($limit -eq 0) {\r + Throw \"Unable to stop Redis service within 15s\"\r + }\r +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "12713171-c487-4118-8a4c-7bd8c2e5e269", + "Name": "rus_ServiceName", + "Label": "Service name", + "HelpText": null, + "DefaultValue": "Redis", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "J-Sek", + "$Meta": { + "ExportedAt": "2017-08-31T23:11:05.000Z", + "OctopusVersion": "3.16.6", + "Type": "ActionTemplate" + }, + "Category": "redis" +} diff --git a/step-templates/register-linux-tentacle.json.human b/step-templates/register-linux-tentacle.json.human new file mode 100644 index 000000000..55cb0b2e5 --- /dev/null +++ b/step-templates/register-linux-tentacle.json.human @@ -0,0 +1,142 @@ +{ + "Id": "0f119877-288c-4110-aec9-0f693c2e7922", + "Name": "Register Linux Tentacle", + "Description": "This Step Template will Register a Linux Tentacle. +[Reference](https://octopus.com/docs/infrastructure/deployment-targets/linux).", + "ActionType": "Octopus.Script", + "Version": 17, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$envname = \"#{Octopus.Environment.Name}\" +$serverurl = \"#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}\" + +$headers = @{\"X-Octopus-ApiKey\"=\"$apikey\"} +$putHeaders = @{\"X-HTTP-Method-Override\"=\"PUT\"; \"X-Octopus-ApiKey\"=\"$apikey\"} + +function Test-SpacesApi { +\tWrite-Verbose \"Checking API compatibility\"; +\t$rootDocument = Invoke-WebRequest \"$serverurl/api\" -Headers $headers -Method Get -UseBasicParsing | ConvertFrom-Json; + if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) { + \tWrite-Verbose \"Spaces API found\" + \treturn $true; + } + Write-Verbose \"Pre-spaces API found\" + return $false; +} + +if(Test-SpacesApi) { +\t$spaceId = $OctopusParameters['Octopus.Space.Id']; + if([string]::IsNullOrWhiteSpace($spaceId)) { + throw \"This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which could indicate you do not have the correct permissions.\"; + } +\t$baseApiUrl = \"/api/$spaceId\" ; +} else { +\t$baseApiUrl = \"/api\" ; +} + +$environments = Invoke-RestMethod \"$serverurl$baseApiUrl/environments/all\" -Headers $headers -Method Get +$theEnvironment = $environments | ? { $_.Name -eq $envname } + +$machines = Invoke-RestMethod \"$serverurl$baseApiUrl/machines/all\" -Headers $headers -Method Get +$theMachine = $machines | ? { $_.Name -eq $machineName } + +$accounts = Invoke-RestMethod \"$serverurl$baseApiUrl/accounts/all\" -Headers $headers -Method Get +$theAccount = $accounts | ? { $_.Name -eq $accountname } + +if (!($theMachine.Name -eq $machineName)) +{ +\t#this returns a MachineResource, but we need to post a DeploymentTargetResource which requires environments and roles +\t$discovered = Invoke-RestMethod \"$serverurl$baseApiUrl/machines/discover?host=$hostdetails&type=Ssh\" -Headers $headers -Method Get + $discovered.Endpoint.AccountId = $theAccount.Id + $discovered.Name = $machineName +\t$discovered | add-member -Name \"Roles\" -value @($role) -MemberType NoteProperty + $discovered | add-member -Name \"EnvironmentIds\" -value @($theEnvironment.Id) -MemberType NoteProperty +\t + $registerStatus = Invoke-RestMethod \"$serverurl$baseApiUrl/machines\" -Headers $headers -Method Post -Body ($discovered | ConvertTo-Json -Depth 10) + + If ($registerStatus.Status -eq \"Online\") + { + Write-Output \"$registerStatus.Name is Successfully Registered\" + } + else + { + Write-Warning \"$hostdetails had issues, Please check Environments Page\" + } +} +else +{ + Write-Output \"Machine with name $machineName already exists with the status $($theMachine.Status)\" +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "c79678c9-4d64-417d-982f-904683631240", + "Name": "apikey", + "Label": "API Key", + "HelpText": "API Key with appropriate access", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "bca181ad-f28b-42a3-ab2c-c79274224c11", + "Name": "accountname", + "Label": "Account Name", + "HelpText": "The Account Name with access to the Host name to register", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "3ace411d-18d2-4349-ab8d-02823e933f6e", + "Name": "hostdetails", + "Label": "HostName/IPaddress", + "HelpText": "HostName or IPaddress of the Machine to register", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "517a89a1-be83-4cce-9e32-8359d0715686", + "Name": "role", + "Label": "Role Name", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "37527e4f-643a-4e06-9650-1a40d11d7b7a", + "Name": "machineName", + "Label": "Machine Name", + "HelpText": "Machine Name to register", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2017-01-09T20:46:19.041+00:00", + "LastModifiedBy": "benjimac93", + "$Meta": { + "ExportedAt": "2021-08-23T12:40:10.975Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "Category": "Linux" +} diff --git a/step-templates/registry-update-configuration-variables-in-export-file.json.human b/step-templates/registry-update-configuration-variables-in-export-file.json.human new file mode 100644 index 000000000..d455c0c17 --- /dev/null +++ b/step-templates/registry-update-configuration-variables-in-export-file.json.human @@ -0,0 +1,109 @@ +{ + "Id": "7f9c1f6d-8305-4a37-a4c6-9b47ea4699d9", + "Name": "Registry - Update Configuration Variables In Export File", + "Description": "Replace values in a .reg file (Registry Export) automatically with octopus variables.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\r +param(\r + [string]$regExports\r +) \r +\r +$ErrorActionPreference = \"Stop\" \r +\r +function Get-Param($Name, [switch]$Required, $Default) {\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + $variable = Get-Variable $Name -EA SilentlyContinue \r + if ($variable -ne $null) {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null) {\r + if ($Required) {\r + throw \"Missing parameter value $Name\"\r + } else {\r + $result = $Default\r + }\r + }\r +\r + return $result\r +}\r +\r +# More custom functions would go here\r +\r +& {\r + param(\r + [string]$regExports\r + ) \r +\r + Write-Host \"Registry Export Configuration Variables\"\r + Write-Host \"regExports: $regExports\"\r +\r + ForEach ($regExp in $regExports.Split(';')) {\r + \r + $regFile = $regExp.Trim()\r + \r + if( $regFile.Length -lt 1 ){ break }\r +\r + $output = \"\"\r +\r + $fi=Get-Item $regFile\r + $file=$fi.OpenText()\r +\r + While(!($file.EndOfStream)){\r + $line=$file.ReadLine()\r + $outputLine = $line\r +\r + if($line -match \"`\"=`\"\"){\r + $keyValue = $line -split \"`\"=`\"\"\r + $key = $keyValue[0] -replace \"^`\"\" , \"\"\r + $oldVal = $keyValue[1] -replace \"`\"$\" , \"\"\r + $newVal = $OctopusParameters[$key]\r + \r + Write-Host \"Looking for key $key in OctopusParameters hash\"\r +\r + if($newVal){\r + Write-Host \"Updating $key from $oldVal to $newVal\"\r + $outputLine = \"`\"$key`\"=`\"$newVal`\"\"\r + }\r + }\r + \r + $output += $outputLine + \"`r`n\"\r + }\r +\r + $output | Out-File \"c:\\temp\\output.reg\"\r + }\r +\r + } `\r +(Get-Param 'regExports' -Required)", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "regExports", + "Label": "Registry Exports", + "HelpText": "A list of .reg files (Registry exports) separated by a ;", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedOn": "2015-07-02T14:58:41.311+00:00", + "LastModifiedBy": "jbennett", + "$Meta": { + "ExportedAt": "2015-07-02T14:59:00.322+00:00", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/releasenoteshub-add-release-association.json.human b/step-templates/releasenoteshub-add-release-association.json.human new file mode 100644 index 000000000..610e0c442 --- /dev/null +++ b/step-templates/releasenoteshub-add-release-association.json.human @@ -0,0 +1,94 @@ +{ + "Id": "61943d4d-1dd5-41d8-84ae-2ad3fd83852e", + "Name": "ReleaseNotesHub Add Associated Release", + "Description": "ReleaseNotesHub is a B2B\\B2C SaaS solution that automates the generation and publication of release notes. With RnHub the creation of Release Notes becomes another step within your CI/CD pipeline. ReleaseNotesHub can pull content from many systems including TFS, Azure Devops, GitHub, Jira and Asana. + +Use this step to create an association between two releases. This can be used to create a \"Single View\" of release notes. Visit [here](https://support.releasenoteshub.com/article/show/116108-how-to-create-a-release-association-from-octopus-deploy) for setup guide. + +https://www.releasenoteshub.com", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$headers = @{ + \"Authorization\" = \"ApiKey $ReleaseNotesHub_ApiKey\" +} + +$body = @{ + \"projectId\" = $ReleaseNotesHub_ProjectId; + \"releaseVersion\" = $ReleaseNotesHub_Version; + \"associatedProjectId\" = $ReleaseNotesHub_AssociatedProjectId; + \"associatedReleaseVersion\" = $ReleaseNotesHub_AssociatedVersion +} | ConvertTo-Json + +try { + Invoke-RestMethod -Method Post -Uri \"https://api.releasenoteshub.com/api/releaseassociations/createforversion\" -Headers $headers -Body $body -ContentType application/json-patch+json +} catch { + Write-Host \"StatusCode:\" $_.Exception.Response.StatusCode.value__ + Write-Host \"StatusDescription:\" $_.Exception.Response.StatusDescription + throw $_.Exception +}" + }, + "Parameters": [ + { + "Id": "34f80cb5-d978-4507-b7b6-8539465409d8", + "Name": "ReleaseNotesHub_ApiKey", + "Label": "ApiKey", + "HelpText": "The ApiKey can be copied from your ReleaseNotesHub Profile [page](https://support.releasenoteshub.com/article/show/105050-how-do-i-get-an-api-key).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "a8844307-20d8-468e-b6aa-22e359706fc9", + "Name": "ReleaseNotesHub_ProjectId", + "Label": "Project Id", + "HelpText": "The ReleaseNotesHub Project Id can be copied from your ReleaseNotesHub Project [page](https://support.releasenoteshub.com/article/show/111560-how-to-get-project-id).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cbc37f56-cb64-4d67-9de7-70e706ac807c", + "Name": "ReleaseNotesHub_Version", + "Label": "Release Version", + "HelpText": "Version of Release. For example \"1.2.5.2\".", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fea217c7-1eef-444e-9719-426bbd800227", + "Name": "ReleaseNotesHub_AssociatedProjectId", + "Label": "Associated Project Id", + "HelpText": "The ReleaseNotesHub Project Id can be copied from your ReleaseNotesHub Project [page](https://support.releasenoteshub.com/article/show/111560-how-to-get-project-id).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6d4ae3dc-cd0d-4a1e-9eb3-146edd8764ea", + "Name": "ReleaseNotesHub_AssociatedVersion", + "Label": "Associated Release Version", + "HelpText": "Version of associated Release. For example \"1.2.5.2\".", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-10-02T06:04:09.192Z", + "OctopusVersion": "2020.4.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "ReleaseNotesHub", + "Category": "RnHub" +} diff --git a/step-templates/releasenoteshub-pull-trigger.json.human b/step-templates/releasenoteshub-pull-trigger.json.human new file mode 100644 index 000000000..59a5afa2d --- /dev/null +++ b/step-templates/releasenoteshub-pull-trigger.json.human @@ -0,0 +1,124 @@ +{ + "Id": "8acee70f-0b6d-43a8-ab16-b832b37e0422", + "Name": "ReleaseNotesHub Pull Trigger", + "Description": "ReleaseNotesHub is a B2B\\B2C SaaS solution that automates the generation and publication of release notes. With ReleaseNotesHub the creation of Release Notes becomes another step within your CI/CD pipeline. ReleaseNotesHub can pull content from many systems including TFS, Azure Devops, GitHub, Jira and Asana. + +Use this step to trigger a pull from your Octopus Deploy process. Visit [here](https://support.releasenoteshub.com/article/show/111570-how-to-trigger-a-pull-from-octopus-deploy) for setup guide. + +https://www.releasenoteshub.com +", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "if ( -not [string]::IsNullOrEmpty( $ReleaseNotesHub_Version ) ) +{ +\t$v = $ReleaseNotesHub_Version +} +else +{ +\t$v = $OctopusParameters['Octopus.Release.Number'] +} + +$headers = @{ + \"Authorization\" = \"ApiKey $ReleaseNotesHub_ApiKey\" +} + +$body = @{ + \"name\" = $ReleaseNotesHub_Name; + \"version\" = $v; + \"createOnNotFound\" = $ReleaseNotesHub_CreateOnNotFound; + \"IgnoreIfExists\" = $ReleaseNotesHub_IgnoreIfExists; + \"publish\" = $ReleaseNotesHub_Publish +} | ConvertTo-Json + +try { + Invoke-RestMethod -Method Post -Uri \"https://api.releasenoteshub.com/api/pull/PullVersion/$ReleaseNotesHub_ProjectId\" -Headers $headers -Body $body -ContentType application/json-patch+json +} catch { + Write-Host \"StatusCode:\" $_.Exception.Response.StatusCode.value__ + Write-Host \"StatusDescription:\" $_.Exception.Response.StatusDescription + throw $_.Exception +}" + }, + "Parameters": [ + { + "Id": "3341141f-ab5d-4204-9d84-b46cb0e1ba71", + "Name": "ReleaseNotesHub_ApiKey", + "Label": "ApiKey", + "HelpText": "The ApiKey can be copied from your ReleaseNotesHub Profile [page](https://support.releasenoteshub.com/article/show/105050-how-do-i-get-an-api-key).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "f1da89f8-94c5-48c3-a15f-690a466e131c", + "Name": "ReleaseNotesHub_Name", + "Label": "Name", + "HelpText": "A Name can be specified when creating a new Release.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "02e58f68-5a86-41cd-9b21-636d5eb162bf", + "Name": "ReleaseNotesHub_Version", + "Label": "Version", + "HelpText": "Version is an optional parameter. If left blank, the Octopus Deploy system variable 'Octopus.Release.Number' will be used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0d3f067f-1432-4b3a-9c63-9a2eb9b6b522", + "Name": "ReleaseNotesHub_ProjectId", + "Label": "ReleaseNotesHub Project Id", + "HelpText": "The ReleaseNotesHub Project Id can be copied from your ReleaseNotesHub Project [page](https://support.releasenoteshub.com/article/show/111560-how-to-get-project-id).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8218468f-36f4-48d3-8e93-4a04745b6bac", + "Name": "ReleaseNotesHub_Publish", + "Label": "Publish", + "HelpText": "Select false to create your release notes in draft status, or select true to Publish.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Name": "ReleaseNotesHub_IgnoreIfExists", + "DefaultValue": "false", + "Label": "Ignore if Release exists", + "HelpText": "If release already exists then ignore pull request." + }, + { + "Id": "49db9d3c-3734-4a56-8dce-364785d58a12", + "Name": "ReleaseNotesHub_CreateOnNotFound", + "Label": "Create release if not found", + "HelpText": "If CreateOnNotFound is set to true and a Release with the specified version cannot be found, a release will automatically be created.", + "DefaultValue": "true", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-12T04:02:39.748Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "ReleaseNotesHub", + "Category": "RnHub" +} diff --git a/step-templates/remove-server-from-azure-load-balancer.json.human b/step-templates/remove-server-from-azure-load-balancer.json.human new file mode 100644 index 000000000..e2d6264cb --- /dev/null +++ b/step-templates/remove-server-from-azure-load-balancer.json.human @@ -0,0 +1,111 @@ +{ + "Id": "5658d525-2a04-47da-85a0-00244976d811", + "Name": "Remove Server from Azure Load Balancer", + "Description": "Uses Service Principal to Remove Server From Azure Load Balancer.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "#region Verify variables + +#Verify rsflbAzureSubscription is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['rsflbAzureSubscription'])) +{ + Throw 'Azure Subscription cannot be null.' +} +$azureSubscription = $OctopusParameters['rsflbAzureSubscription'] +Write-Host ('Azure Subscription: ' + $azureSubscription) + +#Verify rsflbAzureResourceGroup is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['rsflbAzureResourceGroup'])) +{ + Throw 'Azure Resource Group cannot be null.' +} +$azureResourceGroup = $OctopusParameters['rsflbAzureResourceGroup'] +Write-Host ('Azure Resource Group: ' + $azureResourceGroup) + +#Verify rsflbAzureMachineName is not null. +If ([string]::IsNullOrEmpty($OctopusParameters['rsflbAzureMachineName'])) +{ + Throw 'Azure Machine Name cannot be null.' +} +$azureMachineName = $OctopusParameters['rsflbAzureMachineName'] +Write-Host ('Azure Machine Name: ' + $azureMachineName) + +#endregion + +#region Process + +Set-AzureRmContext -SubscriptionName $azureSubscription + +$azureVM = Get-AzureRmVM -ResourceGroupName $azureResourceGroup -Name $azureMachineName +If (!$azureVM) +{ + Throw 'Could not retrieve server from Azure needed to remove from Load Balancer.' +} + +$nic = (Get-AzureRmNetworkInterface -ResourceGroupName $azureResourceGroup | Where-Object {$_.VirtualMachine.Id -eq $azureVM.Id}) +If (!$nic) +{ + Throw 'Could not retrieve NIC from Azure needed to remove from Load Balancer.' +} + +$nic.IpConfigurations[0].LoadBalancerBackendAddressPools = $null +$nic | Set-AzureRmNetworkInterface + +#endregion", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Azure.AccountId": "#{rsflbAzureAccount}" + }, + "Parameters": [ + { + "Id": "6efd155e-8ab7-4236-bc16-7748a582e3af", + "Name": "rsflbAzureAccount", + "Label": "Azure Account", + "HelpText": "Service Principal used to connect to Azure", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "2cff4dbe-685c-47e0-b7c7-6d6ee25bdc48", + "Name": "rsflbAzureSubscription", + "Label": "Azure Subscription", + "HelpText": "Subscription Load Balancer belongs to. Eg. 00000000-0000-0000-0000-000000000000", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "150ef843-5b2c-43e9-ac12-a8d0cc45565e", + "Name": "rsflbAzureResourceGroup", + "Label": "Azure Resource Group", + "HelpText": "Resource Group Load Balancer belongs to. Eg. ProductionAus", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7397be30-76e3-4c16-9b8b-05c451a23e70", + "Name": "rsflbAzureMachineName", + "Label": "Azure Machine Name", + "HelpText": "Name of Virtual Machine to remove from Load Balancer. Eg Web01", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-22T10:40:51.529Z", + "OctopusVersion": "2020.2.8", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "benjimac93", + "Category": "Azure" +} diff --git a/step-templates/retention-policy.json.human b/step-templates/retention-policy.json.human new file mode 100644 index 000000000..743b1a9b0 --- /dev/null +++ b/step-templates/retention-policy.json.human @@ -0,0 +1,189 @@ +{ + "Id": "46705230-c116-41c6-ba4b-b58d43f4f0e1", + "Name": "Retention Policy", + "Description": "Applies retention policy for built-in package repository by specified package id. Useful when you are using variables in PackageId parameter of deploy package step and built-in retention policy for package repository is not deleting packages.", + "ActionType": "Octopus.Script", + "Version": 15, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Function Get-Parameter($Name, [switch]$Required, $Default, [switch]$FailOnValidate) { + $result = $null + $errMessage = [string]::Empty + + If ($OctopusParameters -ne $null) { + $octopusParameterName = (\"Retention.\" + $Name) + $result = $OctopusParameters[$octopusParameterName] + Write-Host \"Octopus paramter value for $octopusParameterName : $result\" + } + + If ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + If ($result -eq $null) { + If ($Required) { + $errMessage = \"Missing parameter value $Name\" + } Else { + $result = $Default + } + } + + If (-Not [string]::IsNullOrEmpty($errMessage)) { + If ($FailOnValidate) { + Throw $errMessage + } Else { + Write-Warning $errMessage + } + } + + return $result +} + +Function Validate-Parameters([switch]$FailOnValidate) { + $errMessage = [string]::Empty + + If ($retentionCriteria.ToLower() -eq \"days\") { + If ($retentionValue -lt 3) { + $errMessage = \"Retention Value not specified or must be greater than 3 days!\" + } + } ElseIf ($retentionCriteria.ToLower() -eq \"number\") { + If ($retentionValue -lt 10) { + $errMessage = \"Retention Value not specified or must be greater than 9 packages!\" + } + } Else { + $errMessage = \"Retention Criteria must be 'days' or 'number'!\" + } + + If ([string]::IsNullOrEmpty($errMessage)) { + return $true; + } Else { + If ($FailOnValidate) { + Throw $errMessage + } Else { + Write-Warning $errMessage + return $false; + } + } +} + +& { + Write-Host \"Start RetentionPolicy\" + + $retentionFailOnValidate = [System.Convert]::ToBoolean([string](Get-Parameter \"FailOnValidate\" $false \"False\" $false)) + $packagesRootDirectoryPath = [string] (Get-Parameter \"PackagesRootDirectory\" $true [string]::Empty $retentionFailOnValidate) + $retentionCriteria = [string] (Get-Parameter \"Criteria\" $true \"days\" $retentionFailOnValidate) + $retentionValue = [int] (Get-Parameter \"Value\" $true 30 $retentionFailOnValidate) + $retentionPackageId = [string] (Get-Parameter \"PackageId\" $true [string]::Empty $retentionFailOnValidate) + + If ((Validate-Parameters $retentionFailOnValidate)) { + + # Filter out package folders by name if parameter specified + $packageDirectories = Get-ChildItem $packagesRootDirectoryPath | ?{ $_.PSIsContainer } | ?{ $_.Name -eq $retentionPackageId } + + If ($packageDirectories.Length -le 0) { + Write-Warning \"No package directories found!\" + } Else { + ForEach ($packageDirectory in $packageDirectories) { + $packageFiles = Get-ChildItem $packageDirectory.FullName + If ($packageFiles.Length -gt 0) { + Write-Host (\"Package files found in directory: \" + $packageDirectory.FullName + \" - \" + $packageFiles.Length) + $packageFilesObsolete = @() + + If ($retentionCriteria -eq \"days\") { + $packageFilesObsolete = $packageFiles | ?{ $_.LastWriteTime -le ((Get-Date).AddDays($retentionValue * -1)) } + } ElseIf ($retentionCriteria -eq \"number\") { + $filesToDelete = ($packageFiles.Length - $retentionValue) + If ($filesToDelete -gt 0) { + $packageFilesObsolete = $packageFiles | Sort-Object LastWriteTime | Select-Object -First $filesToDelete + } + } + + If ($packageFilesObsolete.Length -gt 0) { + Write-Host (\"Applying retention policy for \" + $packageFilesObsolete.Length + \" obsolete files in directory: \" + $packageDirectory.FullName) + ForEach ($packageVersionFileObsolete in $packageFilesObsolete) { + Remove-Item -Path $packageVersionFileObsolete.FullName -Force -Recurse + } + } Else { + Write-Host (\"No package files deleted, all files match policy rules!\") + } + } Else { + Write-Host (\"No files found, removing empty directory: \" + $packageDirectory.FullName) + Remove-Item -Path $packageDirectory.FullName -Force -Recurse + } + } + } + } ElseIf ($retentionFailOnValidate -eq $true) { + throw \"Missing or invalid parameter values!\" + } + + Write-Host \"End RetentionPolicy\" +} + +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "Retention.PackagesRootDirectory", + "Label": "Packages root directory", + "HelpText": "Packages directory path on Octopus server (e.g. D:\\Octopus\\Packages).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Retention.Criteria", + "Label": "Criteria", + "HelpText": "Criteria by which to apply retention policy - days or number of packages.", + "DefaultValue": "days", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "days|Days +number|Number of packages" + } + }, + { + "Name": "Retention.Value", + "Label": "Value", + "HelpText": "Value for selected criteria. +Min value for days criteria - 3. +Min value for number criteria - 10.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Retention.PackageId", + "Label": "Package Id", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Retention.FailOnValidate", + "Label": "Fail on validate", + "HelpText": "If true throws exception when parameter validation fails. If false just outputs warning messages to deployment log and doesn't fail whole deployment.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "sarbis", + "$Meta": { + "ExportedAt": "2016-08-27T20:53:13.764+00:00", + "OctopusVersion": "3.3.16", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} diff --git a/step-templates/rollbar-notify-deployment.json.human b/step-templates/rollbar-notify-deployment.json.human new file mode 100644 index 000000000..b05df1ef7 --- /dev/null +++ b/step-templates/rollbar-notify-deployment.json.human @@ -0,0 +1,115 @@ +{ + "Id": "2b204b54-165f-4c5b-a856-ac932dfa979e", + "Name": "Rollbar - Notify Deployment", + "Description": "Posts a deployment notification to Rollbar.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptBody": "try {\r $uri = $OctopusParameters[\"URI\"]; \r $accessToken = $OctopusParameters[\"AccessToken\"];\r $environment = $OctopusParameters[\"Environment\"];\r $revision = $OctopusParameters[\"Revision\"];\r $revisionFilename = $OctopusParameters[\"RevisionFilename\"];\r $localUsername = $OctopusParameters[\"LocalUsername\"];\r $rollbarUsername = $OctopusParameters[\"RollbarUsername\"];\r $comment = $OctopusParameters[\"Comment\"];\r \r if ($revisionFilename) {\r $revision = Get-Content $revisionFilename;\r }\r \r $arguments = \"access_token=$accessToken&environment=$environment&revision=$revision&local_username=$localUsername&rollbar_username=$rollbarUsername&comment=$comment\";\r \r Write-Host 'Notifying Deployment to Rollbar';\r Write-Host $arguments;\r \r (new-object net.webclient).UploadString($uri, $arguments);\r \r} catch {\r $ErrorMessage = $_.Exception.Message;\r Write-Error $ErrorMessage;\r}\r", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "8ad2b4ef-72c2-491a-8cc9-8034144f6580", + "Name": "AccessToken", + "Label": "Access Token", + "HelpText": "Your project access token. Required.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2252ae4e-d664-4b92-af45-1f8e4049afb1", + "Name": "Environment", + "Label": "Environment", + "HelpText": "Name of the environment being deployed, e.g. \"production\". Required.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "47651ca8-9d07-4981-8aa6-c2ccee241bc9", + "Name": "Revision", + "Label": "Revision", + "HelpText": "Revision number/sha being deployed. If using git, use the full sha. Required unless using Revision Filename.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f8b27505-bac6-4690-b1f8-d171a3bc399a", + "Name": "RevisionFilename", + "Label": "Revision Filename", + "HelpText": "Name of a file to read revision number/sha being deployed from. If using git, use the full sha. Required unless using Revision.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2b5926f9-dde9-4abf-97e4-1e71e54ed19d", + "Name": "LocalUsername", + "Label": "Local Username", + "HelpText": "User who deployed. Optional.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "5e2656b6-8d61-4813-83d0-cd3d3ff2e559", + "Name": "RollbarUsername", + "Label": "Rollbar Username", + "HelpText": "Rollbar username of the user who deployed. Optional.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "81203f12-0c6e-4771-a205-ea9c69a6a3a3", + "Name": "Comment", + "Label": "Comment", + "HelpText": "Deploy comment (e.g. what is being deployed). Optional. Will be rendered as Rollbar-flavored Markdown.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "03fcb027-dd94-49c0-9cb4-8b93771118cd", + "Name": "URI", + "Label": "URI", + "HelpText": "Specifies the Rollbar API deploy endpoint.", + "DefaultValue": "https://api.rollbar.com/api/1/deploy/", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2017-04-26T14:41:00.000Z", + "LastModifiedBy": "sandord", + "$Meta": { + "ExportedAt": "2017-02-07T13:42:26.852Z", + "OctopusVersion": "3.7.10", + "Type": "ActionTemplate" + }, + "Category": "rollbar" +} diff --git a/step-templates/rotate_azure_load_balancer_pool.json.human b/step-templates/rotate_azure_load_balancer_pool.json.human new file mode 100644 index 000000000..457139159 --- /dev/null +++ b/step-templates/rotate_azure_load_balancer_pool.json.human @@ -0,0 +1,177 @@ +{ + "Id": "947623c6-940d-4a54-a18b-c755a1035dce", + "Name": "Rotate Azure Load Balancer Pool", + "Description": "Updates all rules on an Azure load balancer to point to the next backend pool in a specified list. The current backend pool will be determined and the next pool in a provided list will become the target of all rules. If the current pool doesn't exist in the list, the first pool in the list will be used. This means that a specific pool can be chosen by specifying only a single pool.", + "ActionType": "Octopus.AzurePowerShell", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "OctopusUseBundledTooling": "False", + "Octopus.Action.Script.ScriptBody": "$LoadBalancerNames = [string]$OctopusParameters['RotateAzureLoadBalancerPool.LoadBalancerName']\r +$AvailablePools = [string]$OctopusParameters['RotateAzureLoadBalancerPool.AvailablePools']\r +$RuleNames = [string]$OctopusParameters['RotateAzureLoadBalancerPool.RuleNames']\r +$WhatIf = [bool]::Parse($OctopusParameters['RotateAzureLoadBalancerPool.WhatIf'])\r +\r +if ($null -eq $LoadBalancerNames) {\r + throw 'No load balancers selected. Please select at least one load balancer.'\r +} else {\r + # Trim white space and blank lines and get all load balancers that match the names.\r + $loadBalancers = $LoadBalancerNames.Split(\"`n\").Trim().Where({ $_ }) | ForEach-Object { Get-AzLoadBalancer -Name $_ }\r +}\r +if ($null -eq $AvailablePools) {\r + throw 'No pools selected. Please select at least one pool.'\r +} else {\r + $AvailablePools = $AvailablePools.Split(\"`n\").Trim().Where({ $_ }) # Trim white space and blank lines.\r +}\r +if ($null -eq $RuleNames) {\r + throw 'No rules selected. Please select at least one rule name or use an asterisk (*) to select all rules.'\r +} else {\r + $RuleNames = $RuleNames.Split(\"`n\").Trim().Where({ $_ }) # Trim white space and blank lines.\r +}\r +\r +foreach ($loadBalancer in $loadBalancers) {\r + $loadBalancerName = $loadBalancer.Name\r + $resourceGroupName = $loadBalancer.ResourceGroupName\r + $allPools = Get-AzLoadBalancerBackendAddressPool -LoadBalancerName $loadBalancerName -ResourceGroupName $resourceGroupName\r +\r + Write-Host \"Updating Load Balancer '$loadBalancerName'.\"\r +\r + # Start by assuming no rules match\r + $rules = @()\r +\r + # Add each distinct rule that matches one of the rule names\r + foreach ($ruleName in $RuleNames) {\r + $rules += $LoadBalancer.LoadBalancingRules | Where-Object {\r + $_.Id.Split('/')[-1] -like $ruleName -and\r + $_.Id -notin $rules.Id\r + }\r + }\r +\r + if ($rules.Count -eq 0) {\r + Write-Warning \"No matching rules were found on load balancer '$loadBalancerName'.\"\r + continue\r + }\r +\r + # Resolve any wildcards in AvailablePools to get a list of valid pool names.\r + $validPoolNames = @()\r + foreach ($name in $AvailablePools) {\r + $validPoolNames += $allPools.Name | Where-Object { $_ -like $name }\r + }\r +\r + # The same pool could match multiple wildcards so get a unique list\r + if ($validPoolNames) {\r + $validPoolNames = [array]($validPoolNames | Select-Object -Unique)\r + } else {\r + Write-Warning \"No valid pools were found on load balancer '$loadBalancerName'.\"\r + continue\r + }\r +\r + # Update each rule with a new pool \r + foreach ($rule in $rules) {\r + $currentPoolName = $rule.BackendAddressPool.Id.Split('/')[-1]\r +\r + # This will find the next pool in the list, cycling back to the beginning if at the end. If the current pool isn't in the list,\r + # its index will be -1. The next index will be zero so the first pool will be selected. \r + $index = $validPoolNames.IndexOf($currentPoolName)\r + $nextIndex = ($index + 1) % $validPoolNames.Count\r + $newPoolName = $validPoolNames[$nextIndex]\r +\r + # Get the new pool to use\r + if ($newPoolName -in $allPools.Name) {\r + $newPool = Get-AzLoadBalancerBackendAddressPool -ResourceGroupName $resourceGroupName -LoadBalancerName $loadBalancerName -Name $newPoolName\r + } else {\r + throw \"Backend Pool '$newPoolName' does not exist on load balancer '$loadBalancerName'.\"\r + }\r +\r + if ($currentPoolName -eq $newPoolName) {\r + Write-Highlight \"Rule '$($rule.Name)' is already pointing to pool '$currentPoolName' on load balancer '$loadBalancerName'.\"\r + } else {\r + Write-Highlight \"Rule '$($rule.Name)' is pointing to pool '$currentPoolName'. Updating to pool '$newPoolName' on load balancer '$loadBalancerName'.\"\r + $rule.BackendAddressPool.Id = $newPool.Id\r +\r + foreach ($pool in $rule.BackendAddressPools) {\r + $pool.Id = $newPool.Id\r + }\r + }\r + }\r +\r + if ($WhatIf) {\r + Write-Highlight \"WhatIf is set to true so skipping changes on Azure for load balancer '$loadBalancerName'.\"\r + } else {\r + Write-Verbose \"Writing changes to Azure for load balancer '$loadBalancerName'.\"\r + Set-AzLoadBalancer -LoadBalancer $loadBalancer | Out-Null\r + }\r +}\r +", + "Octopus.Action.Azure.AccountId": "#{RotateAzureLoadBalancerPool.Account}" + }, + "Parameters": [ + { + "Id": "cef3a407-e9e1-44e5-9dcf-9dcf586f4958", + "Name": "RotateAzureLoadBalancerPool.Account", + "Label": "Account", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "7a8c14dd-072d-432e-9071-296381dd9cc9", + "Name": "RotateAzureLoadBalancerPool.LoadBalancerName", + "Label": "Load Balancer", + "HelpText": "The name of the load balancer to use. Multiple load balancers can be selected if they have the same pool names. Wildcards are supported. + +Enter one load balancer per line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "1295c989-efbd-4605-a362-b72ea9762c4f", + "Name": "RotateAzureLoadBalancerPool.AvailablePools", + "Label": "Available Pools", + "HelpText": "A list of available pools to use. If multiple pools are specified, rules will be updated to use the next pool in the list. If a single pool is specified, rules will use that pool. Wildcards are supported. + +Enter one pool per line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "ed5fc080-37c8-44ce-a89a-76313c89f10d", + "Name": "RotateAzureLoadBalancerPool.RuleNames", + "Label": "Rule Names", + "HelpText": "A list of rule names to update. Wildcards are supported. + +Enter one rule per line.", + "DefaultValue": "*", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "27caa59c-5f1a-42f8-a545-5075d47f3182", + "Name": "RotateAzureLoadBalancerPool.WhatIf", + "Label": "What If", + "HelpText": "Will just report expected changes, but not update the load balancer.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.AzurePowerShell", + "$Meta": { + "ExportedAt": "2023-11-22T03:48:35.019Z", + "OctopusVersion": "2023.2.13113", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "REOScotte", + "Category": "octopus" +} diff --git a/step-templates/roundhouse-database-migration.json.human b/step-templates/roundhouse-database-migration.json.human new file mode 100644 index 000000000..b07702db3 --- /dev/null +++ b/step-templates/roundhouse-database-migration.json.human @@ -0,0 +1,600 @@ +{ + "Id": "6da0afee-ed55-4c75-a13b-5e8ce42ef027", + "Name": "RoundhousE Database Migrations", + "Description": "Database migrations using [RoundhousE](https://github.com/chucknorris/roundhouse). +With this template you can either include RoundhousE with your package or use the `Download RoundhousE?` feature to download it at deploy time. If you're downloading, you can choose the version by specifying it in the `Version of RoundhousE`. + +NOTE: + - AWS EC2 IAM Role authentication requires the AWS CLI be installed. + - To run on Linux, the machine must have both PowerShell Core and .NET Core 3.1 installed. + +Note - The RoundhousE GitHub Project has been abandoned and as a result this template is now deprecated. Version 9 will be the last release of this template.", + "ActionType": "Octopus.Script", + "Version": 9, + "Author": "twerthi", + "Packages": [ + { + "Id": "249cd84b-2e41-4001-8222-f1a60f9a50ea", + "Name": "roundhousePackage", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "roundhousePackage" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Configure template + +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) +{ +\tWrite-Host \"Determining Operating System...\" + switch ([System.Environment]::OSVersion.Platform) + { + \t\"Win32NT\" + { + \t# Set variable + $IsWindows = $true + $IsLinux = $false + } + \"Unix\" + { + \t$IsWindows = $false + $IsLinux = $true + } + } +} + +# Define parameters +$roundhouseExecutable = \"\" +$roundhouseOutputPath = [System.IO.Path]::Combine($OctopusParameters[\"Octopus.Action.Package[RoundhousEPackage].ExtractedPath\"], \"output\") +$roundhouseSsl = [System.Convert]::ToBoolean($roundhouseSsl) + +# Determines latest version of github repo +Function Get-LatestVersionNumber +{ + # Define parameters + param ($GitHubRepository) + + # Define local variables + $releases = \"https://api.github.com/repos/$GitHubRepository/releases\" + + # Get latest version + Write-Host \"Determining latest release ($releases) ...\" + + $tags = (Invoke-WebRequest $releases -UseBasicParsing | ConvertFrom-Json) + + # Find the latest version with a downloadable asset + foreach ($tag in $tags) + { + if ($tag.assets.Count -gt 0) + { + #return $tag.assets.browser_download_url + return $tag.tag_name + } + } + + # Return the version + return $null +} + +# Change the location to the extract path +Set-Location -Path $OctopusParameters[\"Octopus.Action.Package[RoundhousEPackage].ExtractedPath\"] + +# Check to see if download is specified +if ([System.Boolean]::Parse($roundhouseDownloadNuget)) +{ + # Set secure protocols + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +\t# Check to see if version number specified + if ([string]::IsNullOrEmpty($roundhouseNugetVersion)) + { + \t# Get the latest version number + $roundhouseNugetVersion = Get-LatestVersionNumber -GitHubRepository \"chucknorris/roundhouse\" + } + + # Check for download folder + if ((Test-Path -Path \"$PSSCriptRoot\\roundhouse\") -eq $false) + { + # Create the folder + New-Item -ItemType Directory -Path \"$PSSCriptRoot\\roundhouse\" + } + + # Download nuget package + Write-Output \"Downloading https://github.com/chucknorris/roundhouse/releases/download/$roundhouseNugetVersion/dotnet-roundhouse.$roundhouseNugetVersion.nupkg ...\" + Invoke-WebRequest -Uri \"https://github.com/chucknorris/roundhouse/releases/download/$roundhouseNugetVersion/dotnet-roundhouse.$roundhouseNugetVersion.nupkg\" -OutFile \"$PSSCriptRoot\\roundhouse\\dotnet-roundhouse.$roundhouseNugetVersion.nupkg\" + + # Change file extension + $nugetPackage = Get-ChildItem -Path \"$PSSCriptRoot\\roundhouse\\dotnet-roundhouse.$roundhouseNugetVersion.nupkg\" + $nugetPackage | Rename-Item -NewName $nugetPackage.Name.Replace(\".nupkg\", \".zip\") + + # Extract the package + Write-Output \"Extracting dotnet-roundhouse.$roundhouseNugetVersion.nupkg ...\" + Expand-Archive -Path \"$PSSCriptRoot\\roundhouse\\dotnet-roundhouse.$roundhouseNugetVersion.zip\" -DestinationPath \"$PSSCriptRoot\\roundhouse\" +} + +# Set Executable depending on OS +if ($IsWindows) +{ + # Look for older .exe + $roundhouseExecutable = Get-ChildItem -Path $PSSCriptRoot -Recurse | Where-Object {$_.Name -eq \"rh.exe\"} +} + +if ([string]::IsNullOrWhitespace($roundhouseExecutable)) +{ +\t# Look for just rh.dll + $roundhouseExecutable = Get-ChildItem -Path $PSSCriptRoot -Recurse | Where-Object {$_.Name -eq \"rh.dll\"} +} + +if ([string]::IsNullOrWhitespace($roundhouseExecutable)) +{ + # Couldn't find RoundhousE + Write-Error \"Couldn't find the RoundhousE executable!\" +} + +# Build the arguments +$roundhouseSwitches = @() + +# Update the connection string based on authentication method +switch ($roundhouseAuthenticationMethod) +{ + \"awsiam\" + { + # Region is part of the RDS endpoint, extract + $region = ($roundhouseServerName.Split(\".\"))[2] + + Write-Host \"Generating AWS IAM token ...\" + $roundhouseUserPassword = (aws rds generate-db-auth-token --hostname $roundhouseServerName --region $region --port $roundhouseServerPort --username $roundhouseUserName) + $roundhouseUserInfo = \"Uid=$roundhouseUserName;Pwd=$roundhouseUserPassword;\" + + break + } + + \"usernamepassword\" + { + \t# Append remaining portion of connection string + $roundhouseUserInfo = \"Uid=$roundhouseUserName;Pwd=$roundhouseUserPassword;\" + +\t\tbreak +\t} + + \"windowsauthentication\" + { + # Append remaining portion of connection string +\t $roundhouseUserInfo = \"integrated security=true;\" + + # Append username (required for non + $roundhouseUserInfo += \"Uid=$roundhouseUserName;\" + } +} + + +# Configure connnection string based on technology +switch ($roundhouseDatabaseServerType) +{ + \"sqlserver\" + { + # Check to see if port has been defined + if (![string]::IsNullOrEmpty($roundhouseServerPort)) + { + # Append to servername + $roundhouseServerName += \",$roundhouseServerPort\" + + # Empty the port + $roundhouseServerPort = [string]::Empty + } + } + \"mariadb\" + { + \t# Use the MySQL client + $roundhouseDatabaseServerType = \"mysql\" + $roundhouseServerPort = \"Port=$roundhouseServerPort;\" + } + default + { + $roundhouseServerPort = \"Port=$roundhouseServerPort;\" + } +} + +# Build base connection string +$roundhouseServerConnectionString = \"--connectionstring=Server=$roundhouseServerName;$roundhouseServerPort $roundhouseUserInfo Database=$roundhouseDatabaseName;\" + +if ($roundhouseSsl -eq $true) +{ +\tif (($roundhouseDatabaseServerType -eq \"mariadb\") -or ($roundhouseDatabaseServerType -eq \"mysql\") -or ($roundhouseDatabaseServerType -eq \"postgres\")) + { + \t# Add sslmode + $roundhouseServerConnectionString += \"SslMode=Require;Trust Server Certificate=true;\" + } + else + { + \tWrite-Warning \"Invalid Database Server Type selection for SSL, ignoring setting.\" + } +} + +$roundhouseSwitches += $roundhouseServerConnectionString + +$roundhouseSwitches += \"--databasetype=$roundhouseDatabaseServerType\" +$roundhouseSwitches += \"--silent\" + + +# Check for folder definitions +if (![string]::IsNullOrEmpty($roundhouseUpFolder)) +{ + # Add up folder + $roundhouseSwitches += \"--up=$roundhouseUpFolder\" +} + +if (![string]::IsNullOrEmpty($roundhouseAlterDatabaseFolder)) +{ + $roundhouseSwitches += \"--alterdatabasefolder=$roundhouseAlterDatabaseFolder\" +} + +if (![string]::IsNullOrEmpty($roundhouseRunBeforeUpFolder)) +{ + $roundhouseSwitches += \"--runbeforeupfolder=$roundhouseRunBeforeUpFolder\" +} + +if (![string]::IsNullOrEmpty($roundhouseFunctionsFolder)) +{ + $roundhouseSwitches += \"--functionsfolder=$roundhouseFunctionsFolder\" +} + +if (![string]::IsNullOrEmpty($roundhouseViewsFolder)) +{ + $roundhouseSwitches += \"--viewsfolder=$roundhouseViewsFolder\" +} + +if (![string]::IsNullOrEmpty($roundhouseSprocsFolder)) +{ + $roundhouseSwitches += \"--sprocsfolder=$roundhouseSprocsFolder\" +} + +if (![string]::IsNullOrEmpty($roundhouseIndexFolder)) +{ + $roundhouseSwitches += \"--indexesfolder=$roundhouseIndexFolder\" +} + +if (![string]::IsNullOrEmpty($roundhouseRunAfterAnyTimeFolder)) +{ + $roundhouseSwitches += \"--runAfterOtherAnyTimeScriptsFolder=$roundhouseRunAfterAnyTimeFolder\" +} + +if (![string]::IsNullOrEmpty($roundhousePermissionsFolder)) +{ + $roundhouseSwitches += \"--permissionsfolder=$roundhousePermissionsFolder\" +} + +if (![string]::IsNullOrEmpty($roundhouseTriggerFolder)) +{ + $roundhouseSwitches += \"--triggersfolder=$roundhouseTriggerFolder\" +} + +if ([System.Boolean]::Parse($roundhouseDryRun)) +{ + $roundhouseSwitches += \"--dryrun\" +} + +if ([System.Boolean]::Parse($roundhouseRecordOutput)) +{ + $roundhouseSwitches += \"--outputpath=$roundhouseOutputPath\" +} + +# Add transaction switch +$roundhouseSwitches += \"--withtransaction=$($roundhouseWithTransaction.ToLower())\" + +# Check for version +if (![string]::IsNullOrEmpty($roundhouseVersion)) +{ + # Add version + $roundhouseSwitches += \"--version=$roundhouseVersion\" +} + +# Display what's going to be run +if (![string]::IsNullOrWhitespace($roundhouseUserPassword)) +{ +\tWrite-Host \"Executing $($roundhouseExecutable.FullName) with $($roundhouseSwitches.Replace($roundhouseUserPassword, \"****\"))\" +} +else +{ +\tWrite-Host \"Executing $($roundhouseExecutable.FullName) with $($roundhouseSwitches)\" +} + +# Execute RoundhousE +if ($roundhouseExecutable.FullName.EndsWith(\".dll\")) +{ +\t& dotnet $roundhouseExecutable.FullName $roundhouseSwitches +} +else +{ +\t& $roundhouseExecutable.FullName $roundhouseSwitches +} + +# If the output path was specified, attach artifacts +if ([System.Boolean]::Parse($roundhouseRecordOutput)) +{ + # Zip up output folder content + Add-Type -Assembly 'System.IO.Compression.FileSystem' + + $zipFile = \"$($OctopusParameters[\"Octopus.Action.Package[RoundhousEPackage].ExtractedPath\"])/output.zip\" + +\t[System.IO.Compression.ZipFile]::CreateFromDirectory($roundhouseOutputPath, $zipFile) + New-OctopusArtifact -Path \"$zipFile\" -Name \"output.zip\" +} +" + }, + "Parameters": [ + { + "Id": "7ab35b02-b5c7-4818-b282-4915a93d78ce", + "Name": "roundhousePackage", + "Label": "RoundhousEPackage", + "HelpText": "The package containing the scripts for RoundhousE to deploy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "b2bb212c-cf5a-4c48-88c7-045ae7841fcc", + "Name": "roundhouseServerName", + "Label": "Database Server Name", + "HelpText": "Name or IP address of the server being deployed to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7d78b1fb-e45a-484a-bcc7-e3360ab69342", + "Name": "roundhouseServerPort", + "Label": "Database Server Port", + "HelpText": "Port number for the database server. Uses default server port if left blank.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8363dfa2-4509-4c57-9784-5baf27e96397", + "Name": "roundhouseAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Method used to authenticate to the database server.", + "DefaultValue": "usernamepassword", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "awsiam|AWS EC2 IAM Role +usernamepassword|Username\\Password +windowsauthentication|Windows Authentication" + } + }, + { + "Id": "a49823d8-9269-48e1-92fb-819712fb56a4", + "Name": "roundhouseDatabaseName", + "Label": "Database Name", + "HelpText": "Name of the database to deploy to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c4988c69-539d-48c7-8218-51b82b6253a6", + "Name": "roundhouseSsl", + "Label": "Force SSL", + "HelpText": "Check this box for force connection string to use SSL. Only applicable to MariaDB, MySQL, and PostgreSQL database types.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "99efe391-c705-4124-9b89-b8c95481d537", + "Name": "roundhouseVersion", + "Label": "Database Version", + "HelpText": "Version number of your database migration. Default value is the version of the RoundhousE package.", + "DefaultValue": "#{Octopus.Action.Package[RoundhousEPackage].PackageVersion}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2e27695f-4789-456a-9158-a847b2059d32", + "Name": "roundhouseUsername", + "Label": "Database Username", + "HelpText": "Username of the account with sufficient permissions to execute scripts. (Leave blank for Integrated Authentication.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2bf4e801-263f-49d0-af19-339310c63856", + "Name": "roundhouseUserPassword", + "Label": "Database User Password", + "HelpText": "Password for the Database Username account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "9ddd49a7-92a9-419a-aa07-70c569d88540", + "Name": "roundhouseDatabaseServerType", + "Label": "Database Server Type", + "HelpText": "The database technology being deployed to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "mariadb|MariaDB +mysql|MySQL +oracle|Oracle +postgres|PostgreSQL +sqlserver|SQL Server" + } + }, + { + "Id": "0d748383-fd88-4dc2-ad8e-7b6f12137e3b", + "Name": "roundhouseWithTransaction", + "Label": "Use Transaction?", + "HelpText": "Check this box if you want all scripts to be run within the same transaction.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "06f538af-087a-4d59-958d-90c2e4decebf", + "Name": "roundhouseDryRun", + "Label": "Dry Run?", + "HelpText": "Check this box if you want to perform a dry run. Results are recorded and attached as deployment artifacts if you check Record Output.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "ec772c86-c12a-4f4f-8710-6426152faa72", + "Name": "roundhouseRecordOutput", + "Label": "Record Output?", + "HelpText": "Check this box to record the output of the run. Useful for gathering what would be changed for approval purposes.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "46ee4671-67b2-4e45-b484-6773b41c2bf8", + "Name": "roundhouseUpFolder", + "Label": "Up Folder Path", + "HelpText": "Relative location of the Up folder. If left blank, .\\up is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "55e0bf86-b675-4204-9e0b-d39d20b15eea", + "Name": "roundhouseAlterDatabaseFolder", + "Label": "AlterDatabase Folder Path", + "HelpText": "AlterDatabase folder path. If left blank, .\\alterDatabase is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "cd473440-f9a9-4a5d-b48c-46ee63a92e7a", + "Name": "roundhouseRunBeforeUpFolder", + "Label": "RunBeforeUp Folder Path", + "HelpText": "RunBeforeUp folder path. If left blank, .\\runbeforeUp is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "53fab9f0-f310-482c-985c-e6efdf206117", + "Name": "roundhouseFunctionsFolder", + "Label": "Functions Folder Path", + "HelpText": "Filepath for user defined functions. If left blank, .\\functions is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "66e7ce09-798a-42a6-9e4e-f4c952789af8", + "Name": "roundhouseViewsFolder", + "Label": "Views Folder Path", + "HelpText": "Filepath for view scripts. If left blank, .\\views is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5d905d17-c041-4981-9521-7a827bac3a87", + "Name": "roundhouseSprocsFolder", + "Label": "Sprocs Folder Path", + "HelpText": "File path for stored procedures. If left blank, .\\sprocs is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d9994dc7-3118-45c6-b8eb-80501daf03e9", + "Name": "roundhouseTriggerFolder", + "Label": "Trigger folder path", + "HelpText": "File path for database triggers. If left blank .\\triggers is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fa34cd00-3479-47cd-94d1-637263ebab71", + "Name": "roundhouseIndexFolder", + "Label": "Index Folder Path", + "HelpText": "File path for Index scripts. If left blank, .\\indexes is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e97ba5b9-da0d-4395-887f-f49dcce521f3", + "Name": "roundhouseRunAfterAnyTimeFolder", + "Label": "RunAfterAnytime Folder Path", + "HelpText": "File path for running scripts after the Other Anytime scripts. If left blank, .\\runAfterOtherAnyTimeScripts is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ac179614-fa54-4a5c-a736-7a0cbaa7e4bf", + "Name": "roundhousePermissionsFolder", + "Label": "Permissions Folder path", + "HelpText": "Folder path that holds permission scripts. If left blank, .\\permissions is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fcf2e6c4-db73-4b44-bf36-44e7b1cdc8c1", + "Name": "roundhouseDownloadNuget", + "Label": "Download RoundhousE?", + "HelpText": "Check this box if you want the template to download RoundhousE and use the downloaded version for deployment. Requires .NET Core be installed on the machine executing the deployment.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "62b65afa-1a88-4013-83ab-0b23e162d3cf", + "Name": "roundhouseNugetVersion", + "Label": "Version of RoundhousE", + "HelpText": "Version of RoundhousE to download (used with Download RoundhousE option), leave blank for latest.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2022-06-15T00:15:18.003Z", + "OctopusVersion": "2022.3.1591", + "Type": "ActionTemplate" + }, + "Category": "roundhouse" + } diff --git a/step-templates/run-entity-framework-migrations.json.human b/step-templates/run-entity-framework-migrations.json.human new file mode 100644 index 000000000..bea31242d --- /dev/null +++ b/step-templates/run-entity-framework-migrations.json.human @@ -0,0 +1,183 @@ +{ + "Id": "a6cd35d6-164a-4e57-a0f8-0d327129783a", + "Name": "Run Entity Framework migrations (Update-Database)", + "Description": "Runs `Update-Database` to update the database to the latest Entity Framework migrations", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# A collection of functions that can be used by script steps to determine where packages installed\r +# by previous steps are located on the filesystem.\r + \r +function Find-InstallLocations {\r + $result = @()\r + $OctopusParameters.Keys | foreach {\r + if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) {\r + $result += $OctopusParameters[$_]\r + }\r + }\r + return $result\r +}\r + \r +function Find-InstallLocation($stepName) {\r + $result = $OctopusParameters.Keys | where {\r + $_.Equals(\"Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath\", [System.StringComparison]::OrdinalIgnoreCase)\r + } | select -first 1\r + \r + if ($result) {\r + return $OctopusParameters[$result]\r + }\r + \r + throw \"No install location found for step: $stepName\"\r +}\r + \r +function Find-SingleInstallLocation {\r + $all = @(Find-InstallLocations)\r + if ($all.Length -eq 1) {\r + return $all[0]\r + }\r + if ($all.Length -eq 0) {\r + throw \"No package steps found\"\r + }\r + throw \"Multiple package steps have run; please specify a single step\"\r +}\r +\r +function Test-LastExit($cmd) {\r + if ($LastExitCode -ne 0) {\r + Write-Host \"##octopus[stderr-error]\"\r + write-error \"$cmd failed with exit code: $LastExitCode\"\r + }\r +}\r +\r +\r +\r +\r +$stepName = $OctopusParameters['NugetPackageStepName']\r +\r +$stepPath = \"\"\r +if (-not [string]::IsNullOrEmpty($stepName)) {\r + Write-Host \"Finding path to package step: $stepName\"\r + $stepPath = Find-InstallLocation $stepName\r +} else {\r + $stepPath = Find-SingleInstallLocation\r +}\r +Write-Host \"Package was installed to: $stepPath\"\r +\r +$baseDirectory = $OctopusParameters['BaseDirectory']\r +\r +$binPath = Join-Path $stepPath $baseDirectory\r +\r +#Locate Migrate.exe\r +$efToolsFolder = $OctopusParameters['EfToolsFolder']\r +$originalMigrateExe = Join-Path $efToolsFolder \"migrate.exe\"\r +\r +if (-Not(Test-Path $originalMigrateExe)){\r + throw (\"Unable to locate migrate.exe file. Specifed path $originalMigrateExe does not exist.\")\r +}\r +Write-Host(\"Found Migrate.Exe from $originalMigrateExe\")\r +\r +$migrateExe = Join-Path $binPath \"migrate.exe\"\r +if (-Not(Test-Path $migrateExe)) {\r + # Move migrate.exe to ASP.NET Project's bin folder as per https://msdn.microsoft.com/de-de/data/jj618307.aspx?f=255&MSPPError=-2147217396\r + Copy-Item $originalMigrateExe -Destination $binPath\r + Write-Host(\"Copied $originalMigrateExe into $binPath\")\r +}\r +\r +#Locate Assembly with DbContext class\r +$contextDllName = $OctopusParameters['AssemblyDllName']\r +$contextDllPath = Join-Path $binPath $contextDllName\r +if (-Not(Test-Path $contextDllPath)){\r + throw (\"Unable to locate assembly file with DbContext class. Specifed path $contextDllPath does not exist.\")\r +}\r +Write-Host(\"Using $contextDllName from $contextDllPath\")\r +\r +#Locate web.config. Migrate.exe needs it for some reason, even if connection string is provided\r +$configFile = $OctopusParameters['ConfigFileName']\r +$configPath = Join-Path $stepPath $configFile\r +if (-Not(Test-Path $configPath)){\r + throw (\"Unable to locate config file. Specifed path $webConfigPath does not exist.\")\r +}\r +\r +$connectionStringName = $OctopusParameters['ConnectionStringName']\r +\r +$migrateCommand = \"& \"\"$migrateExe\"\" \"\"$contextDllName\"\" /connectionStringName=\"\"$connectionStringName\"\" /startupConfigurationFile=\"\"$configPath\"\" /startUpDirectory=\"\"$binPath\"\" /Verbose\"\r +\r +Write-Host \"##octopus[stderr-error]\" # Stderr is an error\r +Write-Host \"Executing: \" $migrateCommand\r +Write-Host \r +\r +Invoke-Expression $migrateCommand | Write-Host\r +\r +# Remove migrate.exe from the bin folder as it is not part of the application\r +If (Test-Path $migrateExe)\r +{\r + Write-Host \"Deleting \" $migrateExe\r + Remove-Item $migrateExe\r +}\r +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Name": "EfToolsFolder", + "Label": "Path to EF Tools folder", + "HelpText": "Please provide a full path to a folder where Migrate.exe (along with other EF files are contained, available from EF nuget package) on Tentacle", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BaseDirectory", + "Label": "Base Directory", + "HelpText": "Path to where your application files are located. For ASP.NET applications this would be `/bin`", + "DefaultValue": "/bin", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NugetPackageStepName", + "Label": "Site Path", + "HelpText": "Name of the previously-deployed package step that contains the applications files", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Name": "configFileName", + "Label": "Config File Name", + "HelpText": "Name of your applications config file. For ASP.NET applications this would be `web.config`, for Windows Service applications this would be `yourapplication.dll.config` and for console applications this would be `yourapplication.exe.config`", + "DefaultValue": "web.config", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AssemblyDllName", + "Label": "Name of Assembly file with EF-Context", + "HelpText": "Please provide a name of the assembly file where EF-context is stored", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ConnectionStringName", + "Label": "ConnectionStringName", + "HelpText": "Name of the key in section in Web.Config", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "rikrak", + "$Meta": { + "ExportedAt": "2019-10-18T09:11:34.909Z", + "OctopusVersion": "2018.10.1", + "Type": "ActionTemplate" + }, + "Category": "entityframework" +} diff --git a/step-templates/run-mabl-tests.json.human b/step-templates/run-mabl-tests.json.human new file mode 100644 index 000000000..62d80b71c --- /dev/null +++ b/step-templates/run-mabl-tests.json.human @@ -0,0 +1,313 @@ +{ + "Id": "422be361-640c-4ac1-a305-cd5d618ccf10", + "Name": "Run mabl tests", + "Description": "Trigger specific application or environment plans in mabl.", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "param( + [string]$mablApiKey, + [string]$mablEnvId, + [string]$mablAppId, + [string]$mablPlanLabels, + [string]$mablBranch, + [string]$mablAwaitCompletion +) + +$ErrorActionPreference = \"Stop\" + +# Constants +$PollSec = 10 +$UserAgent = \"mabl-octopus-plugin/0.0.3\" +$ApiBase = \"https://api.mabl.com\" +$DeploymentEventsUri = \"$ApiBase/events/deployment\" +$ExecutionResultBaseUri = \"$ApiBase/execution/result/event\" + +function Get-Param($Name, [switch]$Required, $MatchingPattern, $Explanation) { + $result = $null + + if ($null -ne $OctopusParameters) { + $result = $OctopusParameters[$Name] + } + + if ($null -eq $result) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($null -ne $variable) { + $result = $variable.Value + } + } + + if ($null -eq $result -or $result -eq \"\") { + if ($Required) { + throw \"Missing parameter value $Name\" + } + } + + if ($null -ne $result -and \"\" -ne $result -and $null -ne $MatchingPattern -and $result -notmatch $MatchingPattern) { + throw \"$Explanation\" + } + + return $result +} + +function Write-Result($Result) { + foreach ($execution in $Result.executions) { + $planName = $execution.plan.name + $planId = $execution.plan.id + $planStatus = $execution.plan_execution.status + $t = New-TimeSpan -seconds (($execution.stop_time - $execution.start_time) / 1000) + $planTime = Get-Date -Hour $t.Hours -Minute $t.Minutes -Second $t.Seconds -UFormat \"%T\" + + Write-Host \"Plan name: ${planName}, id: ${planId}, status: ${planStatus}, run time: ${planTime}\" + + $tests = $execution.journeys + $testExecutions = $execution.journey_executions + + foreach ($test in $tests) { + $testId = $test.id + $testName = $test.name + + foreach ($testExecution in $testExecutions) { + if ($testExecution.journey_id -eq $testId) { + $t = New-TimeSpan -seconds (($testExecution.stop_time - $testExecution.start_time) / 1000) + $testTime = Get-Date -Hour $t.Hours -Minute $t.Minutes -Second $t.Seconds -UFormat \"%T\" + $testStatus = $testExecution.status + $testBrowser = $testExecution.browser_type + $testExecutionId = $testExecution.journey_execution_id + Write-Host \" Test name: ${testName}, id: ${testExecutionId}, status: ${testStatus},\" ` + \"browser: ${testBrowser}, run time: ${testTime}\" + break + } + } + + } + } +} + +& { + param ( + [string]$mablApiKey, + [string]$mablEnvId, + [string]$mablAppId, + [string]$mablPlanLabels, + [string]$mablAwaitCompletion + ) + + $kv = \"key:$($mablApiKey)\" + $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($kv)) + $basicAuthValue = \"Basic $encodedCreds\" + $headers = @{ + Authorization = $basicAuthValue + accept = \"application/json\" + } + + # Submit Deployment Event + $resp = \"\" + $eventId = \"\" + try { + $m = @{} + if ($null -ne $mablEnvId -and \"\" -ne $mablEnvId) { + $m.add(\"environment_id\", $mablEnvId) + } + if ($null -ne $mablAppId -and \"\" -ne $mablAppId) { + $m.add(\"application_id\", $mablAppId) + } + if ($m.count -eq 0) { + Write-Host \"Either an environment ID or an application ID must be provided\" + exit 1 + } + if ($null -ne $mablPlanLabels -and \"\" -ne $mablPlanLabels) { + $planLabelArray = $mablPlanLabels.Split(\",\") + $m.add(\"plan_labels\", $planLabelArray) + } + if ($null -ne $mablBranch -and \"\" -ne $mablBranch) { + $m.add(\"source_control_tag\", $mablBranch) + } + $body = ConvertTo-Json -InputObject $m + Write-Host \"Creating Deployment...\" + $resp = Invoke-RestMethod -URI $DeploymentEventsUri -Method Post ` + -Headers $headers -ContentType 'application/json' ` + -UserAgent $UserAgent -Body $body + + $workspaceId = $resp.workspace_id + $eventId = $resp.id + Write-Host \"View output at https://app.mabl.com/workspaces/${workspaceId}/events/${eventId}\" + } + catch { + $statusCode = $_.Exception.Response.StatusCode.value__ + + Write-Host \"Failed to invoke deployment events API, status code: \" ` + $statusCode \" description: \" ` + $_.Exception.Response.StatusDescription + + switch ($statusCode) { + 400 { + Write-Host \"At least one of environment ID or application ID must be specified\" + break + } + 401 { + Write-Host \"Invalid API key has been provided\" + break + } + 403 { + Write-Host \"The provided API key is not authorized to submit deployment events\" + break + } + 404 { + Write-Host \"The provided application or environment could not be found\" + break + } + } + exit 1 + } + + + # Poll Execution Result at least once based on await completion parameter + $awaitCompletion = $mablAwaitCompletion -eq \"True\" + $totalPlans = 0 + $passedPlans = 0 + $failedPlans = 0 + $totalTests = 0 + $passedTests = 0 + $failedTests = 0 + $execResult = \"\" + try { + $complete = -Not $awaitCompletion + do { + Start-Sleep -s $PollSec + $eventId = $resp.id + $uri = \"$ExecutionResultBaseUri/$eventId\" + $execResult = Invoke-RestMethod -URI $uri -Method Get -Headers $headers + $totalPlans = $execResult.plan_execution_metrics.total + $passedPlans = $execResult.plan_execution_metrics.passed + $failedPlans = $execResult.plan_execution_metrics.failed + $totalTests = $execResult.journey_execution_metrics.total + $passedTests = $execResult.journey_execution_metrics.passed + $failedTests = $execResult.journey_execution_metrics.failed + + if ($passedPlans + $failedPlans -eq $totalPlans) { + $complete = $TRUE + } + elseif (!$complete) { + Write-Host \"Plan runs\" ` + \"[passed: $passedTests, failed: $failedTests, total: $totalTests]\" + } + } while (!$complete) + } + catch { + $statusCode = $_.Exception.Response.StatusCode.value__ + + Write-Host \"Failed to invoke execution result API, status code:\" ` + $statusCode \" description: ` + \" $_.Exception.Response.StatusDescription + + switch ($statusCode) { + 401 { + Write-Host \"Invalid API key has been provided\" + break + } + 403 { + Write-Host \"The provided API key is not authorized to retrieve execution results\" + break + } + 404 { + Write-Host \"The deployment event could not be found\" + break + } + } + + exit 1 + } + + if ($awaitCompletion) { + # Display results + Write-Host \"Tests complete with status\" ` + $(If ($execResult.event_status.succeeded) { \"PASSED\" } else { \"FAILED\"}) + Write-Result($execResult) + + If ($execResult.event_status.succeeded) { exit 0 } else { exit 1 } + } + + Write-Host \"Successfully triggered $totalPlans plan(s)\" + exit 0 +} ` +(Get-Param 'mablApiKey' -Required) ` +(Get-Param 'mablEnvId' '-e$' 'Environment IDs must end with -e') ` +(Get-Param 'mablAppId' '-a$' 'Application IDs must end with -a') ` +(Get-Param 'mablPlanLabels') ` +(Get-Param 'mablAwaitCompletion') +" + }, + "Parameters": [ + { + "Id": "f5a9d523-f97e-49a9-b946-0c56e1018345", + "Name": "mablApiKey", + "Label": "mabl Integration Key", + "HelpText": "(Required) The API key of your workspace. You can find the API key on the Settings/API page.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "364c4c30-0ba3-4eee-9b01-9fb00f718ade", + "Name": "mablEnvId", + "Label": "Environment ID", + "HelpText": "The environment ID to run the tests in. Note that either an environment ID or an application ID must be provided.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0bf3fd34-1104-4d29-8677-0d91e78fc483", + "Name": "mablAppId", + "Label": "Application ID", + "HelpText": "The application ID to run tests on. Note that either an environment ID or an application ID must be provided.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "549d9ef4-2461-45d4-a8bc-44fb79758b52", + "Name": "mablPlanLabels", + "Label": "Plan Labels", + "HelpText": "(Optional) A comma-separated list of plan labels. Plans with any of the labels will be executed.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "db7398c4-2abc-4c56-99af-d32d054db50d", + "Name": "mablBranch", + "Label": "Test Branch", + "HelpText": "(Optional) The mabl branch to run test against.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "DefaultValue": "True", + "Name": "mablAwaitCompletion", + "Label": "Await Completion", + "HelpText": "When checked, the step will wait for all triggered tests to complete." + } + ], + "$Meta": { + "ExportedAt": "2020-08-26T20:24:23.9520000Z", + "OctopusVersion": "2020.3.3", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "bertold", + "Category": "mabl" +} diff --git a/step-templates/run-mstest.json.human b/step-templates/run-mstest.json.human new file mode 100644 index 000000000..eed3b7c45 --- /dev/null +++ b/step-templates/run-mstest.json.human @@ -0,0 +1,71 @@ +{ + "Id": "75746f93-6c2b-4a52-b3b6-97e8dee09a81", + "Name": "Run MSTests", + "Description": "This Step template should be used for running MSTests by passing list of assemblies.", + "ActionType": "Octopus.Script", + "Version": 14, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Write-Output \"Running MsTests tests...\"\r +\r +$exePath = '\"' + $MsTestExePath + '\"'\r +if (-not $exePath) {\r + $exePath = \"mstest.exe\"\r +}\r +\r +$runMsTest = \"& $exePath \"\r +\r +$MsTestAssemblies.Split(\";\") | ForEach {\r + $asm = \" /testcontainer:\"+$_.Trim()\r + Write-Output \"Including test container assembly $asm\"\r + $runMsTest += \"$asm\"\r +}\r +Write-Host $runMsTest\r +cd $MsTestWorkingDirectoryPath\r +Write-Host $MsTestWorkingDirectoryPath \r +\r +iex $runMsTest\r +$mstestExit = $lastExitCode\r +\r +\r +exit $mstestExit", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "MsTestExePath", + "Label": "Ms Test Path", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MsTestWorkingDirectoryPath", + "Label": "Working directory", + "HelpText": "The folder that contains the test assemblies. Generally this will be bound to an output variable from a previous step. Example: _#{Octopus.Action[Deploy integration tests].Output.Package.InstallationDirectoryPath}_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MsTestAssemblies", + "Label": "Test assemblies", + "HelpText": "A semicolon-separated list of assembly names containing tests. Example: _MyCompany.IntegrationTests.dll; MyCompany.SmokeTests.dll_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-04-30T12:44:24.644+00:00", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "tests" +} diff --git a/step-templates/run-octopus-runbook.json.human b/step-templates/run-octopus-runbook.json.human new file mode 100644 index 000000000..0db475375 --- /dev/null +++ b/step-templates/run-octopus-runbook.json.human @@ -0,0 +1,1378 @@ +{ + "Id": "0444b0b3-088e-4689-b755-112d1360ffe3", + "Name": "Run Octopus Deploy Runbook", + "Description": "This step will kick off a runbook. The runbook can exist in the same space and project, or it can exist on a different instance altogether. + +**Please Note**: Prompted variable values have to be text or sensitive variables. Variable types such as AWS or Azure accounts will not work. + +This step should be called from a worker machine. If it is called from a target and the runbook runs on the same target you run the risk of a deadlock. + +", + "ActionType": "Octopus.Script", + "Version": 16, + "Author": "bobjwalker", + "Packages": [], + "GitDependencies": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +# Octopus Variables +$octopusSpaceId = $OctopusParameters[\"Octopus.Space.Id\"] +$parentTaskId = $OctopusParameters[\"Octopus.Task.Id\"] +$parentReleaseId = $OctopusParameters[\"Octopus.Release.Id\"] +$parentChannelId = $OctopusParameters[\"Octopus.Release.Channel.Id\"] +$parentEnvironmentId = $OctopusParameters[\"Octopus.Environment.Id\"] +$parentRunbookId = $OctopusParameters[\"Octopus.Runbook.Id\"] +$parentEnvironmentName = $OctopusParameters[\"Octopus.Environment.Name\"] +$parentReleaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] + +# Step Template Parameters +$runbookRunName = $OctopusParameters[\"Run.Runbook.Name\"] +$runbookBaseUrl = $OctopusParameters[\"Run.Runbook.Base.Url\"] +$runbookApiKey = $OctopusParameters[\"Run.Runbook.Api.Key\"] +$runbookEnvironmentName = $OctopusParameters[\"Run.Runbook.Environment.Name\"] +$runbookTenantName = $OctopusParameters[\"Run.Runbook.Tenant.Name\"] +$runbookWaitForFinish = $OctopusParameters[\"Run.Runbook.Waitforfinish\"] +$runbookUseGuidedFailure = $OctopusParameters[\"Run.Runbook.UseGuidedFailure\"] +$runbookUsePublishedSnapshot = $OctopusParameters[\"Run.Runbook.UsePublishedSnapShot\"] +$runbookPromptedVariables = $OctopusParameters[\"Run.Runbook.PromptedVariables\"] +$runbookCancelInSeconds = $OctopusParameters[\"Run.Runbook.CancelInSeconds\"] +$runbookProjectName = $OctopusParameters[\"Run.Runbook.Project.Name\"] +$runbookCustomNotesToggle = $OctopusParameters[\"Run.Runbook.CustomNotes.Toggle\"] +$runbookCustomNotes = $OctopusParameters[\"Run.Runbook.CustomNotes\"] + +$runbookSpaceName = $OctopusParameters[\"Run.Runbook.Space.Name\"] +$runbookFutureDeploymentDate = $OctopusParameters[\"Run.Runbook.DateTime\"] +$runbookMachines = $OctopusParameters[\"Run.Runbook.Machines\"] +$autoApproveRunbookRunManualInterventions = $OctopusParameters[\"Run.Runbook.AutoApproveManualInterventions\"] +$approvalEnvironmentName = $OctopusParameters[\"Run.Runbook.ManualIntervention.EnvironmentToUse\"] + +function Write-OctopusVerbose +{ + param($message) + + Write-Verbose $message +} + +function Write-OctopusInformation +{ + param($message) + + Write-Host $message +} + +function Write-OctopusSuccess +{ + param($message) + + Write-Highlight $message +} + +function Write-OctopusWarning +{ + param($message) + + Write-Warning \"$message\" +} + +function Write-OctopusCritical +{ + param ($message) + + Write-Error \"$message\" +} + +function Invoke-OctopusApi +{ + param + ( + $octopusUrl, + $endPoint, + $spaceId, + $apiKey, + $method, + $item + ) + + if ([string]::IsNullOrWhiteSpace($SpaceId)) + { + $url = \"$OctopusUrl/api/$EndPoint\" + } + else + { + $url = \"$OctopusUrl/api/$spaceId/$EndPoint\" + } + + try + { + if ($null -eq $item) + { + Write-Verbose \"No data to post or put, calling bog standard invoke-restmethod for $url\" + return Invoke-RestMethod -Method $method -Uri $url -Headers @{\"X-Octopus-ApiKey\" = \"$ApiKey\" } -ContentType 'application/json; charset=utf-8' + } + + $body = $item | ConvertTo-Json -Depth 10 + Write-Verbose $body + + Write-Host \"Invoking $method $url\" + return Invoke-RestMethod -Method $method -Uri $url -Headers @{\"X-Octopus-ApiKey\" = \"$ApiKey\" } -Body $body -ContentType 'application/json; charset=utf-8' + } + catch + { + if ($null -ne $_.Exception.Response) + { + if ($_.Exception.Response.StatusCode -eq 401) + { + Write-Error \"Unauthorized error returned from $url, please verify API key and try again\" + } + elseif ($_.Exception.Response.statusCode -eq 403) + { + Write-Error \"Forbidden error returned from $url, please verify API key and try again\" + } + else + { + Write-Error -Message \"Error calling $url $($_.Exception.Message) StatusCode: $($_.Exception.Response.StatusCode )\" + } + } + else + { + Write-Verbose $_.Exception + } + } + + Throw \"There was an error calling the Octopus API please check the log for more details\" +} + +function Test-RequiredValues +{ +\tparam ( + \t$variableToCheck, + $variableName + ) + + if ([string]::IsNullOrWhiteSpace($variableToCheck) -eq $true) + { + \tWrite-OctopusCritical \"$variableName is required.\" + return $false + } + + return $true +} + +function GetCheckBoxBoolean +{ +\tparam ( + \t[string]$Value + ) + + if ([string]::IsNullOrWhiteSpace($value) -eq $true) + { + \treturn $false + } + + return $value -eq \"True\" +} + +function Get-FilteredOctopusItem +{ + param( + $itemList, + $itemName + ) + + if ($itemList.Items.Count -eq 0) + { + Write-OctopusCritical \"Unable to find $itemName. Exiting with an exit code of 1.\" + Exit 1 + } + + $item = $itemList.Items | Where-Object { $_.Name -eq $itemName} + + if ($null -eq $item) + { + Write-OctopusCritical \"Unable to find $itemName. Exiting with an exit code of 1.\" + exit 1 + } + + if ($item -is [array]) + { + \tWrite-OctopusCritical \"More than one item exists with the name $itemName. Exiting with an exit code of 1.\" + exit 1 + } + + return $item +} + +function Get-OctopusItemFromListEndpoint +{ + param( + $endpoint, + $itemNameToFind, + $itemType, + $defaultUrl, + $octopusApiKey, + $spaceId, + $defaultValue + ) + + if ([string]::IsNullOrWhiteSpace($itemNameToFind)) + { + \treturn $defaultValue + } + + Write-OctopusInformation \"Attempting to find $itemType with the name of $itemNameToFind\" + + $itemList = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"$($endpoint)?partialName=$([uri]::EscapeDataString($itemNameToFind))&skip=0&take=100\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + $item = Get-FilteredOctopusItem -itemList $itemList -itemName $itemNameToFind + + Write-OctopusInformation \"Successfully found $itemNameToFind with id of $($item.Id)\" + + return $item +} + +function Get-MachineIdsFromMachineNames +{ + param ( + $targetMachines, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + $targetMachineList = $targetMachines -split \",\" + $translatedList = @() + + foreach ($machineName in $targetMachineList) + { + Write-OctopusVerbose \"Translating $machineName to an Id. First checking to see if it is already an Id.\" + \tif ($machineName.Trim() -like \"Machines*\") + { + Write-OctopusVerbose \"$machineName is already an Id, no need to look that up.\" + \t$translatedList += $machineName + continue + } + + $machineObject = Get-OctopusItemFromListEndpoint -itemNameToFind $machineName.Trim() -itemType \"Deployment Target\" -endpoint \"machines\" -defaultValue $null -spaceId $spaceId -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey + + $translatedList += $machineObject.Id + } + + return $translatedList +} + +function Get-RunbookSnapshotIdToRun +{ + param ( + $runbookToRun, + $runbookUsePublishedSnapshot, + $defaultUrl, + $octopusApiKey, + $spaceId + ) + + $runbookSnapShotIdToUse = $runbookToRun.PublishedRunbookSnapshotId + Write-OctopusInformation \"The last published snapshot for $runbookRunName is $runbookSnapShotIdToUse\" + + if ($null -eq $runbookSnapShotIdToUse -and $runbookUsePublishedSnapshot -eq $true) + { + Write-OctopusCritical \"Use Published Snapshot was set; yet the runbook doesn't have a published snapshot. Exiting.\" + Exit 1 + } + + if ($runbookUsePublishedSnapshot -eq $true) + { + Write-OctopusInformation \"Use published snapshot set to true, using the published runbook snapshot.\" + return $runbookSnapShotIdToUse + } + + if ($null -eq $runbookToRun.PublishedRunbookSnapshotId) + { + Write-OctopusInformation \"There have been no published runbook snapshots, going to create a new snapshot.\" + return New-RunbookUnpublishedSnapshot -runbookToRun $runbookToRun -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey -spaceId $spaceId + } + + $runbookSnapShotTemplate = Invoke-OctopusApi -octopusUrl $defaultUrl -apiKey $octopusApiKey -spaceId $spaceId -endPoint \"runbookSnapshots/$($runbookToRun.PublishedRunbookSnapshotId)/runbookRuns/template\" -method \"Get\" -item $null + + if ($runbookSnapShotTemplate.IsRunbookProcessModified -eq $false -and $runbookSnapShotTemplate.IsVariableSetModified -eq $false -and $runbookSnapShotTemplate.IsLibraryVariableSetModified -eq $false) + { + Write-OctopusInformation \"The runbook has not been modified since the published snapshot was created. Checking to see if any of the packages have a new version.\" + $runbookSnapShot = Invoke-OctopusApi -octopusUrl $defaultUrl -apiKey $octopusApiKey -spaceId $spaceId -endPoint \"runbookSnapshots/$($runbookToRun.PublishedRunbookSnapshotId)\" -method \"Get\" -item $null + $snapshotTemplate = Invoke-OctopusApi -octopusUrl $defaultUrl -apiKey $octopusApiKey -spaceId $spaceId -endPoint \"runbooks/$($runbookToRun.Id)/runbookSnapShotTemplate\" -method \"Get\" -item $null + + foreach ($package in $runbookSnapShot.SelectedPackages) + { + foreach ($templatePackage in $snapshotTemplate.Packages) + { + if ($package.StepName -eq $templatePackage.StepName -and $package.ActionName -eq $templatePackage.ActionName -and $package.PackageReferenceName -eq $templatePackage.PackageReferenceName) + { + $packageVersion = Invoke-OctopusApi -octopusUrl $defaultUrl -apiKey $octopusApiKey -spaceId $spaceId -endPoint \"feeds/$($templatePackage.FeedId)/packages/versions?packageId=$($templatePackage.PackageId)&take=1\" -method \"Get\" -item $null + + if ($packageVersion -ne $package.Version) + { + Write-OctopusInformation \"A newer version of a package was found, going to use that and create a new snapshot.\" + return New-RunbookUnpublishedSnapshot -runbookToRun $runbookToRun -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey -spaceId $spaceId + } + } + } + } + + Write-OctopusInformation \"No new package versions have been found, using the published snapshot.\" + return $runbookToRun.PublishedRunbookSnapshotId + } + + Write-OctopusInformation \"The runbook has been modified since the snapshot was created, creating a new one.\" + return New-RunbookUnpublishedSnapshot -runbookToRun $runbookToRun -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey -spaceId $spaceId +} + +function New-RunbookUnpublishedSnapshot +{ + param ( + $runbookToRun, + $defaultUrl, + $octopusApiKey, + $spaceId + ) + + $octopusProject = Invoke-OctopusApi -octopusUrl $defaultUrl -apiKey $octopusApiKey -spaceId $spaceId -endPoint \"projects/$($runbookToRun.ProjectId)\" -method \"Get\" -item $null + $snapshotTemplate = Invoke-OctopusApi -octopusUrl $defaultUrl -apiKey $octopusApiKey -spaceId $spaceId -endPoint \"runbooks/$($runbookToRun.Id)/runbookSnapShotTemplate\" -method \"Get\" -item $null + + $runbookPackages = @() + foreach ($package in $snapshotTemplate.Packages) + { + $packageVersion = Invoke-OctopusApi -octopusUrl $defaultUrl -apiKey $octopusApiKey -spaceId $spaceId -endPoint \"feeds/$($package.FeedId)/packages/versions?packageId=$($package.PackageId)&take=1\" -method \"Get\" -item $null + + if ($packageVersion.TotalResults -le 0) + { + Write-Error \"Unable to find a package version for $($package.PackageId). This is required to create a new unpublished snapshot. Exiting.\" + exit 1 + } + + $runbookPackages += @{ + StepName = $package.StepName + ActionName = $package.ActionName + Version = $packageVersion.Items[0].Version + PackageReferenceName = $package.PackageReferenceName + } + } + + $runbookSnapShotRequest = @{ + FrozenProjectVariableSetId = \"variableset-$($runbookToRun.ProjectId)\" + FrozenRunbookProcessId = $($runbookToRun.RunbookProcessId) + LibraryVariableSetSnapshotIds = @($octopusProject.IncludedLibraryVariableSetIds) + Name = $($snapshotTemplate.NextNameIncrement) + ProjectId = $($runbookToRun.ProjectId) + ProjectVariableSetSnapshotId = \"variableset-$($runbookToRun.ProjectId)\" + RunbookId = $($runbookToRun.Id) + SelectedPackages = $runbookPackages + } + + $newSnapShot = Invoke-OctopusApi -octopusUrl $defaultUrl -apiKey $octopusApiKey -spaceId $spaceId -endPoint \"runbookSnapshots\" -method \"POST\" -item $runbookSnapShotRequest + + return $($newSnapShot.Id) +} + +function Get-ProjectSlug +{ + param + ( + $runbookToRun, + $projectToUse, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + if ($null -ne $projectToUse) + { + return $projectToUse.Slug + } + + $project = Invoke-OctopusApi -octopusUrl $defaultUrl -spaceId $spaceId -apiKey $octopusApiKey -endPoint \"projects/$($runbookToRun.ProjectId)\" -method \"GET\" -item $null + + return $project.Slug +} + +function Get-RunbookFormValues +{ + param ( + $runbookPreview, + $runbookPromptedVariables + ) + + $runbookFormValues = @{} + + if ([string]::IsNullOrWhiteSpace($runbookPromptedVariables) -eq $true) + { + return $runbookFormValues + } + + $promptedValueList = @(($runbookPromptedVariables -Split \"`n\").Trim()) + Write-OctopusInformation $promptedValueList.Length + + foreach($element in $runbookPreview.Form.Elements) + { + \t$nameToSearchFor = $element.Control.Name + $uniqueName = $element.Name + $isRequired = $element.Control.Required + + $promptedVariablefound = $false + + Write-OctopusInformation \"Looking for the prompted variable value for $nameToSearchFor\" + \tforeach ($promptedValue in $promptedValueList) + { + \t$splitValue = $promptedValue -Split \"::\" + Write-OctopusInformation \"Comparing $nameToSearchFor with provided prompted variable $($promptedValue[0])\" + if ($splitValue.Length -gt 1) + { + \tif ($nameToSearchFor -eq $splitValue[0]) + { + \tWrite-OctopusInformation \"Found the prompted variable value $nameToSearchFor\" + \t$runbookFormValues[$uniqueName] = $splitValue[1] + $promptedVariableFound = $true + break + } + } + } + + if ($promptedVariableFound -eq $false -and $isRequired -eq $true) + { + \tWrite-OctopusCritical \"Unable to find a value for the required prompted variable $nameToSearchFor, exiting\" + Exit 1 + } + } + + return $runbookFormValues +} + +function Invoke-OctopusDeployRunbook +{ + param ( + $runbookBody, + $runbookWaitForFinish, + $runbookCancelInSeconds, + $projectNameForUrl, + $defaultUrl, + $octopusApiKey, + $spaceId, + $parentTaskApprovers, + $autoApproveRunbookRunManualInterventions, + $parentProjectName, + $parentReleaseNumber, + $approvalEnvironmentName, + $parentRunbookId, + $parentTaskId + ) + + $runbookResponse = Invoke-OctopusApi -octopusUrl $defaultUrl -spaceId $spaceId -apiKey $octopusApiKey -item $runbookBody -method \"POST\" -endPoint \"runbookRuns\" + + $runbookServerTaskId = $runBookResponse.TaskId + Write-OctopusInformation \"The task id of the new task is $runbookServerTaskId\" + + $runbookRunId = $runbookResponse.Id + Write-OctopusInformation \"The runbook run id is $runbookRunId\" + + Write-OctopusSuccess \"Runbook was successfully invoked, you can access the launched runbook [here]($defaultUrl/app#/$spaceId/projects/$projectNameForUrl/operations/runbooks/$($runbookBody.RunbookId)/snapshots/$($runbookBody.RunbookSnapShotId)/runs/$runbookRunId)\" + + if ($runbookWaitForFinish -eq $false) + { + Write-OctopusInformation \"The wait for finish setting is set to no, exiting step\" + return + } + + if ($null -ne $runbookBody.QueueTime) + { + \tWrite-OctopusInformation \"The runbook queue time is set. Exiting step\" + return + } + + Write-OctopusSuccess \"The setting to wait for completion was set, waiting until task has finished\" + $startTime = Get-Date + $currentTime = Get-Date + $dateDifference = $currentTime - $startTime +\t + $taskStatusUrl = \"tasks/$runbookServerTaskId\" + $numberOfWaits = 0 + + While ($dateDifference.TotalSeconds -lt $runbookCancelInSeconds) + { + Write-OctopusInformation \"Waiting 5 seconds to check status\" + Start-Sleep -Seconds 5 + $taskStatusResponse = Invoke-OctopusApi -octopusUrl $defaultUrl -spaceId $spaceId -apiKey $octopusApiKey -endPoint $taskStatusUrl -method \"GET\" -item $null + $taskStatusResponseState = $taskStatusResponse.State + + if ($taskStatusResponseState -eq \"Success\") + { + Write-OctopusSuccess \"The task has finished with a status of Success\" + exit 0 + } + elseif($taskStatusResponseState -eq \"Failed\" -or $taskStatusResponseState -eq \"Canceled\") + { + Write-OctopusSuccess \"The task has finished with a status of $taskStatusResponseState status, stopping the run/deployment\" + exit 1 + } + elseif($taskStatusResponse.HasPendingInterruptions -eq $true) + { + if ($autoApproveRunbookRunManualInterventions -eq \"Yes\") + { + Submit-RunbookRunForAutoApproval -createdRunbookRun $createdRunbookRun -parentTaskApprovers $parentTaskApprovers -defaultUrl $DefaultUrl -octopusApiKey $octopusApiKey -spaceId $spaceId -parentProjectName $parentProjectName -parentReleaseNumber $parentReleaseNumber -parentEnvironmentName $approvalEnvironmentName -parentRunbookId $parentRunbookId -parentTaskId $parentTaskId + } + else + { + if ($numberOfWaits -ge 10) + { + Write-OctopusSuccess \"The child project has pending manual intervention(s). Unless you approve it, this task will time out.\" + } + else + { + Write-OctopusInformation \"The child project has pending manual intervention(s). Unless you approve it, this task will time out.\" + } + } + } + + $numberOfWaits += 1 + if ($numberOfWaits -ge 10) + { + \tWrite-OctopusSuccess \"The task state is currently $taskStatusResponseState\" + \t$numberOfWaits = 0 + } + else + { + \tWrite-OctopusInformation \"The task state is currently $taskStatusResponseState\" + } + + $startTime = $taskStatusResponse.StartTime + if ($startTime -eq $null -or [string]::IsNullOrWhiteSpace($startTime) -eq $true) + { + \tWrite-OctopusInformation \"The task is still queued, let's wait a bit longer\" + \t$startTime = Get-Date + } + $startTime = [DateTime]$startTime + + $currentTime = Get-Date + $dateDifference = $currentTime - $startTime + } + + Write-OctopusSuccess \"The cancel timeout has been reached, cancelling the runbook run\" + $cancelResponse = Invoke-RestMethod \"$runbookBaseUrl/api/tasks/$runbookServerTaskId/cancel\" -Headers $header -Method Post + Write-OctopusSuccess \"Exiting with an error code of 1 because we reached the timeout\" + exit 1 +} + +function Get-QueueDate +{ +\tparam ( + \t$futureDeploymentDate + ) + + if ([string]::IsNullOrWhiteSpace($futureDeploymentDate) -or $futureDeploymentDate -eq \"N/A\") + { + \treturn $null + } + + $addOneDay = $false + $textToParse = $futureDeploymentDate.ToLower() + if ($textToParse -like \"tomorrow*\") + { + \tWrite-Host \"The future date $futureDeploymentDate supplied contains tomorrow, will add one day to whatever the parsed result is.\" + \t$addOneDay = $true + $textToParse = $textToParse -replace \"tomorrow\", \"\" + } + + [datetime]$outputDate = New-Object DateTime + $currentDate = Get-Date + $currentDate = $currentDate.AddMinutes(2) + + if ([datetime]::TryParse($textToParse, [ref]$outputDate) -eq $false) + { + Write-OctopusCritical \"The suppplied date $textToParse cannot be parsed by DateTime.TryParse. Please verify format and try again. Please [refer to Microsoft's Documentation](https://docs.microsoft.com/en-us/dotnet/api/system.datetime.tryparse) on supported formats.\" + exit 1 + } + + Write-Host \"The proposed date is $outputDate. Checking to see if this will occur in the past.\" + + if ($addOneDay -eq $true) + { + \t$outputDate = $outputDate.AddDays(1) + \tWrite-host \"The text supplied included tomorrow, adding one day. The new proposed date is $outputDate.\" + } + + if ($currentDate -gt $outputDate) + { + \tWrite-OctopusCritical \"The supplied date $futureDeploymentDate is set for the past. All queued deployments must be in the future.\" + exit 1 + } + + return $outputDate +} + +function Get-QueueExpiryDate +{ +\tparam ( + \t$queueDate + ) + + if ($null -eq $queueDate) + { + \treturn $null + } + + return $queueDate.AddHours(1) +} + +function Get-RunbookSpecificMachines +{ + param ( + $defaultUrl, + $octopusApiKey, + $runbookPreview, + $runbookMachines, + $runbookRunName + ) + + if ($runbookMachines -eq \"N/A\") + { + return @() + } + + if ([string]::IsNullOrWhiteSpace($runbookMachines) -eq $true) + { + return @() + } + + $translatedList = Get-MachineIdsFromMachineNames -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey -spaceId $spaceId -targetMachines $runbookMachines + + $filteredList = @() + foreach ($runbookMachine in $translatedList) + { \t + \t$runbookMachineId = $runbookMachine.Trim().ToLower() + \tWrite-OctopusVerbose \"Checking if $runbookMachineId is set to run on any of the runbook steps\" + + foreach ($step in $runbookPreview.StepsToExecute) + { + foreach ($machine in $step.Machines) + { + \tWrite-OctopusVerbose \"Checking if $runbookMachineId matches $($machine.Id) and it isn't already in the $($filteredList -join \",\")\" + if ($runbookMachineId -eq $machine.Id.Trim().ToLower() -and $filteredList -notcontains $machine.Id) + { + \tWrite-OctopusInformation \"Adding $($machine.Id) to the list\" + $filteredList += $machine.Id + } + } + } + } + + if ($filteredList.Length -le 0) + { + Write-OctopusSuccess \"The current task is targeting specific machines, but the runbook $runBookRunName does not run against any of these machines $runbookMachines. Skipping this run.\" + exit 0 + } + + return $filteredList +} + +function Get-ParentTaskApprovers +{ + param ( + $parentTaskId, + $spaceId, + $defaultUrl, + $octopusApiKey + ) + + $approverList = @() + if ($null -eq $parentTaskId) + { + \tWrite-OctopusInformation \"The deployment task id to pull the approvers from is null, return an empty approver list\" + \treturn $approverList + } + + Write-OctopusInformation \"Getting all the events from the parent project\" + $parentEvents = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"events?regardingAny=$parentTaskId&spaces=$spaceId&includeSystem=true\" -apiKey $octopusApiKey -method \"GET\" + + foreach ($parentEvent in $parentEvents.Items) + { + Write-OctopusVerbose \"Checking $($parentEvent.Message) for manual intervention\" + if ($parentEvent.Message -like \"Submitted interruption*\") + { + Write-OctopusVerbose \"The event $($parentEvent.Id) is a manual intervention approval event which was approved by $($parentEvent.Username).\" + + $approverExists = $approverList | Where-Object {$_.Id -eq $parentEvent.UserId} + + if ($null -eq $approverExists) + { + $approverInformation = @{ + Id = $parentEvent.UserId; + Username = $parentEvent.Username; + Teams = @() + } + + $approverInformation.Teams = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"teammembership?userId=$($approverInformation.Id)&spaces=$spaceId&includeSystem=true\" -apiKey $octopusApiKey -method \"GET\" + + Write-OctopusVerbose \"Adding $($approverInformation.Id) to the approval list\" + $approverList += $approverInformation + } + } + } + + return $approverList +} + +function Get-ApprovalTaskIdFromDeployment +{ + param ( + $parentReleaseId, + $approvalEnvironment, + $parentChannelId, + $parentEnvironmentId, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + $releaseDeploymentList = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"releases/$parentReleaseId/deployments\" -method \"GET\" -apiKey $octopusApiKey -spaceId $spaceId + + $lastDeploymentTime = $(Get-Date).AddYears(-50) + $approvalTaskId = $null + foreach ($deployment in $releaseDeploymentList.Items) + { + if ($deployment.EnvironmentId -ne $approvalEnvironment.Id) + { + Write-OctopusInformation \"The deployment $($deployment.Id) deployed to $($deployment.EnvironmentId) which doesn't match $($approvalEnvironment.Id).\" + continue + } + + Write-OctopusInformation \"The deployment $($deployment.Id) was deployed to the approval environment $($approvalEnvironment.Id).\" + + $deploymentTask = Invoke-OctopusApi -octopusUrl $defaultUrl -spaceId $null -endPoint \"tasks/$($deployment.TaskId)\" -apiKey $octopusApiKey -Method \"Get\" + if ($deploymentTask.IsCompleted -eq $true -and $deploymentTask.FinishedSuccessfully -eq $false) + { + Write-Information \"The deployment $($deployment.Id) was deployed to the approval environment, but it encountered a failure, moving onto the next deployment.\" + continue + } + + if ($deploymentTask.StartTime -gt $lastDeploymentTime) + { + $approvalTaskId = $deploymentTask.Id + $lastDeploymentTime = $deploymentTask.StartTime + } + } + + if ($null -eq $approvalTaskId) + { + \tWrite-OctopusVerbose \"Unable to find a deployment to the environment, determining if it should've happened already.\" + $channelInformation = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint \"channels/$parentChannelId\" -method \"GET\" -apiKey $octopusApiKey -spaceId $spaceId + $lifecycle = Get-OctopusLifeCycle -channel $channelInformation -defaultUrl $defaultUrl -spaceId $spaceId -OctopusApiKey $octopusApiKey + $lifecyclePhases = Get-LifecyclePhases -lifecycle $lifecycle -defaultUrl $defaultUrl -spaceId $spaceid -OctopusApiKey $octopusApiKey + + $foundDestinationFirst = $false + $foundApprovalFirst = $false + + foreach ($phase in $lifecyclePhases.Phases) + { + \tif ($phase.AutomaticDeploymentTargets -contains $parentEnvironmentId -or $phase.OptionalDeploymentTargets -contains $parentEnvironmentId) + { + \tif ($foundApprovalFirst -eq $false) + { + \t$foundDestinationFirst = $true + } + } + + if ($phase.AutomaticDeploymentTargets -contains $approvalEnvironment.Id -or $phase.OptionalDeploymentTargets -contains $approvalEnvironment.Id) + { + \tif ($foundDestinationFirst -eq $false) + { + \t$foundApprovalFirst = $true + } + } + } + + $messageToLog = \"Unable to find a deployment for the environment $approvalEnvironmentName. Auto approvals are disabled.\" + if ($foundApprovalFirst -eq $true) + { + \tWrite-OctopusWarning $messageToLog + } + else + { + \tWrite-OctopusInformation $messageToLog + } + + return $null + } + + return $approvalTaskId +} + +function Get-ApprovalTaskIdFromRunbook +{ + param ( + $parentRunbookId, + $approvalEnvironment, + $defaultUrl, + $spaceId, + $octopusApiKey + ) +} + +function Get-ApprovalTaskId +{ +\tparam ( + \t$autoApproveRunbookRunManualInterventions, + $parentTaskId, + $parentReleaseId, + $parentRunbookId, + $parentEnvironmentName, + $approvalEnvironmentName, + $parentChannelId, + $parentEnvironmentId, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + if ($autoApproveRunbookRunManualInterventions -eq $false) + { + \tWrite-OctopusInformation \"Auto approvals are disabled, skipping pulling the approval deployment task id\" + return $null + } + + if ([string]::IsNullOrWhiteSpace($approvalEnvironmentName) -eq $true) + { + \tWrite-OctopusInformation \"Approval environment not supplied, using the current environment id for approvals.\" + return $parentTaskId + } + + if ($approvalEnvironmentName.ToLower().Trim() -eq $parentEnvironmentName.ToLower().Trim()) + { + Write-OctopusInformation \"The approval environment is the same as the current environment, using the current task id $parentTaskId\" + return $parentTaskId + } + + $approvalEnvironment = Get-OctopusItemFromListEndpoint -itemNameToFind $approvalEnvironmentName -itemType \"Environment\" -defaultUrl $DefaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey -defaultValue $null -endpoint \"environments\" + + if ([string]::IsNullOrWhiteSpace($parentReleaseId) -eq $false) + { + return Get-ApprovalTaskIdFromDeployment -parentReleaseId $parentReleaseId -approvalEnvironment $approvalEnvironment -parentChannelId $parentChannelId -parentEnvironmentId $parentEnvironmentId -defaultUrl $defaultUrl -octopusApiKey $octopusApiKey -spaceId $spaceId + } + + return Get-ApprovalTaskIdFromRunbook -parentRunbookId $parentRunbookId -approvalEnvironment $approvalEnvironment -defaultUrl $defaultUrl -spaceId $spaceId -octopusApiKey $octopusApiKey +} + +function Get-OctopusLifecycle +{ + param ( + $channel, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + Write-OctopusInformation \"Attempting to find the lifecycle information $($channel.Name)\" + if ($null -eq $channel.LifecycleId) + { + $lifecycleName = \"Default Lifecycle\" + $lifecycleList = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"lifecycles?partialName=$([uri]::EscapeDataString($lifecycleName))&skip=0&take=1\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + $lifecycle = $lifecycleList.Items[0] + } + else + { + $lifecycle = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"lifecycles/$($channel.LifecycleId)\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + } + + Write-Host \"Successfully found the lifecycle $($lifecycle.Name) to use for this channel.\" + + return $lifecycle +} + +function Get-LifecyclePhases +{ + param ( + $lifecycle, + $defaultUrl, + $spaceId, + $octopusApiKey + ) + + Write-OctopusInformation \"Attempting to find the phase in the lifecycle $($lifecycle.Name) with the environment $environmentName to find the previous phase.\" + if ($lifecycle.Phases.Count -eq 0) + { + Write-OctopusInformation \"The lifecycle $($lifecycle.Name) has no set phases, calling the preview endpoint.\" + $lifecyclePreview = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"lifecycles/$($lifecycle.Id)/preview\" -spaceId $spaceId -apiKey $octopusApiKey -method \"GET\" + $phases = $lifecyclePreview.Phases + } + else + { + Write-OctopusInformation \"The lifecycle $($lifecycle.Name) has set phases, using those.\" + $phases = $lifecycle.Phases + } + + Write-OctopusInformation \"Found $($phases.Length) phases in this lifecycle.\" + return $phases +} + +function Submit-RunbookRunForAutoApproval +{ + param ( + $createdRunbookRun, + $parentTaskApprovers, + $defaultUrl, + $octopusApiKey, + $spaceId, + $parentProjectName, + $parentReleaseNumber, + $parentRunbookId, + $parentEnvironmentName, + $parentTaskId + ) + + Write-OctopusSuccess \"The task has a pending manual intervention. Checking parent approvals.\" + $manualInterventionInformation = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"interruptions?regarding=$($createdRunbookRun.TaskId)\" -method \"GET\" -apiKey $octopusApiKey -spaceId $spaceId + foreach ($manualIntervention in $manualInterventionInformation.Items) + { + if ($manualIntervention.IsPending -eq $false) + { + Write-OctopusInformation \"This manual intervention has already been approved. Proceeding onto the next one.\" + continue + } + + if ($manualIntervention.CanTakeResponsibility -eq $false) + { + Write-OctopusSuccess \"The user associated with the API key doesn't have permissions to take responsibility for the manual intervention.\" + Write-OctopusSuccess \"If you wish to leverage the auto-approval functionality give the user permissions.\" + continue + } + + $automaticApprover = $null + Write-OctopusVerbose \"Checking to see if one of the parent project approvers is assigned to one of the manual intervention teams $($manualIntervention.ResponsibleTeamIds)\" + foreach ($approver in $parentTaskApprovers) + { + foreach ($approverTeam in $approver.Teams) + { + Write-OctopusVerbose \"Checking to see if $($manualIntervention.ResponsibleTeamIds) contains $($approverTeam.TeamId)\" + if ($manualIntervention.ResponsibleTeamIds -contains $approverTeam.TeamId) + { + $automaticApprover = $approver + break + } + } + + if ($null -ne $automaticApprover) + { + break + } + } + + if ($null -ne $automaticApprover) + { + \tWrite-OctopusSuccess \"Matching approver found auto-approving.\" + if ($manualIntervention.HasResponsibility -eq $false) + { + Write-OctopusInformation \"Taking over responsibility for this manual intervention.\" + $takeResponsiblilityResponse = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"interruptions/$($manualIntervention.Id)/responsible\" -method \"PUT\" -apiKey $octopusApiKey -spaceId $spaceId + Write-OctopusVerbose \"Response from taking responsibility $($takeResponsiblilityResponse.Id)\" + } + + if ([string]::IsNullOrWhiteSpace($parentReleaseNumber) -eq $false) + { + $notes = \"Auto-approving this runbook run. Parent project $parentProjectName release $parentReleaseNumber to $parentEnvironmentName with the task id $parentTaskId was approved by $($automaticApprover.UserName). That user is a member of one of the teams this manual intervention requires. You can view that deployment $defaultUrl/app#/$spaceId/tasks/$parentTaskId\" + } + else + { + $notes = \"Auto-approving this runbook run. Parent project $parentProjectName runbook run $parentRunbookId to $parentEnvironmentName with the task id $parentTaskId was approved by $($automaticApprover.UserName). That user is a member of one of the teams this manual intervention requires. You can view that runbook run $defaultUrl/app#/$spaceId/tasks/$parentTaskId\" + } + if ($runbookCustomNotesToggle -eq $true){ + $notes = $runbookCustomNotes + } + $submitApprovalBody = @{ + Instructions = $null; + Notes = $notes + Result = \"Proceed\" + } + $submitResult = Invoke-OctopusApi -octopusUrl $DefaultUrl -endPoint \"interruptions/$($manualIntervention.Id)/submit\" -method \"POST\" -apiKey $octopusApiKey -item $submitApprovalBody -spaceId $spaceId + Write-OctopusSuccess \"Successfully auto approved the manual intervention $($submitResult.Id)\" + } + else + { + Write-OctopusSuccess \"Couldn't find an approver to auto-approve the child project. Waiting until timeout or child project is approved.\" + } + } +} + + +$runbookWaitForFinish = GetCheckboxBoolean -Value $runbookWaitForFinish +$runbookUseGuidedFailure = GetCheckboxBoolean -Value $runbookUseGuidedFailure +$runbookUsePublishedSnapshot = GetCheckboxBoolean -Value $runbookUsePublishedSnapshot +$runbookCancelInSeconds = [int]$runbookCancelInSeconds + +Write-OctopusInformation \"Wait for Finish Before Check: $runbookWaitForFinish\" +Write-OctopusInformation \"Use Guided Failure Before Check: $runbookUseGuidedFailure\" +Write-OctopusInformation \"Use Published Snapshot Before Check: $runbookUsePublishedSnapshot\" +Write-OctopusInformation \"Runbook Name $runbookRunName\" +Write-OctopusInformation \"Runbook Base Url: $runbookBaseUrl\" +Write-OctopusInformation \"Runbook Space Name: $runbookSpaceName\" +Write-OctopusInformation \"Runbook Environment Name: $runbookEnvironmentName\" +Write-OctopusInformation \"Runbook Tenant Name: $runbookTenantName\" +Write-OctopusInformation \"Wait for Finish: $runbookWaitForFinish\" +Write-OctopusInformation \"Use Guided Failure: $runbookUseGuidedFailure\" +Write-OctopusInformation \"Cancel run in seconds: $runbookCancelInSeconds\" +Write-OctopusInformation \"Use Published Snapshot: $runbookUsePublishedSnapshot\" +Write-OctopusInformation \"Auto Approve Runbook Run Manual Interventions: $autoApproveRunbookRunManualInterventions\" +Write-OctopusInformation \"Auto Approve environment name to pull approvals from: $approvalEnvironmentName\" + +Write-OctopusInformation \"Octopus runbook run machines: $runbookMachines\" +Write-OctopusInformation \"Parent Task Id: $parentTaskId\" +Write-OctopusInformation \"Parent Release Id: $parentReleaseId\" +Write-OctopusInformation \"Parent Channel Id: $parentChannelId\" +Write-OctopusInformation \"Parent Environment Id: $parentEnvironmentId\" +Write-OctopusInformation \"Parent Runbook Id: $parentRunbookId\" +Write-OctopusInformation \"Parent Environment Name: $parentEnvironmentName\" +Write-OctopusInformation \"Parent Release Number: $parentReleaseNumber\" + +$verificationPassed = @() +$verificationPassed += Test-RequiredValues -variableToCheck $runbookRunName -variableName \"Runbook Name\" +$verificationPassed += Test-RequiredValues -variableToCheck $runbookBaseUrl -variableName \"Base Url\" +$verificationPassed += Test-RequiredValues -variableToCheck $runbookApiKey -variableName \"Api Key\" +$verificationPassed += Test-RequiredValues -variableToCheck $runbookEnvironmentName -variableName \"Environment Name\" + +if ($verificationPassed -contains $false) +{ +\tWrite-OctopusInformation \"Required values missing\" +\tExit 1 +} + +$runbookSpace = Get-OctopusItemFromListEndpoint -itemNameToFind $runbookSpaceName -endpoint \"spaces\" -spaceId $null -octopusApiKey $runbookApiKey -defaultUrl $runbookBaseUrl -itemType \"Space\" -defaultValue $octopusSpaceId +$runbookSpaceId = $runbookSpace.Id + +$projectToUse = Get-OctopusItemFromListEndpoint -itemNameToFind $runbookProjectName -endpoint \"projects\" -spaceId $runbookSpaceId -defaultValue $null -itemType \"Project\" -octopusApiKey $runbookApiKey -defaultUrl $runbookBaseUrl +if ($null -ne $projectToUse) +{\t + $runbookEndPoint = \"projects/$($projectToUse.Id)/runbooks\" +} +else +{ +\t$runbookEndPoint = \"runbooks\" +} + +$environmentToUse = Get-OctopusItemFromListEndpoint -itemNameToFind $runbookEnvironmentName -itemType \"Environment\" -defaultUrl $runbookBaseUrl -spaceId $runbookSpaceId -octopusApiKey $runbookApiKey -defaultValue $null -endpoint \"environments\" + +$runbookToRun = Get-OctopusItemFromListEndpoint -itemNameToFind $runbookRunName -itemType \"Runbook\" -defaultUrl $runbookBaseUrl -spaceId $runbookSpaceId -endpoint $runbookEndPoint -octopusApiKey $runbookApiKey -defaultValue $null + +$runbookSnapShotIdToUse = Get-RunbookSnapshotIdToRun -runbookToRun $runbookToRun -runbookUsePublishedSnapshot $runbookUsePublishedSnapshot -defaultUrl $runbookBaseUrl -octopusApiKey $runbookApiKey -spaceId $octopusSpaceId +$projectNameForUrl = Get-ProjectSlug -projectToUse $projectToUse -runbookToRun $runbookToRun -defaultUrl $runbookBaseUrl -octopusApiKey $runbookApiKey -spaceId $runbookSpaceId + +$tenantToUse = Get-OctopusItemFromListEndpoint -itemNameToFind $runbookTenantName -itemType \"Tenant\" -defaultValue $null -spaceId $runbookSpaceId -octopusApiKey $runbookApiKey -endpoint \"tenants\" -defaultUrl $runbookBaseUrl +if ($null -ne $tenantToUse) +{\t + $tenantIdToUse = $tenantToUse.Id + $runBookPreview = Invoke-OctopusApi -octopusUrl $runbookBaseUrl -spaceId $runbookSpaceId -apiKey $runbookApiKey -endPoint \"runbooks/$($runbookToRun.Id)/runbookRuns/preview/$($environmentToUse.Id)/$($tenantIdToUse)\" -method \"GET\" -item $null +} +else +{ +\ttry + { + \tWrite-Host \"Trying the new preview step\" + \t$runBookPreview = Invoke-OctopusApi -octopusUrl $runbookBaseUrl -spaceId $runbookSpaceId -apiKey $runbookApiKey -endPoint \"runbookSnapshots/$($runbookSnapShotIdToUse)/runbookRuns/preview/$($environmentToUse.Id)?includeDisabledSteps=true\" -method \"GET\" -item $null + } + catch + { + \tWrite-Host \"The current version of Octopus Deploy doesn't support Runbook Snapshot Preview\" + \t$runBookPreview = Invoke-OctopusApi -octopusUrl $runbookBaseUrl -spaceId $runbookSpaceId -apiKey $runbookApiKey -endPoint \"runbooks/$($runbookToRun.Id)/runbookRuns/preview/$($environmentToUse.Id)\" -method \"GET\" -item $null + \t} +} + +$childRunbookRunSpecificMachines = Get-RunbookSpecificMachines -defaultUrl $runbookBaseUrl -octopusApiKey $runbookApiKey -runbookPreview $runBookPreview -runbookMachines $runbookMachines -runbookRunName $runbookRunName +$runbookFormValues = Get-RunbookFormValues -runbookPreview $runBookPreview -runbookPromptedVariables $runbookPromptedVariables + +$queueDate = Get-QueueDate -futureDeploymentDate $runbookFutureDeploymentDate +$queueExpiryDate = Get-QueueExpiryDate -queueDate $queueDate + +$runbookBody = @{ + RunbookId = $($runbookToRun.Id); + RunbookSnapShotId = $runbookSnapShotIdToUse; + FrozenRunbookProcessId = $null; + EnvironmentId = $($environmentToUse.Id); + TenantId = $tenantIdToUse; + SkipActions = @(); + QueueTime = $queueDate; + QueueTimeExpiry = $queueExpiryDate; + FormValues = $runbookFormValues; + ForcePackageDownload = $false; + ForcePackageRedeployment = $true; + UseGuidedFailure = $runbookUseGuidedFailure; + SpecificMachineIds = @($childRunbookRunSpecificMachines); + ExcludedMachineIds = @() +} + +$approvalTaskId = Get-ApprovalTaskId -autoApproveRunbookRunManualInterventions $autoApproveRunbookRunManualInterventions -parentTaskId $parentTaskId -parentReleaseId $parentReleaseId -parentRunbookId $parentRunbookId -parentEnvironmentName $parentEnvironmentName -approvalEnvironmentName $approvalEnvironmentName -parentChannelId $parentChannelId -parentEnvironmentId $parentEnvironmentId -defaultUrl $runbookBaseUrl -spaceId $runbookSpaceId -octopusApiKey $runbookApiKey +$parentTaskApprovers = Get-ParentTaskApprovers -parentTaskId $approvalTaskId -spaceId $runbookSpaceId -defaultUrl $runbookBaseUrl -octopusApiKey $runbookApiKey + +Invoke-OctopusDeployRunbook -runbookBody $runbookBody -runbookWaitForFinish $runbookWaitForFinish -runbookCancelInSeconds $runbookCancelInSeconds -projectNameForUrl $projectNameForUrl -defaultUrl $runbookBaseUrl -octopusApiKey $runbookApiKey -spaceId $runbookSpaceId -parentTaskApprovers $parentTaskApprovers -autoApproveRunbookRunManualInterventions $autoApproveRunbookRunManualInterventions -parentProjectName $projectNameForUrl -parentReleaseNumber $parentReleaseNumber -approvalEnvironmentName $approvalEnvironmentName -parentRunbookId $parentRunbookId -parentTaskId $approvalTaskId", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "Parameters": [ + { + "Id": "e9e93cff-973a-4107-afa2-8efa30947979", + "Name": "Run.Runbook.Name", + "Label": "Runbook Name", + "HelpText": "**Required** + +The name of the runbook to run.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d998db57-3574-4598-81f9-7dd145cab81a", + "Name": "Run.Runbook.Base.Url", + "Label": "Base Url", + "HelpText": "**Required** + +The base URL of your instance, IE https://samples.octopus.app. Defaults to the system variable [Octopus.Web.ServerUri](https://octopus.com/docs/projects/variables/system-variables#Systemvariables-Server).", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "24884bf3-ca1d-4c17-8ee0-017339d6d87e", + "Name": "Run.Runbook.Api.Key", + "Label": "Api Key", + "HelpText": "**Required** + +The API key of a user who has permissions to run the runbook specified", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "bc2d33fe-d05b-49bd-b02b-eb6de4737eff", + "Name": "Run.Runbook.Space.Name", + "Label": "Runbook Space", + "HelpText": "**Required** + +The name of the space the child project is located in. Defaults to the current space name using the variable `#{Octopus.Space.Name}`.", + "DefaultValue": "#{Octopus.Space.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a1f44858-809a-48ce-9127-e59f02be40a1", + "Name": "Run.Runbook.Project.Name", + "Label": "Project name", + "HelpText": "**Optional** + +The name of the project containing the runbook. If the project name is not specified then it will use the first runbook with the matching runbook name it can find.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "07bd5b03-4151-4f32-8893-417bf22c4df2", + "Name": "Run.Runbook.Environment.Name", + "Label": "Environment Name", + "HelpText": "**Required** + +Name of the environment to run the runbook on. The default is the current environment name using the system variable `#{Octopus.Environment.Name}`.", + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bf4ae98a-4901-474a-8984-08b0258304ca", + "Name": "Run.Runbook.Tenant.Name", + "Label": "Tenant Name", + "HelpText": "**Optional** + +Name of Tenant to run the runbook for. If you want to run a runbook using the same tenant as the deployment or runbook run then use the system variable `#{Octopus.Deployment.Tenant.Name}`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9c49ba5c-337b-454a-8837-282353276aea", + "Name": "Run.Runbook.UsePublishedSnapShot", + "Label": "Use Published Snapshot", + "HelpText": "Indicates if the run should use the most recent published snapshot. When not set it will use the most recent snapshot, regardless if it was published or not. + +Default is to use only published snapshots. + +If you select to use unpublished snapshots then the runbook will: +- If no published snapshots exist, then it will create a new unpublished snapshot on each run. +- Create a new snapshot if anything has changed since the last published snapshot. +- Use the existing published snapshot if nothing has changed since it was last published. + +**Please note:** A runbook is considered \"changed\" when any of the following are true: + +- Runbook process has changed. +- Project variables have changed. +- Library Variable Sets referenced by the project have changed. +- A newer version of any referenced packages is found.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "1a3e3ff6-456a-49e0-a0ce-83bfb30bfcaa", + "Name": "Run.Runbook.Waitforfinish", + "Label": "Wait for finish", + "HelpText": "Indicates if the process should be paused and wait for the runbook to finish", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "c36715c5-b583-43c4-b3e3-a74f44f2b2c4", + "Name": "Run.Runbook.UseGuidedFailure", + "Label": "Use Guided Failure", + "HelpText": "Should the runbook run use guided failure (not a good idea if you are waiting for this to finish)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "6b951d28-b027-4f16-aaa6-39e91bd906d4", + "Name": "Run.Runbook.CancelInSeconds", + "Label": "Cancel Seconds", + "HelpText": "**Optional** + +The number of seconds to wait before canceling the runbook run. Default is 1800 seconds (30 minutes).", + "DefaultValue": "1800", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c847668d-b4fa-4405-a15b-f03691147597", + "Name": "Run.Runbook.PromptedVariables", + "Label": "Prompted Variable Values", + "HelpText": "**Optional** + +Values for any prompted variables for the runbook. Each new line represents a new variable. This will only work with string variable types, text and sensitive values. + +Use the format **Name::Value** IE: + + +PromptedVariableName::My Super Awesome Value + +OtherPromptedVariable::Other Super Awesome Value", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "dd65f24d-7546-4271-b78b-28101170410c", + "Name": "Run.Runbook.DateTime", + "Label": "Scheduling", + "HelpText": "**Optional** + +Schedule the runbook to run in the future. Please note, if this is set, the `Wait for Finish` option is ignored. + +Uses `DateTime.TryParse` and specific keywords to determine the value sent in. Supported formats: + +- `7:00 PM` will deploy at 7:00 PM today +- `21:00` will deploy at 21:00 hours or 9 PM today +- `YYYY-MM-DD HH:mm:ss` or `2021-01-14 21:00:00` will deploy at 9 PM on the 14th of January, 2021 +- `YYYY/MM/DD HH:mm:ss` or `2021/03/20 22:00:00` will deploy at 10 PM on the 20th of March, 2021 +- `MM/DD/YYYY HH:mm:ss` or `06/25/2021 19:00:00` will deploy at 7 PM on the 25th of June, 2021 +- `DD MMM YYYY HH:mm:ss` or `01 Jan 2021 18:00:00` will deploy at 6 PM on the 1st of January, 2021 +- `Tomorrow HH:mm:ss` or `Tomorrow 18:00:00` will deploy at 6 PM tomorrow + +Uses the Octopus Server's Timezone. The queue expiry time will be set to 1 hour from the supplied date. + +Default is `N/A` or not applicable. ", + "DefaultValue": "N/A", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "22158e31-8061-4ada-b61b-a7bdacb5dd37", + "Name": "Run.Runbook.Machines", + "Label": "Machine List", + "HelpText": "**Optional** + +A comma-separated list of Machine Ids or Machine Names to target with this runbook. + +**Please Note:** The step template will remove any machines that cannot be found or are not applicable to the runbook. + +The default is `N/A`. Set to `#{Octopus.Deployment.Machines}` if you want to target the same machines as the current runbook run or deployment.", + "DefaultValue": "N/A", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "95788fe2-f770-460c-b012-e6a586fe04d7", + "Name": "Run.Runbook.AutoApproveManualInterventions", + "Label": "Auto approve runbook manual interventions", + "HelpText": "**Optional** + +If the child project has manual interventions the step will look for manual interventions in the parent project or parent runbook. + +When a manual intervention in the parent project or parent runbook is found it will check that user's assigned teams. If that user's assigned teams can approve the child project it will do so. + +Please note, the user associated with the API key must be able to approve the child project manual interventions as well. + +The default is `No`, allow this to happen. Set it to `Yes` to enable this functionality.", + "DefaultValue": "No", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "No|No +Yes|Yes" + } + }, + { + "Id": "ea7c213e-380b-46ba-85b7-5c2c0f7b01d7", + "Name": "Run.Runbook.CustomNotes.Toggle", + "Label": "Custom Manual Intervention Notes Toggle", + "HelpText": "Check this box if you would like custom notes to be submitted with the automatic manual intervention approval.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "70549ad5-b451-4587-b8ed-b4afad1752f9", + "Name": "Run.Runbook.CustomNotes", + "Label": "Custom Manual Intervention Approval Notes", + "HelpText": "Use this field to supply custom manual intervention notes if the above toggle is checked.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b1cd0181-c5a8-4d4d-9746-f7cfe41f6794", + "Name": "Run.Runbook.ManualIntervention.EnvironmentToUse", + "Label": "Environment name to pull approvals from", + "HelpText": "**Optional** + +The name of the environment you wish to pull the approvals from for the parent project or parent runbook. It will look at all the deployments for the current release of the parent project and select the latest deployment to the specified environment. If this step is being called from a runbook, it will look at the latest runbook run for the environment specified + +Used when you are deploying to `Production` but want to pull the approvals from `Staging` or a `Prod Approval` environment. + +Defaults to the current environment name.", + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "millerjn21", + "$Meta": { + "ExportedAt": "2024-07-23T14:07:03.309Z", + "OctopusVersion": "2024.2.9274", + "Type": "ActionTemplate" + }, + "Category": "octopus" + } diff --git a/step-templates/run-pulumi-on-linux.json.human b/step-templates/run-pulumi-on-linux.json.human new file mode 100644 index 000000000..2c14c595b --- /dev/null +++ b/step-templates/run-pulumi-on-linux.json.human @@ -0,0 +1,158 @@ +{ + "Id": "76296cd1-7d8c-47e8-b33f-027ecd3ff6b5", + "Name": "Run Pulumi (Linux)", + "Description": "Allows you to run Pulumi commands using the Pulumi CLI. For Pulumi stacks that deploy AWS resources, make sure your Octopus Project contains a variable called `AWS` of type `AWS Account`. For Pulumi stacks that deploy Azure resources, set the variable `Azure` of type `Azure Subscriptions` (Service Principal). + +Learn more about adding an [AWS Account](https://octopus.com/docs/infrastructure/deployment-targets/aws#create-an-aws-account) or [Azure Subscriptions](https://octopus.com/docs/infrastructure/deployment-targets/azure#azure-service-principal) to your Octopus Deploy instance.", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "# Check if the pulumi command is in the path and if not, download and install it. +# Additionally, add pulumi to the path. +if ! [ -x \"$(command -v pulumi)\" ]; then +\tcurl -fsSL https://get.pulumi.com | sh + export PATH=$PATH:$HOME/.pulumi/bin +\techo \"Pulumi version: $(pulumi version)\" +fi + +accessToken=$(get_octopusvariable \"Pulumi.AccessToken\") +if [ -z \"${accessToken:-}\" ]; then +\tfail_step \"Pulumi Access Token must be specified.\" +fi +export PULUMI_ACCESS_TOKEN=$accessToken + +# Check for AWS access key credentials and set those in the env. +export AWS_ACCESS_KEY_ID=$(get_octopusvariable \"AWS.AccessKey\") +export AWS_SECRET_ACCESS_KEY=$(get_octopusvariable \"AWS.SecretKey\") + +# Check for Azure SP/personal account credentials and set those in the env. +export ARM_SUBSCRIPTION_ID=$(get_octopusvariable \"Azure.SubscriptionNumber\") +export ARM_TENANT_ID=$(get_octopusvariable \"Azure.TenantId\") +export ARM_CLIENT_ID=$(get_octopusvariable \"Azure.Client\") +export ARM_CLIENT_SECRET=$(get_octopusvariable \"Azure.Password\") + +if [ -z \"${AWS_SECRET_ACCESS_KEY:-}\" ]; then +\tif [ -z \"${ARM_CLIENT_SECRET:-}\" ]; then + \tfail_step \"Neither secrets for AWS or Azure were detected.\" + fi +fi + +pulumi login + +cwd=$(get_octopusvariable \"Pulumi.WorkingDirectory\") +if [ -n \"${cwd:-}\" ]; then +\tpushd $cwd +fi + +restoreDeps=$(get_octopusvariable \"Pulumi.Restore\") +if [ \"$restoreDeps\" = \"True\" ]; then +\techo \"Restoring dependencies...\" +\tnpm install +fi + +createStackIfNotExists=$(get_octopusvariable \"Pulumi.CreateStack\") +stackName=$(get_octopusvariable \"Pulumi.StackName\") +echo \"Selecting stack $stackName\" +pulumi stack select $stackName || ( +\tif [ \"$createStackIfNotExists\" = \"True\" ]; then + \tpulumi stack init $stackName + fi +) + +pulCmd=$(get_octopusvariable \"Pulumi.Command\") +pulArgs=$(get_octopusvariable \"Pulumi.Args\") +if [ -n \"${pulArgs:-}\" ]; then +\tpulumi $pulCmd $pulArgs +else +\tpulumi $pulCmd +fi + +# If a working directory was specified, we would have `pushd`, so let's `popd` now. +if [ -n \"${cwd:-}\" ]; then +\tpopd +fi +" + }, + "Parameters": [{ + "Id": "94a93122-235e-4edd-8217-adbe63db85f1", + "Name": "Pulumi.StackName", + "Label": "Stack Name", + "HelpText": "The fully-qualified stack name against which the Pulumi commands will be run. Hint: `{orgName}/{projectName}/{stackName}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f00ffc37-a550-458e-85ff-43909eb76c79", + "Name": "Pulumi.CreateStack", + "Label": "Create Stack", + "HelpText": "Whether to create a new stack if it does not exist already.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "29e68d5d-5c46-4290-993c-51672a431812", + "Name": "Pulumi.Command", + "Label": "Command", + "HelpText": "Eg. `preview`, `up`, `destroy` etc. Learn more [here](https://www.pulumi.com/docs/reference/cli/).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "362423a8-aab4-451f-b79b-3896725b8c84", + "Name": "Pulumi.Args", + "Label": "Command Args", + "HelpText": "Arguments to pass to the Pulumi command. Eg. `-v` to set the verbosity or `--logtostderr` to log to the standard error. [Learn more](https://www.pulumi.com/docs/reference/cli/) about the available commands and the arguments you can pass to each of them.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9f6bda9f-39dd-47cf-9233-5499a837c4fb", + "Name": "Pulumi.AccessToken", + "Label": "Pulumi Access Token", + "HelpText": "The [Pulumi access token](https://www.pulumi.com/docs/intro/console/accounts-and-organizations/accounts/#access-tokens) to use. The access token must have access to the stack, which you are deploying. [Click here](https://app.pulumi.com/account/tokens) to go to the Access Tokens page on the Pulumi Console now.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "aa4310d6-52eb-4b37-9575-db6cfe7c3e3a", + "Name": "Pulumi.WorkingDirectory", + "Label": "Pulumi Working Directory", + "HelpText": "The working directory where the Pulumi app is extracted to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d4d357c7-d4a9-4c1c-b38e-6efcb363ce0b", + "Name": "Pulumi.Restore", + "Label": "Restore Dependencies", + "HelpText": "Whether to restore NodeJS dependencies.", + "DefaultValue": "true", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "praneetloke", + "$Meta": { + "ExportedAt": "2019-09-26T01:21:54.376Z", + "OctopusVersion": "2019.8.4", + "Type": "ActionTemplate" + }, + "Category": "pulumi" +} diff --git a/step-templates/run-pulumi-on-windows.json.human b/step-templates/run-pulumi-on-windows.json.human new file mode 100644 index 000000000..21f130138 --- /dev/null +++ b/step-templates/run-pulumi-on-windows.json.human @@ -0,0 +1,198 @@ +{ + "Id": "b63d2573-9ff4-4ffe-83c7-153fad24ea57", + "Name": "Run Pulumi (Windows)", + "Description": "Allows you to run Pulumi commands using the Pulumi CLI. For Pulumi stacks that deploy AWS resources, make sure your Octopus Project contains a variable called `AWS` of type `AWS Account`. For Pulumi stacks that deploy Azure resources, set the variable `Azure` of type `Azure Subscriptions` (Service Principal). + +Learn more about adding an [AWS Account](https://octopus.com/docs/infrastructure/deployment-targets/aws#create-an-aws-account) or [Azure Subscriptions](https://octopus.com/docs/infrastructure/deployment-targets/azure#azure-service-principal) to your Octopus Deploy instance.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Function Test-CommandExists +{ +\tParam ($command) +\t$oldPreference = $ErrorActionPreference +\t$ErrorActionPreference = 'stop' +\tTry { + \tif(Get-Command $command){ + \treturn $true + } + } Catch { + \tWrite-Host \"$command does not exist\" + return $false + } Finally { + \t$ErrorActionPreference=$oldPreference + \t} +} + +If ([string]::IsNullOrWhiteSpace($OctopusParameters[\"Pulumi.AccessToken\"])) { +\tFail-Step \"Parameter Pulumi.AccessToken cannot be empty.\" +} + +$env:PULUMI_ACCESS_TOKEN=$OctopusParameters[\"Pulumi.AccessToken\"] + +If ((Test-CommandExists pulumi) -eq $false) { +\t(new-object net.webclient).DownloadFile(\"https://get.pulumi.com/install.ps1\",\"local.ps1\") + ./local.ps1 +\t$pulumiInstallRoot=(Join-Path $env:UserProfile \".pulumi\") +\t$binRoot=(Join-Path $pulumiInstallRoot \"bin\") +\t$env:Path+=\";$binRoot\" +} + +# Check for AWS access key credentials and set those in the env. +If (![string]::IsNullOrWhiteSpace($OctopusParameters[\"AWS.AccessKey\"])) { +\t$env:AWS_ACCESS_KEY_ID=$OctopusParameters[\"AWS.AccessKey\"] +} + +If (![string]::IsNullOrWhiteSpace($OctopusParameters[\"AWS.SecretKey\"])) { +\t$env:AWS_SECRET_ACCESS_KEY=$OctopusParameters[\"AWS.SecretKey\"] +} + +# Check for Azure SP/personal account credentials and set those in the env. +If (![string]::IsNullOrWhiteSpace($OctopusParameters[\"Azure.Client\"])) { +\t$env:ARM_CLIENT_ID=$OctopusParameters[\"Azure.Client\"] +} + +If (![string]::IsNullOrWhiteSpace($OctopusParameters[\"Azure.Password\"])) { +\t$env:ARM_CLIENT_SECRET=$OctopusParameters[\"Azure.Password\"] +} + +If (![string]::IsNullOrWhiteSpace($OctopusParameters[\"Azure.TenantId\"])) { +\t$env:ARM_TENANT_ID=$OctopusParameters[\"Azure.TenantId\"] +} + +If (![string]::IsNullOrWhiteSpace($OctopusParameters[\"Azure.SubscriptionNumber\"])) { +\t$env:ARM_SUBSCRIPTION_ID=$OctopusParameters[\"Azure.SubscriptionNumber\"] +} + +Write-Host \"Logging in to Pulumi using access token\" +pulumi login + +$cwd=$OctopusParameters[\"Pulumi.WorkingDirectory\"] +If (![string]::IsNullOrWhiteSpace($cwd)) { +\tcd $cwd +} + +$stackName=$OctopusParameters[\"Pulumi.StackName\"] +Write-Host \"Selecting stack $stackName\" +Try { +\tpulumi stack select $stackName +} +Catch { +\t$createStackIfNotExists = $OctopusParameters[\"Pulumi.CreateStack\"] +\tIf ($createStackIfNotExists -eq \"True\") { + \tpulumi stack init $stackName + } Else { + \tFail-Step \"Stack $stackName does not exist.\" + } +} + +$restoreDeps=$OctopusParameters[\"Pulumi.RestoreDeps\"] +If ($restoreDeps -eq \"True\") { +\tWrite-Host \"Restoring dependencies...\" + $restoreDepsCmd = $OctopusParameters[\"Pulumi.RestoreCmd\"] + Invoke-Expression $restoreDepsCmd +} + +$pulCmd=$OctopusParameters[\"Pulumi.Command\"] +$pulArgs=$OctopusParameters[\"Pulumi.Args\"] +If (![string]::IsNullOrWhiteSpace($pulArgs)) { +\tpulumi $pulCmd $pulArgs +} +Else { +\tpulumi $pulCmd +} +" + }, + "Parameters": [{ + "Id": "94a93122-235e-4edd-8217-adbe63db85f1", + "Name": "Pulumi.StackName", + "Label": "Stack Name", + "HelpText": "The fully-qualified stack name against which the Pulumi commands will be run. Hint: `{orgName}/{projectName}/{stackName}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "29e68d5d-5c46-4290-993c-51672a431812", + "Name": "Pulumi.Command", + "Label": "Command", + "HelpText": "Eg. `preview`, `up`, `destroy` etc. Learn more [here](https://www.pulumi.com/docs/reference/cli/).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "362423a8-aab4-451f-b79b-3896725b8c84", + "Name": "Pulumi.Args", + "Label": "Command Args", + "HelpText": "Arguments to pass to the Pulumi command. Eg. `-v` to set the verbosity or `--logtostderr` to log to the standard error. [Learn more](https://www.pulumi.com/docs/reference/cli/) about the available commands and the arguments you can pass to each of them.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9f6bda9f-39dd-47cf-9233-5499a837c4fb", + "Name": "Pulumi.AccessToken", + "Label": "Pulumi Access Token", + "HelpText": "The [Pulumi access token](https://www.pulumi.com/docs/intro/console/accounts-and-organizations/accounts/#access-tokens) to use. The access token must have access to the stack, which you are deploying. [Click here](https://app.pulumi.com/account/tokens) to go to the Access Tokens page on the Pulumi Console now.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "aa4310d6-52eb-4b37-9575-db6cfe7c3e3a", + "Name": "Pulumi.WorkingDirectory", + "Label": "Pulumi Working Directory", + "HelpText": "The working directory where the Pulumi app is extracted to. If this parameter is specified, then the step will `cd` into that directory. To avoid this, you can pass the `--cwd C:\\path\\to\\your\\dir` in the `Command Args` parameter as one of the args.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "04d21e39-3f74-435f-803d-b80c104e5c07", + "Name": "Pulumi.CreateStack", + "Label": "Create Stack", + "HelpText": "Whether to create a new stack if it does not exist already.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "4775234d-4d5c-454e-888c-1171f1b1c258", + "Name": "Pulumi.RestoreDeps", + "Label": "Restore Dependencies", + "HelpText": "Whether to restore dependencies.", + "DefaultValue": "true", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "6a1ddea9-76dd-4c32-bbf5-a5c451d6e251", + "Name": "Pulumi.RestoreCmd", + "Label": "The command used to restore dependencies.", + "HelpText": "(Optional) Required only if `Restore Dependencies` is true. The command used to restore dependencies for your Pulumi app. This is dependent on the runtime your Pulumi app uses. For example, if your Pulumi app uses the `nodejs` runtime, then use the command `npm install` here to restore dependencies.", + "DefaultValue": "npm install", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "praneetloke", + "$Meta": { + "ExportedAt": "2019-12-03T05:49:17.461Z", + "OctopusVersion": "2019.10.4", + "Type": "ActionTemplate" + }, + "Category": "pulumi" +} diff --git a/step-templates/run-windows-installer.json.human b/step-templates/run-windows-installer.json.human new file mode 100644 index 000000000..ffedaa599 --- /dev/null +++ b/step-templates/run-windows-installer.json.human @@ -0,0 +1,405 @@ +{ + "Id": "f56647ac-7762-4986-bc98-c3fb74bb844f", + "Name": "Run - Windows Installer", + "Description": "Runs the Windows Installer to non-interactively install an MSI", + "ActionType": "Octopus.Script", + "Version": 13, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +param( + [string]$MsiFilePath, +\t[ValidateSet(\"Install\", \"Repair\", \"Remove\", IgnoreCase=$true)] +\t[string]$Action, +\t[string]$ActionModifier, + [string]$LoggingOptions = \"*\", + [ValidateSet(\"False\", \"True\")] + [string]$LogAsArtifact, +\t[string]$Properties, +\t[int[]]$IgnoredErrorCodes, + [switch]$WhatIf +) + +$ErrorActionPreference = \"Stop\" + +$ErrorMessages = @{ +\t\"0\" = \"Action completed successfully.\"; +\t\"13\" = \"The data is invalid.\"; +\t\"87\" = \"One of the parameters was invalid.\"; +\t\"1601\" = \"The Windows Installer service could not be accessed. Contact your support personnel to verify that the Windows Installer service is properly registered.\"; +\t\"1602\" = \"User cancel installation.\"; +\t\"1603\" = \"Fatal error during installation.\"; +\t\"1604\" = \"Installation suspended, incomplete.\"; +\t\"1605\" = \"This action is only valid for products that are currently installed.\"; +\t\"1606\" = \"Feature ID not registered.\"; +\t\"1607\" = \"Component ID not registered.\"; +\t\"1608\" = \"Unknown property.\"; +\t\"1609\" = \"Handle is in an invalid state.\"; +\t\"1610\" = \"The configuration data for this product is corrupt. Contact your support personnel.\"; +\t\"1611\" = \"Component qualifier not present.\"; +\t\"1612\" = \"The installation source for this product is not available. Verify that the source exists and that you can access it.\"; +\t\"1613\" = \"This installation package cannot be installed by the Windows Installer service. You must install a Windows service pack that contains a newer version of the Windows Installer service.\"; +\t\"1614\" = \"Product is uninstalled.\"; +\t\"1615\" = \"SQL query syntax invalid or unsupported.\"; +\t\"1616\" = \"Record field does not exist.\"; +\t\"1618\" = \"Another installation is already in progress. Complete that installation before proceeding with this install.\"; +\t\"1619\" = \"This installation package could not be opened. Verify that the package exists and that you can access it, or contact the application vendor to verify that this is a valid Windows Installer package.\"; +\t\"1620\" = \"This installation package could not be opened. Contact the application vendor to verify that this is a valid Windows Installer package.\"; +\t\"1621\" = \"There was an error starting the Windows Installer service user interface. Contact your support personnel.\"; +\t\"1622\" = \"Error opening installation log file. Verify that the specified log file location exists and is writable.\"; +\t\"1623\" = \"This language of this installation package is not supported by your system.\"; +\t\"1624\" = \"Error applying transforms. Verify that the specified transform paths are valid.\"; +\t\"1625\" = \"This installation is forbidden by system policy. Contact your system administrator.\"; +\t\"1626\" = \"Function could not be executed.\"; +\t\"1627\" = \"Function failed during execution.\"; +\t\"1628\" = \"Invalid or unknown table specified.\"; +\t\"1629\" = \"Data supplied is of wrong type.\"; +\t\"1630\" = \"Data of this type is not supported.\"; +\t\"1631\" = \"The Windows Installer service failed to start. Contact your support personnel.\"; +\t\"1632\" = \"The temp folder is either full or inaccessible. Verify that the temp folder exists and that you can write to it.\"; +\t\"1633\" = \"This installation package is not supported on this platform. Contact your application vendor.\"; +\t\"1634\" = \"Component not used on this machine.\"; +\t\"1635\" = \"This patch package could not be opened. Verify that the patch package exists and that you can access it, or contact the application vendor to verify that this is a valid Windows Installer patch package.\"; +\t\"1636\" = \"This patch package could not be opened. Contact the application vendor to verify that this is a valid Windows Installer patch package.\"; +\t\"1637\" = \"This patch package cannot be processed by the Windows Installer service. You must install a Windows service pack that contains a newer version of the Windows Installer service.\"; +\t\"1638\" = \"Another version of this product is already installed. Installation of this version cannot continue. To configure or remove the existing version of this product, use Add/Remove Programs on the Control Panel.\"; +\t\"1639\" = \"Invalid command line argument. Consult the Windows Installer SDK for detailed command line help.\"; +\t\"1640\" = \"Installation from a Terminal Server client session not permitted for current user.\"; +\t\"1641\" = \"The installer has started a reboot.\"; +\t\"1642\" = \"The installer cannot install the upgrade patch because the program being upgraded may be missing, or the upgrade patch updates a different version of the program. Verify that the program to be upgraded exists on your computer and that you have the correct upgrade patch.\"; +\t\"3010\" = \"A restart is required to complete the install. This does not include installs where the ForceReboot action is run. Note that this error will not be available until future version of the installer.\" +}; + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if ($result -eq $null) { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +function Resolve-PotentialPath($Path) { +\t[Environment]::CurrentDirectory = $pwd +\treturn [IO.Path]::GetFullPath($Path) +} + +function Get-LogOptionFile($msiFile, $streamLog) { +\t$logPath = Resolve-PotentialPath \"$msiFile.log\" +\t +\tif (Test-Path $logPath) { +\t\tRemove-Item $logPath +\t} +\t +\treturn $logPath +} + +function Exec +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, + [string]$errorMessage = ($msgs.error_bad_command -f $cmd), +\t\t[switch]$ReturnCode + ) + +\t$lastexitcode = 0 + & $cmd +\t +\tif ($ReturnCode) { +\t\treturn $lastexitcode +\t} else { +\t\tif ($lastexitcode -ne 0) { +\t\t\tthrow (\"Exec: \" + $errorMessage) +\t\t}\t\t +\t} +} + +function Wrap-Arguments($Arguments) +{ +\treturn $Arguments | % { +\t\t +\t\t[string]$val = $_ +\t\t +\t\t#calling msiexec fails when arguments are quoted +\t\tif (($val.StartsWith(\"/\") -and $val.IndexOf(\" \") -eq -1) -or ($val.IndexOf(\"=\") -ne -1) -or ($val.IndexOf('\"') -ne -1)) { +\t\t\treturn $val +\t\t} +\t +\t\treturn '\"{0}\"' -f $val +\t} +} + +function Start-Process2($FilePath, $ArgumentList, [switch]$showCall, [switch]$whatIf) +{ +\t$ArgumentListString = (Wrap-Arguments $ArgumentList) -Join \" \" + +\t$pinfo = New-Object System.Diagnostics.ProcessStartInfo +\t$pinfo.FileName = $FilePath +\t$pinfo.UseShellExecute = $false +\t$pinfo.CreateNoWindow = $true +\t$pinfo.RedirectStandardOutput = $true +\t$pinfo.RedirectStandardError = $true +\t$pinfo.Arguments = $ArgumentListString; +\t$pinfo.WorkingDirectory = $pwd + +\t$exitCode = 0 +\t +\tif (!$whatIf) { +\t +\t\tif ($showCall) { +\t\t\t$x = Write-Output \"$FilePath $ArgumentListString\" +\t\t} +\t\t +\t\t$p = New-Object System.Diagnostics.Process +\t\t$p.StartInfo = $pinfo +\t\t$started = $p.Start() +\t\t$p.WaitForExit() + +\t\t$stdout = $p.StandardOutput.ReadToEnd() +\t\t$stderr = $p.StandardError.ReadToEnd() +\t\t$x = Write-Output $stdout +\t\t$x = Write-Output $stderr +\t\t +\t\t$exitCode = $p.ExitCode +\t} else { +\t\tWrite-Output \"skipping: $FilePath $ArgumentListString\" +\t} +\t +\treturn $exitCode +} + +function Get-EscapedFilePath($FilePath) +{ + return [Management.Automation.WildcardPattern]::Escape($FilePath) +} + +& { + param( + [string]$MsiFilePath, +\t\t[string]$Action, +\t\t[string]$ActionModifier, + [string]$LoggingOptions, + [bool]$LogAsArtifact, +\t\t[string]$Properties, +\t\t[int[]]$IgnoredErrorCodes + ) + + $MsiFilePathLeaf = Split-Path -Path $MsiFilePath -Leaf + $EscapedMsiFilePath = Get-EscapedFilePath (Split-Path -Path $MsiFilePath) + +\t$MsiFilePath = Get-EscapedFilePath (Resolve-Path \"$EscapedMsiFilePath\\$MsiFilePathLeaf\" | Select-Object -First 1).ProviderPath + + Write-Output \"Installing MSI\" + Write-Host \" MsiFilePath: $MsiFilePath\" -f Gray +\tWrite-Host \" Action: $Action\" -f Gray +\tWrite-Host \" Properties: $Properties\" -f Gray +\tWrite-Host + +\tif ((Get-Command msiexec) -Eq $Null) { +\t\tthrow \"Command msiexec could not be found\" +\t} +\t +\tif (!(Test-Path $MsiFilePath)) { +\t\tthrow \"Could not find the file $MsiFilePath\" +\t} + +\t$actions = @{ +\t\t\"Install\" = \"/i\"; +\t\t\"Repair\" = \"/f\"; +\t\t\"Remove\" = \"/x\"; +\t}; +\t +\t$actionOption = $actions[$action] +\t$actionOptionFile = $MsiFilePath +\tif ($ActionModifier) +\t{ +\t\t$actionOption += $ActionModifier +\t} +\t + if ($LoggingOptions) { +\t $logOption = \"/L$LoggingOptions\" +\t $logOptionFile = Get-LogOptionFile $MsiFilePath +\t} +\t$quiteOption = \"/qn\" +\t$noRestartOption = \"/norestart\" +\t +\t$parameterOptions = $Properties -Split \"\\r\ +?|\ +\" | ? { !([string]::IsNullOrEmpty($_)) } | % { $_.Trim() } +\t +\t$options = @($actionOption, $actionOptionFile, $logOption, $logOptionFile, $quiteOption, $noRestartOption) + $parameterOptions + +\t$exePath = \"msiexec.exe\" + +\t$exitCode = Start-Process2 -FilePath $exePath -ArgumentList $options -whatIf:$whatIf -ShowCall +\t +\tWrite-Output \"Exit Code was! $exitCode\" +\t +\tif (Test-Path $logOptionFile) { + +\t\tWrite-Output \"Reading installer log\" + + # always write out these (http://robmensching.com/blog/posts/2010/8/2/the-first-thing-i-do-with-an-msi-log/) + (Get-Content $logOptionFile) | Select-String -SimpleMatch \"value 3\" -Context 10,0 | ForEach-Object { Write-Warning $_ } + + if ($LogAsArtifact) { + New-OctopusArtifact -Path $logOptionFile -Name \"$Action-$([IO.Path]::GetFileNameWithoutExtension($MsiFilePath)).log\" + } else { +\t \tGet-Content $logOptionFile | Write-Output + } + +\t} else { +\t\tWrite-Output \"No logs were generated\" +\t} + +\tif ($exitCode -Ne 0) { +\t\t$errorCodeString = $exitCode.ToString() +\t\t$errorMessage = $ErrorMessages[$errorCodeString] +\t\t +\t\tif ($IgnoredErrorCodes -notcontains $exitCode) { + +\t\t\tthrow \"Error code $exitCodeString was returned: $errorMessage\" +\t\t} +\t\telse { +\t\t\tWrite-Output \"Error code [$exitCodeString] was ignored because it was in the IgnoredErrorCodes [$($IgnoredErrorCodes -join ',')] parameter. Error Message [$errorMessage]\" +\t\t} +\t} +\t +} ` +(Get-Param 'MsiFilePath' -Required) ` +(Get-Param 'Action' -Required) ` +(Get-Param 'ActionModifier') ` +(Get-Param 'LoggingOptions') ` +((Get-Param 'LogAsArtifact') -eq \"True\") ` +(Get-Param 'Properties') ` +(Get-Param 'IgnoredErrorCodes') +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "381f31f3-2cad-45fa-96ea-d99b2299667c", + "Name": "MsiFilePath", + "Label": "Msi File Path", + "HelpText": "This is the path of the MSI file that will be installed. If the path includes wildcards, then the first match will be used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6e056c08-d73e-4e3e-bf45-c12e0d3d1d24", + "Name": "Action", + "Label": "Action", + "HelpText": "The task to perform with the MSI, options include install, repair or remove.", + "DefaultValue": "Install", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Install +Repair +Remove" + } + }, + { + "Id": "7343a832-0673-472c-ae8f-1a328d65cafe", + "Name": "ActionModifier", + "Label": "Action Modifier", + "HelpText": "Use this to specify a different behavior for the Repair action", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "698d3bfb-2c14-4783-aa19-2a2ae2f49645", + "Name": "Properties", + "Label": "Properties", + "HelpText": "Properties that will be passed to the MSI separated by lines. Properties are in the format key=value, note that values with spaces in the must be quoted. + + Key=Value + Key=\"Value\"", + "DefaultValue": "REBOOT=ReallySuppress", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "d78748d0-2f38-485d-b76e-c41e5dbde429", + "Name": "LoggingOptions", + "Label": "Logging Options", + "HelpText": "One or more of: + + [i|w|e|a|r|u|c|m|o|p|v|x|+|!|*] + +- i - Status messages +- w - Nonfatal warnings +- e - All error messages +- a - Start-up of actions +- r - Action-specific records +- u - User requests +- c - Initial UI parameters +- m - Out-of-memory or fatal exit information +- o - Out-of-disk-space messages +- p - Terminal properties +- v - Verbose output +- x - Extra debugging information +- \\+ - Append to existing log file +- ! - Flush each line to the log +- \\* - Log all information, except for v and x options", + "DefaultValue": "*", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "532b5a46-9510-4558-a17c-a580153fbaff", + "Name": "LogAsArtifact", + "Label": "Log as artifact", + "HelpText": "If selected, then return log output as an artifact. +If unselected then return log output as inline content", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "dc10c5e9-d988-4136-9f3e-c0c881a103b3", + "Name": "IgnoredErrorCodes", + "Label": "Ignored Error Codes", + "HelpText": "Use commas to separate integer values.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2019-11-07T17:00:00.000+00:00", + "LastModifiedBy": "jzabroski", + "SpaceId": "Spaces-1", + "$Meta": { + "ExportedAt": "2019-11-07T16:37:00.415Z", + "OctopusVersion": "2019.3.3", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/run-xunit.json.human b/step-templates/run-xunit.json.human new file mode 100644 index 000000000..5916089dc --- /dev/null +++ b/step-templates/run-xunit.json.human @@ -0,0 +1,110 @@ +{ + "Id": "f70a5c02-f6c0-40d4-a59a-7157cb2c2dc8", + "Name": "Execute xUnit Tests", + "Description": "Run xUnit tests with dotnet and vstest and some filters.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Write-Output \"Running xUnit tests with dotnet and vstest...\" +$dotNetVer = dotnet --version +Write-Output \"DotNet version: $dotNetVer\" +$dirPath = $PackageDirectoryPath +$testFiles = $TestPackages +$testFilter = $TestCaseFilter +$xUnitAdditionalParams = $XUnitAdditionalParameters + +If(-Not $dirPath){ + Write-Output \"Directory with tests is missing!\" + exit 1 +} + +If(-Not $testFiles){ + Write-Output \"Test Package(s) missing!\" + exit 1 +} + +Write-Output \"Execute test package(s): $testFiles\" +Write-Output \"With following filter(s): $testFilter\" +Write-Output \"From Package Directory: $dirPath\" + +cd $dirPath + +If($testFilter){ +\t$runxUnit = \"dotnet vstest $testFiles --testcasefilter:'($testFilter)'\" + Write-Output \"Run xUnit with filter $testFilter\" + } Else { + $runxUnit = \"dotnet vstest $testFiles\" + } + +if($xUnitAdditionalParams){ +\t$runxUnit = $runxUnit + \" \" + $xUnitAdditionalParams +\tWrite-Output \"Run xUnit with Additional Params $xUnitAdditionalParams\" +} + +Write-Output \"Run xUnit with command: $runxUnit\" + +iex $runxUnit + +$xunitExit = $lastExitCode + +exit $xunitExit" + }, + "Parameters": [ + { + "Id": "ffd652f9-7186-44a7-9d16-63dcb30bd044", + "Name": "PackageDirectoryPath", + "Label": "Package directory path", + "HelpText": "Path to directory with test package", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "fd5ae46f-1b5b-45bb-bf34-605351eeedea", + "Name": "TestPackages", + "Label": "Test package(s)", + "HelpText": "Provide test file(s) here. Accepted .dll or .exe. +Separate multiple test file names by spaces.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f4630f67-3309-49cf-8fbd-51bcb4b040c8", + "Name": "TestCaseFilter", + "Label": "Test case filter", + "HelpText": "Only tests that match the given expression will be executed. +Separate filter conditions with pipe.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "74312a1f-f6ba-4518-a475-f308b545f398", + "Name": "XUnitAdditionalParameters", + "Label": "xUnit Additional Parameters", + "HelpText": "Pass any additional parameters you like here, they will be added to dotNet vstest command", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "$Meta": { + "ExportedAt": "2018-10-25T10:33:18.672Z", + "OctopusVersion": "2018.3.4", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2018-10-24T12:44:19.041+00:00", + "LastModifiedBy": "yakuramori", + "Category": "xunit" +} diff --git a/step-templates/save-octopus-output-variable-with-scoping.json.human b/step-templates/save-octopus-output-variable-with-scoping.json.human new file mode 100644 index 000000000..4a1315bca --- /dev/null +++ b/step-templates/save-octopus-output-variable-with-scoping.json.human @@ -0,0 +1,380 @@ +{ + "Id": "75a8f263-b690-4a55-84b7-9165ac9b6196", + "Name": "Save Octopus Output Variable With Scoping", + "Description": "Saves an [output variable](https://octopus.com/docs/deploying-applications/variables/output-variables) to the given project / library variable set with scoping", + "ActionType": "Octopus.Script", + "Version": 6, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +$StepTemplate_BaseUrl = $OctopusParameters['#{if Octopus.Web.ServerUri}Octopus.Web.ServerUri#{else}Octopus.Web.BaseUrl#{/if}'].Trim('/') +if ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) { + throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\" +} + +function Invoke-OctopusApi { + param( + [Parameter(Position = 0, Mandatory)]$Uri, + [ValidateSet(\"Get\", \"Put\")]$Method = 'Get', + $Body + ) + $requestParameters = @{ + Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/')) + Method = $Method + Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey } + UseBasicParsing = $true + } + if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) } + Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" + Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output +} + +function Test-SpacesApi { +\tWrite-Verbose \"Checking API compatibility\"; +\t$rootDocument = Invoke-OctopusApi 'api/'; + if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) { + \tWrite-Verbose \"Spaces API found\" + \treturn $true; + } + Write-Verbose \"Pre-spaces API found\" + return $false; +} + +if(Test-SpacesApi) { +\t$spaceId = $OctopusParameters['Octopus.Space.Id']; + if([string]::IsNullOrWhiteSpace($spaceId)) { + throw \"This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which isn't expected - please reach out to our support team at https://help.octopus.com if you encounter this error.\"; + } +\t$baseApiUrl = \"api/$spaceId\" ; +} else { +\t$baseApiUrl = \"api\" ; +} + +function Check-Scope { +\tparam( + \t[Parameter(Position = 0, Mandatory)] + [string]$ScopeName, + [Parameter(Position = 1, Mandatory)] + [AllowEmptyCollection()] + [array]$ScopeValues, + [Parameter(Position = 2)] + [array]$ExistingScopeValue, + [Parameter(Position = 3)] + [string]$LookingForScopeValue + ) + + if ($LookingForScopeValue) { + \t + \tWrite-Host \"Checking $ScopeName Scope\" + + $scopes = Create-Scope $ScopeName $ScopeValues $LookingForScopeValue + + if (-not ($ExistingScopeValue -and (Compare-Object $ExistingScopeValue $scopes) -eq $null)) { + \tWrite-Host \"$ScopeName scope does not match\" + \treturn $false + } + Write-Host \"$ScopeName scope matches\" + } else { + \tif ($ExistingScopeValue) { + \tWrite-Host \"$ScopeName scope does not match\" + \treturn $false + } + } + + return $true +} + +function Create-Scope { +\tparam( + \t[Parameter(Position = 0, Mandatory)] + [string]$ScopeName, + [Parameter(Position = 1, Mandatory)] + [array]$ScopeValues, + [Parameter(Position = 2)] + [string]$ScopeValue + ) + + $scopes = @() + + foreach ($scope in $ScopeValue.Split($StepTemplate_ScopeDelimiter)) { + \tif ($ScopeName -eq \"TenantTag\") { + \t\t$value = $ScopeValues | Where { $_.Id -eq $scope } | Select -First 1 + \t} + else { + \t\t$value = $ScopeValues | Where { $_.Name -eq $scope } | Select -First 1 + \t} + \t$scopes += $value.Id + } + + return $scopes +} + +$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\" +if (!$OctopusParameters.ContainsKey($outputVariableKey)) { + throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\" +} +$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive) +$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" } + +$variableValue = $OctopusParameters[$outputVariableKey] +Write-Host \"Name: $StepTemplate_VariableName\" +Write-Host \"Type: $variableType\" +Write-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\" +Write-Host ' ' + +Write-Host \"Retrieving $StepTemplate_VariableSetType variable set...\" +if ($StepTemplate_VariableSetType -eq 'project') { + $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables } +} +if ($StepTemplate_VariableSetType -eq 'library') { + $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables } +} +if ($null -eq $variableSet) { + throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\" +} + +$variableExists = $false + +$variableSet.Variables | ? Name -eq $StepTemplate_TargetName | % { +\tif (-not (Check-Scope 'Environment' $variableSet.ScopeValues.Environments $_.Scope.Environment $StepTemplate_EnvironmentScope)) { + \treturn + } + +\tif (-not (Check-Scope 'Machine' $variableSet.ScopeValues.Machines $_.Scope.Machine $StepTemplate_MachineScope)) { + \treturn + } + + if (-not (Check-Scope 'Role' $variableSet.ScopeValues.Roles $_.Scope.Role $StepTemplate_RoleScope)) { + \treturn + } + + if (-not (Check-Scope 'Action' $variableSet.ScopeValues.Actions $_.Scope.Action $StepTemplate_ActionScope)) { + \treturn + } + + if (-not (Check-Scope 'Channel' $variableSet.ScopeValues.Channels $_.Scope.Channel $StepTemplate_ChannelScope)) { + \treturn + } + + if (-not (Check-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $_.Scope.TenantTag $StepTemplate_TenantTagScope)) { + \treturn + } + + Write-Host \"Updating existing variable...\" + Write-Host \"Existing value:\" +\tWrite-Host \"$(if ($isSensitive) { \"********\" } else { $_.Value })\" + $_.Value = $variableValue + $_.Type = $variableType + $_.IsSensitive = $isSensitive + $variableExists = $true +} + +if (!$variableExists) { + Write-Host \"Creating new variable...\" + + $variable = @{ + Name = $StepTemplate_TargetName + Value = $variableValue + Type = $variableType + IsSensitive = $isSensitive + Scope = @{} + } + + if ($StepTemplate_EnvironmentScope) { + \t$variable.Scope['Environment'] = (Create-Scope 'Environment' $variableSet.ScopeValues.Environments $StepTemplate_EnvironmentScope) + } + if ($StepTemplate_RoleScope) { + \t$variable.Scope['Role'] = (Create-Scope 'Role' $variableSet.ScopeValues.Roles $StepTemplate_RoleScope) + } + if ($StepTemplate_MachineScope) { + \t$variable.Scope['Machine'] = (Create-Scope 'Machine' $variableSet.ScopeValues.Machines $StepTemplate_MachineScope) + } + if ($StepTemplate_ActionScope) { + \t$variable.Scope['Action'] = (Create-Scope 'Action' $variableSet.ScopeValues.Actions $StepTemplate_ActionScope) + } + if ($StepTemplate_ChannelScope) { + \t$variable.Scope['Channel'] = (Create-Scope 'Channel' $variableSet.ScopeValues.Channels $StepTemplate_ChannelScope) + } + if ($StepTemplate_TenantTagScope) { + $variable.Scope['TenantTag'] = (Create-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $StepTemplate_TenantTagScope) + } + + $variableSet.Variables += $variable +} + +Write-Host \"Saving updated variable set...\" +Invoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null +" + }, + "Parameters": [ + { + "Id": "bdbd77a9-cc0f-49f0-afac-6403e8ba87e4", + "Name": "StepTemplate_ApiKey", + "Label": "API Key", + "HelpText": "Provide an Octopus API Key with appropriate permissions to save the variable", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ed5cf637-06ed-4c38-8a8d-f17c523ec6ba", + "Name": "StepTemplate_DeploymentStep", + "Label": "Deployment Step", + "HelpText": "Select the step with the [output variable](https://octopus.com/docs/deploying-applications/variables/output-variables)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Id": "3d6f35a9-99ae-481e-b393-4319b70e0ac0", + "Name": "StepTemplate_VariableName", + "Label": "Variable Name", + "HelpText": "Name of the [variable used when it was set](https://octopus.com/docs/deploying-applications/variables/output-variables#Outputvariables-Settingoutputvariablesusingscripts)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9265ca02-9c0f-48ad-9cb1-65d28d5a446e", + "Name": "StepTemplate_VariableSetType", + "Label": "Variable Set Type", + "HelpText": "If the output variable is being saved to a project or [library variable set](https://octopus.com/docs/deploying-applications/variables/library-variable-sets)", + "DefaultValue": "project", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "project|Project Variable +library|Library Variable Set" + } + }, + { + "Id": "f511bbd7-1b75-430f-be48-d3b0787d839e", + "Name": "StepTemplate_VariableSetName", + "Label": "Variable Set Name", + "HelpText": "Name of the project or [library](https://octopus.com/docs/deploying-applications/variables/library-variable-sets) variable set where the output variable should be saved", + "DefaultValue": "#{Octopus.Project.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "50a10535-b36b-4972-a9a0-ff9bb42451c7", + "Name": "StepTemplate_TargetName", + "Label": "Target Variable Name", + "HelpText": "The name to use when saving the variable. + +_Note: The original variable name the default value._", + "DefaultValue": "#{StepTemplate_VariableName}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "204ad3d5-804a-43cd-90ad-8bb359800c78", + "Name": "StepTemplate_IsSensitive", + "Label": "Is Sensitive?", + "HelpText": "If the variable should be marked as [sensitive](https://octopus.com/docs/deploying-applications/variables/sensitive-variables) and the value masked from logs", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "fdbcad6b-ff48-4f4f-8cda-afc404fb7f10", + "Name": "StepTemplate_EnvironmentScope", + "Label": "Environment Scope", + "HelpText": "The environment scope to use when creating or updating the variable. + +_This can be a list delimited by the Scope Delimiter (, by default)_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c4df3719-1f46-4d10-a3b7-52227f78815f", + "Name": "StepTemplate_RoleScope", + "Label": "Role Scope", + "HelpText": "The role scope to use when creating or updating the variable. + +_This can be a list delimited by the Scope Delimiter (, by default)_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1782abd1-ed68-4cbb-85b2-efd2baaa1eae", + "Name": "StepTemplate_MachineScope", + "Label": "Target Scope", + "HelpText": "The target scope to use when creating or updating the variable. + +_This can be a list delimited by the Scope Delimiter (, by default)_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "85f14553-6425-4298-ba77-bdf31f852d0b", + "Name": "StepTemplate_ActionScope", + "Label": "Action Scope", + "HelpText": "The action scope to use when creating or updating the variable. + +_This can be a list delimited by the Scope Delimiter (, by default)_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e4152038-fc42-4ed6-8ad5-4aa94d76a5d1", + "Name": "StepTemplate_ChannelScope", + "Label": "Channel Scope", + "HelpText": "The channel scope to use when creating or updating the variable. + +_This can be a list delimited by the Scope Delimiter (, by default)_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a5bcd57b-d0bb-4b1c-be1b-56314e0b9684", + "Name": "StepTemplate_TenantTagScope", + "Label": "Tenant Tag Scope", + "HelpText": "The tenant tag scope to use when creating or updating the variable. The value should be in the format TagSetName/TagValue. + +_This can be a list delimited by the Scope Delimiter (, by default)_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7de754f7-d8b6-4b6e-a7d1-6306cf8f7ab8", + "Name": "StepTemplate_ScopeDelimiter", + "Label": "Scope Delimiter", + "HelpText": "The delimiter used by scope lists.", + "DefaultValue": ",", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedAt": "2021-10-21T15:24:33.519Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2021-10-21T15:24:33.519Z", + "OctopusVersion": "2021.2.7706", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} diff --git a/step-templates/save-octopus-output-variable.json.human b/step-templates/save-octopus-output-variable.json.human new file mode 100644 index 000000000..df39fdc06 --- /dev/null +++ b/step-templates/save-octopus-output-variable.json.human @@ -0,0 +1,198 @@ +{ + "Id": "54b22700-3217-48ac-8749-e4a31b424834", + "Name": "Save Octopus Output Variable", + "Description": "Saves an [output variable](https://octopus.com/docs/deploying-applications/variables/output-variables) to the given project / library variable set", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +$StepTemplate_BaseUrl = $OctopusParameters['#{if Octopus.Web.ServerUri}Octopus.Web.ServerUri#{else}Octopus.Web.BaseUrl#{/if}'].Trim('/') +if ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) { + throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\" +} + +function Invoke-OctopusApi { + param( + [Parameter(Position = 0, Mandatory)]$Uri, + [ValidateSet(\"Get\", \"Put\")]$Method = 'Get', + $Body + ) + $requestParameters = @{ + Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/')) + Method = $Method + Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey } + UseBasicParsing = $true + } + if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) } + Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" + Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output +} + +function Test-SpacesApi { +\tWrite-Verbose \"Checking API compatibility\"; +\t$rootDocument = Invoke-OctopusApi 'api/'; + if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) { + \tWrite-Verbose \"Spaces API found\" + \treturn $true; + } + Write-Verbose \"Pre-spaces API found\" + return $false; +} + +if(Test-SpacesApi) { +\t$spaceId = $OctopusParameters['Octopus.Space.Id']; + if([string]::IsNullOrWhiteSpace($spaceId)) { + throw \"This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which isn't expected - please reach out to our support team at https://help.octopus.com if you encounter this error.\"; + } +\t$baseApiUrl = \"api/$spaceId\" ; +} else { +\t$baseApiUrl = \"api\" ; +} + +function Get-OctopusSetting { + param([Parameter(Position = 0, Mandatory)][string]$Name, [Parameter(Position = 1, Mandatory)]$DefaultValue) + $formattedName = 'Octopus.Action.{0}' -f $Name + if ($OctopusParameters.ContainsKey($formattedName)) { + $value = $OctopusParameters[$formattedName] + if ($DefaultValue -is [bool]) { return ([System.Convert]::ToBoolean($value)) } + if ($DefaultValue -is [array] -or $DefaultValue -is [hashtable] -or $DefaultValue -is [pscustomobject]) { return (ConvertFrom-Json -InputObject $value) } + return $value + } + else { return $DefaultValue } +} + +$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\" +if (!$OctopusParameters.ContainsKey($outputVariableKey)) { + throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\" +} +$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive) +$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" } + +$variableValue = $OctopusParameters[$outputVariableKey] +Write-Host \"Name: $StepTemplate_VariableName\" +Write-Host \"Type: $variableType\" +Write-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\" +Write-Host ' ' + +Write-Host \"Retrieving $StepTemplate_VariableSetType variable set...\" +if ($StepTemplate_VariableSetType -eq 'project') { + $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables } +} +if ($StepTemplate_VariableSetType -eq 'library') { + $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables } +} +if ($null -eq $variableSet) { + throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\" +} + +$variableExists = $false +$variableSet.Variables | ? Name -eq $StepTemplate_VariableName | % { + Write-Host \"Updating existing variable...\" + Write-Verbose \"Existing value: $(if ($isSensitive) { \"********\" } else { $_.Value })\" + $_.Value = $variableValue + $_.Type = $variableType + $_.IsSensitive = $isSensitive + $_.Scope = Get-OctopusSetting Scope $_.Scope + $variableExists = $true +} +if (!$variableExists) { + Write-Host \"Creating new variable...\" + $variableSet.Variables += @{ + Name = $StepTemplate_VariableName + Value = $variableValue + Type = $variableType + IsSensitive = $isSensitive + Scope = (Get-OctopusSetting Scope @{}) + } +} + +Write-Host \"Saving updated variable set...\" +Invoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "41e164af-94b1-42ac-8462-3e1a6ba49bbb", + "Name": "StepTemplate_ApiKey", + "Label": "API Key", + "HelpText": "Provide an Octopus API Key with appropriate permissions to save the variable", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "ceddb0a2-621a-4cce-94e1-42ad1bf4ba72", + "Name": "StepTemplate_DeploymentStep", + "Label": "Deployment Step", + "HelpText": "Select the step with the [output variable](https://octopus.com/docs/deploying-applications/variables/output-variables)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + }, + "Links": {} + }, + { + "Id": "fdd47428-d7ce-4f8d-b73b-e4d513d1aea8", + "Name": "StepTemplate_VariableName", + "Label": "Variable Name", + "HelpText": "Name of the [variable used when it was set](https://octopus.com/docs/deploying-applications/variables/output-variables#Outputvariables-Settingoutputvariablesusingscripts) + +_Note: The same name will be given to the saved variable_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "7a81c569-063f-41cb-ae61-1c4ebb67293a", + "Name": "StepTemplate_VariableSetType", + "Label": "Variable Set Type", + "HelpText": "If the output variable is being saved to a project or [library variable set](https://octopus.com/docs/deploying-applications/variables/library-variable-sets)", + "DefaultValue": "project", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "project|Project Variable +library|Library Variable Set" + }, + "Links": {} + }, + { + "Id": "09e61027-ab16-4171-80c7-c7656f68833a", + "Name": "StepTemplate_VariableSetName", + "Label": "Variable Set Name", + "HelpText": "Name of the project or [library](https://octopus.com/docs/deploying-applications/variables/library-variable-sets) variable set where the output variable should be saved", + "DefaultValue": "#{Octopus.Project.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "6e3cf177-1dcc-4b7c-9d24-ec7805167cef", + "Name": "StepTemplate_IsSensitive", + "Label": "Is Sensitive?", + "HelpText": "If the variable should be marked as [sensitive](https://octopus.com/docs/deploying-applications/variables/sensitive-variables) and the value masked from logs", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2021-10-21T15:24:33.519Z", + "OctopusVersion": "2021.2.7706", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} diff --git a/step-templates/scheduled-task-create-from-xml.json.human b/step-templates/scheduled-task-create-from-xml.json.human new file mode 100644 index 000000000..6c81891de --- /dev/null +++ b/step-templates/scheduled-task-create-from-xml.json.human @@ -0,0 +1,159 @@ +{ + "Id": "26c779af-4cce-447e-98bb-4741c25e0b3c", + "Name": "Create Scheduled Tasks From XML", + "Description": "This will create a schedule task based on exported xml. See https://msdn.microsoft.com/en-us/library/windows/desktop/bb736357%28v=vs.85%29.aspx for instructions on how to export scheduled tasks as xml.", + "ActionType": "Octopus.Script", + "Version": 27, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\r +param(\r + [string]$xmlFileName,\r + [string]$userName,\r + [string]$password\r +)\r +\r +$ErrorActionPreference = \"Stop\" \r +\r +function Get-Param($Name, [switch]$Required, $Default) {\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + $variable = Get-Variable $Name -EA SilentlyContinue \r + if ($variable -ne $null) {\r + $result = $variable.Value\r + }\r + }\r +\r + if ($result -eq $null) {\r + if ($Required) {\r + throw \"Missing parameter value $Name\"\r + } else {\r + $result = $Default\r + }\r + }\r +\r + return $result\r +}\r +\r +Function Create-ScheduledTask($xmlFileName, $taskName, $username, $password){\r +\t$Command = \"schtasks.exe /create /tn $($taskName) /RU $($username) /RP $($password) /XML $($xmlFileName)\"\r +\r +\tWrite-Host $Command\r +\tInvoke-Expression $Command\r + }\r +\r +Function Delete-ScheduledTask($TaskName) { \r +\t$Command = \"schtasks.exe /delete /tn `\"$TaskName`\" /F\" \r +\tInvoke-Expression $Command \r +}\r +\r +Function Stop-ScheduledTask($TaskName) { \r +\t$Command = \"schtasks.exe /end /tn `\"$TaskName`\"\" \r +\tInvoke-Expression $Command \r +}\r +\r +Function Start-ScheduledTask($TaskName) { \r +\t$Command = \"schtasks.exe /run /tn `\"$TaskName`\"\" \r +\tInvoke-Expression $Command \r +}\r +\r +Function ScheduledTask-Exists($taskName) {\r + $schedule = new-object -com Schedule.Service \r + $schedule.connect() \r + $tasks = $schedule.getfolder(\"\\\").gettasks(0)\r +\r + foreach ($task in ($tasks | select Name)) {\r +\t #echo \"TASK: $($task.name)\"\r +\t if($task.Name -eq $taskName) {\r +\t\t #write-output \"$task already exists\"\r +\t\t return $true\r +\t }\r + }\r +\r + return $false\r +}\r +\r +Function GetTaskNameFromXmlPath($xmlFile){\r + return (Split-Path -Path $xmlFile -Leaf -Resolve).Split(\".\")[0]\r +}\r +\r +& {\r + param(\r + [string]$xmlFileName,\r + [string]$userName,\r + [string]$password\r + ) \r +\r + Write-Host \"Create Schedule Task From XML\"\r + Write-Host \"xmlFileName: $xmlFileName\"\r + Write-Host \"userName: $userName\"\r + Write-Host \"password: \"\r +\r + $xmlFileName.Split(\";\") | foreach{\r + $xmlFile = $_.Trim()\r + $taskName = GetTaskNameFromXmlPath($xmlFile)\r +\r +\r + if((ScheduledTask-Exists($taskName))){\r +\t Write-Output \"$taskName already exists, Tearing down...\"\r +\t Write-Output \"Stopping $taskName...\"\r +\t Stop-ScheduledTask($taskName)\r +\t Write-Output \"Successfully Stopped $taskName\"\r +\t Write-Output \"Deleting $taskName...\"\r +\t Delete-ScheduledTask($taskName)\r +\t Write-Output \"Successfully Deleted $taskName\"\r + }\r +\r + Write-Output \"Create a Scheduled Task from $xmlFile called $taskName. Run as $username\" \r + Create-ScheduledTask \"$($xmlFile)\" $taskName $username $password\r + }\r +\r +}`\r +(Get-Param 'xmlFileName' -Required)`\r +(Get-Param 'userName' -Required)`\r +(Get-Param 'password' -Required)", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "xmlFileName", + "Label": "Xml File List", + "HelpText": "A list of XML files, separated by ';', containing the scheduled tasks to create.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "username", + "Label": "User Name", + "HelpText": "The User that the task will run as", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "password", + "Label": "Password", + "HelpText": "The password of the user the task will run as", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedOn": "2015-04-29T14:04:16.639+00:00", + "LastModifiedBy": "josh3ennett", + "$Meta": { + "ExportedAt": "2015-04-29T14:15:45.938+00:00", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "xml" +} diff --git a/step-templates/send-sms-using-messagebird.json.human b/step-templates/send-sms-using-messagebird.json.human new file mode 100644 index 000000000..e943555fc --- /dev/null +++ b/step-templates/send-sms-using-messagebird.json.human @@ -0,0 +1,76 @@ +{ + "Id": "d5dba42f-ab60-4342-bdaf-b8ecd2f763c3", + "Name": "MessageBird Send SMS", + "Description": "Send an SMS using MessageBird API.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#----------------------------- +#Latest Update 2021-01-21 +#Bilal Aljbour - FRISS +#----------------------------- +$url = \"https://rest.messagebird.com/messages?access_key=$MessageBird_Key\" +$params = @{ +href = 'https://rest.messagebird.com/messages' +recipients = \"$MessageBird_Recipients\" +originator = \"$MessageBird_originator\" +body = \"$MessageBird_body\" +} +Invoke-WebRequest $url -Method Post -Body $params -UseBasicParsing | Out-Null +Write-Host 'Message has been sent!'" + }, + "Parameters": [{ + "Id": "8fcbce8e-5d10-442c-a50d-470b2458a481", + "Name": "MessageBird_Key", + "Label": "MessageBird Key", + "HelpText": "API keys give you access to all our REST API methods. +This can be created/copied [here](https://dashboard.messagebird.com/en/developers/access)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7c06f0e6-84e8-4ed0-b804-aed47d580667", + "Name": "MessageBird_originator", + "Label": "From", + "HelpText": "The sender of the message. This can be a telephone number (including country code) or an alphanumeric string. In case of an alphanumeric string, the maximum length is 11 characters. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e4b45a2d-f7ca-4329-88fc-d6f23b1249c4", + "Name": "MessageBird_Recipients", + "Label": "To", + "HelpText": "The number to send the message to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e882db5e-d780-4183-956e-e51806163b60", + "Name": "MessageBird_body", + "Label": "Message", + "HelpText": "The content of the SMS message. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-01-21T13:54:53.610Z", + "OctopusVersion": "2020.4.6", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "bilalmajali", + "Category": "other" +} diff --git a/step-templates/sentry-release-tracking-token-auth.json.human b/step-templates/sentry-release-tracking-token-auth.json.human new file mode 100644 index 000000000..de78ccf82 --- /dev/null +++ b/step-templates/sentry-release-tracking-token-auth.json.human @@ -0,0 +1,85 @@ +{ + "Id": "f957377a-6389-4ed1-87a6-3317e5676a77", + "Name": "Sentry Release Tracking (Token Auth)", + "Description": "Posts a new release to Sentry and links it to one or more sentry projects. + +**Updated version** which uses the newer **API Token** instead of the now depreciated API Key which is only available in legacy Sentry accounts.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$organization = $OctopusParameters[\"SentryReleaseTracking.organization\"] +$projects = $OctopusParameters[\"SentryReleaseTracking.projects\"] +$apiToken = $OctopusParameters[\"SentryReleaseTracking.apiToken\"] + +$url = \"https://sentry.io/api/0/organizations/$organization/releases/\" +Write-Host $url + +$headers = New-Object \"System.Collections.Generic.Dictionary[[String],[String]]\" +$headers.Add(\"Authorization\", \"Bearer $apiToken\") +$body = ConvertTo-Json @{ +\t\"version\" = $OctopusParameters['Octopus.Release.Number'] + \"projects\" = $projects.Split(\";\") +} + +Write-Host $body +Try +{ +\t$response = Invoke-RestMethod -Method Post -Uri \"$url\" -Body $body -Headers $headers -ContentType \"application/json\" +\tWrite-Host $response +} +Catch { +\tif($_.Exception.Response.StatusCode -ne 400) +\t{ + \t\tThrow $_ + } +} +", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "84a0e54c-c914-481a-9277-4b9ab176c9d6", + "Name": "SentryReleaseTracking.organization", + "Label": "Organisation Slug", + "HelpText": "The organisation-name part of the url", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7ae25fac-65b3-4830-9ade-56c3443ac63b", + "Name": "SentryReleaseTracking.projects", + "Label": "Project Slug", + "HelpText": "`;`-separated list of all your sentry api slugs for the apps, (web/api/admin) on this specific Environment. + + myapp-web-dev;myapp-api-dev + +protip: Add them all to a environment-scoped variable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4d2396cc-5313-4188-91ab-18cf7b59369a", + "Name": "SentryReleaseTracking.apiToken", + "Label": "Sentry token", + "HelpText": "Your sentry token", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2021-09-10T12:19:29.946Z", + "OctopusVersion": "2021.2.7428", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "shout", + "Category": "sentry" +} diff --git a/step-templates/sentry-release-tracking.json.human b/step-templates/sentry-release-tracking.json.human new file mode 100644 index 000000000..750d3f243 --- /dev/null +++ b/step-templates/sentry-release-tracking.json.human @@ -0,0 +1,104 @@ +{ + "Id": "ee62b8ac-2731-4147-8cb4-ceda0abe5a80", + "Name": "Sentry Release Tracking", + "Description": "Posts a new release to Sentry, It can optionaly resolve all previous issues. + +If the release already exists, it only applies the resolving.", + "ActionType": "Octopus.Script", + "Version": 47, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$token = [System.Text.Encoding]::UTF8.GetBytes($sentryApiKey+\":\") +$base64Token = [System.Convert]::ToBase64String($token) + +Write-Host $base64Token + +ForEach ($project in $projects.Split(';')) +{ + $url = \"https://app.getsentry.com/api/0/projects/$organization/$project/releases/\" + Write-Host $url + + $headers = New-Object \"System.Collections.Generic.Dictionary[[String],[String]]\" + $headers.Add(\"Authorization\", \"Basic $base64Token\") + + $body = @{ \"version\" = $OctopusParameters['Octopus.Release.Number'] } + + $body = ConvertTo-Json $body + + Write-Host $body + Try + { + $response = Invoke-RestMethod -Method Post -Uri \"$url\" -Body $body -Headers $headers -ContentType \"application/json\" + Write-Host $response + } + Catch [System.Net.WebException] + { + Write-Host $_ + if($_.Exception.Response.StatusCode.Value__ -ne 400) + { + Throw + } + } + if ($resolveIssues) + { + $resolveBody = '{\"status\":\"resolved\"}' + Write-Host $resolveBody + $url = \"https://app.getsentry.com/api/0/projects/$organization/$project/groups/\" + Write-Host $url + $response = Invoke-RestMethod -Method Put -Uri \"$url\" -Body $resolveBody -Headers $headers -ContentType \"application/json\" + Write-Host $response + } +}" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "organization", + "Label": "Organisation Slug", + "HelpText": "The organisation-name part of the url", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "projects", + "Label": "Project Slug", + "HelpText": "`;`-separated list of all your sentry api slugs for the apps, (web/api/admin) on this spesific Environment. + + myapp-web-dev;myapp-api-dev + +protip: Add them all to a environment-scoped variable.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "resolveIssues", + "Label": "Resolve all open issues", + "HelpText": "", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "sentryApiKey", + "Label": "Sentry api key", + "HelpText": "Your sentry api key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-11-27T12:26:31.568+00:00", + "OctopusVersion": "3.2.4", + "Type": "ActionTemplate" + }, + "Category": "sentry" +} diff --git a/step-templates/seq-log-deployment.json.human b/step-templates/seq-log-deployment.json.human new file mode 100644 index 000000000..9d4bf11ca --- /dev/null +++ b/step-templates/seq-log-deployment.json.human @@ -0,0 +1,117 @@ +{ + "Id": "a55a825b-8e8b-4995-8143-0f9dd7b6bcfa", + "Name": "Seq - Log Deployment", + "Description": "Post details of the deployment to a [Seq](https://getseq.net) log server. Add this as the last step in a process, and ensure it is set to run always (on success and failure).", + "ActionType": "Octopus.Script", + "Version": 22, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Open-Seq ([string] $url, [string] $apiKey, $properties = @{})\r +{\r + return @{ Url = $url; ApiKey = $apiKey; Properties = $properties.Clone() }\r +}\r + \r +function Send-SeqEvent (\r + $seq,\r + [string] $text,\r + [string] $level,\r + $properties = @{},\r + [string] $exception = $null,\r + [switch] $template)\r +{\r + if (-not $level) {\r + $level = 'Information'\r + }\r + \r + if (@('Verbose', 'Debug', 'Information', 'Warning', 'Error', 'Fatal') -notcontains $level) {\r + $level = 'Information'\r + }\r + \r + $allProperties = $seq[\"Properties\"].Clone()\r + $allProperties += $properties\r + \r + $messageTemplate = \"{Text}\"\r + \r + if ($template) {\r + $messageTemplate = $text;\r + } else {\r + $allProperties += @{ Text = $text; }\r + }\r + \r + $exceptionProperty = \"\"\r + if ($exception) {\r + $exceptionProperty = \"\"\"Exception\"\": $($exception | ConvertTo-Json),\"\r + }\r + \r + $body = \"{\"\"Events\"\": [ {\r + \"\"Timestamp\"\": \"\"$([System.DateTimeOffset]::Now.ToString('o'))\"\",\r + \"\"Level\"\": \"\"$level\"\",\r + $exceptionProperty\r + \"\"MessageTemplate\"\": $($messageTemplate | ConvertTo-Json),\r + \"\"Properties\"\": $($allProperties | ConvertTo-Json) }]}\"\r + \r + $target = \"$($seq[\"Url\"])/api/events/raw?apiKey=$($seq[\"ApiKey\"])\"\r + \r + Invoke-RestMethod -Uri $target -Body $body -ContentType \"application/json\" -Method POST\r +}\r +\r +Write-Output \"Logging the deployment result to Seq at $SeqServerUrl...\"\r +\r +$seq = Open-Seq $SeqServerUrl -apiKey $SeqApiKey\r +\r +$properties = @{\r + ProjectName = $OctopusParameters['Octopus.Project.Name'];\r + ReleaseNumber = $OctopusParameters['Octopus.Release.Number'];\r + Result = \"succeeded\";\r + EnvironmentName = $OctopusParameters['Octopus.Environment.Name'];\r + DeploymentName = $OctopusParameters['Octopus.Deployment.Name'];\r + Channel = $OctopusParameters['Octopus.Release.Channel.Name'];\r + DeploymentLink = $OctopusParameters['#{if Octopus.Web.ServerUri}Octopus.Web.ServerUri#{else}Octopus.Web.BaseUrl#{/if}'] + $OctopusParameters['Octopus.Web.DeploymentLink']\r +}\r +\r +$level = \"Information\"\r +$exception = $null\r +if ($OctopusParameters['Octopus.Deployment.Error']) {\r + $level = \"Error\"\r + $properties[\"Result\"] = \"failed\"\r + $properties[\"Error\"] = $OctopusParameters['Octopus.Deployment.Error']\r + $exception = $OctopusParameters['Octopus.Deployment.ErrorDetail']\r +}\r +\r +try {\r + Send-SeqEvent $seq \"A deployment of {ProjectName} release {ReleaseNumber} {Result} in {EnvironmentName}\" -level $level -template -properties $properties -exception $exception\r +} catch [Exception] {\r + [System.Console]::Error.WriteLine(\"Unable to write deployment details to Seq\")\r + $_.Exception | format-list -force\r +}\r +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "SeqServerUrl", + "Label": "Seq server URL", + "HelpText": "The URL of the Seq server.", + "DefaultValue": "http://localhost:5341", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SeqApiKey", + "Label": "Seq API key", + "HelpText": "If an [API key](http://docs.getseq.net/docs/api-keys) is required for writing events, specify it here.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2023-02-16T15:38:44.043Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "Category": "seq" +} diff --git a/step-templates/sharepoint-solution-deployment.json.human b/step-templates/sharepoint-solution-deployment.json.human new file mode 100644 index 000000000..d8ff41840 --- /dev/null +++ b/step-templates/sharepoint-solution-deployment.json.human @@ -0,0 +1,143 @@ +{ + "Id": "7ac03a43-cb18-4e83-a114-b158a2bb2a52", + "Name": "SharePoint Solution Deployment", + "Description": "SharePoint Solution Deployment for 2010 & 2013.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$DeployedPath = $OctopusParameters[\"Octopus.Action[$NugetPackageStepName].Output.Package.InstallationDirectoryPath\"]\r +$ReleaseNumber = $OctopusParameters[\"Octopus.Release.Number\"]\r +\r +Write-Host \"Deploy Path: $DeployedPath\"\r +Write-Host \"Release Number: $ReleaseNumber\"\r +\r +function Deploy-SPSolution($wsp) {\r +\r +\t$wspName = $wsp.SubString($wsp.LastIndexOf(\"\\\") + 1)\r +\r +\t$solution = Get-SPSolution -Identity $wspName -ErrorAction silentlycontinue\r +\r +\tif ($solution -ne $null) \r +\t{ \r +\t Write-Output \"'$wspName' solution already installed - removing solution\"\r +\t\t\r +\t\t# need to take a back up of this wsp before uninstalling it.\t\t\r +\t\tif($solution.ContainsWebApplicationResource) {\r +\t $solution | Uninstall-SPSolution -AllWebApplications -Confirm:$false\r +\t }\r +\t else {\r +\t $solution | Uninstall-SPSolution -Confirm:$false\r +\t }\r +\t\t\r +\t\twhile ($solution.JobExists) {\r +\t\t Start-Sleep 30\r +\t\t}\r +\t\t\r +\t\tWrite-Output \"$wspName has been uninstalled successfully.\"\r +\r +\t Write-Output \"Removing '$wspName' solution from farm\" \r +\t $solution | Remove-SPSolution -Force -Confirm:$false \r +\t\r +\t\t# now install \r +\t\tWrite-Output \"Installing solution '$wspName'\" \r +\t\tAdd-SPSolution -LiteralPath \"$wsp\" | Out-Null\r +\t\t\r +\t\tWrite-Output \"$wsp solution added sucessfully\"\r +\t\tif(($solution -ne $null) -and ($solution.ContainsWebApplicationResource)) {\r +\t\t\tInstall-SPSolution -Identity $wspName –AllwebApplications -GACDeployment -Force -Confirm:$false\r +\t\t}\r +\t\telse {\r +\t\t\tInstall-SPSolution -Identity $wspName -GACDeployment -Force -Confirm:$false\r +\t\t}\r +\r +\t\t<#\r +\t\twhile ($Solution.Deployed -eq $false) {\r +\t\t Start-Sleep 30\r +\t\t}\r +\t\t#>\r +\t}\r +\telse {\r +\t\tWrite-Output \"Installing solution '$wspName'\" \r +\t\tAdd-SPSolution -LiteralPath \"$wsp\" -ErrorAction Stop\r +\t\tInstall-SPSolution -Identity $wspName -GACDeployment -Force -ErrorAction Stop\r +\t}\r +}\r +\r +function Start-AdminService() {\r +\t$AdminServiceName = \"SPAdminV4\"\r +\t\r +\tif ($(Get-Service $AdminServiceName).Status -eq \"Stopped\") {\r +\t Start-Service $AdminServiceName\r +\t \tWrite-Host \"$AdminServiceName service was not running, now started.\"\r +\t\treturn $false;\r +\t}\r +\t\r +\treturn $true\r +}\r +\r +function Stop-AdminService($IsAdminServiceWasRunning) {\r +\t$AdminServiceName = \"SPAdminV4\"\t\r +\tif ($IsAdminServiceWasRunning -eq $false ) { \r +\t\tStop-Service $AdminServiceName\t\r +\t}\r +}\r +\r +#region Main\r +try\r +{\r +\t# add powershell snap in for sharepoint functions\r +\tif ((Get-PSSnapin \"Microsoft.SharePoint.PowerShell\" -ErrorAction SilentlyContinue) -eq $null) { \r +\t Add-PSSnapin \"Microsoft.SharePoint.PowerShell\" -ErrorAction SilentlyContinue\r +\t}\r +\t\r +\t#Admin service\r +\t$IsAdminServiceWasRunning = $true;\r +\t\r +\t$IsAdminServiceWasRunning = Start-AdminService\r +\t\r +\t$wspFiles = @()\r +\t\r +\t# get all report files for deployment\r + Write-Host \"Getting all .wsp files\"\r + Get-ChildItem $DeployedPath -Recurse -Filter \"*.wsp\" | ForEach-Object { If(($wspFiles -contains $_.FullName) -eq $false) {$wspFiles += $_.FullName}}\r + Write-Host \"# of wsp files found: $($wspFiles.Count)\"\r +\t\r +\t# loop through array\r + foreach($wsp in $wspFiles) {\r +\t\tDeploy-SPSolution $wsp\r +\t}\t\r +\t\r +\tStop-AdminService $IsAdminServiceWasRunning\r +\t\r +\t#Remove SharePoint Snapin\r +\tRemove-PsSnapin Microsoft.SharePoint.PowerShell\r +}\r +finally\r +{\r + \r +}\r +\r +#endregion", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "NugetPackageStepName", + "Label": "SharePoint Solution Package Step", + "HelpText": "Select the step in this project which downloads the SharePoint package.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + } + ], + "LastModifiedOn": "2015-10-09T12:33:38.798+00:00", + "LastModifiedBy": "jasmin-mistry", + "$Meta": { + "ExportedAt": "2015-10-09T16:20:24.330+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "sharepoint" +} diff --git a/step-templates/sitecore-deploy-scwdp.json.human b/step-templates/sitecore-deploy-scwdp.json.human new file mode 100644 index 000000000..746b0f35b --- /dev/null +++ b/step-templates/sitecore-deploy-scwdp.json.human @@ -0,0 +1,108 @@ +{ + "Id": "9a757194-4c7e-4e9e-a58f-7b0c12b8253a", + "Name": "Sitecore web deploy package(.scwdp) deployment", + "Description": "Step template to deploy Sitecore WDP(Web Deploy Package) package. + +**Useful links:** +[Sitecore documentation.](https://doc.sitecore.com/developers/sat/24/sitecore-azure-toolkit/en/web-deploy-packages-for-a-module.html) +How to create [Sitecore Web Deploy Package](https://hls-consulting.com/2019/05/15/how-to-create-a-wdp-from-a-sitecore-package/) by Hugo Santos. +How to install [Sitecore Web Deploy Package](https://hls-consulting.com/2019/06/03/how-to-install-a-wdp-in-a-sitecore-9-1-on-premises-instance/) by Hugo Santos. + +", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "try { + $applicationPath = $OctopusParameters[\"SCWDP Application Path\"] + $coreConnection = $OctopusParameters[\"SCWDP Core Admin Connection String\"] + $masterConnection = $OctopusParameters[\"SCWDP Master Admin Connection String\"] + $webConnection = $OctopusParameters[\"SCWDP Web Admin Connection String\"] + $msDeploy = $OctopusParameters[\"SCWDP MsDeploy Path\"] + + $package = $OctopusParameters[\"SCWDP Package\"] + + $cmd = \"`\"\"+$msDeploy+\"`\" -verb:sync -source:package=`\"\"+$package+\"`\" -dest:auto -enableRule:DoNotDeleteRule -setParam:`\"Application Path`\"=`\"\"+$applicationPath+\"`\" -setParam:`\"Core Admin Connection String`\"=`\"\"+$coreConnection+\"`\" -setParam:`\"Master Admin Connection String`\"=`\"\"+$masterConnection+\"`\" -setParam:`\"Web Admin Connection String`\"=`\"\"+$webConnection+\"`\" -verbose\" + + Write-Output $cmd + cmd.exe /c $cmd +} +catch { + Write-Error \"An error occurred:\" + Write-Error $_ +}" + }, + "Parameters": [ + { + "Id": "8a064359-6d40-472d-8839-383eb886e1e4", + "Name": "SCWDP Application Path", + "Label": "Application Path", + "HelpText": "Path, where package should be deployed", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "02031f37-6b13-4c58-95c0-b160f02193c7", + "Name": "SCWDP Web Admin Connection String", + "Label": "Web Admin Connection String", + "HelpText": "Connection string to web database", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "95f75e2e-4679-403a-b1c8-dea0e529614a", + "Name": "SCWDP Master Admin Connection String", + "Label": "Master Admin Connection String", + "HelpText": "Connection string to master database", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bc921038-1de9-4d6d-9492-b27f291b25c5", + "Name": "SCWDP Core Admin Connection String", + "Label": "Core Admin Connection String", + "HelpText": "Connection string to core database", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0754e1e3-ee10-41fc-b198-3cc1d945ea13", + "Name": "SCWDP Package", + "Label": "Package", + "HelpText": "Path to .scwdp.zip package or name of package attached to release", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c91f4571-1e61-4f0f-82e4-2dcea7024506", + "Name": "SCWDP MsDeploy Path", + "Label": "Path to MSDeploy Executable", + "HelpText": "It should be \"C:\\Program Files (x86)\\IIS\\Microsoft Web Deploy V3\\msdeploy.exe\" with default server setup", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-06-19T05:40:59.376Z", + "OctopusVersion": "2020.2.13", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "Category": "sitecore" +} diff --git a/step-templates/sitecore-settings-variable-replacement.json.human b/step-templates/sitecore-settings-variable-replacement.json.human new file mode 100644 index 000000000..9be6fdf60 --- /dev/null +++ b/step-templates/sitecore-settings-variable-replacement.json.human @@ -0,0 +1,92 @@ +{ + "Id": "382b9610-64c4-4c64-a31c-34d07f262ed4", + "Name": "Sitecore - Settings & Variable Replacement", + "Description": "The default [Configuration Variables](https://octopus.com/docs/deployment-process/configuration-features#Configurationfiles-ConfigurationVariables) functionality replaces **appSettings** and **connectionStrings** entries. This step template extends this functionality to the Sitecore configuration **settings** and **sc.variable** nodes within the configuration file(s) that you specify. Variables that are defined for the Octopus project will automatically replace those defined in the target Sitecore configuration file(s).", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\" \r +$configFiles = $OctopusParameters[\"Sitecore.ReplaceConfigFiles\"]\r +\r +if ([string]::IsNullOrEmpty($configFiles)) {\r + throw [System.ArgumentNullException] \"Sitecore.ReplaceConfigFiles\"\r +}\r +\r +($configFiles -split '[\\r\ +]') | ForEach-Object {\r + \r + $configPath = $_\r +\r + if ([string]::IsNullOrEmpty($configPath)) { \r + return\r + }\r + \r + $configPath = $configPath.Trim()\r +\r + if (-not (Test-Path -LiteralPath $configPath)) {\r + Write-Host \"$configPath was not found.\"\r + return\r + }\r +\r + Write-Host \"Searching Sitecore config file for replacement variables:\" $configPath\r + \r + $configXml = [xml](Get-Content $configPath)\r + $sitecoreNode = $configXml.sitecore\r + \r + # Look for sitecore node for versions prior to 8.1\r + if ($sitecoreNode -eq $null) {\r + $sitecoreNode = $configXml.configuration.sitecore\r + }\r + \r + # Ensure that we have a sitecore node to work from\r + if ($sitecoreNode -eq $null -or $sitecoreNode.settings -eq $null) {\r + Write-Host \"The sitecore settings node was not found in\" $configPath \". Skipping this file...\"\r + return\r + }\r + \r + foreach ($key in $OctopusParameters.Keys) {\r + \r + # Replace Sitecore settings\r + $setting = $sitecoreNode.settings.setting | where { $_.name -ceq $key }\r + if ($setting -ne $null) {\r + Write-Host $setting.name \"setting will be updated from\" $setting.value \"to\" $OctopusParameters[$key] \"in\" $configPath\r + $setting.value = $OctopusParameters[$key]\r + }\r + \r + # Replace Sitecore variables\r + $variable = $sitecoreNode.'sc.variable' | where { $_.name -ceq $key }\r + if ($variable -ne $null) {\r + Write-Host $variable.name \"Sitecore variable will be updated from\" $settingsNode.value \"to\" $OctopusParameters[$key] \"in\" $configPath\r + $variable.value = $OctopusParameters[$key]\r + }\r + \r + }\r + \r + $configXml.Save($configPath)\r +}", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "Sitecore.ReplaceConfigFiles", + "Label": "Sitecore.ReplaceConfigFiles", + "HelpText": "Enter the full path to your Sitecore configuration file(s) that contains the **sitecore** node, one per line. For versions of Sitecore prior to 8.1, this should point to your primary Web.config at the root of your website, and for versions 8.1+, to the Sitecore.config file within your App_Config folder. Alternatively, this value can be left empty and defined within your Octopus project's variable collection using the variable name **Sitecore.ReplaceConfigFiles**.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedOn": "2016-06-13T17:19:09.003+00:00", + "LastModifiedBy": "dthunziker", + "$Meta": { + "ExportedAt": "2016-06-13T17:19:09.003+00:00", + "OctopusVersion": "3.3.10", + "Type": "ActionTemplate" + }, + "Category": "sitecore" +} diff --git a/step-templates/sitecore-unicorn-sync.json.human b/step-templates/sitecore-unicorn-sync.json.human new file mode 100644 index 000000000..6bf171bc5 --- /dev/null +++ b/step-templates/sitecore-unicorn-sync.json.human @@ -0,0 +1,116 @@ +{ + "Id": "d5adc467-69a8-49ca-b4d0-4f793fad4d62", + "Name": "Sitecore Unicorn Sync", + "Description": "Syncs all the specified configurations via the Unicorn remote sync PowerShell script. Uses the newer MicroChap security layer. Please see the following post for instructions: http://www.sitecorenutsbolts.net/2016/03/14/Octopus-Deploy-Step-for-Unicorn-Sync/", + "ActionType": "Octopus.Script", + "Version": 14, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\r +\r +Add-Type -Path \"${MicroChap}\\MicroCHAP.dll\"\r +\r +Function Sync-Unicorn {\r +\tParam(\r +\t\t[Parameter(Mandatory=$True)]\r +\t\t[string]$ControlPanelUrl,\r +\r +\t\t[Parameter(Mandatory=$True)]\r +\t\t[string]$SharedSecret,\r +\r +\t\t[Parameter(Mandatory=$True)]\r +\t\t[string[]]$Configurations,\r +\r +\t\t[string]$Verb = 'Sync'\r +\t)\r +\r +\t# PARSE THE URL TO REQUEST\r +\t$parsedConfigurations = ($Configurations) -join \"^\"\r +\r +\t$url = \"{0}?verb={1}&configuration={2}\" -f $ControlPanelUrl, $Verb, $parsedConfigurations\r +\r +\tWrite-Host \"Sync-Unicorn: Preparing authorization for $url\"\r +\r +\t# GET AN AUTH CHALLENGE\r +\t$challenge = Get-Challenge -ControlPanelUrl $ControlPanelUrl\r +\r +\tWrite-Host \"Sync-Unicorn: Received challenge: $challenge\"\r +\r +\t# CREATE A SIGNATURE WITH THE SHARED SECRET AND CHALLENGE\r +\t$signatureService = New-Object MicroCHAP.SignatureService -ArgumentList $SharedSecret\r +\r +\t$signature = $signatureService.CreateSignature($challenge, $url, $null)\r +\r +\tWrite-Host \"Sync-Unicorn: Created signature $signature, executing $Verb...\"\r +\r +\t# USING THE SIGNATURE, EXECUTE UNICORN\r +\t$result = Invoke-WebRequest -Uri $url -Headers @{ \"X-MC-MAC\" = $signature; \"X-MC-Nonce\" = $challenge } -TimeoutSec 10800 -UseBasicParsing\r +\r +\t$result.Content\r +}\r +\r +Function Get-Challenge {\r +\tParam(\r +\t\t[Parameter(Mandatory=$True)]\r +\t\t[string]$ControlPanelUrl\r +\t)\r +\r +\t$url = \"$($ControlPanelUrl)?verb=Challenge\"\r +\r +\t$result = Invoke-WebRequest -Uri $url -TimeoutSec 360 -UseBasicParsing\r +\r +\t$result.Content\r +}\r +\r +$configs = $Configurations.split(\"`n\")\r +Sync-Unicorn -ControlPanelUrl \"$($SiteUrl)/unicorn.aspx\" -SharedSecret $SharedSecret -Configurations $configs\r +", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Name": "SharedSecret", + "Label": "Shared Secret", + "HelpText": "The shared secret used for the MicroChap handshake", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "SiteUrl", + "Label": "Site Url", + "HelpText": "The Url of your content authoring system. Must be able to view `/unicorn.aspx`", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MicroChap", + "Label": "MicroCHAP DLL Location", + "HelpText": "The location of the MicroCHAP.dll file in your project", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Configurations", + "Label": "Configurations", + "HelpText": "Add a configuration per line", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedOn": "2016-07-14T11:48:03.577+00:00", + "LastModifiedBy": "GuitarRich", + "$Meta": { + "ExportedAt": "2016-07-14T15:40:31.349+00:00", + "OctopusVersion": "3.3.4", + "Type": "ActionTemplate" + }, + "Category": "sitecore" +} diff --git a/step-templates/slack-detailed-notification-bash.json.human b/step-templates/slack-detailed-notification-bash.json.human new file mode 100644 index 000000000..aee79b0cb --- /dev/null +++ b/step-templates/slack-detailed-notification-bash.json.human @@ -0,0 +1,351 @@ +{ + "Id": "cd3b6172-ed9a-4c42-a588-4c78c3e3b08a", + "Name": "Slack - Detailed Notification - Bash", + "Description": "Posts deployment status to Slack optionally including additional details (release number, environment name, release notes etc.) as well as error description and link to failure log page.", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Get values into variables +includeFieldProject=$(get_octopusvariable \"IncludeFieldProject\") +includeFieldEnvironment=$(get_octopusvariable \"IncludeFieldEnvironment\") +includeFieldMachine=$(get_octopusvariable \"includeFieldMachine\") +includeFieldTenant=$(get_octopusvariable \"IncludeFieldTenant\") +includeFieldUsername=$(get_octopusvariable \"IncludeFieldUsername\") +includeFieldRelease=$(get_octopusvariable \"IncludeFieldRelease\") +includeFieldReleaseNotes=$(get_octopusvariable \"IncludeFieldReleaseNotes\") +includeFieldErrorMessageOnFailure=$(get_octopusvariable \"IncludeErrorMessageOnFailure\") +includeFieldLinkOnFailure=$(get_octopusvariable \"IncludeLinkOnFailure\") + +function convert_ToBoolean () { +\tlocal stringValue=$1 + local returnValue=\"\" + + if [[ \"$stringValue\" == \"True\" ]] + then + \treturnValue=true + else + \treturnValue=false + fi + + echo \"$returnValue\" +} + +function slack_Populate_StatusInfo (){ + \tlocal success=$1 + \tlocal deployment_info=$(get_octopusvariable \"DeploymentInfoText\") +\t\tlocal jsonBody=\"{ \" +\t\t + if [[ \"$success\" == true ]] + then + \tjsonBody+='\"color\": \"good\",' + jsonBody+='\"title\": \"Success\",' + jsonBody+='\"fallback\": \"Deployed successfully ' + jsonBody+=\"$deployment_info\\\"\" + else + \tjsonBody+='\"color\": \"danger\",' + jsonBody+='\"title\": \"Failed\",' + jsonBody+='\"fallback\": \"Failed to deploy ' + jsonBody+=\"$deployment_info\\\"\" + fi + + #jsonBody+=\"}\" + + echo $jsonBody +} + +function populate_field () { +\tlocal title=$1 + local value=$2 + local body=\"\" + + body+='{' + body+='\"short\": \"true\",' + body+='\"title\": ' + body+=\"\\\"$title\\\"\" + body+=\",\" + body+='\"value\": ' + body+=\"\\\"$value\\\"\" + body+='}' + + echo \"$body\" +} + + +function slack_Populate_Fields (){ +\tlocal status_info=$1 + local fieldsJsonBody=\"\" + declare -a testArray + + if [[ \"$includeFieldProject\" == true ]] + then + testArray+=(\"$(populate_field \"Project\" \"$(get_octopusvariable \"Octopus.Project.Name\")\")\") + fi + + if [[ \"$includeFieldEnvironment\" == true ]] + then + testArray+=(\"$(populate_field \"Environment\" \"$(get_octopusvariable \"Octopus.Environment.Name\")\")\") + fi + + if [[ \"$includeFieldMachine\" == true ]] + then + testArray+=(\"$(populate_field \"Machine\" \"$(get_octopusvariable \"Octopus.Machine.Name\")\")\") + fi + + if [[ \"$includeFieldTenant\" == true ]] + then + testArray+=(\"$(populate_field \"Tenant\" \"$(get_octopusvariable \"Octopus.Deployment.Tenant.Name\")\")\") + fi + + if [[ \"$includeFieldUsername\" == true ]] + then + testArray+=(\"$(populate_field \"Username\" \"$(get_octopusvariable \"Octopus.Deployment.CreatedBy.Username\")\")\") + fi + + if [[ \"$includeFieldRelease\" == true ]] + then + testArray+=(\"$(populate_field \"Release\" \"$(get_octopusvariable \"Octopus.Release.Number\")\")\") + fi + + if [[ \"$includeFieldReleaseNotes\" == true ]] + then + testArray+=(\"$(populate_field \"Changes in this release\" \"$(get_octopusvariable \"Octopus.Release.Notes\")\")\") + fi + + if [[ \"$status_info\" == false ]] + then + if [[ \"$includeFieldErrorMessageOnFailure\" == true ]] + then + testArray+=(\"$(populate_field \"Error text\" \"$(get_octopusvariable \"Octopus.Deployment.Error\")\")\") + fi + + if [[ \"$includeFieldLinkOnFailure\" == true ]] + then + baseUrl=\"$(get_octopusvariable \"Octopus.Web.ServerUri\")\" + testArray+=(\"$(populate_field \"See the process\" \"$baseUrl$(get_octopusvariable \"Octopus.Web.DeploymentLink\")\")\") + fi + fi + + + ( IFS=$','; echo \"${testArray[*]}\" ) + +} + +function slack_rich_notification () { +\tlocal success=$1 + local jsonBody=\"{ \" + + jsonBody+='\"channel\": ' + jsonBody+=\"\\\"$(get_octopusvariable \"Channel\")\\\",\" + jsonBody+='\"username\": ' + jsonBody+=\"\\\"$(get_octopusvariable \"Username\")\\\",\" + jsonBody+='\"icon_url\": ' + jsonBody+=\"\\\"$(get_octopusvariable \"IconUrl\")\\\",\" + jsonBody+='\"attachments\": [' + jsonBody+=$(slack_Populate_StatusInfo \"$success\") + jsonBody+=',\"fields\": ' + jsonBody+=\"[$(slack_Populate_Fields \"$success\")]\" + jsonBody+=\"}]}\" + + echo \"$jsonBody\" +} + +# Convert include* variables to actual boolean values +includeFieldProject=$(convert_ToBoolean \"$includeFieldProject\") +includeFieldEnvironment=$(convert_ToBoolean \"$includeFieldEnvironment\") +includeFieldMachine=$(convert_ToBoolean \"$includeFieldMachine\") +includeFieldTenant=$(convert_ToBoolean \"$includeFieldTenant\") +includeFieldUsername=$(convert_ToBoolean \"$includeFieldUsername\") +includeFieldRelease=$(convert_ToBoolean \"$includeFieldRelease\") +includeFieldReleaseNotes=$(convert_ToBoolean \"$includeFieldReleaseNotes\") +includeFieldErrorMessageOnFailure=$(convert_ToBoolean \"$includeFieldErrorMessageOnFailure\") +includeFieldLinkOnFailure=$(convert_ToBoolean \"$includeFieldLinkOnFailure\") + +success=true + +if [[ ! -z $(get_octopusvariable \"Octopus.Deployment.Error\") ]] +then +\tsuccess=false +fi + +# Build json payload +json_payload=$(slack_rich_notification $success) + +webook_url=$(get_octopusvariable \"HookUrl\") + +# Send webhook - redirect stderr to stdout +wget --post-data=\"$json_payload\" --secure-protocol=\"auto\" \"$webook_url\" 2>&1 + +# Check for error +if [[ $? -ne 0 ]] +then + fail_step \"Failed!\" +fi + +", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "d9109767-c4a0-44d4-9501-bdd82be0f935", + "Name": "HookUrl", + "Label": "Webhook URL", + "HelpText": "The Webhook URL provided by Slack, including token.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "d0a459eb-4a63-48e2-8a81-a03540eb1576", + "Name": "Channel", + "Label": "Channel handle", + "HelpText": "Which Slack channel to post notifications to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "706ba5da-6c0f-487f-9ac2-90c32fd04a84", + "Name": "IconUrl", + "Label": "Icon URL", + "HelpText": "The icon to use for this user in Slack", + "DefaultValue": "https://octopus.com/content/resources/favicon.png", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "53a41383-6e54-42ba-ad91-d85c28651299", + "Name": "Username", + "Label": "", + "HelpText": "The username shown in Slack against these notifications", + "DefaultValue": "Octopus Deploy", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1f5c2164-f4a5-485b-adb0-2714eea6cff8", + "Name": "DeploymentInfoText", + "Label": "Main message", + "HelpText": "Long information message shown in slack. This message is prefixed with \"Deployed successfully\" or \"Failed to deploy\" depending on status.", + "DefaultValue": "#{Octopus.Project.Name} release #{Octopus.Release.Number} to #{Octopus.Environment.Name} (#{Octopus.Machine.Name})", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "3538eb46-3c2f-4f81-aad1-434da1e2c30b", + "Name": "IncludeFieldRelease", + "Label": "Include release number field", + "HelpText": "Shows short field with release number name", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "57985f0b-6bf6-4b41-aaf8-f8a6ce65547b", + "Name": "IncludeFieldReleaseNotes", + "Label": "Include release notes Field", + "HelpText": "Release notes are only included on successful deployment", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "97309be1-e7cb-43ab-9721-13ceab6bf7ca", + "Name": "IncludeFieldMachine", + "Label": "Include machine name field", + "HelpText": "Shows short field with machine name", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "b0415f67-00e5-4568-b099-0f107d5e54ae", + "Name": "IncludeFieldProject", + "Label": "Include project field", + "HelpText": null, + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "29772dc0-ddd2-4406-932b-e09e87d3d1c6", + "Name": "IncludeFieldEnvironment", + "Label": "Include environment name field", + "HelpText": "Shows short field with environment name", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "ee313203-4485-4320-b27b-03562724352a", + "Name": "IncludeFieldTenant", + "Label": "Include tenant name field", + "HelpText": "Shows short field with name of tenant", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "0f113c63-9a5c-493d-8942-3db978b41240", + "Name": "IncludeFieldUsername", + "Label": "Include username field", + "HelpText": "Shows the username of user that initiated deployment", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "0cdec31d-400b-4615-b91a-4f2a5b640b45", + "Name": "IncludeLinkOnFailure", + "Label": "Include deployment process link on failure", + "HelpText": "When deployment failed a link \"Open process page\" is added to the notification pointing to deployment process page", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "51fc4209-a97a-4868-8f27-c1076350b581", + "Name": "IncludeErrorMessageOnFailure", + "Label": "Include error message text on failure", + "HelpText": "When deployment failed error text is shown as part of notification", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "d33e02de-a949-472f-a1f4-447425507e6b", + "Name": "OctopusBaseUrl", + "Label": "Octopus base url", + "HelpText": "Base URL to add to the links in the notification. If octopus server dashboard is at \"http://octopus/app\" include all before the \"app\" part (\"http://octopus\").", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2022-07-19T08:50:00.000+00:00", + "LastModifiedBy": "TristanUnibuddy", + "$Meta": { + "ExportedAt": "2022-07-19T08:54:06.595Z", + "OctopusVersion": "2022.3.2617-hotfix.4278", + "Type": "ActionTemplate" + }, + "Category": "Slack" + } diff --git a/step-templates/slack-detailed-notification.json.human b/step-templates/slack-detailed-notification.json.human new file mode 100644 index 000000000..51314caeb --- /dev/null +++ b/step-templates/slack-detailed-notification.json.human @@ -0,0 +1,396 @@ +{ + "Id": "07d64408-ac47-490e-ade2-01bab607ed4f", + "Name": "Slack - Detailed Notification", + "Description": "Posts deployment status to Slack optionally including additional details (release number, environment name, release notes etc.) as well as error description and link to failure log page.", + "ActionType": "Octopus.Script", + "Version": 10, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function Slack-Populate-StatusInfo ([boolean] $Success = $true) { + +\t$deployment_info = $OctopusParameters['DeploymentInfoText']; +\t +\tif ($Success){ +\t\t$status_info = @{ +\t\t\tcolor = \"good\"; +\t\t\t +\t\t\ttitle = \"Success\"; +\t\t\tmessage = \"$deployment_info\"; + +\t\t\tfallback = \"Deployed successfully $deployment_info\"; +\t\t\t +\t\t\tsuccess = $Success; +\t\t} +\t} else { +\t\t$status_info = @{ +\t\t\tcolor = \"danger\"; +\t\t\t +\t\t\ttitle = \"Failed\";\t\t\t +\t\t\tmessage = \"$deployment_info\"; + +\t\t\tfallback = \"Failed to deploy $deployment_info\";\t +\t\t\t +\t\t\tsuccess = $Success; +\t\t} +\t} +\t +\treturn $status_info; +} + +function Slack-Populate-Fields ($StatusInfo) { + +\t# We use += instead of .Add() to prevent returning values returned by .Add() function +\t# it clutters the code, but seems the easiest solution here +\t +\t$fields = @() +\t +\t$fields += +\t\t@{ +\t\t\ttitle = $StatusInfo.title; +\t\t\tvalue = $StatusInfo.message; +\t\t} +\t; + +\t$IncludeFieldEnvironment = [boolean]::Parse($OctopusParameters['IncludeFieldEnvironment']) +\tif ($IncludeFieldEnvironment) { +\t\t$fields += +\t\t\t@{ +\t\t\t\ttitle = \"Environment\"; +\t\t\t\tvalue = $OctopusEnvironmentName; +\t\t\t\tshort = \"true\"; +\t\t\t} +\t\t;\t +\t} + + +\t$IncludeFieldMachine = [boolean]::Parse($OctopusParameters['IncludeFieldMachine']) +\tif ($IncludeFieldMachine) { +\t\t$fields += +\t\t\t@{ +\t\t\t\ttitle = \"Machine\"; +\t\t\t\tvalue = $OctopusParameters['Octopus.Machine.Name']; +\t\t\t\tshort = \"true\"; +\t\t\t} +\t\t;\t +\t}\t +\t +\t$IncludeFieldTenant = [boolean]::Parse($OctopusParameters['IncludeFieldTenant']) +\tif ($IncludeFieldTenant) { +\t\t$fields += +\t\t\t@{ +\t\t\t\ttitle = \"Tenant\"; +\t\t\t\tvalue = $OctopusParameters['Octopus.Deployment.Tenant.Name']; +\t\t\t\tshort = \"true\"; +\t\t\t} +\t\t;\t +\t}\t +\t +\t\t$IncludeFieldUsername = [boolean]::Parse($OctopusParameters['IncludeFieldUsername']) +\tif ($IncludeFieldUsername) { +\t\t$fields += +\t\t\t@{ +\t\t\t\ttitle = \"Username\"; +\t\t\t\tvalue = $OctopusParameters['Octopus.Deployment.CreatedBy.Username']; +\t\t\t\tshort = \"true\"; +\t\t\t} +\t\t;\t +\t}\t +\t +\t$IncludeFieldRelease = [boolean]::Parse($OctopusParameters['IncludeFieldRelease']) +\tif ($IncludeFieldRelease) { +\t\t$fields += +\t\t\t@{ +\t\t\t\ttitle = \"Release\"; +\t\t\t\tvalue = $OctopusReleaseNumber; +\t\t\t\tshort = \"true\"; +\t\t\t} +\t\t;\t +\t} +\t +\t +\t$IncludeFieldReleaseNotes = [boolean]::Parse($OctopusParameters['IncludeFieldReleaseNotes']) +\tif ($StatusInfo[\"success\"] -and $IncludeFieldReleaseNotes) { +\t +\t\t +\t\t$link = $OctopusParameters['Octopus.Web.ReleaseLink']; +\t\t$baseurl = $OctopusParameters['OctopusBaseUrl']; +\t\t +\t\t$notes = $OctopusReleaseNotes +\t\t +\t\tif ($notes.Length -gt 300) { +\t\t\t$shortened = $OctopusReleaseNotes.Substring(0,0); +\t\t\t$notes = \"$shortened `n `<${baseurl}${link}|view all changes`>\" +\t\t} +\t\t +\t\t$fields += +\t\t\t@{ +\t\t\t\ttitle = \"Changes in this release\"; +\t\t\t\tvalue = $notes; +\t\t\t} +\t\t;\t +\t}\t +\t +\t#failure fields +\t +\t$IncludeErrorMessageOnFailure = [boolean]::Parse($OctopusParameters['IncludeErrorMessageOnFailure']) +\tif (-not $StatusInfo[\"success\"] -and $IncludeErrorMessageOnFailure) { +\t\t\t +\t\t$fields += +\t\t\t@{ +\t\t\t\ttitle = \"Error text\"; +\t\t\t\tvalue = $OctopusParameters['Octopus.Deployment.Error']; +\t\t\t} +\t\t;\t +\t}\t +\t\t + +\t$IncludeLinkOnFailure = [boolean]::Parse($OctopusParameters['IncludeLinkOnFailure']) +\tif (-not $StatusInfo[\"success\"] -and $IncludeLinkOnFailure) { +\t\t +\t\t$link = $OctopusParameters['Octopus.Web.DeploymentLink']; +\t\t$baseurl = $OctopusParameters['OctopusBaseUrl']; +\t +\t\t$fields += +\t\t\t@{ +\t\t\t\ttitle = \"See the process\"; +\t\t\t\tvalue = \"`<${baseurl}${link}|Open process page`>\"; +\t\t\t\tshort = \"true\"; +\t\t\t} +\t\t;\t +\t} +\t +\t +\treturn $fields; +\t +} + +function Slack-Rich-Notification ($Success) +{ + $status_info = Slack-Populate-StatusInfo -Success $Success +\t$fields = Slack-Populate-Fields -StatusInfo $status_info + +\t +\t$payload = @{ + channel = $OctopusParameters['Channel'] + username = $OctopusParameters['Username']; + icon_url = $OctopusParameters['IconUrl']; +\t\t + attachments = @( + @{ +\t\t\t\tfallback = $status_info[\"fallback\"]; +\t\t\t\tcolor = $status_info[\"color\"]; +\t\t\t +\t\t\t\tfields = $fields + }; + ); + } +\t +\t#We unescape here to allow links in the Json, as ConvertTo-Json escapes <,> and other special symbols +\t$json_body = ($payload | ConvertTo-Json -Depth 4 | % { [System.Text.RegularExpressions.Regex]::Unescape($_) }); +\t +\t + try { + \t$invokeParameters = @{} + $invokeParameters.Add(\"Method\", \"POST\") + $invokeParameters.Add(\"Body\", $json_body) + $invokeParameters.Add(\"Uri\", $OctopusParameters['HookUrl']) + $invokeParameters.Add(\"ContentType\", \"application/json\") + + # Check for UseBasicParsing + if ((Get-Command Invoke-RestMethod).Parameters.ContainsKey(\"UseBasicParsing\")) + { + # Add the basic parsing argument + $invokeParameters.Add(\"UseBasicParsing\", $true) + } + + + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-RestMethod @invokeParameters + + } catch { + echo \"Something occured\" + echo $_.Exception + echo $_ + #echo $json_body + throw + } + +} + + + +$success = ($OctopusParameters['Octopus.Deployment.Error'] -eq $null); + +Slack-Rich-Notification -Success $success +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "635cd97f-f3aa-48a5-a165-839650921931", + "Name": "HookUrl", + "Label": "Webhook URL", + "HelpText": "The Webhook URL provided by Slack, including token.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "14f5203e-4ce2-4ea2-ae25-379ef538e18b", + "Name": "Channel", + "Label": "Channel handle", + "HelpText": "Which Slack channel to post notifications to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1ab73186-ab66-4be6-818f-f53a39f3f962", + "Name": "IconUrl", + "Label": "Icon URL", + "HelpText": "The icon to use for this user in Slack", + "DefaultValue": "https://octopus.com/content/resources/favicon.png", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a746df23-8cd6-4084-8aea-3713b68b6ec5", + "Name": "Username", + "Label": "", + "HelpText": "The username shown in Slack against these notifications", + "DefaultValue": "Octopus Deploy", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b4918796-8fed-4074-bd3b-f84b555d1015", + "Name": "DeploymentInfoText", + "Label": "Main message", + "HelpText": "Long information message shown in slack. This message is prefixed with \"Deployed successfully\" or \"Failed to deploy\" depending on status.", + "DefaultValue": "#{Octopus.Project.Name} release #{Octopus.Release.Number} to #{Octopus.Environment.Name} (#{Octopus.Machine.Name})", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "60e51e4a-2741-4a36-aaf2-e040c19929d9", + "Name": "IncludeFieldRelease", + "Label": "Include release number field", + "HelpText": "Shows short field with release number name", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "6b383bdb-10b4-4e8b-bf8a-d4c257b0731b", + "Name": "IncludeFieldReleaseNotes", + "Label": "Include release notes Field", + "HelpText": "Release notes are only included on successful deployment", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "df788f61-b026-47da-8a0d-d79e8e48ea4c", + "Name": "IncludeFieldMachine", + "Label": "Include machine name field", + "HelpText": "Shows short field with machine name", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "7a88c0ce-9bd0-4ad3-af76-1d71c1d4f6e4", + "Name": "IncludeFieldEnvironment", + "Label": "Include environment name field", + "HelpText": "Shows short field with environment name", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "f25651aa-920c-4894-b2c6-a4a4c1c6a44d", + "Name": "IncludeFieldTenant", + "Label": "Include tenant name field", + "HelpText": "Shows short field with name of tenant", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "a929f109-b4ae-40f0-b5e5-b15c448ff672", + "Name": "IncludeFieldUsername", + "Label": "Include username field", + "HelpText": "Shows the username of user that initiated deployment", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "3bbe0522-ef27-4793-b249-c87242c818fb", + "Name": "IncludeLinkOnFailure", + "Label": "Include deployment process link on failure", + "HelpText": "When deployment failed a link \"Open process page\" is added to the notification pointing to deployment process page", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "d68b3d72-2944-4f57-bcb9-a96b388c5d0d", + "Name": "IncludeErrorMessageOnFailure", + "Label": "Include error message text on failure", + "HelpText": "When deployment failed error text is shown as part of notification", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "9672e4ab-fe66-4351-bf9b-78cac354d7b6", + "Name": "OctopusBaseUrl", + "Label": "Octopus base url", + "HelpText": "Base URL to add to the links in the notification. If octopus server dashboard is at \"http://octopus/app\" include all before the \"app\" part (\"http://octopus\").", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2022-03-08T04:18:00.000+00:00", + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2022-09-08T18:00:57.940Z", + "OctopusVersion": "2022.2.8136", + "Type": "ActionTemplate" + }, + "Category": "slack" +} diff --git a/step-templates/slack-notify-deployment.json.human b/step-templates/slack-notify-deployment.json.human new file mode 100644 index 000000000..b6fc87710 --- /dev/null +++ b/step-templates/slack-notify-deployment.json.human @@ -0,0 +1,129 @@ +{ + "Id": "21a2ae12-e721-42c1-901d-d6ed08007ca7", + "Name": "Slack - Notify Deployment", + "Description": "Notifies Slack of deployment status. Uses the Octopus Deploy system variable to determine whether a deployment was successful.", + "ActionType": "Octopus.Script", + "Version": 12, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function Slack-Rich-Notification ($notification) +{ + $payload = @{ + channel = $OctopusParameters['Channel'] + username = $OctopusParameters['Username']; + icon_url = $OctopusParameters['IconUrl']; + attachments = @( + @{ + fallback = $notification[\"fallback\"]; + color = $notification[\"color\"]; + fields = @( + @{ + title = $notification[\"title\"]; + title_link = $notification[\"title_link\"]; + value = $notification[\"value\"]; + }); + }; + ); + } + + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-RestMethod -Method POST -Body ($payload | ConvertTo-Json -Depth 4) -Uri $OctopusParameters['HookUrl'] -ContentType 'application/json' -UseBasicParsing +} + +$OctopusBaseUri = $OctopusWebBaseUrl +$UseServerUri = [boolean]::Parse($OctopusParameters['UseServerUri']); +if ($UseServerUri) { +\t$OctopusBaseUri = $OctopusWebServerUri +} + +$IncludeMachineName = [boolean]::Parse($OctopusParameters['IncludeMachineName']); +if ($IncludeMachineName) { + $MachineName = $OctopusParameters['Octopus.Machine.Name']; + if ($MachineName) { + $FormattedMachineName = \"($MachineName)\"; + } +} + +if ($OctopusParameters['Octopus.Deployment.Error'] -eq $null){ + Slack-Rich-Notification @{ + title = \"Success\"; + title_link = \"$OctopusBaseUri$OctopusWebDeploymentLink\"; + value = \"Deploy <$OctopusBaseUri$OctopusWebProjectLink|$OctopusProjectName> release <$OctopusBaseUri$OctopusWebReleaseLink|$OctopusReleaseNumber> to $OctopusEnvironmentName $OctopusActionTargetRoles $OctopusDeploymentTenantName $FormattedMachineName\"; + fallback = \"Deployed $OctopusProjectName release $OctopusReleaseNumber to $OctopusEnvironmentName successfully\"; + color = \"good\"; + }; +} else { + Slack-Rich-Notification @{ + title = \"Failed\"; + title_link = \"$OctopusBaseUri$OctopusWebDeploymentLink\"; + value = \"Deploy <$OctopusBaseUri$OctopusWebProjectLink|$OctopusProjectName> release <$OctopusBaseUri$OctopusWebReleaseLink|$OctopusReleaseNumber> to $OctopusEnvironmentName $OctopusActionTargetRoles $OctopusDeploymentTenantName $FormattedMachineName\"; + fallback = \"Failed to deploy $OctopusProjectName release $OctopusReleaseNumber to $OctopusEnvironmentName\"; + color = \"danger\"; + }; +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "Parameters": [ + { + "Name": "HookUrl", + "Label": "Webhook URL", + "HelpText": "The Webhook URL provided by Slack, including token.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "Channel", + "Label": "Channel handle", + "HelpText": "Which Slack channel to post notifications to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "IconUrl", + "Label": "Icon URL", + "HelpText": "The icon to use for this user in Slack.", + "DefaultValue": "https://octopus.com/content/resources/favicon.png", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Username", + "Label": null, + "HelpText": "The username shown in Slack against these notifications.", + "DefaultValue": "Octopus Deploy", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "IncludeMachineName", + "Label": "Include machine name", + "HelpText": "Should machine name be included in notification to Slack?", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "UseServerUri", + "Label": "Use ServerUri", + "HelpText": "If checked then uses `Octopus.Web.ServerUri` instead of `#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2022-03-08T04:18:00.000+00:00", + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2022-09-08T17:58:51.938Z", + "OctopusVersion": "2022.2.8136", + "Type": "ActionTemplate" + }, + "Category": "slack" +} diff --git a/step-templates/slack-send-notification-using-block-kit.json.human b/step-templates/slack-send-notification-using-block-kit.json.human new file mode 100644 index 000000000..66d174614 --- /dev/null +++ b/step-templates/slack-send-notification-using-block-kit.json.human @@ -0,0 +1,95 @@ +{ + "Id": "ed168ccc-a9b1-4990-86a6-cb6606c88d02", + "Name": "Slack - Send Notification using Block Kit", + "Description": "Send a message notification to Slack using the Block Kit formatting. These messages will be limited to more basic formats (e.g., using functions and inputs probably won't work), but you still will be able to make much nicer looking messages this way with the ability to preview them using the [Block Kit Builder](https://app.slack.com/block-kit-builder).", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$payload = ($OctopusParameters['ssn_BlockObj'] | ConvertFrom-Json) +$payload | Add-Member -MemberType NoteProperty -Name channel -Value $OctopusParameters['ssn_Channel'] +$payload | Add-Member -MemberType NoteProperty -Name username -Value $OctopusParameters['ssn_Username'] +$payload | Add-Member -MemberType NoteProperty -Name icon_url -Value $OctopusParameters['ssn_IconUrl'] +$payload | Add-Member -MemberType NoteProperty -Name link_names -Value \"true\" + +try { +\t[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + if ($PSVersionTable.PSVersion.Major -ge 6) + { + Invoke-Restmethod -Method POST -Body ($payload | ConvertTo-Json -Depth 10) -Uri $OctopusParameters['ssn_HookUrl'] + } + else + { + Invoke-Restmethod -Method POST -Body ($payload | ConvertTo-Json -Depth 10) -Uri $OctopusParameters['ssn_HookUrl'] -UseBasicParsing + } +} catch { + Write-Host \"An error occurred while attempting to send Slack notification\" + Write-Host $_.Exception + Write-Host $_ + throw +}" + }, + "Parameters": [ + { + "Id": "b73ac3c2-1dc9-4196-9d87-71f03a0fb0fc", + "Name": "ssn_HookUrl", + "Label": "Hook URL", + "HelpText": "The Webhook URL provided by Slack In the Incoming Webhook settings. This will provide the channel, Icon, and Username for the message by default.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "c6c11feb-2613-4583-9fcf-3bc379936c11", + "Name": "ssn_Channel", + "Label": "Slack Channel", + "HelpText": "Which Slack channel to post notification to. *This will be ignored if using a webhook from an app.*", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "dba932f9-8a73-4628-9b6f-b5850c29fd46", + "Name": "ssn_Username", + "Label": "Username", + "HelpText": "The username shown in Slack against the notification. *This will be ignored if using a webhook from an app.*", + "DefaultValue": "Octopus Deploy", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9ca22abf-478c-4315-984f-681fa6d6c80c", + "Name": "ssn_IconUrl", + "Label": "Icon URL", + "HelpText": "The icon shown in Slack against the notification. *This will be ignored if using a webhook from an app.*", + "DefaultValue": "https://octopus.com/content/resources/favicon.png", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6a80e10f-d09f-4765-84ba-aed6a3ee2abc", + "Name": "ssn_BlockObj", + "Label": "Block Object", + "HelpText": "Paste the entire JSON object from the [Block Kit Builder](https://app.slack.com/block-kit-builder) here.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-06-02T20:17:55.244Z", + "OctopusVersion": "2023.3.1205-hotfix.1753", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "justin-newman", + "Category": "slack" +} diff --git a/step-templates/slack-send-simple-notification-bash.json.human b/step-templates/slack-send-simple-notification-bash.json.human new file mode 100644 index 000000000..b01b916b8 --- /dev/null +++ b/step-templates/slack-send-simple-notification-bash.json.human @@ -0,0 +1,134 @@ +{ + "Id": "4ec2225e-9ffa-4072-9852-b986e8a98222", + "Name": "Slack - Send Simple Notification - Bash", + "Description": "Send a basic message notification to Slack.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "# Define variables +channel=$(get_octopusvariable \"ssn_Channel\") +username=$(get_octopusvariable \"ssn_Username\") +icon_url=$(get_octopusvariable \"ssn_IconUrl\") +parse=\"full\" +pretext=$(get_octopusvariable \"ssn_Title\") +text=$(get_octopusvariable \"ssn_Message\") +color=$(get_octopusvariable \"ssn_Color\") +webook_url=$(get_octopusvariable \"ssn_HookUrl\") + +# Create JSON payload +json_payload='{\"username\": ' +json_payload+=\"\\\"$username\\\"\" +json_payload+=', \"parse\": ' +json_payload+=\"\\\"$parse\\\"\" +json_payload+=', \"channel\": ' +json_payload+=\"\\\"$channel\\\"\" +json_payload+=', \"icon_url\": ' +json_payload+=\"\\\"$icon_url\\\"\" +json_payload+=', \"attachments\" : [{\"text\": ' +json_payload+=\"\\\"$text\\\"\" +json_payload+=', \"color\": ' +json_payload+=\"\\\"$color\\\"\" +json_payload+=', \"pretext\": ' +json_payload+=\"\\\"$pretext\\\"\" +json_payload+=', \"mrkdwn_in\": [\"pretext\",\"text\"]}]}' + +# Send webhook - redirect stderr to stdout +wget --post-data=\"$json_payload\" --secure-protocol=\"auto\" \"$webook_url\" 2>&1 + +# Check for error +if [[ $? -ne 0 ]] +then + fail_step \"Failed!\" +fi" + }, + "Parameters": [ + { + "Id": "0de9bf5f-302c-4b94-91eb-9e5fb0e37eb4", + "Name": "ssn_HookUrl", + "Label": "Hook URL", + "HelpText": "The Webhook URL provided by Slack, including token.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "d55d5c10-9b82-43b1-8278-aedb72ba165f", + "Name": "ssn_Channel", + "Label": "Channel handle", + "HelpText": "Which Slack channel to post notification to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e753bbfe-efc1-4d3b-a15d-036b5e8ae510", + "Name": "ssn_IconUrl", + "Label": "Icon URL", + "HelpText": "The icon shown in Slack against the notification.", + "DefaultValue": "https://octopus.com/content/resources/favicon.png", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f25b4299-cfd0-4cb0-9c8c-48a706a01f02", + "Name": "ssn_Username", + "Label": "Username", + "HelpText": "The username shown in Slack against the notification.", + "DefaultValue": "Octopus Deploy", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "811722e7-c3d2-495f-822c-eb5a974407e3", + "Name": "ssn_Title", + "Label": "Title", + "HelpText": "The title of the notification in Slack. + +Supported formatting includes: ` ```pre``` `, `_italic_`, `*bold*`, and even `~strike~`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b65a034b-d0b8-486b-a20a-2d105779a6db", + "Name": "ssn_Message", + "Label": "Message", + "HelpText": "The body of the notification in Slack. + +Supported formatting includes: ` ```pre``` `, `_italic_`, `*bold*`, and even `~strike~`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "a9be62b0-a3a8-4940-baa4-47c6cd2f2312", + "Name": "ssn_Color", + "Label": "Color", + "HelpText": "Like traffic signals, color-coding messages can quickly communicate intent and help separate them from the flow of other messages in the timeline.", + "DefaultValue": "good", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "good|Green +warning|Orange +danger|Red" + } + } + ], + "LastModifiedBy": "liam-mackie", + "$Meta": { + "ExportedAt": "2020-03-02T19:22:48.922Z", + "OctopusVersion": "2019.13.7", + "Type": "ActionTemplate" + }, + "Category": "Slack" +} diff --git a/step-templates/slack-send-simple-notification.json.human b/step-templates/slack-send-simple-notification.json.human new file mode 100644 index 000000000..400a13ac5 --- /dev/null +++ b/step-templates/slack-send-simple-notification.json.human @@ -0,0 +1,136 @@ +{ + "Id": "99e6f203-3061-4018-9e34-4a3a9c3c3179", + "Name": "Slack - Send Simple Notification", + "Description": "Send a basic message notification to Slack.", + "ActionType": "Octopus.Script", + "Version": 15, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$payload = @{ + channel = $OctopusParameters['ssn_Channel'] + username = $OctopusParameters['ssn_Username']; + icon_url = $OctopusParameters['ssn_IconUrl']; + link_names = \"true\"; + attachments = @( + @{ + mrkdwn_in = $('pretext', 'text'); + pretext = $OctopusParameters['ssn_Title']; + text = $OctopusParameters['ssn_Message']; + color = $OctopusParameters['ssn_Color']; + } + ) +} + +try { +\t[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + if ($PSVersionTable.PSVersion.Major -ge 6) + { + Invoke-Restmethod -Method POST -Body ($payload | ConvertTo-Json -Depth 4) -Uri $OctopusParameters['ssn_HookUrl'] + } + else + { + Invoke-Restmethod -Method POST -Body ($payload | ConvertTo-Json -Depth 4) -Uri $OctopusParameters['ssn_HookUrl'] -UseBasicParsing + } +} catch { + Write-Host \"An error occurred while attempting to send Slack notification\" + Write-Host $_.Exception + Write-Host $_ + throw +}" + }, + "Parameters": [ + { + "Id": "1229c77a-d992-45c4-935a-47038be21ac1", + "Name": "ssn_HookUrl", + "Label": "Hook URL", + "HelpText": "The Webhook URL provided by Slack, including token.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "1acaf080-aae7-4285-a055-e1148b46f9bb", + "Name": "ssn_Channel", + "Label": "Channel handle", + "HelpText": "Which Slack channel to post notification to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "05817770-0880-484f-880b-2e96b9793e09", + "Name": "ssn_IconUrl", + "Label": "Icon URL", + "HelpText": "The icon shown in Slack against the notification.", + "DefaultValue": "https://octopus.com/content/resources/favicon.png", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "4ffb8455-28bb-4a97-b348-3b889e12c54a", + "Name": "ssn_Username", + "Label": "Username", + "HelpText": "The username shown in Slack against the notification.", + "DefaultValue": "Octopus Deploy", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "137004c7-8e31-45f4-a675-22cf3ccf3383", + "Name": "ssn_Title", + "Label": "Title", + "HelpText": "The title of the notification in Slack. + +Supported formatting includes: ` ```pre``` `, `_italic_`, `*bold*`, and even `~strike~`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "34d06bf1-6407-498b-9877-9d4671511194", + "Name": "ssn_Message", + "Label": "Message", + "HelpText": "The body of the notification in Slack. + +Supported formatting includes: ` ```pre``` `, `_italic_`, `*bold*`, and even `~strike~`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "13def0b7-bb5a-4b95-9538-8bba0201994a", + "Name": "ssn_Color", + "Label": "Color", + "HelpText": "Like traffic signals, color-coding messages can quickly communicate intent and help separate them from the flow of other messages in the timeline.", + "DefaultValue": "good", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "good|Green +warning|Orange +danger|Red" + }, + "Links": {} + } + ], + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2022-12-02T23:43:25.720Z", + "OctopusVersion": "2022.3.10828", + "Type": "ActionTemplate" + }, + "Category": "Slack" +} diff --git a/step-templates/snowchange-deploy-scripts.json.human b/step-templates/snowchange-deploy-scripts.json.human new file mode 100644 index 000000000..e18fb6664 --- /dev/null +++ b/step-templates/snowchange-deploy-scripts.json.human @@ -0,0 +1,246 @@ +{ + "Id": "83d55334-d68e-4062-a84a-810eea445236", + "Name": "Snowchange - Deploy Scripts", + "Description": "[Snowchange](https://github.com/jamesweakley/snowchange#overview) is a Python script that applies migration scripts to [Snowflake](https://www.snowflake.com/) systems. + +**Dependencies:** +This step is a PowerShell script which requires Python (and pip) to run. +For the scripts package, ensure scripts follow the [naming convention](https://github.com/jamesweakley/snowchange#script-naming) presribed by SnowChange, which uses the Flyway naming convention. + +**Activities:** +* Uses pip to update itself and install `wheel` and `snowflake-connector-python`. +* If a path to Snowchange.py is not provided, retrieves the latest version of the file from Github. +* Generates a process-level environment variable, `SNOWSQL_PWD`, which Snowchange requires in order to function.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "4d8e376e-2736-4b9d-bae5-074c07748e85", + "Name": "SnowChangeDeploySnowflakeScriptsPackage", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "SnowChangeDeploySnowflakeScriptsPackage" + } + } + ], + "Properties": { + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if ($result -eq $null) { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +& { +\tparam( + \t[string]$SnowChangeDeploySnowflakeDatabaseName, + \t[string]$SnowChangeDeploySnowflakeWarehouse, + [string]$SnowChangeDeploySnowflakeDeploymentRole, + [string]$SnowChangeDeploySnowflakeDeploymentUser, + [string]$SnowChangeDeploySnowflakeRegion, + [string]$SnowChangeDeploySnowflakeAccountName, + [string]$SnowChangeDeploySNOWSQL_PWD, + [string]$SnowChangeDeployPath + ) + +\tpython --version + +# Acquire snowchange.py dependencies +\tpython.exe -m pip install --upgrade pip +\tpip install --upgrade wheel +\tpip install --upgrade snowflake-connector-python + +# Identify or acquire path to snowchange.py + +\tif (!$SnowChangeDeployPath) { + Write-Host \"Snowchange path not provided. Downloading from Github.\" + + $wc = New-Object System.Net.WebClient + $wc.Encoding = [System.Text.Encoding]::UTF8 + + $targetFolder = Join-Path $Env:OctopusCalamariWorkingDirectory 'snowchange' + $file = \"snowchange.py\" + $targetPath = Join-Path $targetFolder $file + $url = \"https://raw.githubusercontent.com/jamesweakley/snowchange/master/$file\" + + Write-Host -Message \"Attempting to create $targetFolder\" + New-Item -ItemType \"directory\" -Path \"$targetFolder\" + Write-Host -Message \"Attempting to download from $url\" + $wc.DownloadFile(\"$url\", \"$targetPath\") + +\t\t$SnowChangeDeployPath = $targetPath +\t} + +# Identify path to Scripts for snowchange.py to execute + +\t$scriptsPath = $OctopusParameters[\"Octopus.Action.Package[SnowChangeDeploySnowflakeScriptsPackage].ExtractedPath\"] + +# Set Process-level Environment variable for SNOWSQL_PWD + +\t$pword = \"$SnowChangeDeploySNOWSQL_PWD\" +\tSet-Item -Path Env:SNOWSQL_PWD -Value $pword + +# If a DB was specified, generate the metadata table name + +\tif ($SnowChangeDeploySnowflakeDatabaseName) { + \t$metadataTable = \"$SnowChangeDeploySnowflakeDatabaseName\",\".SNOWCHANGE.CHANGE_HISTORY\" -Join \"\" + } + +# Invoke snowchange.py + +\tif ($metadataTable) { + python $SnowChangeDeployPath ` + -f \"$scriptsPath\" ` + -a \"$SnowChangeDeploySnowflakeAccountName\" ` + --snowflake-region \"$SnowChangeDeploySnowflakeRegion\" ` + -u \"$SnowChangeDeploySnowflakeDeploymentUser\" ` + -r \"$SnowChangeDeploySnowflakeDeploymentRole\" ` + -w \"$SnowChangeDeploySnowflakeWarehouse\" ` + -c \"$metadataTable\" + } else { + python $SnowChangeDeployPath ` + -f \"$scriptsPath\" ` + -a \"$SnowChangeDeploySnowflakeAccountName\" ` + --snowflake-region \"$SnowChangeDeploySnowflakeRegion\" ` + -u \"$SnowChangeDeploySnowflakeDeploymentUser\" ` + -r \"$SnowChangeDeploySnowflakeDeploymentRole\" ` + -w \"$SnowChangeDeploySnowflakeWarehouse\" + } +} ` +(Get-Param 'SnowChangeDeploySnowflakeDatabaseName') ` +(Get-Param 'SnowChangeDeploySnowflakeWarehouse' -Required) ` +(Get-Param 'SnowChangeDeploySnowflakeDeploymentRole' -Required) ` +(Get-Param 'SnowChangeDeploySnowflakeDeploymentUser' -Required) ` +(Get-Param 'SnowChangeDeploySnowflakeRegion' -Required) ` +(Get-Param 'SnowChangeDeploySnowflakeAccountName' -Required) ` +(Get-Param 'SnowChangeDeploySNOWSQL_PWD' -Required) ` +(Get-Param 'SnowChangeDeployPath') +" + }, + "Parameters": [ + { + "Id": "4f9e0178-8c8d-4603-b88f-5159e21d2e1f", + "Name": "SnowChangeDeploySnowflakeScriptsPackage", + "Label": "Scripts Package", + "HelpText": "The deployment package that contains the scripts to implement.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "a7dd8952-f09d-4088-b659-d84aca3590f9", + "Name": "SnowChangeDeployPath", + "Label": "Path to Snowchange", + "HelpText": "_Optional:_ Provide an absolute path to an installed copy of snowchange.py. If left blank, the latest version will be downloaded from Github.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "70beb1b0-d737-4fc4-8712-2d8ad8bd9d66", + "Name": "SnowChangeDeploySnowflakeAccountName", + "Label": "Snowflake Account", + "HelpText": "This is the prefix to the Snowflake Url e.g. `contoso` and not `contoso.us-east-1.snowflake...`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5f959b70-5be6-4ad1-ac9b-1390b8a706e9", + "Name": "SnowChangeDeploySnowflakeRegion", + "Label": "Snowflake Region", + "HelpText": "The cloud region of the Snowflake account. e.g. `us-east-1`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "73c860ec-25d1-4775-93a9-68480c2e7ebf", + "Name": "SnowChangeDeploySnowflakeWarehouse", + "Label": "Warehouse", + "HelpText": "The Snowflake warehouse that will receive the changes from the script.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f09080f6-4990-4553-9893-a0455567c56e", + "Name": "SnowChangeDeploySnowflakeDatabaseName", + "Label": "Database Name", + "HelpText": "_Optional:_ Database name is used to contain the metadata changes within the database. If none is provided, a default `METADATA` + database is created at the top level.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "55db8635-2aa6-41c9-991c-75ce8fa8b9ed", + "Name": "SnowChangeDeploySnowflakeDeploymentRole", + "Label": "Role", + "HelpText": "The role used by the user to execute the scripts.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "87385639-099f-4758-adbc-127ccb7774c9", + "Name": "SnowChangeDeploySnowflakeDeploymentUser", + "Label": "Snowflake Deployment User Name", + "HelpText": "The username Snowchange will use to run the scripts.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9debfcd7-e85e-48c6-ae90-2428d76ce13e", + "Name": "SnowChangeDeploySNOWSQL_PWD", + "Label": "Snowflake Deployment User Password", + "HelpText": "The script will create a process-level environment variable SNOWSQL_PWD, which Snowchange uses for authenticating to Snowflake.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-10T15:44:23.481Z", + "OctopusVersion": "2019.13.7", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "apfinger", + "Category": "Snowflake" +} diff --git a/step-templates/solarwinds-manage.json.human b/step-templates/solarwinds-manage.json.human new file mode 100644 index 000000000..9185a2ce4 --- /dev/null +++ b/step-templates/solarwinds-manage.json.human @@ -0,0 +1,141 @@ +{ + "Id": "4e870b0d-0715-4b98-9fa3-48500f6a42e4", + "Name": "Solarwinds - Manage", + "Description": "Start monitoring for a Solarwinds node, application or both", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} + +$solarwindsHost = $OctopusParameters['Host'] +$node = $OctopusParameters['NodeId'] +$application = $OctopusParameters['ApplicationId'] +$username = $OctopusParameters['Username'] +$password = $OctopusParameters['Password'] + +Write-Host \"Starting Solarwinds monitoring for node \" + $node + +if ($node -ne \"\") +{ + $success = $false + try + { + $body = \"[\"\"$node\"\"]\" + $header = @{} + $header.Add(\"Authorization\", \"Basic \"+[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($username+\":\"+$password))) + $uri = \"https://\" + $solarwindsHost + \":17778/SolarWinds/InformationService/v3/Json/Invoke/Orion.Nodes/Remanage\" + + Write-Host \"Sending request $body to $uri\" + + $response = Invoke-WebRequest -Uri $uri -Method Post -Body $body -Headers $header -ContentType \"application/json\" -UseBasicParsing + + if ($response.StatusCode -eq 200) + { + $success = $true + } + } + catch + { + Write-Host \"Something went wrong:\" + Write-Host $_.Exception + } + + if (!$success) + { + throw \"Remanaging node failed.\" + } + + Write-Host \"Remanaged node $node.\" +} + +if ($application -ne \"\") +{ + $success = $false + try + { + $body = \"[\"\"$application\"\"]\" + $header = @{} + $header.Add(\"Authorization\", \"Basic \"+[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($username+\":\"+$password))) + $uri = \"https://\" + $solarwindsHost + \":17778/SolarWinds/InformationService/v3/Json/Invoke/Orion.APM.Application/Remanage\" + + Write-Host \"Sending request $body to $uri\" + + $response = Invoke-WebRequest -Uri $uri -Method Post -Body $body -Headers $header -ContentType \"application/json\" -UseBasicParsing + + if ($response.StatusCode -eq 200) + { + $success = $true + } + } + catch + { + Write-Host \"Something went wrong:\" + Write-Host $_.Exception + } + + if (!$success) + { + throw \"Remanaging application failed.\" + } + + Write-Host \"Remanaged application $application.\" +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Host", + "Label": "Host", + "HelpText": "IP or DNS of the Solarwinds monitoring server", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NodeId", + "Label": "Node ID", + "HelpText": "Solarwinds ID of machine to be unmonitored. Starts with 'N:'.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApplicationId", + "Label": "Application ID", + "HelpText": "Solarwinds ID of application to be unmonitored. Starts with 'AA:'.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Username", + "Label": "Username", + "HelpText": "Username for the Solarwinds console user", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Password", + "Label": "Password", + "HelpText": "Password for the Solarwinds console user", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-16T10:22:27.031+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "solarwinds" +} diff --git a/step-templates/solarwinds-unmanage.json.human b/step-templates/solarwinds-unmanage.json.human new file mode 100644 index 000000000..4b502d52a --- /dev/null +++ b/step-templates/solarwinds-unmanage.json.human @@ -0,0 +1,161 @@ +{ + "Id": "8da75dc4-f430-4c52-ac50-b37629445ff5", + "Name": "Solarwinds - Unmanage", + "Description": "Stop monitoring for a Solarwinds node, application or both", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} + +$solarwindsHost = $OctopusParameters['Host'] +$node = $OctopusParameters['NodeId'] +$application = $OctopusParameters['ApplicationId'] +$timeout = [int] $OctopusParameters['RemanageMinutes'] +$username = $OctopusParameters['Username'] +$password = $OctopusParameters['Password'] + +if ($node -ne \"\") +{ + Write-Host \"Stopping Solarwinds monitoring for node $node\" + + $success = $false + try + { + $now = (Get-Date).ToUniversalTime().AddSeconds(5); + $remanage = $now.AddMinutes($timeout); + $nowString = $now.ToString(\"yyyy-MM-ddTHH:mm:ssZ\") + $remanageString = $remanage.ToString(\"yyyy-MM-ddTHH:mm:ssZ\") + $body = \"[\"\"$node\"\", \"\"$nowString\"\", \"\"$remanageString\"\", \"\"false\"\"]\" + $header = @{} + $header.Add(\"Authorization\", \"Basic \"+[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($username+\":\"+$password))) + $uri = \"https://\" + $solarwindsHost + \":17778/SolarWinds/InformationService/v3/Json/Invoke/Orion.Nodes/Unmanage\" + + Write-Host \"Sending request $body to $uri\" + + $response = Invoke-WebRequest -Uri $uri -Method Post -Body $body -Headers $header -ContentType \"application/json\" -UseBasicParsing + + if ($response.StatusCode -eq 200) + { + $success = $true + } + } + catch + { + Write-Host \"Something went wrong:\" + Write-Host $_.Exception + } + + if (!$success) + { + throw \"Unmanaging node failed.\" + } + + Write-Host \"Unmanaged node $node. Will automatically remanage at $remanage.ToString()\" +} + +if ($application -ne \"\") +{ + Write-Host \"Stopping Solarwinds monitoring for application $application\" + + $success = $false + try + { + $now = (Get-Date).ToUniversalTime().AddSeconds(5); + $remanage = $now.AddMinutes($timeout); + $nowString = $now.ToString(\"yyyy-MM-ddTHH:mm:ssZ\") + $remanageString = $remanage.ToString(\"yyyy-MM-ddTHH:mm:ssZ\") + $body = \"[\"\"$application\"\", \"\"$nowString\"\", \"\"$remanageString\"\", \"\"false\"\"]\" + $header = @{} + $header.Add(\"Authorization\", \"Basic \"+[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($username+\":\"+$password))) + $uri = \"https://\" + $solarwindsHost + \":17778/SolarWinds/InformationService/v3/Json/Invoke/Orion.APM.Application/Unmanage\" + + Write-Host \"Sending request $body to $uri\" + + $response = Invoke-WebRequest -Uri $uri -Method Post -Body $body -Headers $header -ContentType \"application/json\" -UseBasicParsing + + if ($response.StatusCode -eq 200) + { + $success = $true + } + } + catch + { + Write-Host \"Something went wrong:\" + Write-Host $_.Exception + } + + if (!$success) + { + throw \"Unmanaging application failed.\" + } + + Write-Host \"Unmanaged application $application. Will automatically remanage at $remanage.ToString()\" +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Host", + "Label": "Host", + "HelpText": "IP or DNS of the Solarwinds monitoring server", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NodeId", + "Label": "Node ID", + "HelpText": "Solarwinds ID of machine to be unmonitored. Starts with 'N:'.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApplicationId", + "Label": "Application ID", + "HelpText": "Solarwinds ID of application to be unmonitored. Starts with 'AA:'.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "RemanageMinutes", + "Label": "Re-manage after (minutes)", + "HelpText": "Automatically begins monitoring of a node after given minutes.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Username", + "Label": "Username", + "HelpText": "Username for the Solarwinds console user.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Password", + "Label": "Password", + "HelpText": "Password for the Solarwinds console user", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-16T10:24:13.698+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "solarwinds" +} diff --git a/step-templates/spark-sendnotification.json.human b/step-templates/spark-sendnotification.json.human new file mode 100644 index 000000000..caa0826aa --- /dev/null +++ b/step-templates/spark-sendnotification.json.human @@ -0,0 +1,248 @@ +{ + "Id": "cab1e42d-6b8e-4e3e-980d-82fc8e0e2178", + "Name": "Notification - Spark", + "Description": "Send a message to Spark user or room", + "ActionType": "Octopus.Script", + "Version": 16, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "function send-sparkmessage\r +{\r +<#\r +\t.SYNOPSIS\r +\t\tSend a message to a spark user\r +\t\r +\t.DESCRIPTION\r +\t\tA detailed description of the send-sparkmessagetouser function.\r +\t\r +\t.PARAMETER useremail\r +\t\tuser email\r +\t\r +\t.PARAMETER message\r +\t\tMessage to send to the user. Can use markdown.\r +\t\r +\t.PARAMETER auth_token\r +\t\tOAuth token\r +\t\r +\t.PARAMETER api_uri\r +\t\tAPI url if different from default (https://api.ciscospark.com/v1)\r +\t\r +\t.PARAMETER userid\r +\t\tuser id\r +\t\r +\t.PARAMETER proxy\r +\t\tproxy url\r +\t\r +\t.PARAMETER roomid\r +\t\tA description of the roomid parameter.\r +\t\r +\t.PARAMETER room_id\r +\t\tId for room to send message to.\r +\t\r +\t.NOTES\r +\t\tAdditional information about the function.\r +#>\r +\t\r +\tparam\r +\t(\r +\t\t[Parameter(ParameterSetName = 'toPersonEmail',\r +\t\t\t\t Mandatory = $true,\r +\t\t\t\t HelpMessage = 'User email to contact')]\r +\t\t[string]$useremail,\r +\t\t[Parameter(Mandatory = $true,\r +\t\t\t\t HelpMessage = 'Set a message to send to the user. Can use markdown.')]\r +\t\t[string]$message,\r +\t\t[Parameter(Mandatory = $true,\r +\t\t\t\t HelpMessage = 'Set OAuth token')]\r +\t\t[string]$auth_token,\r +\t\t[Parameter(Mandatory = $false,\r +\t\t\t\t HelpMessage = 'API url if different from default.')]\r +\t\t[uri]$api_uri = \"https://api.ciscospark.com/v1\",\r +\t\t[Parameter(ParameterSetName = 'toPersonID',\r +\t\t\t\t Mandatory = $true)]\r +\t\t[string]$userid,\r +\t\t[string]$proxy,\r +\t\t[Parameter(ParameterSetName = 'toRoomID',\r +\t\t\t\t Mandatory = $true)]\r +\t\t[string]$roomid\r +\t)\r +\t\r +\t$header = @{ 'Authorization' = \" Bearer $auth_token\" }\r +\t\r +\tswitch ($PsCmdlet.ParameterSetName)\r +\t{\r +\t\t\"toPersonEmail\" {\r +\t\t\t$body = @{\r +\t\t\t\ttoPersonEmail = $useremail\r +\t\t\t\tmarkdown = $message\r +\t\t\t}\r +\t\t}\r +\t\t\"toPersonID\" {\r +\t\t\t$body = @{\r +\t\t\t\ttoPersonId = $userid\r +\t\t\t\tmarkdown = $message\r +\t\t\t}\r +\t\t}\r +\t\t\"toRoomID\"{\r +\t\t\t$body = @{\r +\t\t\t\troomId = $roomid\r +\t\t\t\tmarkdown = $message\r +\t\t\t}\r +\t\t}\r +\t\t\r +\t}\r +\t\r +\tif ($proxy)\r +\t{\r +\t\tInvoke-RestMethod -Uri \"$api_uri/messages\" -Method Post -headers $header -Body (ConvertTo-Json $body) -ContentType \"application/json\" -Proxy $proxy\r +\t}\r +\telse\r +\t{\r +\t\tInvoke-RestMethod -Uri \"$api_uri/messages\" -Method Post -headers $header -Body (ConvertTo-Json $body) -ContentType \"application/json\"\r +\t}\r +}\r +\r +\r +$useremail = $OctopusParameters['useremail']\r +$message = $OctopusParameters['message']\r +$auth_token = $OctopusParameters['auth_token']\r +$proxy = $OctopusParameters['proxy']\r +$contactmethod = $OctopusParameters['contactmethod']\r +$contactdetails = $OctopusParameters['contactdetails']\r +\r +Write-Verbose \"contact details : $contactdetails\"\r +Write-Verbose \"contact method : $contactmethod\"\r +Write-Verbose \"message : $message\"\r +Write-Verbose \"proxy: $proxy\"\r +foreach ($contactdetail in $contactdetails.Replace(\" \", \"\").Split(\",\"))\r +{\r +\tswitch ($contactmethod)\r +\t{\r +\t\t\"useremail\" {\r +\t\t\tif ($proxy)\r +\t\t\t{\r +\t\t\t\tWrite-Host \"Sending Spark message via $contactmethod to $contactdetail\"\r +\t\t\t\tsend-sparkmessage -useremail $contactdetail -message $message -auth_token $auth_token -proxy $proxy\r +\t\t\t}\r +\t\t\telse\r +\t\t\t{\r +\t\t\t\tWrite-Host \"Sending Spark message via $contactmethod to $contactdetail\"\r +\t\t\t\tsend-sparkmessage -useremail $contactdetail -message $message -auth_token $auth_token\r +\t\t\t}\r +\t\t}\r +\t\t\r +\t\t\r +\t\t\"userid\" {\r +\t\t\tif ($proxy)\r +\t\t\t{\r +\t\t\t\tWrite-Host \"Sending Spark message via $contactmethod to $contactdetail\"\r +\t\t\t\tsend-sparkmessage -userid $contactdetail -message $message -auth_token $auth_token -proxy $proxy\r +\t\t\t}\r +\t\t\telse\r +\t\t\t{\r +\t\t\t\tWrite-Host \"Sending Spark message via $contactmethod to $contactdetail\"\r +\t\t\t\tsend-sparkmessage -userid $contactdetail -message $message -auth_token $auth_token\r +\t\t\t}\r +\t\t}\r +\t\t\r +\t\t\"roomid\"{\r +\t\t\tif ($proxy)\r +\t\t\t{\r +\t\t\t\tWrite-Host \"Sending Spark message via $contactmethod to $contactdetail\"\r +\t\t\t\tsend-sparkmessage -roomid $contactdetail -message $message -auth_token $auth_token -proxy $proxy\r +\t\t\t}\r +\t\t\telse\r +\t\t\t{\r +\t\t\t\tWrite-Host \"Sending Spark message via $contactmethod to $contactdetail\"\r +\t\t\t\tsend-sparkmessage -roomid $contactdetail -message $message -auth_token $auth_token\r +\t\t\t}\r +\t\t}\r +\t}\r +\t\r +}\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "01f83e29-94e6-4fbb-aef5-065a08243d6f", + "Name": "message", + "Label": "Message to send", + "HelpText": "Can use markdown notation", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "d0a2f4f0-61dc-4e04-8d5e-b421f0fe64a3", + "Name": "auth_token", + "Label": "Authentication token", + "HelpText": "Bot token", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "4ae608b1-d659-45d0-b377-223edea6e520", + "Name": "api_uri", + "Label": "API URL", + "HelpText": null, + "DefaultValue": "https://api.ciscospark.com/v1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1c1ca24b-cd7e-4b77-90d0-5cd2d8ad8a74", + "Name": "proxy", + "Label": "Proxy", + "HelpText": "Proxy address", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b5c9688f-d899-4356-ac65-aaa098dd48a7", + "Name": "contactmethod", + "Label": "Contact Method", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "useremail|User Email +userid|User ID +roomid|Room ID" + }, + "Links": {} + }, + { + "Id": "b26814c8-7a05-4a04-bcc6-073691df972b", + "Name": "contactdetails", + "Label": "Contact Details", + "HelpText": "Enter contact details depending on Contact Method choice. Set multiple entries with ','.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "2o1o0", + "$Meta": { + "ExportedAt": "2017-02-24T10:38:27.080Z", + "OctopusVersion": "3.5.1", + "Type": "ActionTemplate" + }, + "Category": "spark" +} diff --git a/step-templates/splunk-forward-file.json.human b/step-templates/splunk-forward-file.json.human new file mode 100644 index 000000000..907da4224 --- /dev/null +++ b/step-templates/splunk-forward-file.json.human @@ -0,0 +1,98 @@ +{ + "Id": "0e5d7444-1be1-47c4-a776-ec0f51cf04b3", + "Name": "Splunk - Forward File", + "Description": "Configures splunk forwarding for the specified file. (The splunk forwarder service should be installed on the target server)", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$file = $OctopusParameters['File'] +$index = $OctopusParameters['Index'] +$appName = $OctopusParameters['AppName'] + +# Enable splunk forwarding + +Set-Service SplunkForwarder -startuptype Automatic + +# Create log file + +if(!(Test-Path \"$file\")) +{ + Write-Host \"Creating new log file\" + New-Item \"$file\" -type File -Force +} +else +{ + Write-Host \"Log file already exists\" +} + +# Create/prepare splunk forwarder directory + +$appPath = \"$env:ProgramFiles\\SplunkUniversalForwarder\\etc\\apps\\$appName\\default\" + +if(Test-Path \"$appPath\") +{ + Write-Host \"Splunk app directory already exists. Removing existing configs\" +\t +\t# Remove-Item recursion does not work correctly - http://technet.microsoft.com/library/hh849765.aspx (-Recurse section) + # Remove files first then directories (leaf -> root) so we don't get the recursion confirm popup + Get-ChildItem $appPath -Recurse | Where { ! $_.PSIsContainer } | Remove-Item -Force + Get-ChildItem $appPath -Recurse | Where { $_.PSIsContainer } | Sort @{ Expression = { $_.FullName.length } } -Descending | Remove-Item -Force +} +else +{ + Write-Host \"Creating splunk app directory\" + New-Item \"$appPath\" -type Directory +} + +# Create forwarder config + +Write-Host \"Creating splunk forwarder config\" + +$str = \"[monitor://$file]`r`ndisabled = false`r`nfollowTail = 0`r`nsourcetype = $appName`r`nindex = $index\" +New-Item \"$appPath\\inputs.conf\" -type File -value $str + +# Restart forwarder service + +Write-Host \"Restarting splunk forwarder\" +Restart-Service \"SplunkForwarder\"", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "File", + "Label": "File", + "HelpText": "The path to the file to forward", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Index", + "Label": "Index", + "HelpText": "The Splunk index to forward to", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AppName", + "Label": "App name", + "HelpText": "The application name outputting to the file", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-16T10:27:19.192+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "splunk" +} diff --git a/step-templates/splunk-log-event-collector.json.human b/step-templates/splunk-log-event-collector.json.human new file mode 100644 index 000000000..9fb246749 --- /dev/null +++ b/step-templates/splunk-log-event-collector.json.human @@ -0,0 +1,114 @@ +{ + "Id": "eb2ef48d-d4d1-40c9-9dab-769c1bac7608", + "Name": "Log to a Splunk Event Collector", + "Description": "A step template that logs a given message to the Splunk [Event Collector](http://dev.splunk.com/view/event-collector/SP-CAAAE6M) along with related Octopus Deploy variables.", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "If ([System.Net.ServicePointManager]::CertificatePolicy -ne $null) +{ +add-type @\" + using System.Net; + using System.Security.Cryptography.X509Certificates; + + public class NoSSLCheckPolicy : ICertificatePolicy { + public NoSSLCheckPolicy() {} + public bool CheckValidationResult( + ServicePoint sPoint, X509Certificate cert, + WebRequest wRequest, int certProb) { + return true; + } + } +\"@ +[System.Net.ServicePointManager]::CertificatePolicy = new-object NoSSLCheckPolicy +} +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType] \"Ssl3\" + + +# Check the parameters. +if (-NOT $SplunkHost) { throw \"You must enter a value for 'Splunk Host'.\" } +if (-NOT $SplunkEventCollectorPort) { throw \"You must enter a value for 'Splunk Event Collector Port'.\" } +if (-NOT $SplunkEventCollectorToken) { throw \"You must enter a value for 'Event Collector Token'.\" } +if (-NOT $Message) { throw \"You must enter a value for 'Message'.\" } + +$properties = @{ +Message = $Message; +ProjectName = $OctopusParameters['Octopus.Project.Name']; +ReleaseNumber = $OctopusParameters['Octopus.Release.Number']; +EnvironmentName = $OctopusParameters['Octopus.Environment.Name']; +DeploymentName = $OctopusParameters['Octopus.Deployment.Name']; +Channel = $OctopusParameters['Octopus.Release.Channel.Name']; +ReleaseUri = $OctopusParameters['Octopus.Web.ReleaseLink']; +DeploymentUri = $OctopusParameters['Octopus.Web.DeploymentLink']; +DeploymentCreatedBy = $OctopusParameters['Octopus.Deployment.CreatedBy.Username']; +Comments = $OctopusParameters['Octopus.Deployment.Comments']; +} + +$exception = $null +if ($OctopusParameters['Octopus.Deployment.Error']) { + $properties[\"DeploymentError\"] = $OctopusParameters['Octopus.Deployment.Error'] + $properties[\"DeploymentDetailedError\"] = $OctopusParameters['Octopus.Deployment.ErrorDetail'] +} + +$body = @{ + event =(ConvertTo-Json $properties) +} + +$uri = \"https://\" + $SplunkHost + \":\" + $SplunkEventCollectorPort + \"/services/collector\" +$header = @{\"Authorization\"=\"Splunk \" + $SplunkEventCollectorToken} + +Invoke-RestMethod -Method Post -Uri $uri -Body (ConvertTo-Json $body) -Header $header +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "SplunkHost", + "Label": "The host endpoint of the Splunk Event Collector", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SplunkEventCollectorPort", + "Label": "The port of the Event Collector", + "HelpText": null, + "DefaultValue": "8088", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SplunkEventCollectorToken", + "Label": "The token for the Event Collector", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Message", + "Label": "The message to send to the event collector", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2016-05-11T06:13:32.800+00:00", + "OctopusVersion": "3.3.12", + "Type": "ActionTemplate" + }, + "Category": "splunk" +} diff --git a/step-templates/sql-add-database-user-to-role.json.human b/step-templates/sql-add-database-user-to-role.json.human new file mode 100644 index 000000000..d83b78920 --- /dev/null +++ b/step-templates/sql-add-database-user-to-role.json.human @@ -0,0 +1,114 @@ +{ + "Id": "1b6b07e5-aa4a-4fc2-b057-afcffada279c", + "Name": "SQL - Add Database User To Role", + "Description": "Adds a database user to a role without using SMO. Please note: this is NOT for server roles or server users.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "if ([string]::IsNullOrWhiteSpace($createSqlLoginUserWhoHasCreateUserRights) -eq $true){ +\tWrite-Host \"No username found, using integrated security\" + $connectionString = \"Server=$createSqlServer;Database=$createDatabaseName;integrated security=true;\" +} +else { +\tWrite-Host \"Username found, using SQL Authentication\" + $connectionString = \"Server=$createSqlServer;Database=$createDatabaseName;User ID=$createSqlLoginUserWhoHasCreateUserRights;Password=$createSqlLoginPasswordWhoHasRights;\" +} + +$sqlConnection = New-Object System.Data.SqlClient.SqlConnection +$sqlConnection.ConnectionString = $connectionString + +$command = $sqlConnection.CreateCommand() +$command.CommandType = [System.Data.CommandType]'Text' + +Write-Host \"Opening the connection to $createSqlServer\" +$sqlConnection.Open() + +Write-Host \"Running the script to add the user $createSqlLogin to the role $createRoleName\" +$command.CommandText = \"ALTER ROLE [$createRoleName] ADD MEMBER [$createSqlLogin]\" +$command.ExecuteNonQuery() + +Write-Host \"Successfully ran the script to add the user $createSqlLogin to the role $createRoleName\" +Write-Host \"Closing the connection to $createSqlServer\" +$sqlConnection.Close()" + }, + "Parameters": [ + { + "Id": "a68967a0-946b-4bd2-8e82-b4eb89dee1e0", + "Name": "createSqlServer", + "Label": "SQL Server", + "HelpText": "The SQL Server to perform the work on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a67b471a-b696-45a3-b730-250eb0a17f8d", + "Name": "createSqlLoginUserWhoHasCreateUserRights", + "Label": "SQL Login", + "HelpText": "The login of the user who has permissions to add members to database roles. + +Leave blank for integrated security. + +See: https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-role-transact-sql?view=sqlallproducts-allversions#permissions for more details.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e9971a71-0483-41ff-a7dc-456848fafb96", + "Name": "createSqlLoginPasswordWhoHasRights", + "Label": "SQL Password", + "HelpText": "The password of the user who has permissions to add members to database roles. + +Leave blank for integrated security. + +See: https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-role-transact-sql?view=sqlallproducts-allversions#permissions for more details.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "40a7f32b-a801-43f6-b233-2556e27c2338", + "Name": "createDatabaseName", + "Label": "Database Name", + "HelpText": "The name of the database to change", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7a303387-295e-4229-8543-f6f9d1277ae9", + "Name": "createSqlLogin", + "Label": "SQL Login", + "HelpText": "The login of the user which will be added to the role", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2158d7fc-1364-4003-9d03-a8967fc7ef03", + "Name": "createRoleName", + "Label": "Role Name", + "HelpText": "The name of the role to add to", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2020-12-01T20:33:56.8643571Z", + "LastModifiedBy": "pstephenson02", + "$Meta": { + "ExportedAt": "2018-07-11T20:25:03.053Z", + "OctopusVersion": "2018.6.10", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-backup-database.json.human b/step-templates/sql-backup-database.json.human new file mode 100644 index 000000000..68db28a21 --- /dev/null +++ b/step-templates/sql-backup-database.json.human @@ -0,0 +1,396 @@ +{ + "Id": "34b4fa10-329f-4c50-ab7c-d6b047264b83", + "Name": "SQL - Backup Database", + "Description": "Backup a MS SQL Server database to the file system.", + "ActionType": "Octopus.Script", + "Version": 12, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\" + +function ConnectToDatabase() { + param($server, $SqlLogin, $SqlPassword, $ConnectionTimeout) + + $server.ConnectionContext.StatementTimeout = $ConnectionTimeout + + if ($null -ne $SqlLogin) { + + if ($null -eq $SqlPassword) { + throw \"SQL Password must be specified when using SQL authentication.\" + } + + $server.ConnectionContext.LoginSecure = $false + $server.ConnectionContext.Login = $SqlLogin + $server.ConnectionContext.Password = $SqlPassword + + Write-Host \"Connecting to server using SQL authentication as $SqlLogin.\" + $server = New-Object Microsoft.SqlServer.Management.Smo.Server $server.ConnectionContext + } + else { + Write-Host \"Connecting to server using Windows authentication.\" + } + + try { + $server.ConnectionContext.Connect() + } + catch { + Write-Error \"An error occurred connecting to the database server!`r`n$($_.Exception.ToString())\" + } +} + +function AddPercentHandler { + param($smoBackupRestore, $action) + + $percentEventHandler = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { Write-Host $dbName $action $_.Percent \"%\" } + $completedEventHandler = [Microsoft.SqlServer.Management.Common.ServerMessageEventHandler] { Write-Host $_.Error.Message } + + $smoBackupRestore.add_PercentComplete($percentEventHandler) + $smoBackupRestore.add_Complete($completedEventHandler) + $smoBackupRestore.PercentCompleteNotification = 10 +} + +function CreateDevice { + param($smoBackupRestore, $directory, $name) + + $devicePath = [System.IO.Path]::Combine($directory, $name) + $smoBackupRestore.Devices.AddDevice($devicePath, \"File\") + return $devicePath +} + +function CreateDevices { + param($smoBackupRestore, $devices, $directory, $dbName, $incremental, $timestamp) + + $targetPaths = New-Object System.Collections.Generic.List[System.String] + + $extension = \".bak\" + + if ($incremental -eq $true) { + $extension = \".trn\" + } + + if ($devices -eq 1) { + $deviceName = $dbName + \"_\" + $timestamp + $extension + $targetPath = CreateDevice $smoBackupRestore $directory $deviceName + $targetPaths.Add($targetPath) + } + else { + for ($i = 1; $i -le $devices; $i++) { + $deviceName = $dbName + \"_\" + $timestamp + \"_\" + $i + $extension + $targetPath = CreateDevice $smoBackupRestore $directory $deviceName + $targetPaths.Add($targetPath) + } + } + return $targetPaths +} + +function BackupDatabase { + param ( + [Microsoft.SqlServer.Management.Smo.Server]$server, + [string]$dbName, + [string]$BackupDirectory, + [int]$devices, + [int]$compressionOption, + [boolean]$incremental, + [boolean]$copyonly, + [string]$timestamp, + [string]$timestampFormat, + [boolean]$RetentionPolicyEnabled, + [int]$RetentionPolicyCount + ) + + $smoBackup = New-Object Microsoft.SqlServer.Management.Smo.Backup + $targetPaths = CreateDevices $smoBackup $devices $BackupDirectory $dbName $incremental $timestamp + + Write-Host \"Attempting to backup database $server.Name.$dbName to:\" + $targetPaths | ForEach-Object { Write-Host $_ } + Write-Host \"\" + + if ($incremental -eq $true) { + $smoBackup.Action = \"Log\" + $smoBackup.BackupSetDescription = \"Log backup of \" + $dbName + $smoBackup.LogTruncation = \"Truncate\" + } + else { + $smoBackup.Action = \"Database\" + $smoBackup.BackupSetDescription = \"Full Backup of \" + $dbName + } + + $smoBackup.BackupSetName = $dbName + \" Backup\" + $smoBackup.MediaDescription = \"Disk\" + $smoBackup.CompressionOption = $compressionOption + $smoBackup.CopyOnly = $copyonly + $smoBackup.Initialize = $true + $smoBackup.Database = $dbName + + try { + AddPercentHandler $smoBackup \"backed up\" + $smoBackup.SqlBackup($server) + Write-Host \"Backup completed successfully.\" + + if ($RetentionPolicyEnabled -eq $true) { + ApplyRetentionPolicy $BackupDirectory $dbName $RetentionPolicyCount $Incremental $Devices $timestampFormat + } + } + catch { + Write-Error \"An error occurred backing up the database!`r`n$($_.Exception.ToString())\" + } +} + +function ApplyRetentionPolicy { + param ( + [string]$BackupDirectory, + [string]$dbName, + [int]$RetentionPolicyCount, + [boolean]$Incremental, + [int]$Devices, + [string]$timestampFormat + ) + + if ($RetentionPolicyCount -le 0) { + Write-Host \"RetentionPolicyCount must be greater than 0. Exiting.\" + return + } + + $extension = if ($Incremental) { '.trn' } else { '.bak' } + # This pattern helps to isolate the timestamp and possible device part from the filename + $pattern = '^' + [regex]::Escape($dbName) + '_(\\d{4}-\\d{2}-\\d{2}-\\d{6})(?:_(\\d+))?' + [regex]::Escape($extension) + '$' + + $allBackups = Get-ChildItem -Path $BackupDirectory -File | Where-Object { $_.Name -match $pattern } + + # Group backups by their base name (assuming base name includes date but not part number) + $backupGroups = $allBackups | Group-Object { if ($_ -match $pattern) { $Matches[1] } } + + # Sort groups by the latest file within each group, assuming the filename includes a sortable date + $sortedGroups = $backupGroups | Sort-Object { [DateTime]::ParseExact($_.Name, \"yyyy-MM-dd-HHmmss\", $null) } -Descending + + # Select the latest groups based on retention policy count + $groupsToKeep = $sortedGroups | Select-Object -First $RetentionPolicyCount + + # Flatten the list of files to keep + $filesToKeep = $groupsToKeep | ForEach-Object { $_.Group } | ForEach-Object { $_.FullName } + + # Identify files to remove + $filesToRemove = $allBackups | Where-Object { $filesToKeep -notcontains $_.FullName } + + foreach ($file in $filesToRemove) { + Remove-Item $file.FullName -Force + Write-Host \"Removed old backup file: $($file.Name)\" + } + + Write-Host \"Retention policy applied successfully. Retained the most recent $RetentionPolicyCount backups.\" +} + + +function Invoke-SqlBackupProcess { + param ( + [hashtable]$OctopusParameters + ) + + # Extracting parameters from the hashtable + $ServerName = $OctopusParameters['Server'] + $DatabaseName = $OctopusParameters['Database'] + $BackupDirectory = $OctopusParameters['BackupDirectory'] + $CompressionOption = [int]$OctopusParameters['Compression'] + $Devices = [int]$OctopusParameters['Devices'] + $Stamp = $OctopusParameters['Stamp'] + $UseSqlServerTimeStamp = $OctopusParameters['UseSqlServerTimeStamp'] + $SqlLogin = $OctopusParameters['SqlLogin'] + $SqlPassword = $OctopusParameters['SqlPassword'] + $ConnectionTimeout = $OctopusParameters['ConnectionTimeout'] + $Incremental = [boolean]::Parse($OctopusParameters['Incremental']) + $CopyOnly = [boolean]::Parse($OctopusParameters['CopyOnly']) + $RetentionPolicyEnabled = [boolean]::Parse($OctopusParameters['RetentionPolicyEnabled']) + $RetentionPolicyCount = [int]$OctopusParameters['RetentionPolicyCount'] + + [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SMO\") | Out-Null + [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SmoExtended\") | Out-Null + [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.ConnectionInfo\") | Out-Null + [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SmoEnum\") | Out-Null + + $server = New-Object Microsoft.SqlServer.Management.Smo.Server $ServerName + + ConnectToDatabase $server $SqlLogin $SqlPassword $ConnectionTimeout + + $database = $server.Databases | Where-Object { $_.Name -eq $DatabaseName } + $timestampFormat = \"yyyy-MM-dd-HHmmss\" + if ($UseSqlServerTimeStamp -eq $true) { + $timestampFormat = \"yyyyMMdd_HHmmss\" + } + $timestamp = if (-not [string]::IsNullOrEmpty($Stamp)) { $Stamp } else { Get-Date -format $timestampFormat } + + if ($null -eq $database) { + Write-Error \"Database $DatabaseName does not exist on $ServerName\" + } + + if ($Incremental -eq $true) { + if ($database.RecoveryModel -eq 3) { + write-error \"$DatabaseName has Recovery Model set to Simple. Log backup cannot be run.\" + } + + if ($database.LastBackupDate -eq \"1/1/0001 12:00 AM\") { + write-error \"$DatabaseName has no Full backups. Log backup cannot be run.\" + } + } + + if ($RetentionPolicyEnabled -eq $true -and $RetentionPolicyCount -gt 0) { + if (-not [int]::TryParse($RetentionPolicyCount, [ref]$null) -or $RetentionPolicyCount -le 0) { + Write-Error \"RetentionPolicyCount must be an integer greater than zero.\" + } + } + + BackupDatabase $server $DatabaseName $BackupDirectory $Devices $CompressionOption $Incremental $CopyOnly $timestamp $timestampFormat $RetentionPolicyEnabled $RetentionPolicyCount +} + +if (Test-Path -Path \"Variable:OctopusParameters\") { + Invoke-SqlBackupProcess -OctopusParameters $OctopusParameters +} +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Server", + "Label": "Server", + "HelpText": "The name of the SQL Server instance that the database resides in.", + "DefaultValue": ".", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Database", + "Label": "Database", + "HelpText": "The name of the database to back up.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BackupDirectory", + "Label": "Backup Directory", + "HelpText": "The output directory to drop the database backup into.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlLogin", + "Label": "SQL login", + "HelpText": "The SQL auth login to connect with. If specified, the SQL Password must also be entered.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlPassword", + "Label": "SQL password", + "HelpText": "The password for the SQL auth login to connect with. Only used if SQL Login is specified.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "Compression", + "Label": "Compression Option", + "HelpText": "- 0 - Use the default backup compression server configuration +- 1 - Enable the backup compression +- 2 - Disable the backup compression", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "0|Default +1|Enabled +2|Disabled" + } + }, + { + "Name": "Devices", + "Label": "Devices", + "HelpText": "The number of backup devices to use for the backup.", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "1|1 +2|2 +3|3 +4|4" + } + }, + { + "Name": "Stamp", + "Label": "Backup file suffix", + "HelpText": "Specify a suffix to add to the backup file names. If left blank the backup will use the current timestamp.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "UseSqlServerTimeStamp", + "Label": "Use SQL Server timestamp format", + "HelpText": "If no suffix is specified, use the MSSQL timestamp format.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "ConnectionTimeout", + "Label": "Connection Timeout", + "HelpText": "Specify the connection timeout settings (in seconds) for the SQL connection. If the backup takes longer than this value, the backup will fail.", + "DefaultValue": "36000", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Incremental", + "Label": "Backup Action", + "HelpText": "Specify the Database backup action", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "false|Full +true|Log (Incremental)" + } + }, + { + "Name": "CopyOnly", + "Label": "Copy Only", + "HelpText": "Specify whether the backup is Copy Only", + "DefaultValue": "true", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "RetentionPolicyEnabled", + "Label": "Retention Policy Enabled", + "HelpText": "Specify if a limit should be imposed on retaining older backups. Will only be applied if Retention Policy Count is set, and is greater than 0.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "RetentionPolicyCount", + "Label": "Retention Policy Count", + "HelpText": "Specify how many old copies of the DB should be retained", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2024-03-26T09:30:00.0000000-07:00", + "LastModifiedBy": "bcullman", + "$Meta": { + "ExportedAt": "2024-03-26T09:30:00.0000000-07:00", + "OctopusVersion": "2022.3.10640", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-create-database-enhanced.json.human b/step-templates/sql-create-database-enhanced.json.human new file mode 100644 index 000000000..5cc97f790 --- /dev/null +++ b/step-templates/sql-create-database-enhanced.json.human @@ -0,0 +1,194 @@ +{ + "Id": "ba43c3a7-fb77-4377-a1e2-e7918fb6df0b", + "Name": "SQL - Create Database If Not Exists Enhanced", + "Description": "Creates a database if the database does not exist without using SMO. ", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Initialize variables +$connectionString = \"\" +$sqlConnection = New-Object System.Data.SqlClient.SqlConnection + +# Determine authentication method +switch ($createAuthenticationMethod) +{ +\t\"AzureADManaged\" + { + \tWrite-Host \"Using Azure Managed Identity authentication ...\" + $connectionString = \"Server=$createSqlServer;Database=master;\" + + $response = Invoke-WebRequest -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fdatabase.windows.net%2F' -Method GET -Headers @{Metadata=\"true\"} -UseBasicParsing + $content = $response.Content | ConvertFrom-Json + $AccessToken = $content.access_token + + $sqlConnection.AccessToken = $AccessToken + break + } + \"SqlAuthentication\" + { + \tWrite-Host \"Using Sql account authentication ...\" + $connectionString = \"Server=$createSqlServer;Database=master;User ID=$createSqlLoginUserWhoHasCreateUserRights;Password=$createSqlLoginPasswordWhoHasRights;\" + break + } + \"WindowsIntegrated\" + { + \tWrite-Host \"Using Windows Integrated authentication ...\" + $connectionString = \"Server=$createSqlServer;Database=master;integrated security=true;\" + break + } +} + + +$sqlConnection.ConnectionString = $connectionString + +$command = $sqlConnection.CreateCommand() +$command.CommandType = [System.Data.CommandType]'Text' +$command.CommandTimeout = $createCommandTimeout + +Write-Host \"Opening the connection to $createSqlServer\" +$sqlConnection.Open() + +$escapedDatabaseName = $createDatabaseName.Replace(\"'\", \"''\") + +Write-Host \"Running the if not exists then create for $createDatabaseName\" +$command.CommandText = \"IF NOT EXISTS (select Name from sys.databases where Name = '$escapedDatabaseName') + create database [$createDatabaseName]\" + +if (![string]::IsNullOrWhiteSpace($createAzureEdition)) +{ +\tWrite-Verbose \"Specifying Azure SqlDb Edition: $($createAzureEdition)\" +\t$command.CommandText += (\"`r`n (EDITION = '{0}')\" -f $createAzureEdition) +} + +if (![string]::IsNullOrWhiteSpace($createAzureBackupStorageRedundancy)) +{ +\tWrite-Verbose \"Specifying Azure Backup storage redundancy: $($createAzureBackupStorageRedundancy)\" +\t$command.CommandText += (\"`r`n WITH BACKUP_STORAGE_REDUNDANCY='{0}'\" -f $createAzureBackupStorageRedundancy) +} + +$command.CommandText += \";\" + +$command.ExecuteNonQuery() + +Write-Host \"Successfully created the account $createDatabaseName\" +Write-Host \"Closing the connection to $createSqlServer\" +$sqlConnection.Close()" + }, + "Parameters": [ + { + "Id": "4a30febf-479b-4427-a382-04a84183e1ae", + "Name": "createSqlServer", + "Label": "SQL Server", + "HelpText": "The SQL Server to perform the work on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9cfe1756-9e7d-4670-af75-1bb4a1899ce1", + "Name": "createAuthenticationMethod", + "Label": "Authentication Method", + "HelpText": "Select the method for authentication to the SQL Server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "AzureADManaged|Azure Active Directory Managed Identity +SqlAuthentication|SQL Authentication +WindowsIntegrated|Windows Integrated" + } + }, + { + "Id": "ba759a10-c230-4530-855a-2d7e210fe822", + "Name": "createSqlLoginUserWhoHasCreateUserRights", + "Label": "SQL Login", + "HelpText": "The login of the user who has permissions to create a database. + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0519fc45-e342-4982-beea-ab5123cad39b", + "Name": "createSqlLoginPasswordWhoHasRights", + "Label": "SQL Password", + "HelpText": "The password of the user who has permissions to create SQL Logins + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "0662b998-acf3-425d-8fff-abd93a4a038d", + "Name": "createDatabaseName", + "Label": "Database to create", + "HelpText": "The name of the database to create", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4617d70a-fb52-4b1b-95f1-07929e47e7c0", + "Name": "createCommandTimeout", + "Label": "Command timeout", + "HelpText": "Number of seconds before throwing a timeout error.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7be71f0c-4df5-4f41-9b2f-3a29800a2e00", + "Name": "createAzureEdition", + "Label": "Azure database edition", + "HelpText": "Defines the database edition for Azure SQL Databases, leave blank if not using Azure.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "basic|Basic Edition +standard|Standard Edition +premium|Premium Edition +generalpurpose|General Purpose Edition +businesscritical|Business Critical Edition +hyperscale|Hyperscale Edition" + } + }, + { + "Id": "85c8ad9c-2515-45d5-b521-49d3518f2a15", + "Name": "createAzureBackupStorageRedundancy", + "Label": "Azure Backup Storage Redundacy", + "HelpText": "Defines the Azure [database backup storage redundancy](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-database-transact-sql?view=azuresqldb-current&tabs=sqlpool#backup_storage_redundancy). The default option is `GRS` when not specified. Leave blank if not using Azure. + +Note: `GZRS` is only available in a subset of Azure regions that have the current requirements:  + +- Database cannot be a `Basic` edition. +- Have a geo-paired region  +- Have multiple availability zones within both data centers (primary and secondary). ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "LOCAL|Locally redundant storage (LRS) +ZONE|Zone-redundant storage (ZRS) +GEO|Geo-redundant storage (GRS) +GEOZONE|Geo-Zone Redundant Storage (GZRS)" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2022-12-15T11:00:58.312Z", + "OctopusVersion": "2023.1.4314", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "sql" + } diff --git a/step-templates/sql-create-database-user.json.human b/step-templates/sql-create-database-user.json.human new file mode 100644 index 000000000..ee574ffa6 --- /dev/null +++ b/step-templates/sql-create-database-user.json.human @@ -0,0 +1,112 @@ +{ + "Id": "9ce258b6-d75d-4fb5-884b-7cd5d7311300", + "Name": "SQL - Create Database User If Not Exists", + "Description": "Will create a database user using an existing server user if that database user does not exist without using SMO.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Write-Host \"SqlLoginWhoHasRights $createSqlLoginUserWhoHasCreateUserRights\" +Write-Host \"CreateSqlServer $createSqlServer\" +Write-Host \"CreateDatabaseName $createDatabaseName\" +Write-Host \"CreateSqlLogin $createSqlLogin\" + +if ([string]::IsNullOrWhiteSpace($createSqlLoginUserWhoHasCreateUserRights) -eq $true){ +\tWrite-Host \"No username found, using integrated security\" + $connectionString = \"Server=$createSqlServer;Database=$createDatabaseName;integrated security=true;\" +} +else { +\tWrite-Host \"Username found, using SQL Authentication\" + $connectionString = \"Server=$createSqlServer;Database=$createDatabaseName;User ID=$createSqlLoginUserWhoHasCreateUserRights;Password=$createSqlLoginPasswordWhoHasRights;\" +} + +$sqlConnection = New-Object System.Data.SqlClient.SqlConnection +$sqlConnection.ConnectionString = $connectionString + +$command = $sqlConnection.CreateCommand() +$command.CommandType = [System.Data.CommandType]'Text' + +Write-Host \"Opening the connection to $createSqlServer\" +$sqlConnection.Open() + +$escapedSqlLogin = $createSqlLogin.Replace(\"'\", \"''\") + +Write-Host \"Running the if not exists then create for $createSqlLogin\" +$command.CommandText = \"If Not Exists (select 1 from sysusers where name = '$escapedSqlLogin') +\tCREATE USER [$createSqlLogin] FOR LOGIN [$createSqlLogin] WITH DEFAULT_SCHEMA=[dbo]\" +$command.ExecuteNonQuery() + +Write-Host \"Successfully created the account $createSqlLogin\" +Write-Host \"Closing the connection to $createSqlServer\" +$sqlConnection.Close()" + }, + "Parameters": [ + { + "Id": "65489614-83f3-4cb6-8fb5-c09271dcc2c1", + "Name": "createSqlServer", + "Label": "SQL Server", + "HelpText": "The SQL Server to perform the work on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e6126a4a-a498-4a60-9f74-e55a08223375", + "Name": "createSqlLoginUserWhoHasCreateUserRights", + "Label": "SQL Login", + "HelpText": "The login of the user who has permissions to create a database user. + +Leave blank for integrated security. + +See: https://docs.microsoft.com/en-us/sql/t-sql/statements/create-user-transact-sql?view=sqlallproducts-allversions#permissions for more details", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "23352169-eb79-4c2d-bb97-41825b49b1a9", + "Name": "createSqlLoginPasswordWhoHasRights", + "Label": "SQL Password", + "HelpText": "The password of the user who has permissions to create a database user. + +Leave blank for integrated security. + +See: https://docs.microsoft.com/en-us/sql/t-sql/statements/create-user-transact-sql?view=sqlallproducts-allversions#permissions for more details", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ccaf0052-36e2-4da8-b942-25a8686d7d1f", + "Name": "createDatabaseName", + "Label": "Database Name", + "HelpText": "The name of the database to create the user on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "eb58a881-3f5c-4fc4-b7a9-f2eee35e599f", + "Name": "createSqlLogin", + "Label": "SQL Login", + "HelpText": "The username to attach to the database if it does not exist", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2020-12-01T20:33:56.8643571Z", + "LastModifiedBy": "pstephenson02", + "$Meta": { + "ExportedAt": "2018-07-11T20:40:58.281Z", + "OctopusVersion": "2018.6.10", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-create-database.json.human b/step-templates/sql-create-database.json.human new file mode 100644 index 000000000..b803bcae5 --- /dev/null +++ b/step-templates/sql-create-database.json.human @@ -0,0 +1,223 @@ +{ + "Id": "771ab2f2-9c27-43a8-be13-6c7c92b435fb", + "Name": "SQL - Create Database If Not Exists", + "Description": "Creates a database if the database does not exist without using SMO.", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "if ([string]::IsNullOrWhiteSpace($createSqlLoginUserWhoHasCreateUserRights) -eq $true) { + Write-Output \"No username found, using integrated security\" + $connectionString = \"Server=$createSqlServer;Database=master;integrated security=true;\" +} +else { + Write-Output \"Username found, using SQL Authentication\" + $connectionString = \"Server=$createSqlServer;Database=master;User ID=$createSqlLoginUserWhoHasCreateUserRights;Password=$createSqlLoginPasswordWhoHasRights;\" +} + + +function Retry-Command { + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [scriptblock]$ScriptBlock, + + [Parameter(Position = 1, Mandatory = $false)] + [int]$Maximum = 1, + + [Parameter(Position = 2, Mandatory = $false)] + [int]$Delay = 100 + ) + + Begin { + $count = 0 + } + + Process { + $ex = $null + do { + $count++ + + try { + Write-Verbose \"Attempt $count of $Maximum\" + $ScriptBlock.Invoke() + return + } + catch { + $ex = $_ + Write-Warning \"Error occurred executing command (on attempt $count of $Maximum): $($ex.Exception.Message)\" + Start-Sleep -Milliseconds $Delay + } + } while ($count -lt $Maximum) + + # Throw an error after $Maximum unsuccessful invocations. Doesn't need + # a condition, since the function returns upon successful invocation. + throw \"Execution failed (after $count attempts): $($ex.Exception.Message)\" + } +} + +[int]$maximum = 0 +[int]$delay = 100 + +if (-not [int]::TryParse($createSqlDatabaseRetryAttempts, [ref]$maximum)) { $maximum = 0 } + +# We add 1 here as if retry attempts is 1, this means we make 2 attempts overall +$maximum = $maximum + 1 + +Retry-Command -Maximum $maximum -Delay $delay -ScriptBlock { +\t + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection + $sqlConnection.ConnectionString = $connectionString + try { + + $command = $sqlConnection.CreateCommand() + $command.CommandType = [System.Data.CommandType]'Text' + $command.CommandTimeout = $createCommandTimeout + + Write-Output \"Opening the connection to $createSqlServer\" + $sqlConnection.Open() + + $escapedDatabaseName = $createDatabaseName.Replace(\"'\", \"''\") + + Write-Output \"Running the if not exists then create for $createDatabaseName\" + $command.CommandText = \"IF NOT EXISTS (select Name from sys.databases where Name = '$escapedDatabaseName') + create database [$createDatabaseName]\" + + if (![string]::IsNullOrWhiteSpace($createAzureEdition)) { + Write-Verbose \"Specifying Azure SqlDb Edition: $($createAzureEdition)\" + $command.CommandText += (\"`r`n (EDITION = '{0}')\" -f $createAzureEdition) + } + + if (![string]::IsNullOrWhiteSpace($createAzureBackupStorageRedundancy)) { + Write-Verbose \"Specifying Azure Backup storage redundancy: $($createAzureBackupStorageRedundancy)\" + $command.CommandText += (\"`r`n WITH BACKUP_STORAGE_REDUNDANCY='{0}'\" -f $createAzureBackupStorageRedundancy) + } + + $command.CommandText += \";\" + + $result = $command.ExecuteNonQuery() + Write-Verbose \"ExecuteNonQuery result: $result\" + + Write-Output \"Successfully executed the database creation script for $createDatabaseName\" + } + + finally { + if ($null -ne $sqlConnection) { + Write-Output \"Closing the connection to $createSqlServer\" + $sqlConnection.Dispose() + } + } +}" + }, + "Parameters": [ + { + "Id": "1ab13f1f-fc67-4042-b8ec-04d2cc552bc5", + "Name": "createSqlServer", + "Label": "SQL Server", + "HelpText": "The SQL Server to perform the work on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f66c4de1-d7d7-4832-be00-f58b10ec3d7b", + "Name": "createSqlLoginUserWhoHasCreateUserRights", + "Label": "SQL Login", + "HelpText": "The login of the user who has permissions to create a database. + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3041c36f-8a5a-472f-b59f-bd6a4d914d21", + "Name": "createSqlLoginPasswordWhoHasRights", + "Label": "SQL Password", + "HelpText": "The password of the user who has permissions to create SQL Logins + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "2ce68c75-0c31-4410-a5c6-d0df6dcd4fa2", + "Name": "createDatabaseName", + "Label": "Database to create", + "HelpText": "The name of the database to create", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e56f7e39-09da-4bf2-84a6-703fa840746c", + "Name": "createCommandTimeout", + "Label": "Command timeout", + "HelpText": "Number of seconds before throwing a timeout error.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ead6dae9-418f-405d-9763-1532c2820474", + "Name": "createAzureEdition", + "Label": "Azure database edition", + "HelpText": "Defines the database edition for Azure SQL Databases, leave blank if not using Azure.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "basic|Basic Edition +standard|Standard Edition +premium|Premium Edition +generalpurpose|General Purpose Edition +businesscritical|Business Critical Edition +hyperscale|Hyperscale Edition" + } + }, + { + "Id": "609fd91a-a39c-4117-a8c0-cc725083f694", + "Name": "createAzureBackupStorageRedundancy", + "Label": "Azure Backup Storage Redundacy", + "HelpText": "Defines the Azure [database backup storage redundancy](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-database-transact-sql?view=azuresqldb-current&tabs=sqlpool#backup_storage_redundancy). The default option is `GRS` when not specified. Leave blank if not using Azure. + +Note: `GZRS` is only available in a subset of Azure regions that have the current requirements:  + +- Database cannot be a `Basic` edition. +- Have a geo-paired region  +- Have multiple availability zones within both data centers (primary and secondary). ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "LOCAL|Locally redundant storage (LRS) +ZONE|Zone-redundant storage (ZRS) +GEO|Geo-redundant storage (GRS) +GEOZONE|Geo-Zone Redundant Storage (GZRS)" + } + }, + { + "Id": "a6506d8d-d9f2-41ae-a78b-e269d9a70632", + "Name": "createSqlDatabaseRetryAttempts", + "Label": "Retry database creation attempts", + "HelpText": "Defines if the database creation attempt should be retried one or more times. Default: `0` (e.g. no retry)", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2023-01-11T13:24:28.274Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2023-01-11T13:24:28.274Z", + "OctopusVersion": "2023.1.5972", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-create-job-category.json.human b/step-templates/sql-create-job-category.json.human new file mode 100644 index 000000000..8c141ce33 --- /dev/null +++ b/step-templates/sql-create-job-category.json.human @@ -0,0 +1,53 @@ +{ + "Id": "7a619d03-0cbc-4b43-9540-90a89bab2ed8", + "Name": "SQL - Create Job Category", + "Description": "Creates a SQL Server Job Category", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "Write-Host \"Running command against database server: $DbServer\" +Write-Host \"Creating Job category: $JobCatName\" +Invoke-Sqlcmd -ServerInstance \"$DbServer\" -Verbose -Query \"EXEC dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'$JobCatName';\" -Database \"msdb\" +Write-Host \"Job category created\"", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "77ef2c98-a8bb-489b-b8bf-397978780d4a", + "Name": "DbServer", + "Type": "String", + "Label": "Database Server", + "HelpText": "Database server instance name", + "DefaultValue": ".", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a6030731-96e3-4fbb-9078-1e307aa9c380", + "Name": "JobCatName", + "Type": "String", + "Label": "Job Category Name", + "HelpText": "The SQL Server's job category name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "ahmedig", + "$Meta": { + "ExportedAt": "2017-03-09T03:35:46.804Z", + "OctopusVersion": "3.11.2", + "Type": "ActionTemplate" + }, + "Category" : "sql" +} diff --git a/step-templates/sql-create-sql-user.json.human b/step-templates/sql-create-sql-user.json.human new file mode 100644 index 000000000..268dd9459 --- /dev/null +++ b/step-templates/sql-create-sql-user.json.human @@ -0,0 +1,190 @@ +{ + "Id": "d4d7d32d-0aec-4a9e-8455-7f91fcd0d6fb", + "Name": "SQL - Create SQL User If Not Exists", + "Description": "Create a SQL Login if the login doesn't already exist without using SMO.", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "Function Test-AzureSQL +{ +\t# Define parameters + param ($SqlConnection) + + # Define local variables + $azureDetected = $false + + # Create command object + $command = $SqlConnection.CreateCommand() + + # Check state + if ($SqlConnection.State -ne [System.Data.ConnectionState]::Open) + { + \t# Open the connection + $SqlConnection.Open() + } + + # Set command text + $command.CommandType = [System.Data.CommandType]::Text + $command.CommandText = \"SELECT SERVERPROPERTY ('edition')\" + + # Execute statement + $reader = $command.ExecuteReader() + + # Read results + while ($reader.Read()) + { + \t# Get value from field + $value = $reader.GetValue(0) + + # Check to see if it's Azure + if ($value -like \"*Azure*\") + { + \t# It's azure + $azureDetected = $true + + # break + break + } + } + + # Check to see if reader is open + if ($reader.IsClosed -eq $false) + { + \t# Close reader object + $reader.Close() + } + + # Not found + return $azureDetected +} + +if ([string]::IsNullOrWhiteSpace($createSqlLoginUserWhoHasCreateUserRights) -eq $true){ +\tWrite-Host \"No username found, using integrated security\" + $connectionString = \"Server=$createSqlServer;Database=master;integrated security=true;\" +} +else { +\tWrite-Host \"Username found, using SQL Authentication\" + $connectionString = \"Server=$createSqlServer;Database=master;User ID=$createSqlLoginUserWhoHasCreateUserRights;Password=$createSqlLoginPasswordWhoHasRights;\" +} + +$sqlConnection = New-Object System.Data.SqlClient.SqlConnection +$sqlConnection.ConnectionString = $connectionString + +$command = $sqlConnection.CreateCommand() +$command.CommandType = [System.Data.CommandType]'Text' + +Write-Host \"Opening the connection to $createSqlServer\" +$sqlConnection.Open() + +$isAzureSQL = Test-AzureSQL -SqlConnection $sqlConnection + +$escapedLogin = $createSqlLogin.Replace(\"'\", \"''\") +Write-Host \"Running the if not exists then create user command on the server for $escapedLogin\" + +if ([string]::IsNullOrWhiteSpace($createSqlPassword) -eq $true) { +\tWrite-Host \"The password sent in was empty, creating account as domain login\" + $command.CommandText = \"IF NOT EXISTS(SELECT 1 FROM sys.server_principals WHERE name = '$escapedLogin') +\tCREATE LOGIN [$createSqlLogin] FROM WINDOWS\" + + if ($isAzureSQL -eq $false) + { + $command.CommandText += \" with default_database=[$createSqlDefaultDatabase]\" + } + +} +else { +\tWrite-Host \"A password was sent in, creating account as SQL Login\" +\t$escapedPassword = $createSqlPassword.Replace(\"'\", \"''\") +\t$command.CommandText = \"IF NOT EXISTS(SELECT 1 FROM sys.sql_logins WHERE name = '$escapedLogin') +\tCREATE LOGIN [$createSqlLogin] with Password='$escapedPassword'\" + + if ($isAzureSQL -eq $false) + { + $command.CommandText += \", default_database=[$createSqlDefaultDatabase]\" + } +} + + +$command.ExecuteNonQuery() + +Write-Host \"Successfully created the account $createSqlLogin\" +Write-Host \"Closing the connection to $createSqlServer\" +$sqlConnection.Close()" + }, + "Parameters": [ + { + "Id": "95996aae-c3ec-4d6f-ac24-486d33513620", + "Name": "createSqlServer", + "Label": "SQL Server", + "HelpText": "The SQL Server to perform the action on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "443c9e33-4f94-4141-bae8-24eec4f133d2", + "Name": "createSqlLoginUserWhoHasCreateUserRights", + "Label": "SQL Login", + "HelpText": "The user who has permissions to create the user + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0bf48c89-31a2-4462-9be3-3f80e816f0de", + "Name": "createSqlLoginPasswordWhoHasRights", + "Label": "SQL Password", + "HelpText": "The password of the user who has permissions to create SQL Logins + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "3f9a4254-db16-4aa5-9ab9-bb8e369ee69e", + "Name": "createSqlLogin", + "Label": "Username to create", + "HelpText": "The SQL Login name that will be created", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b73a9fb0-1b09-473c-956b-07198e5028cc", + "Name": "createSqlPassword", + "Label": "Password", + "HelpText": "The password of the user being created. Leave this blank if you want to use windows authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "f9746a86-33b5-49b2-9bb2-d528585e9759", + "Name": "createSqlDefaultDatabase", + "Label": "Default Database", + "HelpText": "The default database for the user", + "DefaultValue": "master", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-02-12T17:21:40.762Z", + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2021-02-12T17:21:40.762Z", + "OctopusVersion": "2020.5.7", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-delete-database.json.human b/step-templates/sql-delete-database.json.human new file mode 100644 index 000000000..394e0eab4 --- /dev/null +++ b/step-templates/sql-delete-database.json.human @@ -0,0 +1,94 @@ +{ + "Id": "236ee295-056e-44c1-84ab-da4ae6bd8283", + "Name": "SQL - Delete Database", + "Description": "Deletes a database from the server without using SMO.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "if ([string]::IsNullOrWhiteSpace($createSqlLoginUserWhoHasCreateUserRights) -eq $true){ +\tWrite-Host \"No username found, using integrated security\" + $connectionString = \"Server=$createSqlServer;Database=master;integrated security=true;\" +} +else { +\tWrite-Host \"Username found, using SQL Authentication\" + $connectionString = \"Server=$createSqlServer;Database=master;User ID=$createSqlLoginUserWhoHasCreateUserRights;Password=$createSqlLoginPasswordWhoHasRights;\" +} + +$sqlConnection = New-Object System.Data.SqlClient.SqlConnection +$sqlConnection.ConnectionString = $connectionString + +$command = $sqlConnection.CreateCommand() +$command.CommandType = [System.Data.CommandType]'Text' + +Write-Host \"Opening the connection to $createSqlServer\" +$sqlConnection.Open() + +$escapedDatabaseName = $deleteDatabaseName.Replace(\"'\", \"''\") + +Write-Host \"Running the if exists then delete for $createDatabaseName\" +$command.CommandText = \"IF EXISTS (select Name from sys.databases where Name = '$escapedDatabaseName') + drop database [$deleteDatabaseName]\" +$command.ExecuteNonQuery() + + +Write-Host \"Successfully dropped the database $createDatabaseName\" +Write-Host \"Closing the connection to $createSqlServer\" +$sqlConnection.Close()" + }, + "Parameters": [ + { + "Id": "59ae41b4-0f38-45b9-b9d2-2e07dd5c7f6e", + "Name": "createSqlServer", + "Label": "SQL Server", + "HelpText": "The SQL Server to perform the work on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "193c2440-fdb4-4d67-94c2-99a1292c481c", + "Name": "createSqlLoginUserWhoHasCreateUserRights", + "Label": "SQL Login", + "HelpText": "The login of the user who has permissions to create a database. + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3a1090dc-8157-471b-b4d2-f47677f38790", + "Name": "createSqlLoginPasswordWhoHasRights", + "Label": "SQL Password", + "HelpText": "The password of the user who has permissions to create SQL Logins + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "5b686a64-5358-4840-8b51-2097448f57dd", + "Name": "deleteDatabaseName", + "Label": "Database To Delete", + "HelpText": "The name of the database to delete", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2018-07-11T20:39:04.366Z", + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2018-07-11T20:39:04.366Z", + "OctopusVersion": "2018.6.10", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-delete-sql-user.json.human b/step-templates/sql-delete-sql-user.json.human new file mode 100644 index 000000000..7e7212320 --- /dev/null +++ b/step-templates/sql-delete-sql-user.json.human @@ -0,0 +1,93 @@ +{ + "Id": "3c3757a6-eab2-4c37-8e9d-1b713a657357", + "Name": "SQL - Delete SQL User", + "Description": "Deletes a SQL User from the server without using SMO.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "if ([string]::IsNullOrWhiteSpace($createSqlLoginUserWhoHasCreateUserRights) -eq $true){ +\tWrite-Host \"No username found, using integrated security\" + $connectionString = \"Server=$createSqlServer;Database=master;integrated security=true;\" +} +else { +\tWrite-Host \"Username found, using SQL Authentication\" + $connectionString = \"Server=$createSqlServer;Database=master;User ID=$createSqlLoginUserWhoHasCreateUserRights;Password=$createSqlLoginPasswordWhoHasRights;\" +} + +$sqlConnection = New-Object System.Data.SqlClient.SqlConnection +$sqlConnection.ConnectionString = $connectionString + +$command = $sqlConnection.CreateCommand() +$command.CommandType = [System.Data.CommandType]'Text' + +Write-Host \"Opening the connection to $createSqlServer\" +$sqlConnection.Open() + +$escapedLogin = $deleteSqlLogin.Replace(\"'\", \"''\") + +Write-Host \"Running the if not exists then delete user command on the server\" +$command.CommandText = \"IF EXISTS(SELECT 1 FROM sys.server_principals WHERE name = '$escapedLogin') +\tDROP LOGIN [$deleteSqlLogin]\" +$command.ExecuteNonQuery() + +Write-Host \"Successfully deleted the account $createSqlLogin\" +Write-Host \"Closing the connection to $createSqlServer\" +$sqlConnection.Close()" + }, + "Parameters": [ + { + "Id": "a891f549-3de1-40e6-a657-dfe5770484eb", + "Name": "createSqlServer", + "Label": "SQL Server", + "HelpText": "The SQL Server to perform the action on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a26a532f-40e2-469d-9ea3-b556e82ca001", + "Name": "createSqlLoginUserWhoHasCreateUserRights", + "Label": "SQL Login", + "HelpText": "The user who has permissions to create the user + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3f9d69dd-14c8-4854-97ab-c9a186bf9285", + "Name": "createSqlLoginPasswordWhoHasRights", + "Label": "SQL Password", + "HelpText": "The password of the user who has permissions to create SQL Logins + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "b791dca9-e95e-41b7-8388-4585dfa3011e", + "Name": "deleteSqlLogin", + "Label": "Username to delete", + "HelpText": "The SQL Login which will be deleted", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2018-07-11T20:39:04.366Z", + "LastModifiedBy": "octobob", + "$Meta": { + "ExportedAt": "2018-07-11T20:39:04.366Z", + "OctopusVersion": "2018.6.10", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-deploy-dacpac-sqlpackage.json.human b/step-templates/sql-deploy-dacpac-sqlpackage.json.human new file mode 100644 index 000000000..eba0b4af5 --- /dev/null +++ b/step-templates/sql-deploy-dacpac-sqlpackage.json.human @@ -0,0 +1,780 @@ +{ + "Id": "c323cbcd-aab8-4229-b07c-e6c26f7e9a8a", + "Name": "SQL - Deploy DACPAC using SqlPackage", + "Description": "Calls SqlPackage commands such as: + * [Deploy](https://learn.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage-publish?view=sql-server-ver16) + * [Script](https://learn.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage-script?view=sql-server-ver16) + * [DeployReport](https://learn.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage-deploy-drift-report?view=sql-server-ver16) + +As SqlPackage is cross-platform, this template is both Windows and Linux* compatible. + +Results of `Deploy script` and `deploy report` options will upload to Octopus Deploy as an artifact. This allows you to put in place a manual intervention step if required. It is also useful for auditing purposes. + +SqlCmd variables are now supported. To specify SqlCmd variables, create your Octopus variable with the following naming convention: SqlCmdVariable. (case insensitive) and then assign it a value. Examples: +* SqlCmdVariable.Variable1 +* my.sqlcmdvariable.variable2 + +NOTE: + - Requires version 2019.10 or above. + - `TrustServerCertificate=true` is set by default + - Requires PowerShell or *PowerShell Core", + "ActionType": "Octopus.Script", + "Version": 6, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "edff7d94-0feb-48a9-8185-48feb084a94f", + "Name": "DACPACPackage", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "DACPACPackage" + } + } + ], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Set TLS +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +Write-Host \"Determining Operating System...\" +# Check to see if $IsWindows is available +if ($null -eq $IsWindows) +{ + switch ([System.Environment]::OSVersion.Platform) + { + \t\"Win32NT\" + { + \t# Set variable + $IsWindows = $true + $IsLinux = $false + } + \"Unix\" + { + \t$IsWindows = $false + $IsLinux = $true + } + } +} + +if ($IsWindows) +{ +\tWrite-Host \"Detected OS is Windows\" + $ProgressPreference = 'SilentlyContinue' +} +else +{ +\tWrite-Host \"Detected OS is Linux\" +} + +<# + .SYNOPSIS + Finds the DAC File that you specify + + .DESCRIPTION + Looks through the supplied PathList array and searches for the file you specify. It will return the first one that it finds. + + .PARAMETER FileName + Name of the file you are looking for + + .PARAMETER PathList + Array of Paths to search through. + + .EXAMPLE + Find-DacFile -FileName \"Microsoft.SqlServer.TransactSql.ScriptDom.dll\" -PathList @(\"${env:ProgramFiles}\\Microsoft SQL Server\", \"${env:ProgramFiles(x86)}\\Microsoft SQL Server\") +#> +Function Find-DacFile { + Param( + [Parameter(Mandatory=$true)] + [string]$FileName, + [Parameter(Mandatory=$true)] + [string[]]$PathList + ) + + $File = $null + + ForEach($Path in $PathList) + { + Write-Debug (\"Searching: {0}\" -f $Path) + + If (!($File)) + { + $File = ( + Get-ChildItem $Path -ErrorAction SilentlyContinue -Filter $FileName -Recurse | + Sort-Object FullName -Descending | + Select-Object -First 1 + ) + + If ($File) + { + Write-Debug (\"Found: {0}\" -f $File.FullName) + } + } + } + + Return $File +} + + +<# + .SYNOPSIS + Generates a connection string + + .DESCRIPTION + Derive a connection string from the supplied variables + + .PARAMETER ServerName + Name of the server to connect to + + .PARAMETER Database + Name of the database to connect to + + .PARAMETER UseIntegratedSecurity + Boolean value to indicate if Integrated Security should be used or not + + .PARAMETER UserName + User name to use if we are not using integrated security + + .PASSWORD Password + Password to use if we are not using integrated security + + .PARAMETER EnableMultiSubnetFailover + Flag as to whether we should enable multi subnet failover + + .EXAMPLE + Get-ConnectionString -ServerName localhost -UseIntegratedSecurity -Database OctopusDeploy + + .EXAMPLE + Get-ConnectionString -ServerName localhost -UserName sa -Password ProbablyNotSecure -Database OctopusDeploy +#> +Function Get-ConnectionString { + Param( + [Parameter(Mandatory=$True)] + [string]$ServerName, + [string]$UserName, + [string]$Password, + [string]$Database, + [string]$AuthenticationType + ) + + $ApplicationName = \"OctopusDeploy\" + $connectionString = (\"Application Name={0};Server={1}\" -f $ApplicationName, $ServerName) + + switch ($AuthenticationType) + { + \t\"AzureADPassword\" + { + Write-Verbose \"Using Azure Active Directory username and password\" + $connectionString += (\";Authentication='Active Directory Password';Uid={0};Pwd={1}\" -f $UserName, $Password) + break + } + \"AzureADIntegrated\" + { + Write-Verbose \"Using Azure Active Directory integrated\" + $connectionString += (\";Authentication='Active Directory Integrated'\") + break + } + \"AzureADManaged\" + { + \tWrite-Verbose \"Using Azure Active Directory managed identity\" + break + } + \"AzureADServicePrincipal\" + { + Write-Verbose \"Using Azure Active Directory username and password\" + $connectionString += (\";Authentication='ActiveDirectoryServicePrincipal';Uid={0};Pwd={1}\" -f $UserName, $Password) + break \t + } + \"SqlAuthentication\" + { + Write-Verbose \"Using SQL Authentication username and password\" + $connectionString += (\";Uid={0};Pwd={1}\" -f $UserName, $Password) + break + } + \"WindowsIntegrated\" + { + Write-Verbose \"Using integrated security\" + $connectionString += \";Trusted_Connection=True\" + break + } + } + + if ($EnableMultiSubnetFailover) + { + Write-Verbose \"Enabling multi subnet failover\" + $connectionString += \";MultisubnetFailover=True\" + } + + If ($Database) + { + $connectionString += (\";Initial Catalog={0}\" -f $Database) + } + +\t$connectionString += \";TrustServerCertificate=true;\" + + Return $connectionString +} + +<# + .SYNOPSIS + Will find the full path of a given filename (For dacpac or publish profile) + .DESCRIPTION + Will search through an extracted package folder provided as the BasePath and hunt for any matches for the given filename. + .PARAMETER BasePath + String value of the root folder to begine the recursive search. + .PARAMETER FileName + String value of the name of the file to search for. + .PARAMETER FileType + String value of \"DacPac\" or \"PublishProfile\" to identify the type of file to search for. + .EXAMPLE + Get-DacFilePath -BasePath $ExtractPath -FileName $DACPACPackageName -FileType \"DacPac\" +#> +function Get-DacFilePath { + [cmdletbinding()] + param( + [parameter(Mandatory=$true)] + [string]$BasePath, + + [parameter(Mandatory=$true)] + [string]$FileName, + + [parameter(Mandatory=$true)] + [ValidateSet(\"DacPac\",\"PublishProfile\")] + [string]$FileType + ) + + # Add file extension for a dacpac if it's missing + if($FileName.Split(\".\")[-1] -ne \"dacpac\" -and $FileType -eq \"DacPac\"){ + $FileName = \"$FileName.dacpac\" + } + + Write-Verbose \"Looking for $FileType $FileName in $BasePath.\" + + $filePath = (Get-ChildItem -Path $BasePath -Recurse -Filter $FileName).FullName + + if(@($filePath).Length -gt 1){ + Write-Warning \"Found $(@($filePath).Length) instances of $FileName. Using $($filePath[0]).\" + Write-Warning \"Multiple paths for $FileName`: $(@($filePath) -join \"; \")\" + $filePath = $filePath[0] + } + elseif(@($filePath).Length -lt 1 -or $null -eq $filePath){ + Throw \"Could not find $FileName.\" + } + + return $filePath +} + +function Add-SqlCmdVariables +{ +\t# Get all SqlCmdVariables + $sqlCmdVariables = $OctopusParameters.Keys -imatch \"SqlCmdVariable.*\" + $argumentList = @() + +\t# Check to see if something is there +\tif ($null -ne $sqlCmdVariables) + { + \tWrite-Host \"Adding SqlCmdVariables ...\" + +\t\t# Loop through the variable collection + foreach ($sqlCmdVariable in $sqlCmdVariables) + { + \t# Add variable to the deploy options + $sqlCmdVariableKey = $sqlCmdVariable.Substring(($sqlCmdVariable.ToLower().IndexOf(\"sqlcmdvariable.\") + \"sqlcmdvariable.\".Length)) + + Write-Host \"Adding variable: $sqlCmdVariableKey with value: $($OctopusParameters[$sqlCmdVariable])\" + + $argumentList += (\"/variables:{0}={1}\" -f $sqlCmdVariableKey, $OctopusParameters[$sqlCmdVariable]) + } + } + + # return the list of variables + return $argumentList +} + +function Add-AdditionalArguments +{ +\t# Define parameters + param ( + \t$AdditionalArguments + ) + + # Define local variables + $argumentsToAdd = @() + + # Check for emmpty or null + if (![string]::IsNullOrWhitespace($AdditionalArguments)) + { + \t# Split the arguments + \t$argumentsToAdd += $AdditionalArguments.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries).Trim() + } + + # Return list + return $argumentsToAdd +} + +function Get-SqlPackage +{ +\t# Define local variables + $workFolder = $OctopusParameters['Octopus.Action.Package[DACPACPackage].ExtractedPath'] + $downloadUrl = \"\" + +\t# Check to see if a folder needs to be created + if((Test-Path -Path \"$workFolder/sqlpackage\") -eq $false) + { + # Create new folder + New-Item -ItemType Directory -Path \"$workFolder/sqlpackage\" + } + + Write-Host \"Downloading SqlPackage ...\" + + if ($IsWindows) + { + \t# Set url + $downloadUrl = \"https://aka.ms/sqlpackage-windows\" + } + + if ($IsLinux) + { + \t# Set url + $downloadUrl = \"https://aka.ms/sqlpackage-linux\" + } + + # Download sql package + if ($PSVersionTable.PSVersion.Major -ge 6) + { + \t# Download + Invoke-WebRequest -Uri $downloadUrl -OutFile \"$workFolder/sqlpackage/sqlpackage.zip\" + } + else + { + \tInvoke-WebRequest -Uri $downloadUrl -OutFile \"$workFolder/sqlpackage/sqlpackage.zip\" -UseBasicParsing + } + + # Expand the archive + Write-Host \"Extracting .zip ...\" + Expand-Archive -Path \"$workFolder/sqlpackage/sqlpackage.zip\" -DestinationPath \"$workFolder/sqlpackage\" + + # Add to PATH + $env:PATH = \"$workFolder/sqlpackage$([IO.Path]::PathSeparator)\" + $env:PATH + + # Make it executable + if ($IsLinux) + { + \t& chmod a+x \"$workFolder/sqlpackage/sqlpackage\" + } +} + +Function Format-OctopusArgument { + + Param( + [string]$Value + ) + + $Value = $Value.Trim() + + # There must be a better way to do this + Switch -Wildcard ($Value){ + + \"True\" { Return $True } + \"False\" { Return $False } + \"#{*}\" { Return $null } + Default { Return $Value } + } +} + +Function Get-ManagedIdentityToken +{ +\t# Get the identity token + Write-Host \"Getting Azure Managed Identity token ...\" + $token = $null + $tokenUrl = \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fdatabase.windows.net%2F\" + + if ($PSVersionTable.PSVersion.Major -ge 6) + { + \t$token = Invoke-RestMethod -Method GET -Uri $tokenUrl -Headers @{\"MetaData\" = \"true\"} + } + else + { + \t$token = Invoke-RestMethod -Method GET -Uri $tokenUrl -Headers @{\"MetaData\" = \"true\"} -UseBasicParsing + } + + # Return the token + return $token.access_token +} + +function Invoke-SqlPackage +{ +\t# Define parameters + param ( + \t$Action, + $Arguments + ) + + # Add the action + $Arguments += \"/Action:$Action\" + + # Display what's going to be run + if (![string]::IsNullOrWhitespace($Password)) + { + $displayArguments = $Arguments.PSObject.Copy() + for ($i = 0; $i -lt $displayArguments.Count; $i++) + { + if ($null -ne $displayArguments[$i]) + { + if ($displayArguments[$i].Contains($Password)) + { + $DisplayArguments[$i] = $displayArguments[$i].Replace($Password, \"****\") + } + } + } + + Write-Host \"Executing the following command: sqlpackage $displayArguments\" + } + else + { + Write-Host \"Executing the following command: sqlpackage $Arguments\" + } + + & sqlpackage $Arguments + +\t# Check exit code +\tif ($lastExitCode -ne 0) +\t{ +\t\t# Fail the step + \tWrite-Error \"Execution failed!\" +\t} +} + +function Validate-Folder +{ +\t# Define parameters + param ( + \t$TestPath + ) + + # Check for folder + if ((Test-Path -Path $TestPath) -eq $false) + { + \t# Create the folder + New-Item -Path \"$TestPath\" -ItemType \"directory\" + } +} + +Function Remove-InvalidFileNameChars { + +\tParam( +\t\t[string]$FileName +\t) + +\t[IO.Path]::GetinvalidFileNameChars() | ForEach-Object { $FileName = $FileName.Replace($_, \"_\") } +\tReturn $FileName +} + +# Get the supplied parameters +$PublishProfile = $OctopusParameters[\"DACPACPublishProfile\"] +$DACPACReport = Format-OctopusArgument -Value $OctopusParameters[\"DACPACReport\"] +$DACPACScript = Format-OctopusArgument -Value $OctopusParameters[\"DACPACScript\"] +$DACPACDeploy = Format-OctopusArgument -Value $OctopusParameters[\"DACPACDeploy\"] +$DACPACTargetServer = $OctopusParameters[\"DACPACTargetServer\"] +$DACPACTargetDatabase = $OctopusParameters[\"DACPACTargetDatabase\"] +$DACPACAdditionalArguments = $OctopusParameters[\"DACPACAdditionalArguments\"] +$DACPACExeLocation = $OctopusParameters[\"DACPACExeLocation\"] +$DACPACDateTime = ((Get-Date).ToUniversalTime().ToString(\"yyyyMMddHHmmss\")) + +$Username = $OctopusParameters[\"DACPACSQLUsername\"] +$Password = $OctopusParameters[\"DACPACSQLPassword\"] +$PackageReferenceName = \"DACPACPackage\" + +$authenticationType = $OctopusParameters[\"DACPACAuthenticationType\"] + +$ExtractPathKey = (\"Octopus.Action.Package[{0}].ExtractedPath\" -f $PackageReferenceName) +$ExtractPath = $OctopusParameters[$ExtractPathKey] + +if(!(Test-Path $ExtractPath)) { + Throw (\"The package extraction folder '{0}' does not exist or the Octopus Tentacle does not have permission to access it.\" -f $ExtractPath) +} + +# Get the DACPAC location +$dacpacFolderName = [System.IO.Path]::GetDirectoryName($DACPACPackageName) +$dacpacFileName = [System.IO.Path]::GetFileName($DACPACPackageName) +$DACPACPackagePath = Get-DacFilePath -BasePath ($ExtractPath + ([IO.Path]::DirectorySeparatorChar) + $dacpacFolderName) -FileName $dacpacFileName -FileType \"DacPac\" + +# Invoke the DacPac utility +try +{ +\t# Declare working variables + $sqlPackageArguments = @() + + # Build arugment list + $sqlPackageArguments += \"/SourceFile:`\"$DACPACPackagePath`\"\" + $sqlPackageArguments += \"/TargetConnectionString:`\"$(Get-ConnectionString -ServerName $DACPACTargetServer -Database $DACPACTargetDatabase -UserName $UserName -Password $Password -AuthenticationType $AuthenticationType)`\"\" + +\t# Check to see if a publish profile was designated +\tIf ($PublishProfile){ + \t$profileFolderName = [System.IO.Path]::GetDirectoryName($PublishProfile) + $profileFileName = [System.IO.Path]::GetFileName($PublishProfile) + \t$PublishProfilePath = Get-DacFilePath -BasePath ($ExtractPath + ([IO.Path]::DirectorySeparatorChar) + $profileFolderName) -FileName $profileFileName -FileType \"PublishProfile\" + + \t# Add to arguments + \t$sqlPackageArguments += \"/Profile:`\"$PublishProfilePath`\"\" +\t} + + # Check to see if it's using managed identity + if ($authenticationType -eq \"AzureADManaged\") + { + \t# Add access token + $Password = Get-ManagedIdentityToken + $sqlPackageArguments += \"/AccessToken:$Password\" + } + + # Add sqlcmd variables + $sqlPackageArguments += Add-SqlCmdVariables + +\t# Add addtional arguments + $sqlPackageArguments += Add-AdditionalArguments -AdditionalArguments $DACPACAdditionalArguments + + # Check to see if command timeout was specified + if (![string]::IsNullOrWhitespace($DACPACCommandTimeout)) + { + \t# Add timeout parameter + $sqlPackageArguments += \"/Properties:CommandTimeout=$DACPACCommandTimeout\" + } + + # Check to see if sqlpackage needs to be downloaded + if ([string]::IsNullOrWhitespace($DACPACExeLocation)) + { + \t# Download and extract sqlpackage + Get-SqlPackage + } + else + { + \t# Add folder location to path + $env:PATH = \"$([IO.Path]::GetDirectoryName($DACPACExeLocation))$([IO.Path]::PathSeparator)\" + $env:PATH + Write-Host \"It is $($env:PATH)\" + } + + # Execute the actions + if ($DACPACReport) + { + \t$workFolder = \"$($OctopusParameters['Octopus.Action.Package[DACPACPackage].ExtractedPath'])/reports\" + $sqlReportArguments = @() + $reportArtifact = Remove-InvalidFileNameChars -FileName (\"{0}.{1}.{2}.{3}\" -f $DACPACTargetServer, $DACPACTargetDatabase, $DACPACDateTime, \"DeployReport.xml\") + $sqlReportArguments += \"/OutputPath:$workFolder/$reportArtifact\" + + # Validate the folder + Validate-Folder -TestPath $workFolder + + # Execute the action + Invoke-SqlPackage -Action \"DeployReport\" -Arguments ($sqlPackageArguments + $sqlReportArguments) + + # Attach artifacts + foreach ($item in (Get-ChildItem -Path $workFolder)) + { + \t# Upload artifact + New-OctopusArtifact $item.FullName + } + } + + if ($DACPACScript) + { + \t$workFolder = \"$($OctopusParameters['Octopus.Action.Package[DACPACPackage].ExtractedPath'])/scripts\" + $sqlScriptArguments = @() + $scriptArtifact = Remove-InvalidFileNameChars -FileName (\"{0}.{1}.{2}.{3}\" -f $DACPACTargetServer, $DACPACTargetDatabase, $DACPACDateTime, \"DeployScript.sql\") + $sqlScriptArguments += \"/OutputPath:$workFolder/$scriptArtifact\" + + # Validate folder + Validate-Folder -TestPath $workFolder + + # Execute the action + Invoke-SqlPackage -Action \"Script\" -Arguments ($sqlPackageArguments + $sqlScriptArguments) + + # Attach artifacts + foreach ($item in (Get-ChildItem -Path $workFolder)) + { + \t# Upload artifact + New-OctopusArtifact $item.FullName + } + } + + if ($DACPACDeploy) + { + \t# Execute action + Invoke-SqlPackage -Action \"Publish\" -Arguments $sqlPackageArguments + } +} +catch +{ + Write-Host $_.Exception.ToString() + throw; +} +", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "f2fcbf76-89ad-4fa2-9be3-cd80de2e39a1", + "Name": "DACPACPackageName", + "Label": "DACPACPackageName", + "HelpText": "The name of the .dacpac file that contains the SSDT model. Include the .dacpac extensions. To use a specific folder, use the relative location ex: dacpac/mydacpac.dacpac", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0d21c2a7-3e6e-4411-81d7-0e5866faa8fd", + "Name": "DACPACPublishProfile", + "Label": "Publish profile file name", + "HelpText": "Searches the package for the specified file name. To use a specific folder, use the relative location ex: publish/publish-profile.xml", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7b302914-8d6c-4df2-b392-e45a44e0147c", + "Name": "DACPACReport", + "Label": "Report", + "HelpText": "Whether a deployment report should be generated and loaded into OctopusDeploy as an artifact", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "aa612324-c2ea-4e86-921a-8ad7494df752", + "Name": "DACPACScript", + "Label": "Script", + "HelpText": "Whether a deploy script should be generated and loaded into OctopusDeploy as an artifact", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "94c4da0b-5c55-4d3d-ab0b-e175a767694f", + "Name": "DACPACDeploy", + "Label": "Deploy", + "HelpText": "Whether a deployment of the dacpac should occur", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "d2265a95-ee58-4430-8db0-c7bb03826de0", + "Name": "DACPACTargetServer", + "Label": "Target Servername", + "HelpText": "Name of the server to target this deployment against", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "74397a3b-b007-43cf-bc26-7a1fc2690e24", + "Name": "DACPACTargetDatabase", + "Label": "Target Database", + "HelpText": "Name of the database to target this deployment against", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e1710f37-5f38-4d77-8a05-5fbb6d79132e", + "Name": "DACPACAuthenticationType", + "Label": "Authentication type", + "HelpText": "Select the method to authenticate to the SQL Server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "SqlAuthentication|SQL Authentication +WindowsIntegrated|Windows Integrated +AzureADManaged|Azure Active Directory Managed Identity +AzureADPassword|Azure Active Directory Username/Password +AzureADIntegrated|Azure Active Directory Integrated +AzureADServicePrincipal|Azure Active Directory Service Principal" + } + }, + { + "Id": "a51747d3-514d-4110-bf6b-e5f3932d4f22", + "Name": "DACPACSQLUsername", + "Label": "Username", + "HelpText": "User name to use to connect to the server if we are not using Integrated Security. + +If using the Azure Active Directory Service Principal Authentication Type, use the Azure Account variable here. For example, if your Azure Account variable is called MyAccount, the value for this input would be `#{MyAccount.Client}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1afb7ff1-4447-425e-8625-33d76f32c321", + "Name": "DACPACSQLPassword", + "Label": "Password", + "HelpText": "Password to use to connect to the server if we are not using Integrated Security. + +If using the Azure Active Directory Service Principal Authentication Type, use the Azure Account variable here. For example, if your Azure Account variable is called MyAccount, the value for this input would be `#{MyAccount.Password}`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "fa1d3a63-f2c0-4969-a4ac-d797df53bed1", + "Name": "DACPACPackage", + "Label": "DACPAC Package", + "HelpText": "The package containing the `.dacpac` file from the specified repository.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "2afcffb6-6c2c-4012-8ea8-80ce9f3f4a19", + "Name": "DACPACCommandTimeout", + "Label": "Command Timeout", + "HelpText": "Override the default command timeout for longer-running scripts.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "17444647-1f19-42e9-bde2-839601de5347", + "Name": "DACPACExeLocation", + "Label": "SqlPackage executable location", + "HelpText": "Location of the SqlPackage executable. Leave blank to dynamically download.
    +Examples:
    +Embedded within the package:`#{Octopus.Action.Package[DACPACPackage].ExtractedPath}/MySubFolder`
    +On disk:`c:\\sqlpackage\\sqlpackage.exe` or `/etc/sqlpackage/sqlpackage`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "99d63959-edbb-4f06-a4be-15dd7cf24c35", + "Name": "DACPACAdditionalArguments", + "Label": "Additional arguments", + "HelpText": "A comma-delimited list of additional arguments to add to the SqlPackage command.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-06-01T22:27:25.564Z", + "OctopusVersion": "2023.1.10766", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "sql" +} diff --git a/step-templates/sql-deploylog-read.json.human b/step-templates/sql-deploylog-read.json.human new file mode 100644 index 000000000..de27b600e --- /dev/null +++ b/step-templates/sql-deploylog-read.json.human @@ -0,0 +1,160 @@ +{ + "Id": "8a446e55-6554-40fa-bbd9-70bd2a69a13e", + "Name": "SQL Server __DeployLog: Read", + "Description": "To be used with: +SQL Server __DeployLog: Update\r +\r +Requires sqlserver PowerShell module on target machine.\r +\r +For more information: https://octopus.com/blog/100x-faster-db-deploys", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$errorActionPreference = \"stop\" + +# Verifying that sqlserver module is installed +If (-not(Get-InstalledModule sqlserver -ErrorAction silentlycontinue)) { + Write-Error \"This step requires the sqlserver PowerShell module. Please install it and try again.\" +} +Else { + Write-Output \"PowerShell module sqlserver is already installed.\" +} + +# Declaring variabes +$deployed_by = ([Environment]::UserDomainName + \"\\\" + [Environment]::UserName) +$currentPackageVersion = $OctopusParameters[\"Octopus.Action[$DLM_PackageStep].Package.PackageVersion\"] +$deployRequired = \"False\" + +# Logging input variables +Write-Verbose \"DLM_PackageStep step is: $DLM_PackageStep\" +Write-Verbose \"DLM_ServerInstance instance is: $DLM_ServerInstance\" +Write-Verbose \"DLM_Database is: $DLM_Database\" +Write-Verbose \"deployed_by is: $deployed_by\" +Write-Verbose \"currentPackageVersion is: $currentPackageVersion\" + +# For invoke-sqlcmd authentication +$auth=@{} +if($DLM_Username){$auth=@{UserName=$DLM_Username;Password=$DLM_Password}} + +# Script to check whether __DeployLog exists in target database +$CheckDeployLogExists = @' +IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_NAME = N'__DeployLog') + BEGIN + SELECT 'TRUE' + END +ELSE + BEGIN + SELECT 'FALSE' + End +'@ + +# Verifying whether _DeployLog exists +$DeployLogExists = Invoke-Sqlcmd -ServerInstance $DLM_ServerInstance -Database $DLM_Database -Query $CheckDeployLogExists @Auth +$DeployLogExists = $DeployLogExists[0] +if($DeployLogExists -eq 'FALSE') { + Write-Warning \"Table __DeployLog does not exist in $DLM_Database on $DLM_ServerInstance pre-deployment.\" +} + +# Script to read the last successful package version from __DeployLog +$getLastPackageVersion = @' +SELECT TOP (1) package_version +FROM [dbo].[__DeployLog] +WHERE status_code='Succeeded' +ORDER BY utc_time DESC +'@ + +# Condition 1: If there is no __DeployLog table we don't know the prior state so we need to deploy the package +if($DeployLogExists -eq 'FALSE') { + Write-Output \"There is no __DeployLog table on target database. Assuming re-deployment is required.\" + $deployRequired = \"True\"} +else{ + # Condition 2: If the package version has changed we need to deploy the new package + $lastPackageVersion = Invoke-Sqlcmd -ServerInstance $DLM_ServerInstance -Database $DLM_Database -Query $getLastPackageVersion @Auth + try{ + $lastPackageVersion = $lastPackageVersion[0] + } + catch{ + Write-Warning \"__DeployLog is empty\" + $lastPackageVersion = \"NULL\" + } + if($lastPackageVersion -ne $currentPackageVersion){ + Write-Output \"Package version ($currentPackageVersion) differs from previous package version ($lastPackageVersion), re-deployment is required.\" + $deployRequired = \"True\" + } +} + +# If neither condition 1 or 2 above are met, __DeployLog indicates that this +# package has already been successfully deployed - so we can skip the deployment +if($deployRequired -like \"False\"){ + Write-Output \"Skipping the deployment because the __DeployLog table in $DLM_Database on $DLM_ServerInstance indicates that package $currentPackageVersion has already been successfully deployed.\" +} + +Set-OctopusVariable -name \"Deploy:$DLM_ServerInstance-$DLM_Database\" -value $deployRequired +" + }, + "Parameters": [ + { + "Id": "a2328977-2df7-42bb-9f23-263031f27fe7", + "Name": "DLM_PackageStep", + "Label": "Database package deployment step (Required)", + "HelpText": "The step that deploys the package containing the database source code to the target machine.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Id": "871c40e6-9435-4cc5-8f52-a54eaf8c71ad", + "Name": "DLM_ServerInstance", + "Label": "Target SQL Server Instance (Required)", + "HelpText": "The name of the target SQL Server instance. +e.g. SQLSERVER01\\myInstance", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "553359a5-9ee8-43a8-b6fc-aa7e5f9d9d9e", + "Name": "DLM_Database", + "Label": "Target SQL Server Database (Required)", + "HelpText": "The name of the target database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "61f123b1-cb7d-423f-bf3e-da979c3aa539", + "Name": "DLM_Username", + "Label": "SQL Auth User (Optional)", + "HelpText": "The SQL Auth user used to authenticate against the target database. (For Windows Auth, leave blank.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b373aa81-7807-4653-928b-acf39f6a4888", + "Name": "DLM_Password", + "Label": "SQL Auth Password (Optional)", + "HelpText": "The SQL Auth password used to authenticate against the target database. (For Windows Auth, leave blank.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2020-10-02T14:36:09.349Z", + "OctopusVersion": "2020.4.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "Alex-Yates", + "Category": "dlm" +} diff --git a/step-templates/sql-deploylog-update.json.human b/step-templates/sql-deploylog-update.json.human new file mode 100644 index 000000000..ca5f28411 --- /dev/null +++ b/step-templates/sql-deploylog-update.json.human @@ -0,0 +1,215 @@ +{ + "Id": "a9f7644c-3e27-4e46-a591-eee7f3542032", + "Name": "SQL Server __DeployLog: Update", + "Description": "To be used with: +SQL Server __DeployLog: Read\r +\r +Requires sqlserver PowerShell module on target machine.\r +\r +For more information: https://octopus.com/blog/100x-faster-db-deploys", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "<# +Required variables: +$DLM_PackageStep +$DLM_DeployStep +$DLM_ServerInstance +$DLM_Database + +Optional variables (include for SQL Auth, exclude for WinAuth): +$DLM_Username +$DLM_Password +#> + +$errorActionPreference = \"stop\" + +# Verifying that sqlserver module is installed +If (-not(Get-InstalledModule sqlserver -ErrorAction silentlycontinue)) { + Write-Error \"This step requires the sqlserver PowerShell module. Please install it and try again.\" +} +Else { + Write-Output \"PowerShell module sqlserver is already installed.\" +} + +# Declaring variabes +$deployed_by = ([Environment]::UserDomainName + \"\\\" + [Environment]::UserName) +$currentPackageVersion = $OctopusParameters[\"Octopus.Action[$DLM_PackageStep].Package.PackageVersion\"] +$octo_release_number = $OctopusParameters[\"Octopus.Release.Number\"] +$octo_deployment_id = $OctopusParameters[\"Octopus.Deployment.Id\"] +$octo_deployment_created_by = $OctopusParameters[\"Octopus.Deployment.CreatedBy.Username\"] +$deployStatusCode = $OctopusParameters[\"Octopus.Step[$DLM_DeployStep].Status.Code\"] +$deployStatusError = $OctopusParameters[\"Octopus.Step[$DLM_DeployStep].Status.Error\"] +$deployStatusErrorDetail = $OctopusParameters[\"Octopus.Step[$DLM_DeployStep].Status.ErrorDetail\"] +$timestamp = Get-Date +$utcTime = [datetime]::Now.ToUniversalTime().ToString(\"yyyy-MM-dd HH:mm:ss\") + +# Escaping single quotes to avoid breaking T-SQL INSERT statements +$deployStatusError = $deployStatusError -replace \"'\", \"''\" +$deployStatusErrorDetail = $deployStatusErrorDetail -replace \"'\", \"''\" + +# Logging input variables +Write-Verbose \"DLM_PackageStep step is: $DLM_PackageStep\" +Write-Verbose \"DLM_DeployStep step is: $DLM_DeployStep\" +Write-Verbose \"DLM_ServerInstance instance is: $DLM_ServerInstance\" +Write-Verbose \"DLM_Database is: $DLM_Database\" +Write-Verbose \"DLM_Username is: $DLM_Username\" +Write-Verbose \"deployed_by is: $deployed_by\" +Write-Verbose \"currentPackageVersion is: $currentPackageVersion\" +Write-Verbose \"octo_release_number is: $octo_release_number\" +Write-Verbose \"octo_deployment_id is: $octo_deployment_id\" +Write-Verbose \"octo_deployment_created_by is: $octo_deployment_created_by\" +Write-Verbose \"deployStatusCode is: $deployStatusCode\" +Write-Verbose \"deployStatusError is: $deployStatusError\" +Write-Verbose \"deployStatusErrorDetail is: $deployStatusErrorDetail\" +Write-Verbose \"utcTime is: $utcTime\" + +# For invoke-sqlcmd authentication +$auth=@{} +if($DLM_Username){$auth=@{UserName=$DLM_Username;Password=$DLM_Password}} + +# Script to check whether __DeployLog exists in target database +$CheckDeployLogExists = @' +IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_NAME = N'__DeployLog') + BEGIN + SELECT 'TRUE' + END +ELSE + BEGIN + SELECT 'FALSE' + End +'@ + +# Script to create the __DeployLog table if it does not already exist +$CreateDeployLogTbl = @' +CREATE TABLE [dbo].[__DeployLog]( +\t[deploy_id] [int] IDENTITY(1,1) PRIMARY KEY, +\t[package_version] [varchar](255) NOT NULL, + \t[octo_release_number] [nvarchar](50) NOT NULL, + \t[octo_deployment_id] [nvarchar](50) NOT NULL, + [octo_deployment_created_by] [nvarchar](255) NOT NULL, +\t[utc_time] [datetime2](7) NOT NULL, +\t[deployed_by] [nvarchar](50) NULL, + [status_code] [nvarchar](50) NULL, + [status_error] [nvarchar](MAX) NULL, + [status_error_detail] [nvarchar](MAX) NULL + ) +GO +'@ + +# Checking if __DeployLog still exists following deployment +# (it may have been dropped if it wasn't included in source code) +$DeployLogExists = Invoke-Sqlcmd -ServerInstance $DLM_ServerInstance -Database $DLM_Database -Query $CheckDeployLogExists @Auth +$DeployLogExists = $DeployLogExists[0] + +# If __DeployLog has been dropped, recreate it +if($DeployLogExists -eq \"FALSE\") { + Write-Warning \"Table __DeployLog does not exist in $DLM_Database on $DLM_ServerInstance post-deployment. It may have been deleted. You should either add the table to your source code or your filter to avoid data loss.\" + Write-Output \"Redeploying __DeployLog table\" + Invoke-Sqlcmd -ServerInstance $DLM_ServerInstance -Database $DLM_Database -Query $CreateDeployLogTbl @Auth +} + +# Script to update __DeployLog with info about this deployment +$updateDeployLog = @\" +INSERT INTO [dbo].[__DeployLog] + ([package_version] + ,[octo_release_number] + ,[octo_deployment_id] + ,[octo_deployment_created_by] + ,[utc_time] + ,[deployed_by] + ,[status_code] + ,[status_error] + ,[status_error_detail]) + VALUES + ('$currentPackageVersion' + ,'$octo_release_number' + ,'$octo_deployment_id' + ,'$octo_deployment_created_by' + ,'$utcTime' + ,'$deployed_by' + ,'$deployStatusCode' + ,'$deployStatusError' + ,'$deployStatusErrorDetail') +GO +\"@ + +Write-Output \"Updating __DeployLog in $DLM_Database on $DLM_ServerInstance.\" +Invoke-Sqlcmd -ServerInstance $DLM_ServerInstance -Database $DLM_Database -Query $updateDeployLog @Auth" + }, + "Parameters": [ + { + "Id": "a2328977-2df7-42bb-9f23-263031f27fe7", + "Name": "DLM_PackageStep", + "Label": "Database package deployment step (Required)", + "HelpText": "The step that deploys the package containing the database source code to the target machine.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Id": "c4668938-09ea-4b42-9be5-61dfcee9eae6", + "Name": "DLM_DeployStep", + "Label": "Database update step (Required)", + "HelpText": "The step that updates the target database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Id": "871c40e6-9435-4cc5-8f52-a54eaf8c71ad", + "Name": "DLM_ServerInstance", + "Label": "Target SQL Server Instance (Required)", + "HelpText": "The name of the target SQL Server instance. +e.g. SQLSERVER01\\myInstance", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "553359a5-9ee8-43a8-b6fc-aa7e5f9d9d9e", + "Name": "DLM_Database", + "Label": "Target SQL Server Database (Required)", + "HelpText": "The name of the target database.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "61f123b1-cb7d-423f-bf3e-da979c3aa539", + "Name": "DLM_Username", + "Label": "SQL Auth User (Optional)", + "HelpText": "The SQL Auth user used to authenticate against the target database. (For Windows Auth, leave blank.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b373aa81-7807-4653-928b-acf39f6a4888", + "Name": "DLM_Password", + "Label": "SQL Auth Password (Optional)", + "HelpText": "The SQL Auth password used to authenticate against the target database. (For Windows Auth, leave blank.)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2020-10-02T14:46:20.483Z", + "OctopusVersion": "2020.4.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "Alex-Yates", + "Category": "dlm" +} diff --git a/step-templates/sql-execute-script-file.json.human b/step-templates/sql-execute-script-file.json.human new file mode 100644 index 000000000..dede6188f --- /dev/null +++ b/step-templates/sql-execute-script-file.json.human @@ -0,0 +1,61 @@ +{ + "Id": "709b5872-52e2-4cd9-9ec0-b4a135a0444c", + "Name": "SQL - Execute Script File", + "Description": "Execute a SQL script file", + "ActionType": "Octopus.Script", + "Version": 9, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$connection = New-Object System.Data.SqlClient.SqlConnection\r$connection.ConnectionString = $OctopusParameters['ConnectionString']\rRegister-ObjectEvent -inputobject $connection -eventname InfoMessage -action {\r write-host $event.SourceEventArgs\r} | Out-Null\r\rfunction Execute-SqlQuery($query) {\r $queries = [System.Text.RegularExpressions.Regex]::Split($query, \"^\\s*GO\\s*`$\", [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Multiline)\r\r $queries | ForEach-Object {\r $q = $_\r if ((-not [String]::IsNullOrWhiteSpace($q)) -and ($q.Trim().ToLowerInvariant() -ne \"go\")) { \r $command = $connection.CreateCommand()\r $command.CommandText = $q\r $command.CommandTimeout = $OctopusParameters['CommandTimeout']\r $command.ExecuteNonQuery() | Out-Null\r }\r }\r}\r\rWrite-Host \"Connecting\"\rtry {\r $connection.Open()\r Write-Host \"Executing script in\" $OctopusParameters['SqlScriptFile']\r $content = [IO.File]::ReadAllText($OctopusParameters['SqlScriptFile'])\r Execute-SqlQuery -query $content\r}\rcatch {\r\tif ($OctopusParameters['ContinueOnError']) {\r\t\tWrite-Host $_.Exception.Message\r\t}\r\telse {\r\t\tthrow\r\t}\r}\rfinally {\r Write-Host \"Closing connection\"\r $connection.Dispose()\r}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ConnectionString", + "Label": "Connection string", + "HelpText": "Connection string for the SQL connection. Example: + + Server=.\\SQLExpress;Database=OctoFX;Integrated Security=True; + +Bind to a variable to provide different values for different environments.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlScriptFile", + "Label": "SQL Script File", + "HelpText": "Script file to run. Can be bound to a variable. Text output by the PRINT statement in SQL will be logged to the deployment log. Use 'GO' to separate multiple commands.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ContinueOnError", + "Label": "Continue On Error", + "HelpText": "If set to true, an error with the SQL statement will simply write to the log and not cause an error in the deployment.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "CommandTimeout", + "Label": "Command Timeout", + "HelpText": "The SQL Command Timeout. By default is 30 seconds.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "joaoasrosa", + "$Meta": { + "ExportedAt": "2016-02-23T16:47:43.715+00:00", + "OctopusVersion": "3.2.8", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-execute-script-with-authentication.json.human b/step-templates/sql-execute-script-with-authentication.json.human new file mode 100644 index 000000000..5d6923e81 --- /dev/null +++ b/step-templates/sql-execute-script-with-authentication.json.human @@ -0,0 +1,243 @@ +{ + "Id": "3ec610a8-f75c-43da-8d82-8c9b7b334084", + "Name": "SQL - Execute SQL Script with SQL or Windows Authentication", + "Description": "Executes SQL script file(s) against the specified database using either SQL or Windows authentication. SQL Scripts can be hardcoded value or can be from an extracted NuGet package.", + "ActionType": "Octopus.Script", + "Version": 169, + "Properties": { + "Octopus.Action.Script.ScriptBody": "function Get-DBConnection\r +{\r + [CmdletBinding()]\r + param\r + (\r + [Parameter(Position = 0)]\r + [string]\r + [ValidateNotNullorEmpty()]\r + $serverInstance,\r +\r + [switch]\r + $SqlAuthentication,\r +\r + [string]\r + $Username,\r +\r + [string]\r + $Password\r + )\r + try\r + {\r + $connection = (New-Object Microsoft.SqlServer.Management.Smo.Server($serverInstance))\r +\r + if ($SqlAuthentication)\r + {\r + $connection.ConnectionContext.LoginSecure = $false\r + $connection.ConnectionContext.set_Login($Username)\r + $securePassword = ConvertTo-SecureString $Password -AsPlainText -Force\r + $connection.ConnectionContext.set_SecurePassword($securePassword)\r + }\r + \r + $connection.Refresh()\r + \r + return $connection\r + }\r + catch\r + {\r + throw $_.Exception.ToString()\r + }\r + \r +}\r +\r +function Invoke-ExecuteSQLScript {\r +\r + [CmdletBinding()]\r + param\r + (\r + [parameter(Mandatory = $true, Position = 0)]\r + [ValidateNotNullOrEmpty()]\r + [string]\r + $serverInstance,\r +\r + [parameter(Mandatory = $true, Position = 1)]\r + [ValidateNotNullOrEmpty()]\r + [string]\r + $dbName,\r +\r + [string]\r + $Authentication,\r +\r + [string]\r + $Username,\r +\r + [string]\r + $Password,\r +\r + [string]\r + $SQLScripts\r + )\r +\r + [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SMO\") | out-null\r +\r + if ($Authentication -eq \"SqlAuthentication\")\r + {\r + $SqlServer = Get-DBConnection -serverInstance $serverInstance -SqlAuthentication -Username $Username -Password $Password\r + }\r + else\r + {\r + $SqlServer = Get-DBConnection -serverInstance $serverInstance\r + }\r +\r + if ($null -eq $SqlServer.Databases[$dbName])\r + {\r + throw \"Database $dbName does not exist on server $serverInstance\"\r + }\r + \r + if ($null -ne $SqlServer)\r + {\r + foreach ($SQLScript in $SQLScripts.Split(\"`n\"))\r + {\r + try \r + {\r + $children = $SQLScript -replace \".*\\\\\"\r + $replacematch = $children -replace \"\\*\",\"\\*\" -replace \"\\.\",\"\\.\"\r + $parent = $SQLScript -replace $replacematch\r +\r + $scripts = Get-ChildItem -Path $parent -Filter $children\r +\r + foreach ($script in $scripts)\r + {\r + $sr = New-Object System.IO.StreamReader($script.FullName)\r + $scriptContent = $sr.ReadToEnd()\r + $SqlServer.Databases[$dbName].ExecuteNonQuery($scriptContent)\r + $sr.Close()\r +\r +\t\t\t\t\twrite-verbose (\"Executed manual script - {0}\" -f $script.Name)\r + }\r + }\r + catch \r + {\r + Write-Error $_.Exception\r + }\r + }\r + }\r +}\r +\r +if (Test-Path Variable:OctopusParameters)\r +{\r +\tif ($null -ne $DacpacPackageExtractStepName -and $DacpacPackageExtractStepName -ne '')\r + {\r + Write-Verbose \"Dacpac Package Extract Step Name not empty. Locating scripts located in the Dacpac Extract Step.\"\r + $installDirPathKey = 'Octopus.Action[{0}].Output.Package.InstallationDirectoryPath' -f $DacpacPackageExtractStepName\r + $installDirPath = $OctopusParameters[$installDirPathKey]\r + $ScriptsToExecute = Join-Path $installDirPath $SqlScripts\r + }\r + else\r + { \r + Write-Verbose \"Locating scripts from the literal entry of Octopus Parameter SQLScripts\"\r + $ScriptsToExecute = $OctopusParameters[\"SQLScripts\"]\r + }\r + if ($OctopusParameters[\"Authentication\"] -eq \"SqlAuthentication\")\r + {\r + Write-Verbose \"Using Sql Authentication\"\r + Invoke-ExecuteSQLScript -serverInstance $OctopusParameters[\"serverInstance\"] `\r + -dbName $OctopusParameters[\"dbName\"] `\r + -Authentication $OctopusParameters[\"Authentication\"] `\r + -Username $OctopusParameters[\"Username\"] `\r + -Password $OctopusParameters[\"Password\"] `\r + -SQLScripts $ScriptsToExecute\r + }\r + else\r + {\r + Write-Verbose \"Using Windows Integrated Authentication\"\r + Invoke-ExecuteSQLScript -serverInstance $OctopusParameters[\"serverInstance\"] `\r + -dbName $OctopusParameters[\"dbName\"] `\r + -SQLScripts $ScriptsToExecute\r + }\r +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "0ac8c815-697d-4212-aa73-85e265bd1a7a", + "Name": "serverInstance", + "Label": "Server Instance Name", + "HelpText": "The SQL Server Instance name", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "63a2671c-cd1e-4bd3-acad-59f656f9a698", + "Name": "dbName", + "Label": "Database Name", + "HelpText": "The database name", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bc768cdf-3d5f-4a94-8b08-647056eb3977", + "Name": "Authentication", + "Label": "Authentication", + "HelpText": "The authentication method", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "SqlAuthentication +WindowsIntegrated" + } + }, + { + "Id": "e4d6eca3-5de6-4901-8f94-5253c2aea18d", + "Name": "Username", + "Label": "Username", + "HelpText": "The username to use to connect (only applies with SqlAuthentication selected)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d00b988d-bdf3-4376-aec7-90954e3cb635", + "Name": "Password", + "Label": "Password", + "HelpText": "The password to use to connect (only applies with SqlAuthentication selected)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4de1507a-3824-46b0-bf11-126b953c73da", + "Name": "SQLScripts", + "Label": "SQL Scripts", + "HelpText": "Full path to each script name on a new line +Wildcards are accepted, eg. C:\\Scripts\\*.sql, C:\\Scripts\\Deploy*.sql", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "0fd3b146-02d1-41fc-9f5c-a830e062b239", + "Name": "DacpacPackageExtractStepName", + "Label": "DACPAC Package Extract Step Name", + "HelpText": "Optional: The step in which the DACPAC package was installed. Can be left as blank if SQLScripts is a hardcoded value.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + } + ], + "LastModifiedOn": "2021-09-16T08:42:00.000+00:00", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2021-09-16T08:42:00.000+00:00", + "OctopusVersion": "2021.3.2156", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-execute-script.json.human b/step-templates/sql-execute-script.json.human new file mode 100644 index 000000000..8575d70dd --- /dev/null +++ b/step-templates/sql-execute-script.json.human @@ -0,0 +1,172 @@ +{ + "Id": "73f89638-51d1-4fbb-b68f-b71ba9e86720", + "Name": "SQL - Execute Script", + "Description": "Execute a SQL script", + "ActionType": "Octopus.Script", + "Version": 6, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Parameters +$ConnectionString = $OctopusParameters[\"ConnectionString\"] +$ContinueOnError = $OctopusParameters[\"ContinueOnError\"] -ieq \"True\" +$SqlQuery = $OctopusParameters[\"SqlScript\"] +$CommandTimeout = $OctopusParameters[\"CommandTimeout\"] +$CaptureOutputToVariables = $OctopusParameters[\"CaptureOutputToVariables\"] -ieq \"True\" + +# Local Variables +$connection = New-Object System.Data.SqlClient.SqlConnection +$connection.ConnectionString = $ConnectionString +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +$global:outputs = @() + +Register-ObjectEvent -InputObject $connection -EventName InfoMessage -Action { + Write-Output $event.SourceEventArgs +} | Out-Null + +function Execute-SqlQuery($SqlQuery) { + $queries = [System.Text.RegularExpressions.Regex]::Split($SqlQuery, \"^s*GOs*`$\", [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Multiline) + + $queries | ForEach-Object { + $query = $_ + if ((-not [String]::IsNullOrWhiteSpace($query)) -and ($query.Trim() -ine \"GO\")) { + $command = $connection.CreateCommand() + $command.CommandText = $query + $command.CommandTimeout = $CommandTimeout + $command.ExecuteNonQuery() | Out-Null + } + } +} + +$handler = [System.Data.SqlClient.SqlInfoMessageEventHandler] { param($sender, $event) + $eventMessage = $event.Message + Write-Verbose $eventMessage + if ($CaptureOutputToVariables -eq $True) { + $global:outputs += $eventMessage + } +} + +try { +\t + Write-Output \"Attach InfoMessage event handler\" + $connection.add_InfoMessage($handler) + + Write-Output \"Connecting\" + $connection.Open() + + Write-Output \"Executing script\" + Execute-SqlQuery -SqlQuery $SqlQuery +} +catch { + if ($ContinueOnError) { + Write-Output \"Error: $($_.Exception.Message)\" + } + else { + throw + } +} +finally { + if ($null -ne $connection) { + Write-Output \"Detach InfoMessage event handler\" + $connection.remove_InfoMessage($handler) + Write-Output \"Closing connection\" + $connection.Dispose() + } +} + +if ($CaptureOutputToVariables -eq $True) { + Write-Output \"Capture output to variables is true\" + Write-Output \"Output Count: $($global:outputs.Length)\" + if ($global:outputs.Length -gt 0) { + Write-Verbose \"Setting Octopus output variables\" + for ($i = 0; $i -lt $global:outputs.Length; $i++) { + $variableName = \"SQLOutput-$($i+1)\" + $variableValue = $global:outputs[$i] + Set-OctopusVariable -Name $variableName -Value $variableValue + Write-Verbose \"Created output variable: ##{Octopus.Action[$StepName].Output.$variableName}\" + } + } + else { + Write-Verbose \"No Octopus output variables to set\" + } +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "802db0cd-f6eb-4cc6-9388-c5f6a4ae63ea", + "Name": "ConnectionString", + "Label": "Connection string", + "HelpText": "Connection string for the SQL connection. Example: + + Server=.\\SQLExpress;Database=OctoFX;Integrated Security=True; + +Bind to a variable to provide different values for different environments.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ec5a5223-3415-4cda-a888-3655f2b3e98c", + "Name": "SqlScript", + "Label": "SQL Script", + "HelpText": "Script to run. Can be bound to a variable split over multiple lines. Text output by the PRINT statement in SQL will be logged to the deployment log. Use 'GO' to separate multiple commands + +Example: + + USE MASTER + go + + BACKUP DATABASE [OctoFX] TO DISK = N'#{FilePath}' WITH COPY_ONLY, NOFORMAT, NOINIT, NAME = N'Backup created by Octopus', SKIP, NOREWIND, NOUNLOAD, STATS = 10", + "DefaultValue": "PRINT 'Hello from SQL'", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "1360f453-ad5b-4cc9-bf56-c565f62e2542", + "Name": "ContinueOnError", + "Label": "Continue On Error", + "HelpText": "If set to true, an error with the SQL statement will simply write to the log and not cause an error in the deployment.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "d2f77e7a-a3d2-453c-a784-e3587db28e12", + "Name": "CommandTimeout", + "Label": "Command Timeout", + "HelpText": "The SQL Command Timeout. By default is 30 seconds.", + "DefaultValue": "30", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "90b930a8-39d9-43a2-991d-e2a7d374d30b", + "Name": "CaptureOutputToVariables", + "Label": "Capture SQL Output to Variable(s)", + "HelpText": "If set to true, output received from each command (via the `SqlInfoMessageEventHandler`) will be added to an Octopus [output variable](https://octopus.com/docs/projects/variables/output-variables) named **SQLOutput**, and will be suffixed with a number corresponding to the command that generated the output. The default is `False`. + +Example: + +1. `#{Octopus.Action[STEP NAME].Output.SQLOutput-1}` +2. `#{Octopus.Action[STEP NAME].Output.SQLOutput-2}`", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2022-01-21T11:31:23.426Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2022-01-21T11:31:23.426Z", + "OctopusVersion": "2021.3.12033", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-execute-scripts-ordered.json.human b/step-templates/sql-execute-scripts-ordered.json.human new file mode 100644 index 000000000..5aab9b9e3 --- /dev/null +++ b/step-templates/sql-execute-scripts-ordered.json.human @@ -0,0 +1,173 @@ +{ + "Id": "55aff8d8-61b4-4657-9a00-d002b394790e", + "Name": "SQL - Execute Scripts Ordered", + "Description": "Given a path to a folder containing SQL scripts, this module will execute each script on the database server and catalog provided. It will execute them in order based on their name.", + "ActionType": "Octopus.Script", + "Version": 64, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": " +$paramContinueOnError = $OctopusParameters['ContinueOnError'] +if($paramContinueOnError -eq $null) { $paramContinueOnError = 'False' } + +$paramVersionRegEx = $OctopusParameters['VersionRegEx'] +if($paramVersionRegEx -eq $null) { $paramVersionRegEx = 'Release(\\d+)_(\\d+)\\.' } + +$paramPathToScripts = $OctopusParameters['PathToScripts'] +if($paramPathToScripts -eq $null) { throw \"*** Path to scrips must be defined.\" } + +$paramCommandTimeout = $OctopusParameters['CommandTimeout'] +if($paramCommandTimeout -eq $null) { $paramCommandTimeout = '0' } + +$paramConnectionString = $OctopusParameters['ConnectionString'] +if($paramConnectionString -eq $null) { throw \"*** Connection string must be defined.\" } + +$continueOnError = $paramContinueOnError.ToLower() -eq 'true' + +$connection = New-Object System.Data.SqlClient.SqlConnection +$connection.ConnectionString = $paramConnectionString + +Register-ObjectEvent -inputobject $connection -eventname InfoMessage -action { + write-host $event.SourceEventArgs +} | Out-Null + +function Execute-SqlQuery($fileName) +{ + Write-Host \"Executing scripts in file '$fileName'\" + + $content = gc $fileName -raw + $queries = [System.Text.RegularExpressions.Regex]::Split($content, '\\r\ +\\s*GO\\s*\\r\ +', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) | ? { $_ -ne '' } + + foreach($q in $queries) + { + if ((-not [String]::IsNullOrWhiteSpace($q)) -and ($q.Trim().ToLowerInvariant() -ne \"go\")) + { + $command = $connection.CreateCommand() + $command.CommandText = $q + $command.CommandTimeout = $paramCommandTimeout + $command.ExecuteNonQuery() | Out-Null + } + } +} + +try +{ + Write-Host \"Executing scripts in folder '$paramPathToScripts'\" + + Write-Host \"Sorting script files based on regular expression '$paramVersionRegEx'\" + + Write-Host \"Opening SQL server connection...\" + $connection.Open() + + Get-ChildItem $paramPathToScripts *.sql | + % { + $matches = [System.Text.RegularExpressions.Regex]::Match($_.Name, $paramVersionRegEx, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase ) + new-object psobject -Property @{ \"File\"=$_; \"Level1\"=$matches.Groups[1]; \"Level2\"=$matches.Groups[2] } + } | + sort Level1, Level2 | + % { + Execute-SqlQuery -fileName $_.File.FullName + } +} +catch +{ +\tif ($continueOnError) +\t{ +\t\tWrite-Host $_.Exception.Message +\t} +\telse +\t{ +\t\tthrow +\t} +} +finally +{ + Write-Host \"Closing connection.\" + $connection.Dispose() +} +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "696365b9-e02e-49e9-8ff7-6d9145ec36df", + "Name": "ConnectionString", + "Type": "String", + "Label": "Connection String", + "HelpText": "Connection string for the SQL connection. Example: + +Server=.\\SQLExpress;Database=OctoFX;Integrated Security=True; + +Bind to a variable to provide different values for different environments.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "004c2019-b733-4010-bf76-03f3757b1e9d", + "Name": "ContinueOnError", + "Type": "String", + "Label": "Continue On Error", + "HelpText": "If set to true, an error with the SQL statement will simply write to the log and not cause an error in the deployment.", + "DefaultValue": "\"False\"", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "52add130-b220-410f-8b46-e653e2b1ef2a", + "Name": "CommandTimeout", + "Type": "String", + "Label": "Individual Go Timeout", + "HelpText": "Each individual go statements timeout value in seconds. + +A value of 0 indicates no limit (an attempt to execute a command will wait indefinitely).", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "19a1de2e-988c-4ccf-867d-bd64fb0338df", + "Name": "PathToScripts", + "Type": "String", + "Label": "Path to Script Files", + "HelpText": "The path to the SQL script files you wish to execute.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1e09f697-563a-46a6-ad6d-0c1b2013d044", + "Name": "VersionRegEx", + "Type": "String", + "Label": "Filename Version Reg Ex", + "HelpText": "The regular expression to extract major and minor version number from file name. Given Release2_61.sql, use \"Release(\\d+)\\_(\\d+)\\\\.\".", + "DefaultValue": "Release(\\d+)_(\\d+)\\.", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy":"tylerrbrown", + "$Meta": { + "ExportedAt": "2017-03-20T21:28:30.917Z", + "OctopusVersion": "3.11.12", + "Type": "ActionTemplate" + }, + "Category":"SQL" +} diff --git a/step-templates/sql-execute-sql-agent-job.json.human b/step-templates/sql-execute-sql-agent-job.json.human new file mode 100644 index 000000000..f393d158e --- /dev/null +++ b/step-templates/sql-execute-sql-agent-job.json.human @@ -0,0 +1,183 @@ +{ + "Id": "7bf5fc6b-9174-48ab-8da5-abf0eeef297a", + "Name": "SQL - Execute SQL Agent Job", + "Description": "Execute a SQL Agent Job and wait for results.", + "ActionType": "Octopus.Script", + "Version": 14, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$connection = New-Object System.Data.SqlClient.SqlConnection\r +$connection.ConnectionString = $OctopusParameters['ConnectionString']\r +Register-ObjectEvent -inputobject $connection -eventname InfoMessage -action {\r + write-host $event.SourceEventArgs\r +} | Out-Null\r +\r +function Run-SqlAgentJob($jobname,$timeout,$stepid) {\r +\t$sqlstring = @\"\r +\t\tSET NOCOUNT ON\r +\r +\t\t--Declaration\r +\t\tDECLARE @jobtorun VARCHAR(MAX) = ''\r +\t\tDECLARE @jobid\tVARCHAR(50) = ''\r +\t\tDECLARE @previousid INT\r + DECLARE @previous_status INT \r +\t\tdeclare @newid\tINT\r +\t\tDECLARE @runstatus\tINT\r +\r +\t\tCREATE TABLE #results\r +\t\t(\r +\t\t\tinstance_id INT,\r +\t\t\tjob_id\tvarchar(255),\r +\t\t\tjob_name VARCHAR(255),\r +\t\t\tstep_id\tINT,\r +\t\t\tstep_name VARCHAR(255),\r +\t\t\tsql_message_id INT,\r +\t\t\tsql_severity INT,\r +\t\t\tmessage VARCHAR(MAX),\r +\t\t\trun_status INT,\r +\t\t\trun_date INT,\r +\t\t\trun_time INT,\r +\t\t\trun_duration INT,\r +\t\t\toperator_emailed VARCHAR(255),\r +\t\t\toperator_netsent VARCHAR(255),\r +\t\t\toperator_paged VARCHAR(255),\r +\t\t\tretries_attempted INT,\r +\t\t\tserver sysname\r +\t\t)\r +\r +\t\t--Get Job ID\r +\t\tSELECT @jobid = job_id FROM msdb.dbo.sysjobs where name = @jobtorun\r +\t\tIF @jobid = ''\r + BEGIN \r + \tRAISERROR ('Job Name Not Found.', -- Message text.\r + \t\t\t\t16, -- Severity.\r + \t\t\t\t1 -- State.\r + \t\t\t\t);\r + \tRETURN\r + END\r +\r +\t\t--Store previous job history\r +\t\tINSERT INTO #results\r +\t\tEXEC sp_help_jobhistory @job_id = @jobid, @mode = 'full', @step_id = \r + SELECT @previousid = t.instance_id, @previous_status = t.run_status FROM (SELECT TOP 1 instance_id, run_status FROM #results ORDER BY instance_id DESC) t\r + PRINT 'Previous job ID: ' + CAST(@previousid AS VARCHAR(5)) + '\t\tRun Status:' + CAST(@previous_status AS VARCHAR(5))\r +\t\tSET @newid = @previousid\r +\r +\t\t--Start SQL Agent Job\r +\t\tEXEC msdb.dbo.sp_start_job @jobtorun\r +\r +\t\t--Loop for x seconds or until jobhistory has been updated with a new record\r +\t\tDECLARE @loopct\tINT = 1\r +\t\tWHILE (@newid = @previousid) and (@loopct < )\r +\t\tBEGIN\r +\t\t\tTRUNCATE TABLE #results\r +\t\t\tINSERT INTO #results\r +\t\t\t\tEXEC sp_help_jobhistory @job_id = @jobid, @mode = 'full', @step_id = \r +\r +\t\t\tSELECT @newid = instance_id, @runstatus = run_status FROM #results WHERE instance_id = (SELECT MAX(instance_id) FROM #results)\r +\r +\t\t\tPRINT 'Poll ' + CAST(@loopct AS VARCHAR(5)) + '\t\tTime: ' + CONVERT(VARCHAR(8), GETDATE(), 108) \r +\r +\t\t\tSET @loopct += 1\r +\t\t\tWAITFOR DELAY '00:00:05'\r +\t\tEND\r +\r +\t\tIF @newid = @previousid\r +\t\t\tRAISERROR ('Job did not complete in time.', -- Message text.\r +\t\t\t\t\t 16, -- Severity.\r +\t\t\t\t\t 1 -- State.\r +\t\t\t\t\t );\r +\t\tIF @runstatus <> 1\r +\t\t\tRAISERROR ('Job did not complete successfully.', -- Message text.\r +\t\t\t\t\t 16, -- Severity.\r +\t\t\t\t\t 1 -- State.\r +\t\t\t\t\t );\r +\r +\t\tPRINT ''\r +\t\tPRINT 'Time: ' + CONVERT(VARCHAR(8), GETDATE(), 108) + '\tNew Job ID:' + CAST(@newid AS VARCHAR(5)) + '\t\tRun Status:' + CAST(@runstatus AS VARCHAR(5))\r +\r +\t\t--Cleanup\r +\t\tDROP TABLE #results\r +\"@\r +\r + $jobname = $jobname -replace \"'\", \"''\"\r +\t$sqlstring = $sqlstring -replace \"\", $jobname\r +\t$sqlstring = $sqlstring -replace \"\", $timeout\r +\t$sqlstring = $sqlstring -replace \"\", $stepid\r +\t\r +\t#Debug Code\r +\t#Write-Host $sqlstring\r +\t\r +\t$command = $connection.CreateCommand()\r +\t$command.CommandText = $sqlstring\r +\t$command.CommandTimeout = 0\r +\t$command.ExecuteNonQuery() | Out-Null\r +}\r +\r +Write-Host \"Connecting\"\r +try {\r + $connection.Open()\r +\r + Write-Host \"Running SQL Agent Job\"\r + Run-SqlAgentJob -jobname $OctopusParameters['JobName'] -timeout $OctopusParameters['Timeout'] -step $OctopusParameters['Step']\r +}\r +finally {\r + Write-Host \"Closing connection\"\r + $connection.Dispose()\r +}\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ConnectionString", + "Label": "Connection String", + "HelpText": "Connection string for the SQL connection. Example: + +Server=.\\SQLExpress;Database=OctoFX;Integrated Security=True; +Bind to a variable to provide different values for different environments.", + "DefaultValue": "Server=;Database=msdb;Integrated Security=True;", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "JobName", + "Label": "Job Name", + "HelpText": "SQL Agent job to run. Can be bound to a variable split. +Text output by the PRINT statement in SQL will be logged to the deployment log.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Timeout", + "Label": "Timeout Value", + "HelpText": "The maximum length of time in 5 second intervals to wait for job completion. +The default value is 600 seconds (120 intervals x 5s = 600s)", + "DefaultValue": "120", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Step", + "Label": "Last Job Step", + "HelpText": "The number of the last step to run for the given job. +The default value is 1", + "DefaultValue": 1, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-04-06T20:01:31.948+00:00", + "LastModifiedBy": "chrisgelhaus", + "$Meta": { + "ExportedAt": "2015-04-06T20:01:54.552+00:00", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-execute-sql-files.json.human b/step-templates/sql-execute-sql-files.json.human new file mode 100644 index 000000000..4085aed48 --- /dev/null +++ b/step-templates/sql-execute-sql-files.json.human @@ -0,0 +1,346 @@ +{ + "Id": "2bd3b8ef-35b4-43e9-b6de-8e0c515f3f10", + "Name": "SQL - Execute SQL Script Files", + "Description": "Executes SQL script file(s) against the specified database using the `SQLServer` Powershell Module. This template includes an `Authentication` selector and supports SQL Authentication, Windows Authentication, and Azure Managed Identity. + +Note: If the `SqlServer` PowerShell module is not present, the template will download a temporary copy to perform the task.", + "ActionType": "Octopus.Script", + "Version": 6, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Id": "8473acaf-aaeb-4c23-923a-91f664290f16", + "Name": "template.Package", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "template.Package", + "Purpose": "" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptBody": " +function Get-ModuleInstalled { + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) { + # It is installed + return $true + } + else { + # Module not installed + return $false + } +} + +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Install-PowerShellModule { + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + + # Set TLS order + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + + # Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + # Display that we need the nuget package provider + Write-Output \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + + # Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force +} + + +function Invoke-ExecuteSQLScript { + + [CmdletBinding()] + param + ( + [parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [string] + $serverInstance, + + [parameter(Mandatory = $true, Position = 1)] + [ValidateNotNullOrEmpty()] + [string] + $dbName, + + [string] + $Authentication, + + [string] + $SQLScripts, + + [bool] + $DisplaySqlServerOutput, + + [bool] + $TrustServerCertificate + ) + + # Check to see if SqlServer module is installed + if ((Get-ModuleInstalled -PowerShellModuleName \"SqlServer\") -ne $true) { + # Display message + Write-Output \"PowerShell module SqlServer not present, downloading temporary copy ...\" + + # Download and install temporary copy + Install-PowerShellModule -PowerShellModuleName \"SqlServer\" -LocalModulesPath $LocalModules + } + + # Display + Write-Output \"Importing module SqlServer ...\" + + # Import the module + Import-Module -Name \"SqlServer\" + + $ExtractedPackageLocation = $($OctopusParameters['Octopus.Action.Package[template.Package].ExtractedPath']) + + $matchingScripts = @() + + # 1. Locate matching scripts + foreach ($SQLScript in $SQLScripts.Split(\"`n\", [System.StringSplitOptions]::RemoveEmptyEntries)) { + try { + + Write-Verbose \"Searching for scripts matching '$($SQLScript)'\" + $scripts = @() + $parent = Split-Path -Path $SQLScript -Parent + $leaf = Split-Path -Path $SQLScript -Leaf + Write-Verbose \"Parent: '$parent', Leaf: '$leaf'\" + if (-not [string]::IsNullOrWhiteSpace($parent)) { + $path = Join-Path $ExtractedPackageLocation $parent + if (Test-Path $path) { + Write-Verbose \"Searching for items in '$path' matching '$leaf'\" + $scripts += @(Get-ChildItem -Path $path -Filter $leaf) + } + else { + Write-Warning \"Path '$path' not found. Please check the path exists, and is relative to the package contents.\" + } + } + else { + Write-Verbose \"Searching in root of package for '$leaf'\" + $scripts += @(Get-ChildItem -Path $ExtractedPackageLocation -Filter $leaf) + } + + Write-Output \"Found $($scripts.Count) SQL scripts matching input '$SQLScript'\" + + $matchingScripts += $scripts + } + catch { + Write-Error $_.Exception + } + } + + # Create arguments hash table + $sqlcmdArguments = @{} + +\t# Add bound parameters + $sqlcmdArguments.Add(\"ServerInstance\", $serverInstance) + $sqlcmdArguments.Add(\"Database\", $dbName) + #$sqlcmdArguments.Add(\"Query\", $SQLScripts) + + if ($DisplaySqlServerOutput) + { + \tWrite-Host \"Adding Verbose to argument list to display output ...\" + $sqlcmdArguments.Add(\"Verbose\", $DisplaySqlServerOutput) + } + + if ($TrustServerCertificate) + { + \t$sqlcmdArguments.Add(\"TrustServerCertificate\", $TrustServerCertificate) + } + + # Only execute if we have matching scripts + if ($matchingScripts.Count -gt 0) { + foreach ($script in $matchingScripts) { + $sr = New-Object System.IO.StreamReader($script.FullName) + $scriptContent = $sr.ReadToEnd() + + # Execute based on selected authentication method + switch ($Authentication) { + \"AzureADManaged\" { + # Get login token + Write-Verbose \"Authenticating with Azure Managed Identity ...\" + + $response = Invoke-WebRequest -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fdatabase.windows.net%2F' -Method GET -Headers @{Metadata = \"true\" } -UseBasicParsing + $content = $response.Content | ConvertFrom-Json + $AccessToken = $content.access_token + + $sqlcmdArguments.Add(\"AccessToken\", $AccessToken) + + break + } + \"SqlAuthentication\" { + Write-Verbose \"Authentication with SQL Authentication ...\" + $sqlcmdArguments.Add(\"Username\", $username) + $sqlcmdArguments.Add(\"Password\", $password) + + break + } + \"WindowsIntegrated\" { + Write-Verbose \"Authenticating with Windows Authentication ...\" + break + } + } + + $sqlcmdArguments.Add(\"Query\", $scriptContent) + + # Invoke sql cmd + Invoke-SqlCmd @sqlcmdArguments + + $sr.Close() + + Write-Verbose (\"Executed manual script - {0}\" -f $script.Name) + } + } +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules$([System.IO.Path]::PathSeparator)$env:PSModulePath\" + +if (Test-Path Variable:OctopusParameters) { + Write-Verbose \"Locating scripts from the literal entry of Octopus Parameter SQLScripts\" + $ScriptsToExecute = $OctopusParameters[\"SQLScripts\"] + $DisplaySqlServerOutput = $OctopusParameters[\"ExecuteSQL.DisplaySQLServerOutput\"] -ieq \"True\" + $TemplateTrustServerCertificate = [System.Convert]::ToBoolean($OctopusParameters[\"ExecuteSQL.TrustServerCertificate\"]) + + Invoke-ExecuteSQLScript -serverInstance $OctopusParameters[\"serverInstance\"] ` + -dbName $OctopusParameters[\"dbName\"] ` + -Authentication $OctopusParameters[\"Authentication\"] ` + -SQLScripts $ScriptsToExecute ` + -DisplaySqlServerOutput $DisplaySqlServerOutput ` + -TrustServerCertificate $TemplateTrustServerCertificate +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "1f2b60c9-b85c-4c23-a313-fc18e82cd500", + "Name": "serverInstance", + "Label": "Server Instance Name", + "HelpText": "The SQL Server Instance name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9884b8c1-01a0-4c6f-97b1-ff5146fa0836", + "Name": "dbName", + "Label": "Database Name", + "HelpText": "The database name", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "05dc20d9-f75c-4971-9efb-e9aaad82a3a9", + "Name": "Authentication", + "Label": "Authentication", + "HelpText": "The authentication method", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "SqlAuthentication|SQL Authentication +WindowsIntegrated|Windows Integrated +AzureADManaged|Azure Active Directory Managed Identity" + } + }, + { + "Id": "4c2fc1b4-bdd0-4a9a-adb1-da1e818e62bc", + "Name": "Username", + "Label": "Username", + "HelpText": "The username to use to connect (only applies with SqlAuthentication selected)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "819f9b19-042d-42d8-9d19-5f5bf28c06b7", + "Name": "Password", + "Label": "Password", + "HelpText": "The password to use to connect (only applies with SqlAuthentication selected)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "dd22f955-8317-4d58-8173-fc7d44df1192", + "Name": "SQLScripts", + "Label": "SQL Scripts", + "HelpText": "Provide the path to search for matching scripts, each one on a new line. Wildcards for filenames only are accepted, e.g. +- `/Scripts/*.sql` +- `/Scripts/SQL/Deploy*.sql` +- `src/Permissions/Pre*Permissions.sql` + +**Please Note:** The step looks for files relative to the extracted package location, and does *not* recursively search the folder hierarchy.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "3bfed638-6649-438f-a02b-353e36a63c87", + "Name": "template.Package", + "Label": "Package", + "HelpText": "Package containing the SQL scripts to be executed.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "c6b85a12-bf2f-4963-8526-ebbe8f14707d", + "Name": "ExecuteSQL.DisplaySQLServerOutput", + "Label": "Display SQL Output", + "HelpText": "You can display SQL Server message output, such as those that result from the SQL `PRINT` statement, by checking this parameter", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "52d8f897-696b-4f77-87b5-383d6ce559c3", + "Name": "ExecuteSQL.TrustServerCertificate", + "Label": "Trust Server Certificate", + "HelpText": "Force connection to trust the server certificate.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2024-07-12T22:26:51.480Z", + "OctopusVersion": "2024.2.9303", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "sql" + } diff --git a/step-templates/sql-fix-orphaned-user.json.human b/step-templates/sql-fix-orphaned-user.json.human new file mode 100644 index 000000000..97d53cfba --- /dev/null +++ b/step-templates/sql-fix-orphaned-user.json.human @@ -0,0 +1,185 @@ +{ + "Id": "e56e9b28-1cf2-4646-af70-93e31bcdb86b", + "Name": "SQL - Fix Orphaned User", + "Description": "Will fix an orphaned user in the database by re-associating the SID.", + "ActionType": "Octopus.Script", + "Version": 1, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Install-PowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + + # Set TLS order + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12 + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force + +} + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\" + +# Check to see if SqlServer module is installed +if (((Get-ModuleInstalled -PowerShellModuleName \"SqlServer\") -ne $true) -and ((Get-ModuleInstalled -PowerShellModuleName \"SQLPS\") -ne $true)) +{ + # Display message + Write-Output \"PowerShell module SqlServer not present, downloading temporary copy ...\" + + # Download and install temporary copy + Install-PowerShellModule -PowerShellModuleName \"SqlServer\" -LocalModulesPath $LocalModules + + # Display + Write-Output \"Importing module SqlServer ...\" + + # Import the module + Import-Module -Name \"SqlServer\" +} + +Write-Host \"SqlLoginWhoHasRights $autoFixSqlLoginUserWhoHasRights\" +Write-Host \"SqlServer $autoFixSqlServer\" +Write-Host \"DatabaseName $autoFixDatabaseName\" +Write-Host \"SqlLogin $autoFixSqlLogin\" + +if ([string]::IsNullOrWhiteSpace($autoFixSqlLoginUserWhoHasRights) -eq $true){ +\tWrite-Host \"No username found, using integrated security\" + $connectionString = \"Server=$autoFixSqlServer;Database=$autoFixDatabaseName;integrated security=true;\" +} +else { +\tWrite-Host \"Username found, using SQL Authentication\" + $connectionString = \"Server=$autoFixSqlServer;Database=$autoFixDatabaseName;User ID=$autoFixSqlLoginUserWhoHasRights;Password=$autoFixSqlLoginPasswordWhoHasRights;\" +} + +# Build sql query +$sqlQuery = @\" +DECLARE @OrphanedUsers TABLE +( +\tUserName VARCHAR(50) null, +\tUserSID VARBINARY(100) null +) + +INSERT INTO @OrphanedUsers EXEC sp_change_users_login 'Report' + +IF EXISTS ( SELECT UserName FROM @OrphanedUsers WHERE UserName = '$autoFixSqlLogin' ) +\tBEGIN +\t\tPRINT '$autoFixSqlLogin is orphaned, fixing ...' + EXEC sp_change_users_login 'Auto_Fix', '$autoFixSqlLogin' + END +ELSE +\tPRINT '$autoFixSqlLogin is not orphaned.' +\"@ + +# Execute the command to find orphaned users, then fix if matching +Invoke-SqlCmd -ConnectionString $connectionString -Query $sqlQuery -Verbose + +" + }, + "Parameters": [ + { + "Id": "083897f2-d65d-45a5-b9fb-ef760a727303", + "Name": "autoFixSqlServer", + "Label": "SQL Server", + "HelpText": "The SQL Server to perform the work on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ead6a85f-71e6-4c42-b6b2-ef048edabcbd", + "Name": "autoFixSqlLoginUserWhoHasRights", + "Label": "SQL Login", + "HelpText": "The login of the user who has permissions to create a database. + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "eb648cdd-0390-4569-8af9-e4e51946585f", + "Name": "autoFixSqlLoginPasswordWhoHasRights", + "Label": "SQL Password", + "HelpText": "The password of the user who has permissions to create SQL Logins + +Leave blank for integrated security", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "835159c0-7c5a-4714-ad9e-888dd29e6cd3", + "Name": "autoFixDatabaseName", + "Label": "Database Name", + "HelpText": "The name of the database to create the user on", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7be27e8c-efef-4700-8b7f-fba78a25788f", + "Name": "autoFixSqlLogin", + "Label": "SQL Login", + "HelpText": "The username to attach to the database if it does not exist", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-07-22T00:41:40.086Z", + "OctopusVersion": "2020.2.16", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "sql" + } diff --git a/step-templates/sql-restore-database.json.human b/step-templates/sql-restore-database.json.human new file mode 100644 index 000000000..a68be08a3 --- /dev/null +++ b/step-templates/sql-restore-database.json.human @@ -0,0 +1,263 @@ +{ + "Id": "469b6d9d-761a-4f94-9745-20e9c2f93841", + "Name": "SQL - Restore Database", + "Description": "Restore a MS SQL Server database to the file system.", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$ServerName = $OctopusParameters['Server'] +$DatabaseName = $OctopusParameters['Database'] +$BackupDirectory = $OctopusParameters['BackupDirectory'] +$CompressionOption = [int]$OctopusParameters['Compression'] +$Devices = [int]$OctopusParameters['Devices'] +$Stamp = $OctopusParameters['Stamp'] +$SqlLogin = $OctopusParameters['SqlLogin'] +$SqlPassword = $OctopusParameters['SqlPassword'] +$DateFormat = $OctopusParameters['DateFormat'] +$Separator = $OctopusParameters['Separator'] +$ErrorActionPreference = \"Stop\" + +function ConnectToDatabase() +{ + param($server, $SqlLogin, $SqlPassword) + + if ($SqlLogin -ne $null) { + + if ($SqlPassword -eq $null) { + throw \"SQL Password must be specified when using SQL authentication.\" + } + + $server.ConnectionContext.LoginSecure = $false + $server.ConnectionContext.Login = $SqlLogin + $server.ConnectionContext.Password = $SqlPassword + + Write-Host \"Connecting to server using SQL authentication as $SqlLogin.\" + $server = New-Object Microsoft.SqlServer.Management.Smo.Server $server.ConnectionContext + } + else { + Write-Host \"Connecting to server using Windows authentication.\" + } + + try { + $server.ConnectionContext.Connect() + } catch { + Write-Error \"An error occurred connecting to the database server!`r`n$($_.Exception.ToString())\" + } +} + +function AddPercentHandler { + param($smoBackupRestore, $action) + + $percentEventHandler = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { Write-Host $dbName $action $_.Percent \"%\" } + $completedEventHandler = [Microsoft.SqlServer.Management.Common.ServerMessageEventHandler] { Write-Host $_.Error.Message} + + $smoBackupRestore.add_PercentComplete($percentEventHandler) + $smoBackupRestore.add_Complete($completedEventHandler) + $smoBackupRestore.PercentCompleteNotification=10 +} + +function CreateDevice { + param($smoBackupRestore, $directory, $name) + + $devicePath = Join-Path $directory ($name) + $smoBackupRestore.Devices.AddDevice($devicePath, \"File\") + return $devicePath +} + +function CreateDevices { + param($smoBackupRestore, $devices, $directory, $dbName) + + $targetPaths = New-Object System.Collections.Generic.List[System.String] + + if ($devices -eq 1){ + $deviceName = $dbName + $Separator + $timestamp + \".bak\" + $targetPath = CreateDevice $smoBackupRestore $directory $deviceName + $targetPaths.Add($targetPath) + } else { + for ($i=1; $i -le $devices; $i++){ + $deviceName = $dbName + \"_\" + $timestamp + \"_\" + $i + \".bak\" + $targetPath = CreateDevice $smoBackupRestore $directory $deviceName + $targetPaths.Add($targetPath) + } + } + return $targetPaths +} + +function RelocateFiles{ + param($smoRestore) + + foreach($file in $smoRestore.ReadFileList($server)) + { + $relocateFile = New-Object Microsoft.SqlServer.Management.Smo.RelocateFile + $relocateFile.PhysicalFileName = $server.Settings.DefaultFile + $file.LogicalName + [System.IO.Path]::GetExtension($file.PhysicalName) + $relocateFile.LogicalFileName = $file.LogicalName + $smoRestore.RelocateFiles.Add($relocateFile) + } +} + +function RestoreDatabase { + param($dbName, $devices) + + $smoRestore = New-Object Microsoft.SqlServer.Management.Smo.Restore + $targetPaths = CreateDevices $smoRestore $devices $BackupDirectory $dbName $timestamp + + Write-Host \"Attempting to restore database $ServerName.$dbName from:\" + $targetPaths + Write-Host \"\" + + foreach ($path in $targetPaths) { + if (-not (Test-Path $path)) { + Write-Host \"Cannot find backup device \"($path) + return + } + } + + if ($server.Databases[$dbName] -ne $null) + { + $server.KillAllProcesses($dbName) + $server.KillDatabase($dbName) + } + + $smoRestore.Action = \"Database\" + $smoRestore.NoRecovery = $false; + $smoRestore.ReplaceDatabase = $true; + $smoRestore.Database = $dbName + + RelocateFiles $smoRestore + + try { + AddPercentHandler $smoRestore \"restored\" + $smoRestore.SqlRestore($server) + } catch { + Write-Error \"An error occurred restoring the database!`r`n$($_.Exception.ToString())\" + } + + Write-Host \"Restore completed successfully.\" +} + +[System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SMO\") | Out-Null +[System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SmoExtended\") | Out-Null +[System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.ConnectionInfo\") | Out-Null +[System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SmoEnum\") | Out-Null + +$server = New-Object Microsoft.SqlServer.Management.Smo.Server $ServerName + +ConnectToDatabase $server $SqlLogin $SqlPassword + +$database = $server.Databases | Where-Object { $_.Name -eq $DatabaseName } +$timestamp = if(-not [string]::IsNullOrEmpty($Stamp)) { $Stamp } else { Get-Date -format $DateFormat } + +RestoreDatabase $DatabaseName $Devices", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Server", + "Label": "Server", + "HelpText": "The name of the SQL Server instance that the database resides in.", + "DefaultValue": ".", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Database", + "Label": "Database", + "HelpText": "The name of the database to restore.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BackupDirectory", + "Label": "Backup Directory", + "HelpText": "The backup directory to retrieve the database backup from.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlLogin", + "Label": "SQL login", + "HelpText": "The SQL auth login to connect with. If specified, the SQL Password must also be entered.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlPassword", + "Label": "SQL password", + "HelpText": "The password for the SQL auth login to connect with. Only used if SQL Login is specified.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "Compression", + "Label": "Compression Option", + "HelpText": "- 0 - Use the default backup compression server configuration +- 1 - Enable the backup compression +- 2 - Disable the backup compression", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "0|Default +1|Enabled +2|Disabled" + } + }, + { + "Name": "Devices", + "Label": "Devices", + "HelpText": "The number of backup devices to use for the backup.", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "1|1 +2|2 +3|3 +4|4" + } + }, + { + "Name": "Stamp", + "Label": "Backup file suffix", + "HelpText": "Specify a suffix to add to the backup file names. If left blank, the current date, in the format given by the DateFormat parameter, is used.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "Separator", + "Label": "Separator", + "HelpText": "Separator used between database name and suffix.", + "DefaultValue": "_" + }, + { + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "DateFormat", + "Label": "Date Format", + "HelpText": "Date format to use if backup is suffixed with a date stamp (e.g. yyyy-MM-dd)", + "DefaultValue": "yyyy-MM-dd" + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2016-03-24T16:32:09.272+00:00", + "OctopusVersion": "3.2.8", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-smo-change-usermode.json.human b/step-templates/sql-smo-change-usermode.json.human new file mode 100644 index 000000000..0bc28c802 --- /dev/null +++ b/step-templates/sql-smo-change-usermode.json.human @@ -0,0 +1,146 @@ +{ + "Id": "28a8ba94-d4cd-4638-b0e9-6e0e415300fc", + "Name": "SQL - Change User Mode Using SMO", + "Description": "This uses Sql Management Objects to change user access mode. If the username and password are both empty then it will attempt a trusted connection using integrated security.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SMO\") | out-null + +$SqlUsername = $OctopusParameters['SqlUsername'] +$SqlServer = $OctopusParameters['SqlServer'] +$SqlPassword = $OctopusParameters['SqlPassword'] +$SqlDatabase = $OctopusParameters['SqlDatabase'] +$UserAccess = $OctopusParameters['UserAccess'] +$Condition = $OctopusParameters['Condition'] + +try +{ + $server = new-object ('Microsoft.SqlServer.Management.Smo.Server') $SqlServer + + if ($SqlUsername -and $SqlPassword) + { + Write-Host \"Connecting to $SqlServer as $SqlUsername\" + $server.ConnectionContext.LoginSecure = $false + $server.ConnectionContext.set_Login($SqlUsername) + $server.ConnectionContext.set_Password($SqlPassword) + } + else { + Write-Host \"Connecting to $SqlServer with integrated security\" + $server.ConnectionContext.LoginSecure = $true + } + + $db = $server.Databases[$SqlDatabase] +\tif ($db -eq $null) +\t{ + Write-Host \"Database $SqlDatabase not found, skipping step...\" +\t} else { +\t Write-Host \"Setting user mode to $UserAccess for database $SqlDatabase with condition $Condition\" + $db.UserAccess = $UserAccess + + if ($Condition -eq 'Force'){ + $db.Alter([Microsoft.SqlServer.Management.Smo.TerminationClause]::RollbackTransactionsImmediately) + } elseif ($Condition -eq 'Fail'){ + $db.Alter([Microsoft.SqlServer.Management.Smo.TerminationClause]::FailOnOpenTransactions) + } else { + $db.Alter() + } +\t} +} +catch +{ + $error[0] | format-list -force + Exit 1 +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "98edcfe4-ffdf-483b-b3a5-ac4ba499cb6e", + "Name": "SqlServer", + "Label": "Sql Server", + "HelpText": "SQL Server Instance with Port", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "65fa5933-9e1d-498e-afd1-36a58bf7a4f6", + "Name": "SqlUsername", + "Label": "Sql Username (optional)", + "HelpText": "The SQL Account which has access to Create SQL Database", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b9e77f91-c083-47b4-b6ac-514929dd96f6", + "Name": "SqlPassword", + "Label": "Sql Password (optional)", + "HelpText": "The password for the SQL Account", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "df1c87f9-786e-4d87-9c69-4dd447ac6f81", + "Name": "SqlDatabase", + "Label": "Sql Database", + "HelpText": "Name of Database to be created if not already there", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "23a3485a-56c3-47ae-9755-9095eb2005f4", + "Name": "UserAccess", + "Label": "User Access", + "HelpText": "The User Access mode to set database to", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Multiple|Multiple +Restricted|Restricted +Single|Single" + }, + "Links": {} + }, + { + "Id": "92f2235d-6010-4387-b19d-5a23b57f7125", + "Name": "Condition", + "Type": "String", + "Label": "Alter Condition", + "HelpText": "Condition to use when calling [Alter](https://msdn.microsoft.com/en-us/library/ms205110.aspx) method.", + "DefaultValue": "Force", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Force|Force +Fail|Fail on open transactions +Wait|Wait for no connections" + }, + "Links": {} + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2017-03-29T12:50:15.802Z", + "OctopusVersion": "3.10.0", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-smo-create-database.json.human b/step-templates/sql-smo-create-database.json.human new file mode 100644 index 000000000..c4185f621 --- /dev/null +++ b/step-templates/sql-smo-create-database.json.human @@ -0,0 +1,99 @@ +{ + "Id": "1f08d4e0-025d-483d-a8d6-efa67fcdd1c1", + "Name": "SQL - Create Database Using SMO (only if does not exists)", + "Description": "This uses Sql Management Objects to create a database. If the username and password are both empty then it will attempt a trusted connection using integrated security.", + "ActionType": "Octopus.Script", + "Version": 11, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SMO\") | out-null + + +$SqlUsername = $OctopusParameters['SqlUsername'] +$SqlServer = $OctopusParameters['SqlServer'] +$SqlPassword = $OctopusParameters['SqlPassword'] +$SqlDatabase = $OctopusParameters['SqlDatabase'] + +try +{ + $server = new-object ('Microsoft.SqlServer.Management.Smo.Server') $SqlServer + + if ($SqlUsername -and $SqlPassword) + { + Write-Host \"Connecting to $SqlServer as $SqlUsername\" + $server.ConnectionContext.LoginSecure = $false + $server.ConnectionContext.set_Login($SqlUsername) + $server.ConnectionContext.set_Password($SqlPassword) + } + else { + Write-Host \"Connecting to $SqlServer with integrated security\" + $server.ConnectionContext.LoginSecure = $true + } + +\tif ($server.databases[$SqlDatabase] -eq $null) +\t{ +\t Write-Host \"Creating database $SqlDatabase\" + \t$db = New-Object Microsoft.SqlServer.Management.Smo.Database($server, $SqlDatabase) + $db.Create() +\t} else { +\t Write-Host \"Database $SqlDatabase already exists, skipping step...\" +\t} +} +catch +{ + $error[0] | format-list -force + Exit 1 +} + ", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "SqlServer", + "Label": "Sql Server", + "HelpText": "SQL Server Instance with Port", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlUsername", + "Label": "Sql Username (optional)", + "HelpText": "The SQL Account which has access to Create SQL Database", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlPassword", + "Label": "Sql Password (optional)", + "HelpText": "The password for the SQL Account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlDatabase", + "Label": "Sql Database", + "HelpText": "Name of Database to be created if not already there", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2016-07-12T12:33:49.751+00:00", + "OctopusVersion": "3.3.10", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-smo-create-login-and-user.json.human b/step-templates/sql-smo-create-login-and-user.json.human new file mode 100644 index 000000000..2801b6110 --- /dev/null +++ b/step-templates/sql-smo-create-login-and-user.json.human @@ -0,0 +1,136 @@ +{ + "Id": "7ed93dfa-b137-4341-9c6c-84fa0565d865", + "Name": "SQL - Create Database Login and User using SMO", + "Description": "Requires SMO to be installed on the machine where this step will be run.", + "ActionType": "Octopus.Script", + "Version": 9, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null\r +\r +try\r +{\r + $server = New-Object ('Microsoft.SqlServer.Management.Smo.Server') $SMO_SqlServer\r + \r + $server.ConnectionContext.LoginSecure = $true\r +\r + if(!$server.Databases.Contains($SMO_SqlDatabase))\r + {\r + throw \"Server $SMO_SqlServer does not contain a database named $SMO_SqlDatabase\"\r + }\r +\r + if ($server.Logins.Contains($SMO_LoginName))\r + {\r + Write-Host \"Login $SMO_LoginName already exists in the server $SMO_SqlServer\"\r + }\r + else\r + {\r + Write-Host \"Login $SMO_LoginName does not exist, creating\"\r + $login = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Login -ArgumentList $SMO_SqlServer, $SMO_LoginName\r + $login.LoginType = [Microsoft.SqlServer.Management.Smo.LoginType]::WindowsUser\r + $login.PasswordExpirationEnabled = $false\r + $login.Create()\r + Write-Host \"Login $SMO_LoginName created successfully.\"\r + }\r +\r + $database = $server.Databases[$SMO_SqlDatabase]\r +\r + if ($database.Users[$SMO_LoginName])\r + {\r + Write-Host \"User $SMO_LoginName already exists in the database $SMO_SqlDatabase\"\r + }\r + else\r + {\r + Write-Host \"User $SMO_LoginName does not exist in the database $SMO_SqlDatabase, creating.\"\r + $dbUser = New-Object -TypeName Microsoft.SqlServer.Management.Smo.User -ArgumentList $database, $SMO_LoginName\r + $dbUser.Login = $SMO_LoginName\r + $dbUser.Create()\r + Write-Host \"User $SMO_LoginName created successfully in the database $SMO_SqlDatabase.\"\r + }\r +\r + if($SMO_SqlRole -ne $null)\r + {\r + $SMO_SqlRoles = $SMO_SqlRole.Split(\",\")\r + \r + # Remove the user from any roles which aren't specified in the $SMO_SqlRole parameter if they are a member\r + $database.Users[$SMO_LoginName].EnumRoles() | ForEach {\r + if (!$SMO_SqlRoles.Contains($_)) {\r + $dbRole = $database.Roles[$_]\r + $dbRole.DropMember($SMO_LoginName)\r + $dbRole.Alter()\r + Write-Host \"User $SMO_LoginName removed from $_ role in the database $SMO_SqlDatabase.\"\r + }\r + }\r + \r + # Add the user to any roles which are specified in the $SMO_SqlRole parameter if they are not already a member\r + $SMO_SqlRoles | ForEach {\r + $dbRole = $database.Roles[$_]\r + if(!$dbRole) { throw \"Database $SMO_SqlDatabase does not contain a role named $_\" }\r +\r + if (!$dbRole.EnumMembers().Contains($SMO_LoginName))\r + {\r + $dbRole.AddMember($SMO_LoginName)\r + $dbRole.Alter()\r + Write-Host \"User $SMO_LoginName successfully added to $_ role in the database $SMO_SqlDatabase.\"\r + }\r + }\r + }\r +}\r +catch\r +{\r + $error[0] | format-list -force\r + Exit 1\r +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Id": "24633e35-94cb-4b69-befe-0ef2616c3071", + "Name": "SMO_SqlServer", + "Label": "Database Server Name", + "HelpText": "Name of the to create the login for.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b79baa48-af97-4ee0-bf79-822fbd529636", + "Name": "SMO_SqlDatabase", + "Label": "Database Name", + "HelpText": "Name of the database. The created Login and User will get the role dbowner by defaultfor this database.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d189015a-237d-46eb-839b-84c3572c40d1", + "Name": "SMO_LoginName", + "Label": "Windows Login Name", + "HelpText": "The login name to create a login and user in the database for. In our projects we use integrated security - you should too.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9bf187dd-bcf1-4fba-a5b0-2bcddde7ef73", + "Name": "SMO_SqlRole", + "Label": "Database Role Names", + "HelpText": "We default to `db_owner`, you might want to change this to suit your needs. You may specify multiple roles separated by a comma (e.g. `db_datareader,db_datawriter`)", + "DefaultValue": "db_owner", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2022-08-30T13:37:42.214+00:00", + "LastModifiedBy": "thomasdc", + "$Meta": { + "ExportedAt": "2022-08-30T13:37:42.214Z", + "OctopusVersion": "3.12.5", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-smo-drop-database.json.human b/step-templates/sql-smo-drop-database.json.human new file mode 100644 index 000000000..db0ff43a6 --- /dev/null +++ b/step-templates/sql-smo-drop-database.json.human @@ -0,0 +1,84 @@ +{ + "Id": "255a7317-460f-4bc4-8017-a99b4563aad3", + "Name": "SQL - Drop Database Using SMO", + "Description": "This uses Sql Management Objects to drop a database if it exists. If the username and password are both empty then it will attempt a trusted connection.", + "ActionType": "Octopus.Script", + "Version": 9, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SMO\") | out-null + +try +{ + $server = new-object ('Microsoft.SqlServer.Management.Smo.Server') $SqlServer + + if (!$SqlUsername -and !$SqlPassword) + { + $server.ConnectionContext.LoginSecure = $true + } else { + $server.ConnectionContext.LoginSecure = $false + $server.ConnectionContext.set_Login($SqlUsername) + $server.ConnectionContext.set_Password($SqlPassword) + } + +\tif ($server.databases[$SqlDatabase] -ne $null) +\t{ + \t$server.killallprocesses($SqlDatabase) + \t$server.databases[$SqlDatabase].drop() +\t} +} +catch +{ + $error[0] | format-list -force + Exit 1 +} + ", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "SqlServer", + "Label": "Sql Server", + "HelpText": "SQL Server Instance with Port", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlUsername", + "Label": "Sql Username (optional)", + "HelpText": "The SQL Account which has access to Create SQL Database", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlPassword", + "Label": "Sql Password (optional)", + "HelpText": "The password for the SQL Account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlDatabase", + "Label": "Sql Database", + "HelpText": "Name of Database to be Dropped", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-05-20T13:00:12.964+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-test-connection-string.json.human b/step-templates/sql-test-connection-string.json.human new file mode 100644 index 000000000..83b419f5a --- /dev/null +++ b/step-templates/sql-test-connection-string.json.human @@ -0,0 +1,51 @@ +{ + "Id": "7e43cab7-e56c-4c37-a471-f4d552815169", + "Name": "SQL - Test Connection String", + "Description": "Tests a SQL Server connection string by attempting to connect to the database.", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.ScriptBody": "#Create SQL Connection\r +$con = new-object \"System.data.sqlclient.SQLconnection\"\r +Write-Host \"Opening SQL connection to $ConnectionString\"\r +\r +$con.ConnectionString =(\"$ConnectionString\")\r +try {\r + $con.Open()\r + Write-Host \"Successfully opened connection to the database\"\r +}\r +catch {\r + $error[0]\r + exit 1\r +}\r +finally{\r + Write-Host \"Closing SQL connection\"\r + $con.Close()\r + $con.Dispose()\r + Write-Host \"Connection closed.\"\r +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ConnectionString", + "Label": "Connection string", + "HelpText": "The connection string. For example: + +> Data Source=MyServer;Initial Catalog=MyDatabase;User Id=admin; Password=supersecretpassword;", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-06-26T10:19:31.547+00:00", + "OctopusVersion": "2.4.8.107", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sql-update-job.json.human b/step-templates/sql-update-job.json.human new file mode 100644 index 000000000..2575874aa --- /dev/null +++ b/step-templates/sql-update-job.json.human @@ -0,0 +1,174 @@ +{ + "Id": "91bbd24f-8975-4d0e-9f55-736587f945e9", + "Name": "SQL - Update Job", + "Description": "Updates a MS SQL server job with provided ID to be enabled or disabled", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "param( + [string]$ConnectionString, + [string]$JobId, + [string]$JobName, + [string]$JobStatus +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if ($result -eq $null) { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +function Execute-SqlQuery($query) { + $queries = [System.Text.RegularExpressions.Regex]::Split($query, \"^\\s*GO\\s*$$\", [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Multiline) + + $queries | ForEach-Object { + $q = $_ + if (!(StringIsNullOrWhitespace($q)) -and ($q.Trim().ToLowerInvariant() -ne \"go\")) { + $command = $connection.CreateCommand() + $command.CommandText = $q + $command.ExecuteNonQuery() | Out-Null + } + } +} + +& { + param( + [string]$ConnectionString, + [string]$JobId, + [string]$JobName, + [string]$JobStatus + ) + + $jobStatusText = '' + if ($JobStatus -eq '1') { + $jobStatusText = \"Enabling\" + } elseif ($JobStatus -eq '0') { + $jobStatusText = \"Disabling\" + } + + $jobDisplayName = '' + if ($JobName) { + $jobDisplayName = $JobName + } else { + \t$jobDisplayName = $JobId + } + + Write-Highlight \"$jobStatusText SQL Server job: [$jobDisplayName]\" + Write-Verbose \"SQL Server Job Id: [$JobId]\" + + $query = @\" +GO +USE [msdb] +GO +EXEC msdb.dbo.sp_update_job @job_id=N'$JobId', @enabled=$JobStatus +GO +\"@ + +\t$connection = New-Object System.Data.SqlClient.SqlConnection + $connection.ConnectionString = $ConnectionString + Register-ObjectEvent -inputobject $connection -eventname InfoMessage -action { + write-host $event.SourceEventArgs + } | Out-Null + + Write-Verbose \"Connecting\" + try { + $connection.Open() + + Write-Verbose \"Executing script\" + Write-Verbose $query + Execute-SqlQuery -query $query + } + catch [Exception] + { + Write-Verbose $_.Exception|format-list -force + throw $_ + } + finally { + Write-Verbose \"Closing connection\" + $connection.Dispose() + } + + } ` + (Get-Param 'ConnectionString' -Required) ` + (Get-Param 'JobId' -Required) ` + (Get-Param 'JobName') ` + (Get-Param 'JobStatus' -Required)" + }, + "Parameters": [ + { + "Id": "41a33da5-012d-4871-a3e8-983fa4a5dcbe", + "Name": "JobId", + "Label": "Job Id", + "HelpText": "The SQL server job id which is a `GUID`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ba6bdd3b-ebfb-4c47-b214-f95044c8460e", + "Name": "JobName", + "Label": "Job name", + "HelpText": "Optional job name to show on the logs instead of JobId", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a2ea4eca-77e3-4733-9714-9fa2b87929e7", + "Name": "JobStatus", + "Label": "Job Status", + "HelpText": "Choose `Enable` to enable the job, and `Disabled` to disable the job", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "0|Disabled +1|Enabled" + } + }, + { + "Id": "0bdc16b0-0086-4597-9dcd-970ddbdda258", + "Name": "ConnectionString", + "Label": "ConnectionString", + "HelpText": "The connection string to connect to the target SQL Server", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-11-20T12:26:07.817Z", + "OctopusVersion": "2020.4.0", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "aqovia", + "Category": "sql" + } diff --git a/step-templates/sqlazure-restore-database-from-azure-storage.json.human b/step-templates/sqlazure-restore-database-from-azure-storage.json.human new file mode 100644 index 000000000..d17dd304f --- /dev/null +++ b/step-templates/sqlazure-restore-database-from-azure-storage.json.human @@ -0,0 +1,292 @@ +{ + "Id": "6936e720-17a2-4a17-97d6-8f19ee040b01", + "Name": "SQLAzure - Restore a SQL Azure database from a .bacpac located in Azure Storage", + "Description": "Given an existing [.bacpac](https://msdn.microsoft.com/en-us/library/azure/hh335292.aspx) in Azure Storage this template restores a SQL Azure database. + +**Note** - The storage account used needs to reside in the the same subscription as the SQL Azure Server.", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.ScriptBody": "#Check for the PowerShell cmdlets +try{ + Import-Module Azure -ErrorAction Stop +}catch{ + + $azureServiceModulePath = \"C:\\Program Files (x86)\\Microsoft SDKs\\Azure\\PowerShell\\ServiceManagement\\Azure\\Azure.psd1\" + Write-Output \"Unable to find the module checking $azureServiceModulePath\" + + try{ + Import-Module $azureServiceModulePath + + } + catch{ + throw \"Windows Azure PowerShell not found! Please make sure to install them from http://www.windowsazure.com/en-us/downloads/#cmd-line-tools\" + } +} + +function Set-TempAzureSubscription{ + param( + [Parameter(Mandatory=$true)][string] $subscriptionId, + [Parameter(Mandatory=$true)][string] $subscriptionName, + [Parameter(Mandatory=$true)][string] $managementCertificate + ) + + #Ensure no other subscriptions or accounts + Get-AzureSubscription | ForEach-Object { + $id = $_.SubscriptionId + Write-Output \"Removing Subscription $id\" + Remove-AzureSubscription -SubscriptionId $id -Force + } + + #Ensure there are no other + Get-AzureAccount | ForEach-Object { Remove-AzureAccount $_.ID -Force } + + [byte[]]$certificateData = [System.Convert]::FromBase64String($managementCertificate) + $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2] $certificateData + + Set-AzureSubscription -Certificate $certificate -SubscriptionId $subscriptionId -SubscriptionName $subscriptionName + Select-AzureSubscription $subscriptionName + + Write-Output \"Azure Subscription set Id: $subscriptionId Name: $subscriptionName \" + + $subscription = Get-AzureSubscription -Current + + return $subscription +} + + +function Restore-SqlAzureDatabase{ + Param( + [Parameter(Mandatory=$true)][string]$databaseServerName, + [Parameter(Mandatory=$true)][string]$databaseName, + [Parameter(Mandatory=$true)][string]$edition, + [Parameter(Mandatory=$true)][string]$serviceObjectiveName, + [Parameter(Mandatory=$true)][string]$storageName, + [Parameter(Mandatory=$true)][string]$containerName, + [Parameter(Mandatory=$true)][string]$sqlAdminUser, + [Parameter(Mandatory=$true)][string]$sqlAdminPassword, + [Parameter(Mandatory=$true)][string]$bacpacFileName, + [Parameter(Mandatory=$false)][bool]$errorIfDatabaseExists=$true + ) + + $subscription = Get-AzureSubscription -Current + + $storageKey = (Get-AzureStorageKey -StorageAccountName $storageName).Primary + $storageCtx = New-AzureStorageContext -storageaccountname $storageName -storageaccountkey $storageKey + $password = $sqlAdminPassword | ConvertTo-SecureString -asPlainText -Force + $sqlCred = New-Object System.Management.Automation.PSCredential($sqlAdminUser,$password) + $sqlCtx = New-AzureSqlDatabaseServerContext -ServerName $databaseServerName -Credential $sqlCred + + $databases = Get-AzureSqlDatabase -ServerName $databaseServerName + + #Check to see there is a database on the server + Foreach ($d in $databases){ + if($d.Name -eq $databaseName){ + $database = $d + break + } + } + + if ($database -eq $null) { + Write-Output \"The SQL Azure Database: $databaseName WAS NOT found on $databaseServerName\" + } + + if($database -ne $null){ + if ($errorIfDatabaseExists -eq $true) { + Write-Output \"A database named $databaseName already exists. If you wish to override this database set the -errorIfDatabaseExists parameter to false.\" + return; + } else { + #Delete the existing database. + Write-Output \"The SQL Azure Database: $databaseName WAS found on $databaseServerName\" + Write-Output \"WARNING! Removing SQL Azure database: $databaseName\" + Remove-AzureSqlDatabase -ServerName $databaseServerName -DatabaseName $databaseName -Force + } + } + + Write-Output \"Starting database import...\" + $importRequest = Start-AzureSqlDatabaseImport -SqlConnectionContext $sqlCtx -StorageContext $storageCtx -StorageContainerName $containerName -DatabaseName $databaseName -BlobName $bacpacFileName -Edition $edition + + Write-Output \"Database import request submitted. Request Id: $($importRequest.RequestGuid)\" + + Write-Output \"Checking import status...\" + $status = Get-AzureSqlDatabaseImportExportStatus -Username $sqlAdminUser -Password $sqlAdminPassword -ServerName $databaseServerName -RequestId $importRequest.RequestGuid + Write-Output \"Status: $($status.Status)\" + + while($status.Status.StartsWith(\"Running\") -Or $status.Status.StartsWith(\"Pending\")){ + Start-Sleep -s 10 + Write-Output \"Checking import status...\" + $status = Get-AzureSqlDatabaseImportExportStatus -Username $sqlAdminUser -Password $sqlAdminPassword -ServerName $databaseServerName -RequestId $importRequest.RequestGuid + Write-Output \"Status: $($status.Status)\" + } + + Get-AzureSqlDatabaseImportExportStatus -Username $sqlAdminUser -Password $sqlAdminPassword -ServerName $databaseServerName -RequestId $importRequest.RequestGuid + + if ($status.Status -eq \"Completed\") { + Write-Output \"Updating database service objective...\" + + #Get the service objective. + $serviceObjective = Get-AzureSqlDatabaseServiceObjective -ServerName $databaseServerName -ServiceObjectiveName $serviceObjectiveName + Set-AzureSqlDatabase -ConnectionContext $sqlCtx -DatabaseName $databaseName -Force -ServiceObjective $serviceObjective + + Write-Output \"Updated database service objective.\" + } + + return $importRequest +} + + +#Set the Azure Subscription +$subscription = Set-TempAzureSubscription -managementCertificate $AzureManagementCertificate -subscriptionId $AzureSubscriptionId -subscriptionName $AzureSubscriptionName + +Write-Output \"=============================================================\" +Write-Output \"Using SQL Azure Server $SQLAzureServerName\" +Write-Output \"Using Azure Storage Account: $AzureStorageAccountName\" +Write-Output \"Using Azure Storeage Container: $AzureStorageContainerName\" +Write-Output \"Using bacpac file: $BacPacFileName\" +Write-Output \"=============================================================\" + +Restore-SqlAzureDatabase -databaseServerName $SQLAzureServerName ` + -databaseName $databaseName ` + -edition $SQLAzureDatabaseEdition ` + -serviceObjectiveName $SQLAzureServiceObjective ` + -storageName $AzureStorageAccountName ` + -containerName $AzureStorageContainerName ` + -sqlAdminUser $SqlAzureAdminUser ` + -sqlAdminPassword $SqlAzureAdminUserPassword ` + -bacpacFileName $BacPacFileName ` + -errorIfDatabaseExists $false +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "AzureManagementCertificate", + "Label": "The Azure management certificate associated with the subscription", + "HelpText": "The Azure Management Certificate that can be sourced via https://manage.windowsazure.com/publishsettings", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AzureSubscriptionId", + "Label": "The Azure Subscription ID", + "HelpText": "The Azure Subscription ID that can be sourced via the [Azure Portal](https://portal.azure.com) or https://manage.windowsazure.com/publishsettings", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AzureSubscriptionName", + "Label": "The Azure Subscription Name", + "HelpText": "The Azure Subscription Name that can be sourced via the [Azure Portal](https://portal.azure.com) or https://manage.windowsazure.com/publishsettings", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlAzureAdminUser", + "Label": "The SQL Azure Server Admin User", + "HelpText": "The SQL Azure Server Administrator User", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SqlAzureAdminUserPassword", + "Label": "The SQL Azure Admin Password", + "HelpText": "The SQL Azure Server Administrator Password", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "SQLAzureServerName", + "Label": "The SQL Azure Server", + "HelpText": "The name of the SQL Azure Server. Just the server name e.g. wyn4its2by", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DatabaseName", + "Label": "The SQL Azure Database used for the bacpac restore", + "HelpText": "The name of the SQL Azure Database to which you wish to restore the .bacpac file.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SQLAzureDatabaseEdition", + "Label": "The SQL Azure database edition to use", + "HelpText": "The [SQL Azure Database Edition](https://msdn.microsoft.com/en-us/library/dn546725.aspx) to use for the restore", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText", + "Octopus.SelectOptions": "Web|Web +Business|Business" + } + }, + { + "Name": "SQLAzureServiceObjective", + "Label": "The SQL Azure SQL Service Objective to use", + "HelpText": "The [SQL Azure Database Objective](https://msdn.microsoft.com/en-us/library/dn546721.aspx) for the database being restored. + +Basic, S0, S1, S2, P1, P2, or P3.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Basic|Basic +S0|Standard (S0) +S1|Standard (S1) +S2|Standard (S2) +S3|Standard (S3) +P1|Premium (P1) +P2|Premium (P2) +P3|Premium (P3)" + } + }, + { + "Name": "AzureStorageAccountName", + "Label": "The Azure Storage Account where the .bacpac file is located", + "HelpText": "The Azure Storage Account where the .bacpac file is located", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AzureStorageContainerName", + "Label": "The container name of the bacpac file", + "HelpText": "The container in Azure storage where the .bacpac file is located.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BacPacFileName", + "Label": "The name of the bacpac file to restore", + "HelpText": "The name of the .bacpac file to restore.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-04-01T11:36:52.686+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sqlserver-high-availability-group.json.human b/step-templates/sqlserver-high-availability-group.json.human new file mode 100644 index 000000000..ad8822c0e --- /dev/null +++ b/step-templates/sqlserver-high-availability-group.json.human @@ -0,0 +1,103 @@ +{ + "Id": "735d2f76-fdbb-4232-9f36-07020cad120d", + "Name": "Check SQL Server in High Availability Group", + "Description": "Checks for SQL Node currently being serving as primary on high availability group and sets Octopus variable : SQLIsOnSecondary to true if secondary is active in High Availability Group", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "function CheckSQLServerInAOAG($SqlServer, $PrimaryNode)\r +{\r + $serverConn = new-object ('Microsoft.SqlServer.Management.Common.ServerConnection') $SqlServer\r + \r + try{\r + $serverConn.Connect();\r + $server = new-object ('Microsoft.SqlServer.Management.Smo.Server') $serverConn \r + if (!$server.IsHadrEnabled)\r + {\r + \t Write-Host \"The SQL Server [$SqlServer] is not configured with High Availability Group.\"\r + }\r + else\r + {\r + # Get SQL Availability Group\r + $SQLAvailabilityGroup = $server.AvailabilityGroups[0]\r + \r + Write-Host \"Getting High Availability Group properties.\"\r + \r + # Get SQL Availability Groups Properties\r +\t $SQLAvailabilityGroupName = $SQLAvailabilityGroup.Name;\r +\t $SQLAvailabilityGroupID = $SQLAvailabilityGroup.Id;\r +\t $SQLAvailabilityGroupGuid = $SQLAvailabilityGroup.UniqueId;\r +\t $SQLLocalReplicaRole = $SQLAvailabilityGroup.LocalReplicaRole;\r +\t $SQLPrimaryReplicaServerName = $SQLAvailabilityGroup.PrimaryReplicaServerName;\r +\t \r +\t Write-Host\t\"SQLAvailabilityGroupName : $SQLAvailabilityGroupName\"\r + Write-Host\t\"SQLAvailabilityGroupID : $SQLAvailabilityGroupID\"\r + Write-Host\t\"SQLAvailabilityGroupGuid : $SQLAvailabilityGroupGuid\"\r + Write-Host\t\"SQLLocalReplicaRole : $SQLLocalReplicaRole\"\r + Write-Host\t\"SQLPrimaryReplicaServerName : $SQLPrimaryReplicaServerName\" \r + \r + if ($SQLPrimaryReplicaServerName -eq $PrimaryNode)\r + {\r + \t Write-Host \"Setting Octopus variable SQLIsOnSecondary false\"\r + Set-OctopusVariable -name \"SQLIsOnSecondary\" -value \"false\" \r + }\r + else \r + {\r + \t Write-Host \"Setting Octopus variable SQLIsOnSecondary true\"\r + \t Set-OctopusVariable -name \"SQLIsOnSecondary\" -value \"true\"\r + }\r + }\r + }\r + catch\r + {\r + throw \"Could not connect to server $SqlServer. Exception is:`r`n$($_ | fl -force | out-string)\"\r + }\r + finally\r + {\r + $serverConn.Disconnect();\r + }\r +}\r +\r +[System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SMO\") | out-null\r +[System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.ConnectionInfo\") | out-null\r +CheckSQLServerInAOAG $HAGroupSQLServer $HAGroupPrimaryNode", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "afe518f5-5778-45b4-86c7-3715fa55b4d9", + "Name": "HAGroupSQLServer", + "Label": "Enter server name", + "HelpText": "SQL Server name used in connection string from website code", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c3c49b74-9982-400d-bc8f-b292ea7b2488", + "Name": "HAGroupPrimaryNode", + "Label": "Enter primary node", + "HelpText": "Enter primary SQL server name which serves traffic", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "vasant-horapeti", + "$Meta": { + "ExportedAt": "2019-10-13T08:11:56.532Z", + "OctopusVersion": "2018.10.2", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/sqlserver-project-deployment-model-ispac-deploy.json.human b/step-templates/sqlserver-project-deployment-model-ispac-deploy.json.human new file mode 100644 index 000000000..88c74b6dd --- /dev/null +++ b/step-templates/sqlserver-project-deployment-model-ispac-deploy.json.human @@ -0,0 +1,344 @@ +{ + "Id": "efe39ac7-3ab8-4f99-bfdc-aba342278d1a", + "Name": "SQL Server - Project Deployment Model - Deploy ISPAC", + "Description": "This is to deploy ssis packages using 'project deployment model' (ISPAC file )", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "#################################################################################################\r +# Change source and destination properties\r +#################################################################################################\r +\r +# Source\r +$IspacFilePath = \"#{ISPAC_FILE_PATH}\"\r + \r +# Destination\r +$SsisServer = $OctopusParameters['deploy.dts.server'] \r +$FolderName = $OctopusParameters['SSIS_Folder']\r +$ProjectName = $OctopusParameters['SSIS_Project']\r +\r +# Environment\r +$EnvironmentName = $OctopusParameters['Environment_Name'] \r +$EnvironmentFolderName = $OctopusParameters['SSIS_Folder']\r +\r +\r +# Replace empty projectname with filename\r +if (-not $ProjectName)\r +{\r + $ProjectName = [system.io.path]::GetFileNameWithoutExtension($IspacFilePath)\r +}\r +# Replace empty Environment folder with project folder\r +if (-not $EnvironmentFolderName)\r +{\r + $EnvironmentFolderName = $FolderName\r +}\r +\r +clear\r +Write-Host \"========================================================================================================================================================\"\r +Write-Host \"== Used parameters ==\"\r +Write-Host \"========================================================================================================================================================\"\r +Write-Host \"Ispac File Path : \" $IspacFilePath\r +Write-Host \"SSIS Server : \" $SsisServer\r +Write-Host \"Project Folder Path : \" $FolderName\r +Write-Host \"Project Name : \" $ProjectName\r +Write-Host \"Environment Name : \" $EnvironmentName\r +Write-Host \"Environment Folder Path: \" $EnvironmentFolderName\r +Write-Host \"========================================================================================================================================================\"\r +Write-Host \"\"\r +\r +###########################\r +########## ISPAC ##########\r +###########################\r +# Check if ispac file exists\r +if (-Not (Test-Path $IspacFilePath))\r +{\r + Throw [System.IO.FileNotFoundException] \"Ispac file $IspacFilePath doesn't exists!\"\r +}\r +else\r +{\r + $IspacFileName = split-path $IspacFilePath -leaf\r + Write-Host \"Ispac file\" $IspacFileName \"found\"\r +}\r +\r +\r +############################\r +########## SERVER ##########\r +############################\r +# Load the Integration Services Assembly\r +Write-Host \"Connecting to server $SsisServer \"\r +$SsisNamespace = \"Microsoft.SqlServer.Management.IntegrationServices\"\r +[System.Reflection.Assembly]::LoadWithPartialName($SsisNamespace) | Out-Null;\r +\r +# Create a connection to the server\r +$SqlConnectionstring = \"Data Source=\" + $SsisServer + \";Initial Catalog=master;Integrated Security=SSPI;\"\r +$SqlConnection = New-Object System.Data.SqlClient.SqlConnection $SqlConnectionstring\r +\r +# Create the Integration Services object\r +$IntegrationServices = New-Object $SsisNamespace\".IntegrationServices\" $SqlConnection\r +\r +# Check if connection succeeded\r +if (-not $IntegrationServices)\r +{\r + Throw [System.Exception] \"Failed to connect to server $SsisServer \"\r +}\r +else\r +{\r + Write-Host \"Connected to server\" $SsisServer\r +}\r +\r +\r +#############################\r +########## CATALOG ##########\r +#############################\r +# Create object for SSISDB Catalog\r +$Catalog = $IntegrationServices.Catalogs[\"SSISDB\"]\r +\r +# Check if the SSISDB Catalog exists\r +if (-not $Catalog)\r +{\r + # Catalog doesn't exists. The user should create it manually.\r + # It is possible to create it, but that shouldn't be part of\r + # deployment of packages.\r + Throw [System.Exception] \"SSISDB catalog doesn't exist. Create it manually!\"\r +}\r +else\r +{\r + Write-Host \"Catalog SSISDB found\"\r +}\r +\r +\r +############################\r +########## FOLDER ##########\r +############################\r +# Create object to the (new) folder\r +$Folder = $Catalog.Folders[$FolderName]\r +\r +# Check if folder already exists\r +if (-not $Folder)\r +{\r + # Folder doesn't exists, so create the new folder.\r + Write-Host \"Creating new folder\" $FolderName\r + $Folder = New-Object $SsisNamespace\".CatalogFolder\" ($Catalog, $FolderName, $FolderName)\r + $Folder.Create()\r +}\r +else\r +{\r + Write-Host \"Folder\" $FolderName \"found\"\r +}\r +\r +\r +#############################\r +########## PROJECT ##########\r +#############################\r +# Deploying project to folder\r +if($Folder.Projects.Contains($ProjectName)) {\r + Write-Host \"Deploying\" $ProjectName \"to\" $FolderName \"(REPLACE)\"\r +}\r +else\r +{\r + Write-Host \"Deploying\" $ProjectName \"to\" $FolderName \"(NEW)\"\r +}\r +# Reading ispac file as binary\r +[byte[]] $IspacFile = [System.IO.File]::ReadAllBytes($IspacFilePath)\r +$Folder.DeployProject($ProjectName, $IspacFile)\r +$Project = $Folder.Projects[$ProjectName]\r +if (-not $Project)\r +{\r + # Something went wrong with the deployment\r + # Don't continue with the rest of the script\r + return \"\"\r +}\r +\r +\r +#################################\r +########## ENVIRONMENT ##########\r +#################################\r +# Check if environment name is filled\r +if (-not $EnvironmentName)\r +{\r + # Kill connection to SSIS\r + $IntegrationServices = $null \r +\r + # Stop the deployment script\r + Return \"Ready deploying $IspacFileName without adding environment references\"\r +}\r +\r +# Create object to the (new) folder\r +$EnvironmentFolder = $Catalog.Folders[$EnvironmentFolderName]\r +\r +# Check if environment folder exists\r +if (-not $EnvironmentFolder)\r +{\r + Throw [System.Exception] \"Environment folder $EnvironmentFolderName doesn't exist\"\r +}\r +\r +# Check if environment exists\r +if(-not $EnvironmentFolder.Environments.Contains($EnvironmentName))\r +{\r + Throw [System.Exception] \"Environment $EnvironmentName doesn't exist in $EnvironmentFolderName \"\r +}\r +else\r +{\r + # Create object for the environment\r + $Environment = $Catalog.Folders[$EnvironmentFolderName].Environments[$EnvironmentName]\r +\r + if ($Project.References.Contains($EnvironmentName, $EnvironmentFolderName))\r + {\r + Write-Host \"Reference to\" $EnvironmentName \"found\"\r + }\r + else\r + {\r + Write-Host \"Adding reference to\" $EnvironmentName\r + $Project.References.Add($EnvironmentName, $EnvironmentFolderName)\r + $Project.Alter() \r + }\r +}\r +\r +\r +########################################\r +########## PROJECT PARAMETERS ##########\r +########################################\r +$ParameterCount = 0\r +# Loop through all project parameters\r +foreach ($Parameter in $Project.Parameters)\r +{\r + # Get parameter name and check if it exists in the environment\r + $ParameterName = $Parameter.Name\r + if ($ParameterName.StartsWith(\"CM.\",\"CurrentCultureIgnoreCase\")) \r + { \r + # Ignoring connection managers \r + } \r + elseif ($ParameterName.StartsWith(\"INTERN_\",\"CurrentCultureIgnoreCase\")) \r + { \r + # Optional:\r + # Internal parameters are ignored (where name starts with INTERN_) \r + Write-Host \"Ignoring Project parameter\" $ParameterName \" (internal use only)\" \r + } \r + elseif ($Environment.Variables.Contains($Parameter.Name))\r + {\r + $ParameterCount = $ParameterCount + 1\r + Write-Host \"Project parameter\" $ParameterName \"connected to environment\"\r + $Project.Parameters[$Parameter.Name].Set([Microsoft.SqlServer.Management.IntegrationServices.ParameterInfo+ParameterValueType]::Referenced, $Parameter.Name)\r + $Project.Alter()\r + }\r + else\r + {\r + # Variable with the name of the project parameter is not found in the environment\r + # Throw an exeception or remove next line to ignore parameter\r + Throw [System.Exception] \"Project parameter $ParameterName doesn't exist in environment\"\r + }\r +}\r +Write-Host \"Number of project parameters mapped:\" $ParameterCount\r +\r +\r +########################################\r +########## PACKAGE PARAMETERS ##########\r +########################################\r +$ParameterCount = 0\r +# Loop through all packages\r +foreach ($Package in $Project.Packages)\r +{\r + # Loop through all package parameters\r + foreach ($Parameter in $Package.Parameters)\r + {\r + # Get parameter name and check if it exists in the environment\r + $PackageName = $Package.Name\r + $ParameterName = $Parameter.Name \r + if ($ParameterName.StartsWith(\"CM.\",\"CurrentCultureIgnoreCase\")) \r + { \r + # Ignoring connection managers \r + } \r + elseif ($ParameterName.StartsWith(\"INTERN_\",\"CurrentCultureIgnoreCase\")) \r + { \r + # Optional:\r + # Internal parameters are ignored (where name starts with INTERN_) \r + Write-Host \"Ignoring Package parameter\" $ParameterName \" (internal use only)\" \r + } \r + elseif ($Environment.Variables.Contains($Parameter.Name))\r + {\r + $ParameterCount = $ParameterCount + 1\r + Write-Host \"Package parameter\" $ParameterName \"from package\" $PackageName \"connected to environment\"\r + $Package.Parameters[$Parameter.Name].Set([Microsoft.SqlServer.Management.IntegrationServices.ParameterInfo+ParameterValueType]::Referenced, $Parameter.Name)\r + $Package.Alter()\r + }\r + else\r + {\r + # Variable with the name of the package parameter is not found in the environment\r + # Throw an exeception or remove next line to ignore parameter\r + Throw [System.Exception] \"Package parameter $ParameterName from package $PackageName doesn't exist in environment\"\r + }\r + }\r +}\r +Write-Host \"Number of package parameters mapped:\" $ParameterCount\r +\r +\r +###########################\r +########## READY ##########\r +###########################\r +# Kill connection to SSIS\r +$IntegrationServices = $null \r +\r +\r +Return \"Ready deploying $IspacFileName \"", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "ISPAC_FILE_PATH", + "Label": "Ispac file path", + "HelpText": "Once the SSIS project is compiled \"ispac\" file gets created, this variable must hold the path of the ispac file.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "deploy.dts.server", + "Label": "SSIS Server name", + "HelpText": "SSIS Server name where this ssis packages must be deployed.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SSIS_Folder", + "Label": "SSIS Folder name", + "HelpText": "SSIS folder name which is created under SSISDB", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SSIS_Project", + "Label": "Project Name", + "HelpText": "SSIS Project name - this is the physical folder name where the OD is referring to .", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Environment_Name", + "Label": "Environment Name", + "HelpText": "This is the environment name where variables exists.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2016-07-20T10:15:20.241+00:00", + "OctopusVersion": "3.3.10", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/ssas-deploy-from-package.json.human b/step-templates/ssas-deploy-from-package.json.human new file mode 100644 index 000000000..066cd2d23 --- /dev/null +++ b/step-templates/ssas-deploy-from-package.json.human @@ -0,0 +1,215 @@ +{ + "Id": "1409c3dd-e87d-49f1-9b4f-382af800b75d", + "Name": "Deploy SSAS from Package", + "Description": "Deploys SSAS packages using Microsoft.AnalysisServices.Deployment.exe.", + "ActionType": "Octopus.Script", + "Version": 14, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop' + +function Confirm-Argument($name, $value) { + if (!$value) { + throw ('Missing required value for parameter ''{0}''.' -f $name) + } + return $value +} + +# Returns the Microsoft.AnalysisServices.Deployment.exe path +function Get-SSASexe +{ +\t# Search for file + $ssasExe = Get-ChildItem -Path \"C:\\Program Files (x86)\" -Recurse | Where-Object {$_.Name -eq \"Microsoft.AnalysisServices.Deployment.exe\"} + + # Check for null + if ($null -eq $ssasExe) + { + \t# Display error + Write-Error \"Unable to find Microsoft.AnalysisServices.Deployment.exe!\" + } + + # Check for mulitple results + if ($ssasExe.GetType().IsArray) + { + # Declare local variables + $highestVersion = $null + + # Display multiple returned + Write-Host \"Multiple files returned, finding highest version ...\" + + # Loop through results + foreach ($file in $ssasExe) + { + # Check version + if (($null -eq $highestVersion) -or ([version]$file.VersionInfo.ProductVersion) -gt [version]$highestVersion.VersionInfo.ProductVersion) + { + # Assign it + $highestVersion = $file + } + } + + # Overwrite original + $ssasExe = $highestVersion + } + + # Return the path + return $ssasExe.FullName +} + +# Update Deploy xml (.deploymenttargets) +function Update-Deploy { +\t[xml]$deployContent = Get-Content $file +\t$deployContent.DeploymentTarget.Database = $ssasDatabase +\t$deployContent.DeploymentTarget.Server = $ssasServer +\t$deployContent.DeploymentTarget.ConnectionString = 'DataSource=' + $ssasServer + ';Timeout=0' +\t$deployContent.Save($file) +} +# Update Config xml (.configsettings) +function Update-Config { +\t[xml]$configContent = Get-Content $file + $configContent.ConfigurationSettings.Database.DataSources.DataSource.ConnectionString = 'Provider=SQLNCLI11.1;Data Source=' + $dbServer + ';Integrated Security=SSPI;Initial Catalog=' + $dbDatabase +\t$configContent.Save($file) +} +# Update Config xml (.deploymentoptions) +function Update-Option { +\t[xml]$optionContent = Get-Content $file + $optionContent.DeploymentOptions.ProcessingOption = 'DoNotProcess' +\t$optionContent.Save($file) +} + +# Get arguments +$ssasPackageStepName = Confirm-Argument 'SSAS Package Step Name' $OctopusParameters['SsasPackageStepName'] +$ssasServer = Confirm-Argument 'SSAS server name' $OctopusParameters['SsasServer'] +$ssasDatabase = Confirm-Argument 'SSAS database name' $OctopusParameters['SsasDatabase'] +$dbServer = Confirm-Argument 'SSAS source server' $OctopusParameters['SrcServer'] +$dbDatabase = Confirm-Argument 'SSAS source database' $OctopusParameters['SrcDatabase'] + +# Set .NET CurrentDirectory to package installation path +$installDirPathFormat = 'Octopus.Action[{0}].Output.Package.InstallationDirectoryPath' -f $ssasPackageStepName +$installDirPath = $OctopusParameters[$installDirPathFormat] + +Write-Verbose ('Setting CurrentDirectory to ''{0}''' -f $installDirPath) +[System.Environment]::CurrentDirectory = $installDirPath + +# Get SSAS exe location +$exe = Get-SSASexe + +$files = Get-ChildItem –Path $installDirPath\\* -Include *.deploymenttargets +foreach ($file in $files) { + $name = [IO.Path]::GetFileNameWithoutExtension($file) + + Write-Host 'Updating' $file + Update-Deploy + $file = $installDirPath + '\\' + $name + '.configsettings' + if(Test-Path $file) { + Write-Host 'Updating' $file + Update-Config + } else { + Write-Host \"Config settings doesn't exist. Skipping.\" + } + $file = $installDirPath + '\\' + $name + '.deploymentoptions' + Write-Host 'Updating' $file + Update-Option + + $ssasArguments = @() + $ssasArguments += ('\"' + $installDirPath + '\\' + $name + '.asdatabase\"') + $ssasArguments += '/s:\"' + $installDirPath + '\\Log.txt\"' + + Write-Host $exe $ssasArguments + & $exe $ssasArguments + + # Get last exit code + $ssasExitCode = $LastExitcode + + # Check to make sure log file exists + if ((Test-Path -Path \"$installDirPath\\Log.txt\") -eq $true) + { + # Upload log as artifact + New-OctopusArtifact -Path \"$installDirPath\\Log.txt\" -Name \"Log.txt\" + } + else + { + # Write error + Write-Error \"Error: $installDirPath\\Log.txt not found!\" + } + + # Check the code + if ($ssasExitCode -ne 0) + { + \tWrite-Error \"Operation failed, see log for details.\" + } +} +", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "435fbf52-470d-442f-88d3-0b708dfc3657", + "Name": "SsasPackageStepName", + "Label": "", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + }, + "Links": { } + }, + { + "Id": "a0713297-cdeb-47cd-ac43-4bf9603c8052", + "Name": "SsasServer", + "Label": "Server", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": { } + }, + { + "Id": "7bf5ec64-3383-40ae-935e-e78c236cdf21", + "Name": "SsasDatabase", + "Label": "Database", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": { } + }, + { + "Id": "d7d4fcbc-d0d4-4a3b-b477-28d23fbe1f9b", + "Name": "SrcServer", + "Label": "Data Source - Server", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": { } + }, + { + "Id": "45b60a3f-9339-4c69-bc14-df94b47be9e7", + "Name": "SrcDatabase", + "Label": "Data Source - Database", + "HelpText": null, + "DefaultValue": "Warehouse", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": { } + } + ], + "LastModifiedOn": "2020-09-25T23:49:43.003Z", + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2020-09-25T23:49:43.003Z", + "OctopusVersion": "2020.4.0", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/ssis-deploy-ispac-from-package-parameter.json.human b/step-templates/ssis-deploy-ispac-from-package-parameter.json.human new file mode 100644 index 000000000..b9e6bf800 --- /dev/null +++ b/step-templates/ssis-deploy-ispac-from-package-parameter.json.human @@ -0,0 +1,816 @@ +{ + "Id": "27567d46-b935-4ee6-8b2d-8c165edada4e", + "Name": "Deploy ispac SSIS project from a Package parameter", + "Description": "This step template will deploy SSIS ispac projects to SQL Server Integration Services Catalog. The template uses a referenced package and is Worker compatible. + +This template will install the Nuget package provider if it is not present on the machine it is running on. + +NOTE: The SqlServer PowerShell module this template utilizes removed the assemblies necessary to interface with SSIS as of version 22.0.59. Version 21.1.18256 has been pinned and will be used if the SqlServer PowerShell module is not installed.", + "ActionType": "Octopus.Script", + "Version": 6, + "Author": "twerthi", + "Packages": [ + { + "Id": "5ce9da08-a4ed-4b69-92d1-ab88c705cf08", + "Name": "SSIS.Template.ssisPackageId", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "SSIS.Template.ssisPackageId" + } + } + ], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#region Functions + +# Define functions +function Get-SqlModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Install-SqlServerPowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force -RequiredVersion \"21.1.18256\" + +\t# Display + Write-Output \"Importing module $PowerShellModuleName ...\" + + # Import the module + Import-Module -Name $PowerShellModuleName +} + +Function Load-SqlServerAssmblies +{ +\t# Declare parameters + +\t# Get the folder where the SqlServer module ended up in +\t$sqlServerModulePath = [System.IO.Path]::GetDirectoryName((Get-Module SqlServer).Path) + + # Loop through the assemblies + foreach($assemblyFile in (Get-ChildItem -Path $sqlServerModulePath -Exclude msv*.dll | Where-Object {$_.Extension -eq \".dll\"})) + { + # Load the assembly + [Reflection.Assembly]::LoadFile($assemblyFile.FullName) | Out-Null + } +} + +#region Get-Catalog +Function Get-Catalog +{ + # define parameters + Param ($CatalogName) + # NOTE: using $integrationServices variable defined in main + + # define working varaibles + $Catalog = $null + # check to see if there are any catalogs + if($integrationServices.Catalogs.Count -gt 0 -and $integrationServices.Catalogs[$CatalogName]) + { + \t# get reference to catalog + \t$Catalog = $integrationServices.Catalogs[$CatalogName] + } + else + { + \tif((Get-CLREnabled) -eq 0) + \t{ + \t\tif(-not $EnableCLR) + \t\t{ + \t\t\t# throw error + \t\t\tthrow \"SQL CLR is not enabled.\" + \t\t} + \t\telse + \t\t{ + \t\t\t# display sql clr isn't enabled + \t\t\tWrite-Warning \"SQL CLR is not enabled on $($sqlConnection.DataSource). This feature must be enabled for SSIS catalogs.\" + + \t\t\t# enablign SQLCLR + \t\t\tWrite-Host \"Enabling SQL CLR ...\" + \t\t\tEnable-SQLCLR + \t\t\tWrite-Host \"SQL CLR enabled\" + \t\t} + \t} + + \t# Provision a new SSIS Catalog + \tWrite-Host \"Creating SSIS Catalog ...\" + + \t$Catalog = New-Object \"$ISNamespace.Catalog\" ($integrationServices, $CatalogName, $OctopusParameters['SSIS.Template.CatalogPwd']) + \t$Catalog.Create() + + + } + + # return the catalog + return $Catalog +} +#endregion + +#region Get-CLREnabled +Function Get-CLREnabled +{ + # define parameters + # Not using any parameters, but am using $sqlConnection defined in main + + # define working variables + $Query = \"SELECT * FROM sys.configurations WHERE name = 'clr enabled'\" + + # execute script + $CLREnabled = Invoke-Sqlcmd -ServerInstance $sqlConnection.DataSource -Database \"master\" -Query $Query | Select value + + # return value + return $CLREnabled.Value +} +#endregion + +#region Enable-SQLCLR +Function Enable-SQLCLR +{ + $QueryArray = \"sp_configure 'show advanced options', 1\", \"RECONFIGURE\", \"sp_configure 'clr enabled', 1\", \"RECONFIGURE \" + # execute script + + foreach($Query in $QueryArray) + { + \tInvoke-Sqlcmd -ServerInstance $sqlConnection.DataSource -Database \"master\" -Query $Query + } + + # check that it's enabled + if((Get-CLREnabled) -ne 1) + { + \t# throw error + \tthrow \"Failed to enable SQL CLR\" + } +} +#endregion + +#region Get-Folder +Function Get-Folder +{ + # parameters + Param($FolderName, $Catalog) + + $Folder = $null + # try to get reference to folder + + if(!($Catalog.Folders -eq $null)) + { + \t$Folder = $Catalog.Folders[$FolderName] + } + + # check to see if $Folder has a value + if($Folder -eq $null) + { + \t# display + \tWrite-Host \"Folder $FolderName doesn't exist, creating folder...\" + + \t# create the folder + \t$Folder = New-Object \"$ISNamespace.CatalogFolder\" ($Catalog, $FolderName, $FolderName) + \t$Folder.Create() + } + + # return the folde reference + return $Folder +} +#endregion + +#region Get-Environment +Function Get-Environment +{ + # define parameters + Param($Folder, $EnvironmentName) + + $Environment = $null + # get reference to Environment + if(!($Folder.Environments -eq $null) -and $Folder.Environments.Count -gt 0) + { + \t$Environment = $Folder.Environments[$EnvironmentName] + } + + # check to see if it's a null reference + if($Environment -eq $null) + { + \t# display + \tWrite-Host \"Environment $EnvironmentName doesn't exist, creating environment...\" + + \t# create environment + \t$Environment = New-Object \"$ISNamespace.EnvironmentInfo\" ($Folder, $EnvironmentName, $EnvironmentName) + \t$Environment.Create() + } + + # return the environment + return $Environment +} +#endregion + +#region Set-EnvironmentReference +Function Set-EnvironmentReference +{ + # define parameters + Param($Project, $Environment, $Folder) + + # get reference + $Reference = $null + + if(!($Project.References -eq $null)) + { + \t$Reference = $Project.References[$Environment.Name, $Folder.Name] + + } + + # check to see if it's a null reference + if($Reference -eq $null) + { + \t# display + \tWrite-Host \"Project does not reference environment $($Environment.Name), creating reference...\" + + \t# create reference + \t$Project.References.Add($Environment.Name, $Folder.Name) + \t$Project.Alter() + } +} +#endregion + +#region Set-ProjectParametersToEnvironmentVariablesReference +Function Set-ProjectParametersToEnvironmentVariablesReference +{ + # define parameters + Param($Project, $Environment) + + $UpsertedVariables = @() + + if($Project.Parameters -eq $null) + { + Write-Host \"No project parameters exist\" + return + } + + # loop through project parameters + foreach($Parameter in $Project.Parameters) + { + # skip if the parameter is included in custom filters + if ($UseCustomFilter) + { + if ($Parameter.Name -match $CustomFilter) + { + Write-Host \"- $($Parameter.Name) skipped due to CustomFilters.\" + continue + } + } + + # Add variable to list of variable + $UpsertedVariables += $Parameter.Name + + $Variable = $null + if(!($Environment.Variables -eq $null)) + { + \t # get reference to variable + \t $Variable = $Environment.Variables[$Parameter.Name] + } + + \t# check to see if variable exists + \tif($Variable -eq $null) + \t{ + \t\t# add the environment variable + \t\tAdd-EnvironmentVariable -Environment $Environment -Parameter $Parameter -ParameterName $Parameter.Name + + \t\t# get reference to the newly created variable + \t\t$Variable = $Environment.Variables[$Parameter.Name] + \t} + + \t# set the environment variable value + \tSet-EnvironmentVariableValue -Variable $Variable -Parameter $Parameter -ParameterName $Parameter.Name + } + + # alter the environment + $Environment.Alter() + $Project.Alter() + + return $UpsertedVariables +} +#endregion + +Function Set-PackageVariablesToEnvironmentVariablesReference +{ + # define parameters + Param($Project, $Environment) + + $Variables = @() + $UpsertedVariables = @() + + # loop through packages in project in order to store a temp collection of variables + foreach($Package in $Project.Packages) + { + \t# loop through parameters of package + \tforeach($Parameter in $Package.Parameters) + \t{ + \t\t# add to the temporary variable collection + \t\t$Variables += $Parameter.Name + \t} + } + + # loop through packages in project + foreach($Package in $Project.Packages) + { + \t# loop through parameters of package + \tforeach($Parameter in $Package.Parameters) + \t{ + if ($UseFullyQualifiedVariableNames) + { + # Set fully qualified variable name + $ParameterName = $Parameter.ObjectName.Replace(\".dtsx\", \"\")+\".\"+$Parameter.Name + } + else + { + # check if exists a variable with the same name + $VariableNameOccurrences = $($Variables | Where-Object { $_ -eq $Parameter.Name }).count + $ParameterName = $Parameter.Name + + if ($VariableNameOccurrences -gt 1) + { + $ParameterName = $Parameter.ObjectName.Replace(\".dtsx\", \"\")+\".\"+$Parameter.Name + } + } + + if ($UseCustomFilter) + { + if ($ParameterName -match $CustomFilter) + { + Write-Host \"- $($Parameter.Name) skipped due to CustomFilters.\" + continue + } + } + + # get reference to variable + \t\t$Variable = $Environment.Variables[$ParameterName] + + # Add variable to list of variable + $UpsertedVariables += $ParameterName + + # check to see if the parameter exists + \t\tif(!$Variable) + \t\t{ + \t\t\t# add the environment variable + \t\t\tAdd-EnvironmentVariable -Environment $Environment -Parameter $Parameter -ParameterName $ParameterName + + \t\t\t# get reference to the newly created variable + \t\t\t$Variable = $Environment.Variables[$ParameterName] + \t\t} + + \t\t# set the environment variable value + \t\tSet-EnvironmentVariableValue -Variable $Variable -Parameter $Parameter -ParameterName $ParameterName + \t} + + \t# alter the package + \t$Package.Alter() + } + + # alter the environment + $Environment.Alter() + + return $UpsertedVariables +} + +Function Sync-EnvironmentVariables +{ + # define parameters + Param($Environment, $VariablesToPreserveInEnvironment) + + foreach($VariableToEvaluate in $Environment.Variables) + { + if ($VariablesToPreserveInEnvironment -notcontains $VariableToEvaluate.Name) + { + Write-Host \"- Removing environment variable: $($VariableToEvaluate.Name)\" + $VariableToRemove = $Environment.Variables[$VariableToEvaluate.Name] + $Environment.Variables.Remove($VariableToRemove) | Out-Null + } + } + + # alter the environment + $Environment.Alter() +} + +#region Add-EnvironmentVariable +Function Add-EnvironmentVariable +{ + # define parameters + Param($Environment, $Parameter, $ParameterName) + + # display + Write-Host \"- Adding environment variable $($ParameterName)\" + + # check to see if design default value is emtpy or null + if([string]::IsNullOrEmpty($Parameter.DesignDefaultValue)) + { + \t# give it something + \t$DefaultValue = \"\" # sensitive variables will not return anything so when trying to use the property of $Parameter.DesignDefaultValue, the Alter method will fail. + } + else + { + \t# take the design + \t$DefaultValue = $Parameter.DesignDefaultValue + } + + # add variable with an initial value + $Environment.Variables.Add($ParameterName, $Parameter.DataType, $DefaultValue, $Parameter.Sensitive, $Parameter.Description) +} +#endregion + +#region Set-EnvironmentVariableValue +Function Set-EnvironmentVariableValue +{ + # define parameters + Param($Variable, $Parameter, $ParameterName) + + # check to make sure variable value is available + if($OctopusParameters -and $OctopusParameters.ContainsKey($ParameterName)) + { + # display + Write-Host \"- Updating environment variable $($ParameterName)\" + + \t# set the variable value + \t$Variable.Value = $OctopusParameters[\"$($ParameterName)\"] + } + else + { + \t# warning + \tWrite-Host \"**- OctopusParameters collection is empty or $($ParameterName) not in the collection -**\" + } + + # Set reference + $Parameter.Set([Microsoft.SqlServer.Management.IntegrationServices.ParameterInfo+ParameterValueType]::Referenced, \"$($ParameterName)\") +} +#endregion + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\" + +# Check to see if SqlServer module is installed +if ((Get-SqlModuleInstalled -PowerShellModuleName \"SqlServer\") -ne $true) +{ +\t# Display message + Write-Output \"PowerShell module SqlServer not present, downloading temporary copy ...\" + +\t#Enable TLS 1.2 as default protocol +\t[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 + + # Download and install temporary copy + Install-SqlServerPowerShellModule -PowerShellModuleName \"SqlServer\" -LocalModulesPath $LocalModules + +} +else +{ +\t# Import the module + Import-Module -Name SqlServer +} + +#region Dependent assemblies +Load-SqlServerAssmblies + +#endregion + +# Store the IntegrationServices Assembly namespace to avoid typing it every time +$ISNamespace = \"Microsoft.SqlServer.Management.IntegrationServices\" + +#endregion + +#region Main +try +{ + # ensure all boolean variables are true booleans + $EnableCLR = [System.Convert]::ToBoolean(\"$($OctopusParameters['SSIS.Template.EnableCLR'])\") + $UseEnvironment = [System.Convert]::ToBoolean(\"$($OctopusParameters['SSIS.Template.UseEnvironment'])\") + $ReferenceProjectParametersToEnvironmentVairables = [System.Convert]::ToBoolean(\"$($OctopusParameters['SSIS.Template.ReferenceProjectParametersToEnvironmentVairables'])\") + + $ReferencePackageParametersToEnvironmentVairables = [System.Convert]::ToBoolean(\"$($OctopusParameters['SSIS.Template.ReferencePackageParametersToEnvironmentVairables'])\") + $UseFullyQualifiedVariableNames = [System.Convert]::ToBoolean(\"$($OctopusParameters['SSIS.Template.UseFullyQualifiedVariableNames'])\") + $SyncEnvironment = [System.Convert]::ToBoolean(\"$($OctopusParameters['SSIS.Template.SyncEnvironment'])\") + # custom names for filtering out the excluded variables by design + $UseCustomFilter = [System.Convert]::ToBoolean(\"$($OctopusParameters['SSIS.Template.UseCustomFilter'])\") + $CustomFilter = [System.Convert]::ToString(\"$($OctopusParameters['SSIS.Template.CustomFilter'])\") + # list of variables names to keep in target environment + $VariablesToPreserveInEnvironment = @() + $ssisPackageId = $OctopusParameters['SSIS.Template.ssisPackageId'] + +\t# Get the extracted path +\t$DeployedPath = $OctopusParameters[\"Octopus.Action.Package[$ssisPackageId].ExtractedPath\"] + +\t# Get all .ispac files from the deployed path +\t$IsPacFiles = Get-ChildItem -Recurse -Path $DeployedPath | Where {$_.Extension.ToLower() -eq \".ispac\"} + +\t# display number of files +\tWrite-Host \"$($IsPacFiles.Count) .ispac file(s) found.\" + +\tWrite-Host \"Connecting to server ...\" + +\t# Create a connection to the server + $sqlConnectionString = \"Data Source=$($OctopusParameters['SSIS.Template.ServerName']);Initial Catalog=SSISDB;\" + + if (![string]::IsNullOrEmpty($OctopusParameters['SSIS.Template.sqlAccountUsername']) -and ![string]::IsNullOrEmpty($OctopusParameters['SSIS.Template.sqlAccountPassword'])) + { + \t# Add username and password to connection string + $sqlConnectionString += \"User ID=$($OctopusParameters['SSIS.Template.sqlAccountUsername']); Password=$($OctopusParameters['SSIS.Template.sqlAccountPassword']);\" + } + else + { + \t# Use integrated + $sqlConnectionString += \"Integrated Security=SSPI;\" + } + + + # Create new connection object with connection string + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $sqlConnectionString + +\t# create integration services object +\t$integrationServices = New-Object \"$ISNamespace.IntegrationServices\" $sqlConnection + +\t# get reference to the catalog +\tWrite-Host \"Getting reference to catalog $($OctopusParameters['SSIS.Template.CataLogName'])\" +\t$Catalog = Get-Catalog -CatalogName $OctopusParameters['SSIS.Template.CataLogName'] + +\t# get folder reference +\t$Folder = Get-Folder -FolderName $OctopusParameters['SSIS.Template.FolderName'] -Catalog $Catalog + +\t# loop through ispac files +\tforeach($IsPacFile in $IsPacFiles) +\t{ +\t\t# read project file +\t\t$ProjectFile = [System.IO.File]::ReadAllBytes($IsPacFile.FullName) + $ProjectName = $IsPacFile.Name.SubString(0, $IsPacFile.Name.LastIndexOf(\".\")) + +\t\t# deploy project +\t\tWrite-Host \"Deploying project $($IsPacFile.Name)...\" +\t\t$Folder.DeployProject($ProjectName, $ProjectFile) | Out-Null + +\t\t# get reference to deployed project +\t\t$Project = $Folder.Projects[$ProjectName] + +\t\t# check to see if they want to use environments +\t\tif($UseEnvironment) +\t\t{ +\t\t\t# get environment reference +\t\t\t$Environment = Get-Environment -Folder $Folder -EnvironmentName $OctopusParameters['SSIS.Template.EnvironmentName'] + +\t\t\t# set environment reference +\t\t\tSet-EnvironmentReference -Project $Project -Environment $Environment -Folder $Folder + +\t\t\t# check to see if the user wants to convert project parameters to environment variables +\t\t\tif($ReferenceProjectParametersToEnvironmentVairables) +\t\t\t{ +\t\t\t\t# set environment variables +\t\t\t\tWrite-Host \"Referencing Project Parameters to Environment Variables...\" +\t\t\t\t$VariablesToPreserveInEnvironment += Set-ProjectParametersToEnvironmentVariablesReference -Project $Project -Environment $Environment +\t\t\t} + +\t\t\t# check to see if the user wants to convert the package parameters to environment variables +\t\t\tif($ReferencePackageParametersToEnvironmentVairables) +\t\t\t{ +\t\t\t\t# set package variables +\t\t\t\tWrite-Host \"Referencing Package Parameters to Environment Variables...\" +\t\t\t\t$VariablesToPreserveInEnvironment += Set-PackageVariablesToEnvironmentVariablesReference -Project $Project -Environment $Environment +\t\t\t} + + # Removes all unused variables from the environment + if ($SyncEnvironment) + { + Write-Host \"Sync package environment variables...\" + Sync-EnvironmentVariables -Environment $Environment -VariablesToPreserveInEnvironment $VariablesToPreserveInEnvironment + } +\t\t} +\t} +} + +finally +{ +\t# check to make sure sqlconnection isn't null +\tif($sqlConnection) +\t{ +\t\t# check state of sqlconnection +\t\tif($sqlConnection.State -eq [System.Data.ConnectionState]::Open) +\t\t{ +\t\t\t# close the connection +\t\t\t$sqlConnection.Close() +\t\t} + +\t\t# cleanup +\t\t$sqlConnection.Dispose() +\t} +} +#endregion +", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "53aa9e3e-1292-4e75-8095-3666ebd5886b", + "Name": "SSIS.Template.ServerName", + "Label": "Database server name (\\instance)", + "HelpText": "Name of the SQL Server you are deploying to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c0605196-f184-44f6-9b3d-c043323e017c", + "Name": "SSIS.Template.sqlAccountUsername", + "Label": "SQL Authentication Username", + "HelpText": "(Optional) Username of the SQL Authentication account. Use this approach when deploying to Azure Databases with SSIS configured. If SQL Authentication Username and Password are blank, Integrated Authentication is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "224b913f-657c-4d1c-a3d8-0f286203c1ec", + "Name": "SSIS.Template.sqlAccountPassword", + "Label": "SQL Authentication Password", + "HelpText": "(Optional) Password of the SQL Authentication account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "0abbb06c-5f98-44d8-b20d-b811b6bf18da", + "Name": "SSIS.Template.EnableCLR", + "Label": "Enable SQL CLR", + "HelpText": "This will reconfigure SQL Server to enable the SQL CLR. It is highly recommended that this be previously authorized by your Database Administrator.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "ebe9ab8b-3433-4aeb-bafb-642033a9b0a0", + "Name": "SSIS.Template.CatalogName", + "Label": "Catalog name", + "HelpText": "Name of the catalog to create in Integration Services Catalogs on SQL Server. When using the GUI, this value gets hardcoded to SSISDB and cannot be changed. It is recommended that you do not change the default value.", + "DefaultValue": "SSISDB", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d111401b-504f-41f4-96de-b7667fcbf990", + "Name": "SSIS.Template.CatalogPwd", + "Label": "Catalog password", + "HelpText": "Password to the Integration Services Catalog.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "d2fefbf6-c68f-4a66-a5d1-3beb9e51a331", + "Name": "SSIS.Template.FolderName", + "Label": "Folder name", + "HelpText": "Name of the folder to use within the Integration Services Catalog", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ec3589c1-360b-44fc-a19c-f2de0f57a323", + "Name": "SSIS.Template.UseEnvironment", + "Label": "Use environment", + "HelpText": "This will make a project reference to the defined environment.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "718f2836-1c49-4119-a294-9b5488ead33b", + "Name": "SSIS.Template.EnvironmentName", + "Label": "Environment name", + "HelpText": "Name of the environment to reference the project to. If the environment doesn't exist, it will create it.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "303dd486-a389-48c2-a653-fe881c3b7d9a", + "Name": "SSIS.Template.ReferenceProjectParametersToEnvironmentVairables", + "Label": "Reference project parameters to environment variables", + "HelpText": "Checking this box will make Project Parameters reference Environment Variables. If the Environment Variable doesn't exist, it will create it. This expects that an Octopus variable of the same name exists.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "06079e96-3c10-4137-9c99-83da2051f8be", + "Name": "SSIS.Template.ReferencePackageParametersToEnvironmentVairables", + "Label": "Reference package parameters to environment variables", + "HelpText": "Checking this box will make Package Parameters reference Environment Variables. If the Environment Variable doesn't exist, it will create it. This expects than an Octopus variable of the same name exists.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "8c219bf1-0715-4c21-a131-f7bfb44690cd", + "Name": "SSIS.Template.UseFullyQualifiedVariableNames", + "Label": "Use Fully Qualified Variable Names", + "HelpText": "When true the package variables names must be represented in `dtsx_name_without_extension.variable_name`", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "6b29c969-3928-4faf-af17-0911d777d5b1", + "Name": "SSIS.Template.UseCustomFilter", + "Label": "Use Custom Filter for connection manager properties", + "HelpText": "Custom filter should contain the regular expression for ignoring properties when setting will occur during the auto-mapping", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "b33575e2-7246-4c04-b4fa-cd58e2b5e516", + "Name": "SSIS.Template.CustomFilter", + "Label": "Custom Filter for connection manager properties", + "HelpText": "Regular expression for filtering out the connection manager properties during the auto-mapping process. This string is used when `UseCustomFilter` is set to true", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "92b546d9-abf3-45e4-a61b-0315c21455df", + "Name": "SSIS.Template.SyncEnvironment", + "Label": "Clean obsolete variables from environment", + "HelpText": "When `true` synchronizes the environment: +- Removes obsolete variables +- Removes renamed variables +- Replaces values of valid variables (also when `false`)", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "6a1fa441-9e14-4c13-b7f3-9c932d9b4e8e", + "Name": "SSIS.Template.ssisPackageId", + "Label": "Package Id", + "HelpText": "Id of the package to deploy, used to support deployment with Workers.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + } + ], + "$Meta": { + "ExportedAt": "2023-04-14T17:41:15.309Z", + "OctopusVersion": "2023.1.9791", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "sql" +} diff --git a/step-templates/ssis-deploy-ispac-from-package.json.human b/step-templates/ssis-deploy-ispac-from-package.json.human new file mode 100644 index 000000000..487c63293 --- /dev/null +++ b/step-templates/ssis-deploy-ispac-from-package.json.human @@ -0,0 +1,686 @@ +{ + "Id": "bf005449-60c2-4746-8e07-8ba857f93605", + "Name": "Deploy ispac SSIS project from a package", + "Description": "This step template will deploy SSIS ispac projects to SQL Server Integration Services Catalog", + "ActionType": "Octopus.Script", + "Version": 15, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#region Functions + +#region Get-Catalog +Function Get-Catalog +{ + # define parameters + Param ($CatalogName) + # NOTE: using $integrationServices variable defined in main + + # define working varaibles + $Catalog = $null + # check to see if there are any catalogs + if($integrationServices.Catalogs.Count -gt 0 -and $integrationServices.Catalogs[$CatalogName]) + { + \t# get reference to catalog + \t$Catalog = $integrationServices.Catalogs[$CatalogName] + } + else + { + \tif((Get-CLREnabled) -eq 0) + \t{ + \t\tif(-not $EnableCLR) + \t\t{ + \t\t\t# throw error + \t\t\tthrow \"SQL CLR is not enabled.\" + \t\t} + \t\telse + \t\t{ + \t\t\t# display sql clr isn't enabled + \t\t\tWrite-Warning \"SQL CLR is not enabled on $($sqlConnection.DataSource). This feature must be enabled for SSIS catalogs.\" + + \t\t\t# enablign SQLCLR + \t\t\tWrite-Host \"Enabling SQL CLR ...\" + \t\t\tEnable-SQLCLR + \t\t\tWrite-Host \"SQL CLR enabled\" + \t\t} + \t} + + \t# Provision a new SSIS Catalog + \tWrite-Host \"Creating SSIS Catalog ...\" + + \t$Catalog = New-Object \"$ISNamespace.Catalog\" ($integrationServices, $CatalogName, $CatalogPwd) + \t$Catalog.Create() + + + } + + # return the catalog + return $Catalog +} +#endregion + +#region Get-CLREnabled +Function Get-CLREnabled +{ + # define parameters + # Not using any parameters, but am using $sqlConnection defined in main + + # define working variables + $Query = \"SELECT * FROM sys.configurations WHERE name = 'clr enabled'\" + + # execute script + $CLREnabled = Invoke-Sqlcmd -ServerInstance $sqlConnection.DataSource -Database \"master\" -Query $Query | Select value + + # return value + return $CLREnabled.Value +} +#endregion + +#region Enable-SQLCLR +Function Enable-SQLCLR +{ + $QueryArray = \"sp_configure 'show advanced options', 1\", \"RECONFIGURE\", \"sp_configure 'clr enabled', 1\", \"RECONFIGURE \" + # execute script + + foreach($Query in $QueryArray) + { + \tInvoke-Sqlcmd -ServerInstance $sqlConnection.DataSource -Database \"master\" -Query $Query + } + + # check that it's enabled + if((Get-CLREnabled) -ne 1) + { + \t# throw error + \tthrow \"Failed to enable SQL CLR\" + } +} +#endregion + +#region Get-Folder +Function Get-Folder +{ + # parameters + Param($FolderName, $Catalog) + + $Folder = $null + # try to get reference to folder + + if(!($Catalog.Folders -eq $null)) + { + \t$Folder = $Catalog.Folders[$FolderName] + } + + # check to see if $Folder has a value + if($Folder -eq $null) + { + \t# display + \tWrite-Host \"Folder $FolderName doesn't exist, creating folder...\" + + \t# create the folder + \t$Folder = New-Object \"$ISNamespace.CatalogFolder\" ($Catalog, $FolderName, $FolderName) + \t$Folder.Create() + } + + # return the folde reference + return $Folder +} +#endregion + +#region Get-Environment +Function Get-Environment +{ + # define parameters + Param($Folder, $EnvironmentName) + + $Environment = $null + # get reference to Environment + if(!($Folder.Environments -eq $null) -and $Folder.Environments.Count -gt 0) + { + \t$Environment = $Folder.Environments[$EnvironmentName] + } + + # check to see if it's a null reference + if($Environment -eq $null) + { + \t# display + \tWrite-Host \"Environment $EnvironmentName doesn't exist, creating environment...\" + + \t# create environment + \t$Environment = New-Object \"$ISNamespace.EnvironmentInfo\" ($Folder, $EnvironmentName, $EnvironmentName) + \t$Environment.Create() + } + + # return the environment + return $Environment +} +#endregion + +#region Set-EnvironmentReference +Function Set-EnvironmentReference +{ + # define parameters + Param($Project, $Environment, $Folder) + + # get reference + $Reference = $null + + if(!($Project.References -eq $null)) + { + \t$Reference = $Project.References[$Environment.Name, $Folder.Name] + + } + + # check to see if it's a null reference + if($Reference -eq $null) + { + \t# display + \tWrite-Host \"Project does not reference environment $($Environment.Name), creating reference...\" + + \t# create reference + \t$Project.References.Add($Environment.Name, $Folder.Name) + \t$Project.Alter() + } +} +#endregion + +#region Set-ProjectParametersToEnvironmentVariablesReference +Function Set-ProjectParametersToEnvironmentVariablesReference +{ + # define parameters + Param($Project, $Environment) + + $UpsertedVariables = @() + + if($Project.Parameters -eq $null) + { + Write-Host \"No project parameters exist\" + return + } + + # loop through project parameters + foreach($Parameter in $Project.Parameters) + { + # skip if the parameter is included in custom filters + if ($UseCustomFilter) + { + if ($Parameter.Name -match $CustomFilter) + { + Write-Host \"- $($Parameter.Name) skipped due to CustomFilters.\" + continue + } + } + + # Add variable to list of variable + $UpsertedVariables += $Parameter.Name + + $Variable = $null + if(!($Environment.Variables -eq $null)) + { + \t # get reference to variable + \t $Variable = $Environment.Variables[$Parameter.Name] + } + + \t# check to see if variable exists + \tif($Variable -eq $null) + \t{ + \t\t# add the environment variable + \t\tAdd-EnvironmentVariable -Environment $Environment -Parameter $Parameter -ParameterName $Parameter.Name + + \t\t# get reference to the newly created variable + \t\t$Variable = $Environment.Variables[$Parameter.Name] + \t} + + \t# set the environment variable value + \tSet-EnvironmentVariableValue -Variable $Variable -Parameter $Parameter -ParameterName $Parameter.Name + } + + # alter the environment + $Environment.Alter() + $Project.Alter() + + return $UpsertedVariables +} +#endregion + +Function Set-PackageVariablesToEnvironmentVariablesReference +{ + # define parameters + Param($Project, $Environment) + + $Variables = @() + $UpsertedVariables = @() + + # loop through packages in project in order to store a temp collection of variables + foreach($Package in $Project.Packages) + { + \t# loop through parameters of package + \tforeach($Parameter in $Package.Parameters) + \t{ + \t\t# add to the temporary variable collection + \t\t$Variables += $Parameter.Name + \t} + } + + # loop through packages in project + foreach($Package in $Project.Packages) + { + \t# loop through parameters of package + \tforeach($Parameter in $Package.Parameters) + \t{ + if ($UseFullyQualifiedVariableNames) + { + # Set fully qualified variable name + $ParameterName = $Parameter.ObjectName.Replace(\".dtsx\", \"\")+\".\"+$Parameter.Name + } + else + { + # check if exists a variable with the same name + $VariableNameOccurrences = $($Variables | Where-Object { $_ -eq $Parameter.Name }).count + $ParameterName = $Parameter.Name + + if ($VariableNameOccurrences -gt 1) + { + $ParameterName = $Parameter.ObjectName.Replace(\".dtsx\", \"\")+\".\"+$Parameter.Name + } + } + + if ($UseCustomFilter) + { + if ($ParameterName -match $CustomFilter) + { + Write-Host \"- $($Parameter.Name) skipped due to CustomFilters.\" + continue + } + } + + # get reference to variable + \t\t$Variable = $Environment.Variables[$ParameterName] + + # Add variable to list of variable + $UpsertedVariables += $ParameterName + + # check to see if the parameter exists + \t\tif(!$Variable) + \t\t{ + \t\t\t# add the environment variable + \t\t\tAdd-EnvironmentVariable -Environment $Environment -Parameter $Parameter -ParameterName $ParameterName + + \t\t\t# get reference to the newly created variable + \t\t\t$Variable = $Environment.Variables[$ParameterName] + \t\t} + + \t\t# set the environment variable value + \t\tSet-EnvironmentVariableValue -Variable $Variable -Parameter $Parameter -ParameterName $ParameterName + \t} + + \t# alter the package + \t$Package.Alter() + } + + # alter the environment + $Environment.Alter() + + return $UpsertedVariables +} + +Function Sync-EnvironmentVariables +{ + # define parameters + Param($Environment, $VariablesToPreserveInEnvironment) + + foreach($VariableToEvaluate in $Environment.Variables) + { + if ($VariablesToPreserveInEnvironment -notcontains $VariableToEvaluate.Name) + { + Write-Host \"- Removing environment variable: $($VariableToEvaluate.Name)\" + $VariableToRemove = $Environment.Variables[$VariableToEvaluate.Name] + $Environment.Variables.Remove($VariableToRemove) | Out-Null + } + } + + # alter the environment + $Environment.Alter() +} + +#region Add-EnvironmentVariable +Function Add-EnvironmentVariable +{ + # define parameters + Param($Environment, $Parameter, $ParameterName) + + # display + Write-Host \"- Adding environment variable $($ParameterName)\" + + # check to see if design default value is emtpy or null + if([string]::IsNullOrEmpty($Parameter.DesignDefaultValue)) + { + \t# give it something + \t$DefaultValue = \"\" # sensitive variables will not return anything so when trying to use the property of $Parameter.DesignDefaultValue, the Alter method will fail. + } + else + { + \t# take the design + \t$DefaultValue = $Parameter.DesignDefaultValue + } + + # add variable with an initial value + $Environment.Variables.Add($ParameterName, $Parameter.DataType, $DefaultValue, $Parameter.Sensitive, $Parameter.Description) +} +#endregion + +#region Set-EnvironmentVariableValue +Function Set-EnvironmentVariableValue +{ + # define parameters + Param($Variable, $Parameter, $ParameterName) + + # check to make sure variable value is available + if($OctopusParameters -and $OctopusParameters.ContainsKey($ParameterName)) + { + # display + Write-Host \"- Updating environment variable $($ParameterName)\" + + \t# set the variable value + \t$Variable.Value = $OctopusParameters[\"$($ParameterName)\"] + } + else + { + \t# warning + \tWrite-Host \"**- OctopusParameters collection is empty or $($ParameterName) not in the collection -**\" + } + + # Set reference + $Parameter.Set([Microsoft.SqlServer.Management.IntegrationServices.ParameterInfo+ParameterValueType]::Referenced, \"$($ParameterName)\") +} +#endregion + + +#endregion + +#region Dependent assemblies + +# Load the IntegrationServices Assembly +[Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.Management.IntegrationServices\") | Out-Null # Out-Null supresses a message that would normally be displayed saying it loaded out of GAC + +# Store the IntegrationServices Assembly namespace to avoid typing it every time +$ISNamespace = \"Microsoft.SqlServer.Management.IntegrationServices\" + +#endregion + +#region Main +try +{ +\t# ensure all boolean variables are true booleans + $EnableCLR = [System.Convert]::ToBoolean(\"$EnableCLR\") + $UseEnvironment = [System.Convert]::ToBoolean(\"$UseEnvironment\") + $ReferenceProjectParametersToEnvironmentVairables = [System.Convert]::ToBoolean(\"$ReferenceProjectParametersToEnvironmentVairables\") + Write-Host \"Value is $ReferencePackageParametersToEnvironmentVairables\" + $ReferencePackageParametersToEnvironmentVairables = [System.Convert]::ToBoolean(\"$ReferencePackageParametersToEnvironmentVairables\") + $UseFullyQualifiedVariableNames = [System.Convert]::ToBoolean(\"$UseFullyQualifiedVariableNames\") + $SyncEnvironment = [System.Convert]::ToBoolean(\"$SyncEnvironment\") + # custom names for filtering out the excluded variables by design + $UseCustomFilter = [System.Convert]::ToBoolean(\"$UseCustomFilter\") + $CustomFilter = [System.Convert]::ToString(\"$CustomFilter\") + # list of variables names to keep in target environment + $VariablesToPreserveInEnvironment = @() + + # Get file path where Octopus deployed it + $DeployedPath = $OctopusParameters[\"Octopus.Action[$NugetPackageStepName].Output.Package.InstallationDirectoryPath\"] + + + # display the path where it's referencing + Write-Host \"Package deployed to $DeployedPath\" + +\t# Get all .ispac files from the deployed path +\t$IsPacFiles = Get-ChildItem -Recurse -Path $DeployedPath | Where {$_.Extension.ToLower() -eq \".ispac\"} + +\t# display number of files +\tWrite-Host \"$($IsPacFiles.Count) .ispac file(s) found.\" + +\tWrite-Host \"Connecting to server ...\" + +\t# Create a connection to the server + $sqlConnectionString = \"Data Source=$ServerName;Initial Catalog=master;Integrated Security=SSPI;\" + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $sqlConnectionString + +\t# create integration services object +\t$integrationServices = New-Object \"$ISNamespace.IntegrationServices\" $sqlConnection + +\t# get reference to the catalog +\tWrite-Host \"Getting reference to catalog $CataLogName\" +\t$Catalog = Get-Catalog -CatalogName $CataLogName + +\t# get folder reference +\t$Folder = Get-Folder -FolderName $FolderName -Catalog $Catalog + +\t# loop through ispac files +\tforeach($IsPacFile in $IsPacFiles) +\t{ +\t\t# read project file +\t\t$ProjectFile = [System.IO.File]::ReadAllBytes($IsPacFile.FullName) + +\t\t# deploy project +\t\tWrite-Host \"Deploying project $($IsPacFile.Name)...\" +\t\t$Folder.DeployProject($ProjectName, $ProjectFile) | Out-Null + +\t\t# get reference to deployed project +\t\t$Project = $Folder.Projects[$ProjectName] + +\t\t# check to see if they want to use environments +\t\tif($UseEnvironment) +\t\t{ +\t\t\t# get environment reference +\t\t\t$Environment = Get-Environment -Folder $Folder -EnvironmentName $EnvironmentName + +\t\t\t# set environment reference +\t\t\tSet-EnvironmentReference -Project $Project -Environment $Environment -Folder $Folder + +\t\t\t# check to see if the user wants to convert project parameters to environment variables +\t\t\tif($ReferenceProjectParametersToEnvironmentVairables) +\t\t\t{ +\t\t\t\t# set environment variables +\t\t\t\tWrite-Host \"Referencing Project Parameters to Environment Variables...\" +\t\t\t\t$VariablesToPreserveInEnvironment += Set-ProjectParametersToEnvironmentVariablesReference -Project $Project -Environment $Environment +\t\t\t} + +\t\t\t# check to see if the user wants to convert the package parameters to environment variables +\t\t\tif($ReferencePackageParametersToEnvironmentVairables) +\t\t\t{ +\t\t\t\t# set package variables +\t\t\t\tWrite-Host \"Referencing Package Parameters to Environment Variables...\" +\t\t\t\t$VariablesToPreserveInEnvironment += Set-PackageVariablesToEnvironmentVariablesReference -Project $Project -Environment $Environment +\t\t\t} + + # Removes all unused variables from the environment + if ($SyncEnvironment) + { + Write-Host \"Sync package environment variables...\" + Sync-EnvironmentVariables -Environment $Environment -VariablesToPreserveInEnvironment $VariablesToPreserveInEnvironment + } +\t\t} +\t} +} + +finally +{ +\t# check to make sure sqlconnection isn't null +\tif($sqlConnection) +\t{ +\t\t# check state of sqlconnection +\t\tif($sqlConnection.State -eq [System.Data.ConnectionState]::Open) +\t\t{ +\t\t\t# close the connection +\t\t\t$sqlConnection.Close() +\t\t} + +\t\t# cleanup +\t\t$sqlConnection.Dispose() +\t} +} +#endregion +", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "da92ad6a-23ac-4f24-b243-b42507737422", + "Name": "ServerName", + "Label": "Database server name (\\instance)", + "HelpText": "Name of the SQL Server you are deploying to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a3e3e7e5-aa53-429c-b63b-166ddc875e45", + "Name": "EnableCLR", + "Label": "Enable SQL CLR", + "HelpText": "This will reconfigure SQL Server to enable the SQL CLR. It is highly recommended that this be previously authorized by your Database Administrator.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "bb659731-de74-470b-94fe-24b61e27e176", + "Name": "CatalogName", + "Label": "Catalog name", + "HelpText": "Name of the catalog to create in Integration Services Catalogs on SQL Server. When using the GUI, this value gets hardcoded to SSISDB and cannot be changed. It is recommended that you do not change the default value.", + "DefaultValue": "SSISDB", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "af66e1ad-3631-4d4b-99d4-215a1b2beb9d", + "Name": "CatalogPwd", + "Label": "Catalog password", + "HelpText": "Password to the Integration Services Catalog.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "db080c05-a746-4c27-b964-d98b63b00a29", + "Name": "FolderName", + "Label": "Folder name", + "HelpText": "Name of the folder to use within the Integration Services Catalog", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c6264016-2c14-4e24-949c-d8a9d73599ac", + "Name": "ProjectName", + "Label": "Project name", + "HelpText": "Name of the project within the folder of the Integration Services catalog", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f628a71c-5399-4137-926b-3c8d170ba61c", + "Name": "UseEnvironment", + "Label": "Use environment", + "HelpText": "This will make a project reference to the defined environment.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "66eee7ee-7f89-4675-a32a-f33dac895311", + "Name": "EnvironmentName", + "Label": "Environment name", + "HelpText": "Name of the environment to reference the project to. If the environment doesn't exist, it will create it.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a014a370-9b9d-4284-b6be-a603f9f97942", + "Name": "ReferenceProjectParametersToEnvironmentVairables", + "Label": "Reference project parameters to environment variables", + "HelpText": "Checking this box will make Project Parameters reference Environment Variables. If the Environment Variable doesn't exist, it will create it. This expects that an Octopus variable of the same name exists.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "4441ed46-3cc3-42bb-9baa-5753cd4d6d5c", + "Name": "ReferencePackageParametersToEnvironmentVairables", + "Label": "Reference package parameters to environment variables", + "HelpText": "Checking this box will make Package Parameters reference Environment Variables. If the Environment Variable doesn't exist, it will create it. This expects than an Octopus variable of the same name exists.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "3d4e78ec-fe77-4221-be40-1b3d2fbfc3c4", + "Name": "NugetPackageStepName", + "Label": "NuGet package step", + "HelpText": "The step that uploaded the NuGet package to the server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Id": "7c9d808d-60f3-4648-b8a4-e4865268740b", + "Name": "UseFullyQualifiedVariableNames", + "Label": "Use Fully Qualified Variable Names", + "HelpText": "When true the package variables names must be represented in `dtsx_name_without_extension.variable_name`", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "fef889a5-1f5b-48c7-bb65-2f2c3498e6cd", + "Name": "UseCustomFilter", + "Label": "Use Custom Filter for connection manager properties", + "HelpText": "Custom filter should contain the regular expression for ignoring properties when setting will occur during the auto-mapping", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "ebefb099-ce7a-45bf-b52a-bb8ea9915b1b", + "Name": "CustomFilter", + "Label": "Custom Filter for connection manager properties", + "HelpText": "Regular expression for filtering out the connection manager properties during the auto-mapping process. This string is used when `UseCustomFilter` is set to true", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "dbe29935-d3c6-4b77-b796-26347abdbf2c", + "Name": "SyncEnvironment", + "Label": "Clean obsolete variables from environment", + "HelpText": "When `true` synchronizes the environment: +- Removes obsolete variables +- Removes renamed variables +- Replaces values of valid variables (also when `false`)", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "suxstellino", + "$Meta": { + "ExportedAt": "2019-04-18T13:43:05.493Z", + "OctopusVersion": "2018.10.5", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/ssis-deploy-ispac-from-referenced-package.json.human b/step-templates/ssis-deploy-ispac-from-referenced-package.json.human new file mode 100644 index 000000000..2dd19f0bb --- /dev/null +++ b/step-templates/ssis-deploy-ispac-from-referenced-package.json.human @@ -0,0 +1,830 @@ +{ + "Id": "0c8167e9-49fe-4f2a-a007-df5ef2e63fac", + "Name": "Deploy ispac SSIS project from Referenced Package", + "Description": "This step template will deploy SSIS ispac projects to SQL Server Integration Services Catalog. The template uses a referenced package and is Worker compatible. + +This template will install the Nuget package provider if it is not present on the machine it is running on.", + "ActionType": "Octopus.Script", + "Version": 7, + "Author": "twerthi", + "Packages": [ + { + "Id": "7a1f5eb6-0b6a-4319-a0e8-7b4d13ea609e", + "Name": "SSIS Package", + "PackageId": "#{ssisPackageId}", + "FeedId": "#{TemplatePackageFeedId}", + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True" + } + } + ], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "#region Functions + +# Define functions +function Get-SqlModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + +function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Install-SqlServerPowerShellModule +{ + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + +\t# Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) + { + \t# Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + +\t# Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force -RequiredVersion \"21.1.18256\" + +\t# Display + Write-Output \"Importing module $PowerShellModuleName ...\" + + # Import the module + Import-Module -Name $PowerShellModuleName +} + +Function Load-SqlServerAssmblies +{ +\t# Declare parameters + +\t# Get the folder where the SqlServer module ended up in +\t$sqlServerModulePath = [System.IO.Path]::GetDirectoryName((Get-Module SqlServer).Path) + + # Loop through the assemblies + foreach($assemblyFile in (Get-ChildItem -Path $sqlServerModulePath -Exclude msv*.dll | Where-Object {$_.Extension -eq \".dll\"})) + { + # Load the assembly + [Reflection.Assembly]::LoadFile($assemblyFile.FullName) | Out-Null + } +} + +#region Get-Catalog +Function Get-Catalog +{ + # define parameters + Param ($CatalogName) + # NOTE: using $integrationServices variable defined in main + + # define working varaibles + $Catalog = $null + # check to see if there are any catalogs + if($integrationServices.Catalogs.Count -gt 0 -and $integrationServices.Catalogs[$CatalogName]) + { + \t# get reference to catalog + \t$Catalog = $integrationServices.Catalogs[$CatalogName] + } + else + { + \tif((Get-CLREnabled) -eq 0) + \t{ + \t\tif(-not $EnableCLR) + \t\t{ + \t\t\t# throw error + \t\t\tthrow \"SQL CLR is not enabled.\" + \t\t} + \t\telse + \t\t{ + \t\t\t# display sql clr isn't enabled + \t\t\tWrite-Warning \"SQL CLR is not enabled on $($sqlConnection.DataSource). This feature must be enabled for SSIS catalogs.\" + + \t\t\t# enablign SQLCLR + \t\t\tWrite-Host \"Enabling SQL CLR ...\" + \t\t\tEnable-SQLCLR + \t\t\tWrite-Host \"SQL CLR enabled\" + \t\t} + \t} + + \t# Provision a new SSIS Catalog + \tWrite-Host \"Creating SSIS Catalog ...\" + + \t$Catalog = New-Object \"$ISNamespace.Catalog\" ($integrationServices, $CatalogName, $CatalogPwd) + \t$Catalog.Create() + + + } + + # return the catalog + return $Catalog +} +#endregion + +#region Get-CLREnabled +Function Get-CLREnabled +{ + # define parameters + # Not using any parameters, but am using $sqlConnection defined in main + + # define working variables + $Query = \"SELECT * FROM sys.configurations WHERE name = 'clr enabled'\" + + # execute script + $CLREnabled = Invoke-Sqlcmd -ServerInstance $sqlConnection.DataSource -Database \"master\" -Query $Query | Select value + + # return value + return $CLREnabled.Value +} +#endregion + +#region Enable-SQLCLR +Function Enable-SQLCLR +{ + $QueryArray = \"sp_configure 'show advanced options', 1\", \"RECONFIGURE\", \"sp_configure 'clr enabled', 1\", \"RECONFIGURE \" + # execute script + + foreach($Query in $QueryArray) + { + \tInvoke-Sqlcmd -ServerInstance $sqlConnection.DataSource -Database \"master\" -Query $Query + } + + # check that it's enabled + if((Get-CLREnabled) -ne 1) + { + \t# throw error + \tthrow \"Failed to enable SQL CLR\" + } +} +#endregion + +#region Get-Folder +Function Get-Folder +{ + # parameters + Param($FolderName, $Catalog) + + $Folder = $null + # try to get reference to folder + + if(!($Catalog.Folders -eq $null)) + { + \t$Folder = $Catalog.Folders[$FolderName] + } + + # check to see if $Folder has a value + if($Folder -eq $null) + { + \t# display + \tWrite-Host \"Folder $FolderName doesn't exist, creating folder...\" + + \t# create the folder + \t$Folder = New-Object \"$ISNamespace.CatalogFolder\" ($Catalog, $FolderName, $FolderName) + \t$Folder.Create() + } + + # return the folde reference + return $Folder +} +#endregion + +#region Get-Environment +Function Get-Environment +{ + # define parameters + Param($Folder, $EnvironmentName) + + $Environment = $null + # get reference to Environment + if(!($Folder.Environments -eq $null) -and $Folder.Environments.Count -gt 0) + { + \t$Environment = $Folder.Environments[$EnvironmentName] + } + + # check to see if it's a null reference + if($Environment -eq $null) + { + \t# display + \tWrite-Host \"Environment $EnvironmentName doesn't exist, creating environment...\" + + \t# create environment + \t$Environment = New-Object \"$ISNamespace.EnvironmentInfo\" ($Folder, $EnvironmentName, $EnvironmentName) + \t$Environment.Create() + } + + # return the environment + return $Environment +} +#endregion + +#region Set-EnvironmentReference +Function Set-EnvironmentReference +{ + # define parameters + Param($Project, $Environment, $Folder) + + # get reference + $Reference = $null + + if(!($Project.References -eq $null)) + { + \t$Reference = $Project.References[$Environment.Name, $Folder.Name] + + } + + # check to see if it's a null reference + if($Reference -eq $null) + { + \t# display + \tWrite-Host \"Project does not reference environment $($Environment.Name), creating reference...\" + + \t# create reference + \t$Project.References.Add($Environment.Name, $Folder.Name) + \t$Project.Alter() + } +} +#endregion + +#region Set-ProjectParametersToEnvironmentVariablesReference +Function Set-ProjectParametersToEnvironmentVariablesReference +{ + # define parameters + Param($Project, $Environment) + + $UpsertedVariables = @() + + if($Project.Parameters -eq $null) + { + Write-Host \"No project parameters exist\" + return + } + + # loop through project parameters + foreach($Parameter in $Project.Parameters) + { + # skip if the parameter is included in custom filters + if ($UseCustomFilter) + { + if ($Parameter.Name -match $CustomFilter) + { + Write-Host \"- $($Parameter.Name) skipped due to CustomFilters.\" + continue + } + } + + # Add variable to list of variable + $UpsertedVariables += $Parameter.Name + + $Variable = $null + if(!($Environment.Variables -eq $null)) + { + \t # get reference to variable + \t $Variable = $Environment.Variables[$Parameter.Name] + } + + \t# check to see if variable exists + \tif($Variable -eq $null) + \t{ + \t\t# add the environment variable + \t\tAdd-EnvironmentVariable -Environment $Environment -Parameter $Parameter -ParameterName $Parameter.Name + + \t\t# get reference to the newly created variable + \t\t$Variable = $Environment.Variables[$Parameter.Name] + \t} + + \t# set the environment variable value + \tSet-EnvironmentVariableValue -Variable $Variable -Parameter $Parameter -ParameterName $Parameter.Name + } + + # alter the environment + $Environment.Alter() + $Project.Alter() + + return $UpsertedVariables +} +#endregion + +Function Set-PackageVariablesToEnvironmentVariablesReference +{ + # define parameters + Param($Project, $Environment) + + $Variables = @() + $UpsertedVariables = @() + + # loop through packages in project in order to store a temp collection of variables + foreach($Package in $Project.Packages) + { + \t# loop through parameters of package + \tforeach($Parameter in $Package.Parameters) + \t{ + \t\t# add to the temporary variable collection + \t\t$Variables += $Parameter.Name + \t} + } + + # loop through packages in project + foreach($Package in $Project.Packages) + { + \t# loop through parameters of package + \tforeach($Parameter in $Package.Parameters) + \t{ + if ($UseFullyQualifiedVariableNames) + { + # Set fully qualified variable name + $ParameterName = $Parameter.ObjectName.Replace(\".dtsx\", \"\")+\".\"+$Parameter.Name + } + else + { + # check if exists a variable with the same name + $VariableNameOccurrences = $($Variables | Where-Object { $_ -eq $Parameter.Name }).count + $ParameterName = $Parameter.Name + + if ($VariableNameOccurrences -gt 1) + { + $ParameterName = $Parameter.ObjectName.Replace(\".dtsx\", \"\")+\".\"+$Parameter.Name + } + } + + if ($UseCustomFilter) + { + if ($ParameterName -match $CustomFilter) + { + Write-Host \"- $($Parameter.Name) skipped due to CustomFilters.\" + continue + } + } + + # get reference to variable + \t\t$Variable = $Environment.Variables[$ParameterName] + + # Add variable to list of variable + $UpsertedVariables += $ParameterName + + # check to see if the parameter exists + \t\tif(!$Variable) + \t\t{ + \t\t\t# add the environment variable + \t\t\tAdd-EnvironmentVariable -Environment $Environment -Parameter $Parameter -ParameterName $ParameterName + + \t\t\t# get reference to the newly created variable + \t\t\t$Variable = $Environment.Variables[$ParameterName] + \t\t} + + \t\t# set the environment variable value + \t\tSet-EnvironmentVariableValue -Variable $Variable -Parameter $Parameter -ParameterName $ParameterName + \t} + + \t# alter the package + \t$Package.Alter() + } + + # alter the environment + $Environment.Alter() + + return $UpsertedVariables +} + +Function Sync-EnvironmentVariables +{ + # define parameters + Param($Environment, $VariablesToPreserveInEnvironment) + + foreach($VariableToEvaluate in $Environment.Variables) + { + if ($VariablesToPreserveInEnvironment -notcontains $VariableToEvaluate.Name) + { + Write-Host \"- Removing environment variable: $($VariableToEvaluate.Name)\" + $VariableToRemove = $Environment.Variables[$VariableToEvaluate.Name] + $Environment.Variables.Remove($VariableToRemove) | Out-Null + } + } + + # alter the environment + $Environment.Alter() +} + +#region Add-EnvironmentVariable +Function Add-EnvironmentVariable +{ + # define parameters + Param($Environment, $Parameter, $ParameterName) + + # display + Write-Host \"- Adding environment variable $($ParameterName)\" + + # check to see if design default value is emtpy or null + if([string]::IsNullOrEmpty($Parameter.DesignDefaultValue)) + { + \t# give it something + \t$DefaultValue = \"\" # sensitive variables will not return anything so when trying to use the property of $Parameter.DesignDefaultValue, the Alter method will fail. + } + else + { + \t# take the design + \t$DefaultValue = $Parameter.DesignDefaultValue + } + + # add variable with an initial value + $Environment.Variables.Add($ParameterName, $Parameter.DataType, $DefaultValue, $Parameter.Sensitive, $Parameter.Description) +} +#endregion + +#region Set-EnvironmentVariableValue +Function Set-EnvironmentVariableValue +{ + # define parameters + Param($Variable, $Parameter, $ParameterName) + + # check to make sure variable value is available + if($OctopusParameters -and $OctopusParameters.ContainsKey($ParameterName)) + { + # display + Write-Host \"- Updating environment variable $($ParameterName)\" + + \t# set the variable value + \t$Variable.Value = $OctopusParameters[\"$($ParameterName)\"] + } + else + { + \t# warning + \tWrite-Host \"**- OctopusParameters collection is empty or $($ParameterName) not in the collection -**\" + } + + # Set reference + $Parameter.Set([Microsoft.SqlServer.Management.IntegrationServices.ParameterInfo+ParameterValueType]::Referenced, \"$($ParameterName)\") +} +#endregion + +# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\" + +# Check to see if SqlServer module is installed +if ((Get-SqlModuleInstalled -PowerShellModuleName \"SqlServer\") -ne $true) +{ +\t# Display message + Write-Output \"PowerShell module SqlServer not present, downloading temporary copy ...\" + +\t#Enable TLS 1.2 as default protocol +\t[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 + + # Download and install temporary copy + Install-SqlServerPowerShellModule -PowerShellModuleName \"SqlServer\" -LocalModulesPath $LocalModules + +\t#region Dependent assemblies +\tLoad-SqlServerAssmblies +} +else +{ +\t# Load the IntegrationServices Assembly +\t[Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.Management.IntegrationServices\") | Out-Null # Out-Null supresses a message that would normally be displayed saying it loaded out of GAC +} + +#endregion + +# Store the IntegrationServices Assembly namespace to avoid typing it every time +$ISNamespace = \"Microsoft.SqlServer.Management.IntegrationServices\" + +#endregion + +#region Main +try +{ + # ensure all boolean variables are true booleans + $EnableCLR = [System.Convert]::ToBoolean(\"$EnableCLR\") + $UseEnvironment = [System.Convert]::ToBoolean(\"$UseEnvironment\") + $ReferenceProjectParametersToEnvironmentVairables = [System.Convert]::ToBoolean(\"$ReferenceProjectParametersToEnvironmentVairables\") + + $ReferencePackageParametersToEnvironmentVairables = [System.Convert]::ToBoolean(\"$ReferencePackageParametersToEnvironmentVairables\") + $UseFullyQualifiedVariableNames = [System.Convert]::ToBoolean(\"$UseFullyQualifiedVariableNames\") + $SyncEnvironment = [System.Convert]::ToBoolean(\"$SyncEnvironment\") + # custom names for filtering out the excluded variables by design + $UseCustomFilter = [System.Convert]::ToBoolean(\"$UseCustomFilter\") + $CustomFilter = [System.Convert]::ToString(\"$CustomFilter\") + # list of variables names to keep in target environment + $VariablesToPreserveInEnvironment = @() + +\t# Get the extracted path +\t$DeployedPath = $OctopusParameters[\"Octopus.Action.Package[$ssisPackageId].ExtractedPath\"] + +\t# Get all .ispac files from the deployed path +\t$IsPacFiles = Get-ChildItem -Recurse -Path $DeployedPath | Where {$_.Extension.ToLower() -eq \".ispac\"} + +\t# display number of files +\tWrite-Host \"$($IsPacFiles.Count) .ispac file(s) found.\" + +\tWrite-Host \"Connecting to server ...\" + +\t# Create a connection to the server + $sqlConnectionString = \"Data Source=$ServerName;Initial Catalog=SSISDB;\" + + if (![string]::IsNullOrEmpty($sqlAccountUsername) -and ![string]::IsNullOrEmpty($sqlAccountPassword)) + { + \t# Add username and password to connection string + $sqlConnectionString += \"User ID=$sqlAccountUsername; Password=$sqlAccountPassword;\" + } + else + { + \t# Use integrated + $sqlConnectionString += \"Integrated Security=SSPI;\" + } + + + # Create new connection object with connection string + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $sqlConnectionString + +\t# create integration services object +\t$integrationServices = New-Object \"$ISNamespace.IntegrationServices\" $sqlConnection + +\t# get reference to the catalog +\tWrite-Host \"Getting reference to catalog $CataLogName\" +\t$Catalog = Get-Catalog -CatalogName $CataLogName + +\t# get folder reference +\t$Folder = Get-Folder -FolderName $FolderName -Catalog $Catalog + +\t# loop through ispac files +\tforeach($IsPacFile in $IsPacFiles) +\t{ +\t\t# read project file +\t\t$ProjectFile = [System.IO.File]::ReadAllBytes($IsPacFile.FullName) + +\t\t# deploy project +\t\tWrite-Host \"Deploying project $($IsPacFile.Name)...\" +\t\t$Folder.DeployProject($ProjectName, $ProjectFile) | Out-Null + +\t\t# get reference to deployed project +\t\t$Project = $Folder.Projects[$ProjectName] + +\t\t# check to see if they want to use environments +\t\tif($UseEnvironment) +\t\t{ +\t\t\t# get environment reference +\t\t\t$Environment = Get-Environment -Folder $Folder -EnvironmentName $EnvironmentName + +\t\t\t# set environment reference +\t\t\tSet-EnvironmentReference -Project $Project -Environment $Environment -Folder $Folder + +\t\t\t# check to see if the user wants to convert project parameters to environment variables +\t\t\tif($ReferenceProjectParametersToEnvironmentVairables) +\t\t\t{ +\t\t\t\t# set environment variables +\t\t\t\tWrite-Host \"Referencing Project Parameters to Environment Variables...\" +\t\t\t\t$VariablesToPreserveInEnvironment += Set-ProjectParametersToEnvironmentVariablesReference -Project $Project -Environment $Environment +\t\t\t} + +\t\t\t# check to see if the user wants to convert the package parameters to environment variables +\t\t\tif($ReferencePackageParametersToEnvironmentVairables) +\t\t\t{ +\t\t\t\t# set package variables +\t\t\t\tWrite-Host \"Referencing Package Parameters to Environment Variables...\" +\t\t\t\t$VariablesToPreserveInEnvironment += Set-PackageVariablesToEnvironmentVariablesReference -Project $Project -Environment $Environment +\t\t\t} + + # Removes all unused variables from the environment + if ($SyncEnvironment) + { + Write-Host \"Sync package environment variables...\" + Sync-EnvironmentVariables -Environment $Environment -VariablesToPreserveInEnvironment $VariablesToPreserveInEnvironment + } +\t\t} +\t} +} + +finally +{ +\t# check to make sure sqlconnection isn't null +\tif($sqlConnection) +\t{ +\t\t# check state of sqlconnection +\t\tif($sqlConnection.State -eq [System.Data.ConnectionState]::Open) +\t\t{ +\t\t\t# close the connection +\t\t\t$sqlConnection.Close() +\t\t} + +\t\t# cleanup +\t\t$sqlConnection.Dispose() +\t} +} +#endregion +", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "97e5d9db-fd73-4620-84f7-91d6e31e0c4b", + "Name": "ServerName", + "Label": "Database server name (\\instance)", + "HelpText": "Name of the SQL Server you are deploying to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b5adc9a7-2bcb-45c8-9a64-02e0dc286981", + "Name": "sqlAccountUsername", + "Label": "SQL Authentication Username", + "HelpText": "(Optional) Username of the SQL Authentication account. Use this approach when deploying to Azure Databases with SSIS configured. If SQL Authentication Username and Password are blank, Integrated Authentication is used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0646a66e-5ba5-4761-a388-8a8bc73492ec", + "Name": "sqlAccountPassword", + "Label": "SQL Authentication Password", + "HelpText": "(Optional) Password of the SQL Authentication account.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "46e7103c-2833-4bf0-9f98-b5d2a0bbcabf", + "Name": "EnableCLR", + "Label": "Enable SQL CLR", + "HelpText": "This will reconfigure SQL Server to enable the SQL CLR. It is highly recommended that this be previously authorized by your Database Administrator.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "970702e3-e0bf-4ea7-b1ba-067128a3f8c3", + "Name": "CatalogName", + "Label": "Catalog name", + "HelpText": "Name of the catalog to create in Integration Services Catalogs on SQL Server. When using the GUI, this value gets hardcoded to SSISDB and cannot be changed. It is recommended that you do not change the default value.", + "DefaultValue": "SSISDB", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "ccc0a5bf-467b-4695-bcef-912043e27d49", + "Name": "CatalogPwd", + "Label": "Catalog password", + "HelpText": "Password to the Integration Services Catalog.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "5c02a114-b06e-4547-bb0f-b2aba1869d8e", + "Name": "FolderName", + "Label": "Folder name", + "HelpText": "Name of the folder to use within the Integration Services Catalog", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "88ffb943-5525-4484-9963-0a34c89a7085", + "Name": "ProjectName", + "Label": "Project name", + "HelpText": "Name of the project within the folder of the Integration Services catalog", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b8c29eac-a60f-4b25-96b8-f3b2e3fd8806", + "Name": "UseEnvironment", + "Label": "Use environment", + "HelpText": "This will make a project reference to the defined environment.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "156a4b55-26e4-4c70-bf91-8729ca131c62", + "Name": "EnvironmentName", + "Label": "Environment name", + "HelpText": "Name of the environment to reference the project to. If the environment doesn't exist, it will create it.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0339168d-9a53-4d8a-97d9-7a76605bdc55", + "Name": "ReferenceProjectParametersToEnvironmentVairables", + "Label": "Reference project parameters to environment variables", + "HelpText": "Checking this box will make Project Parameters reference Environment Variables. If the Environment Variable doesn't exist, it will create it. This expects that an Octopus variable of the same name exists.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "5f6b7788-4957-451a-bec7-092e471291f2", + "Name": "ReferencePackageParametersToEnvironmentVairables", + "Label": "Reference package parameters to environment variables", + "HelpText": "Checking this box will make Package Parameters reference Environment Variables. If the Environment Variable doesn't exist, it will create it. This expects than an Octopus variable of the same name exists.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "cc4819a4-a6e4-4220-a581-0f2aed51f4a6", + "Name": "UseFullyQualifiedVariableNames", + "Label": "Use Fully Qualified Variable Names", + "HelpText": "When true the package variables names must be represented in `dtsx_name_without_extension.variable_name`", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "bb22984a-43ed-45c7-978d-72fddec96fe1", + "Name": "UseCustomFilter", + "Label": "Use Custom Filter for connection manager properties", + "HelpText": "Custom filter should contain the regular expression for ignoring properties when setting will occur during the auto-mapping", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "d9bba05b-d8a7-40b0-9893-4c878a264de6", + "Name": "CustomFilter", + "Label": "Custom Filter for connection manager properties", + "HelpText": "Regular expression for filtering out the connection manager properties during the auto-mapping process. This string is used when `UseCustomFilter` is set to true", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "77237f87-bc01-4541-b477-005f7416dce2", + "Name": "SyncEnvironment", + "Label": "Clean obsolete variables from environment", + "HelpText": "When `true` synchronizes the environment: +- Removes obsolete variables +- Removes renamed variables +- Replaces values of valid variables (also when `false`)", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "34dcc677-6091-4e92-8b40-9ab09ed18fa3", + "Name": "ssisPackageId", + "Label": "Package Id", + "HelpText": "Id of the package to deploy, used to support deployment with Workers.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8338dc00-27ab-4c60-809a-2badd22250e9", + "Name": "TemplatePackageFeedId", + "Label": "Package Feed Id", + "HelpText": "ID of the package feed the referenced package resides in.", + "DefaultValue": "feeds-builtin", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2020-06-15T18:38:53.170Z", + "OctopusVersion": "2020.2.13", + "Type": "ActionTemplate" + }, + "Category": "sql" + } diff --git a/step-templates/ssis-deploy-ispac-with-enviroment.json.human b/step-templates/ssis-deploy-ispac-with-enviroment.json.human new file mode 100644 index 000000000..63d947b7e --- /dev/null +++ b/step-templates/ssis-deploy-ispac-with-enviroment.json.human @@ -0,0 +1,502 @@ +{ + "Id": "b791c0c2-03ce-40ef-9446-78beb585c0b8", + "Name": "SSIS deploy ISPAC With Environment", + "Description": "A Template which will deploy any .ISPAC files from the published package step selected to the SQL Server catalog database. + +The template will create a environment config and use variables that are wrapped in SSIS[] extracting the SSIS Parameter Path using that as the name and value linked the Octopus Variable and then assign parameter in the SSIS project to the name environment configuration. + +When the SSIS Parameter Path is at the Package level then SSIS Parameter will need to have the Package Name (without .dtsx) added to front of the SSIS Parameter path. + +To remove and environment config delete the variable or remove the SSIS[]. +", + "ActionType": "Octopus.Script", + "Version": 18, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define PowerShell Modules path +$LocalModules = (New-Item \"$PSScriptRoot\\Modules\" -ItemType Directory -Force).FullName +$env:PSModulePath = \"$LocalModules;$env:PSModulePath\" + +#region Functions + +#region SQLPS functions +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-SqlModuleInstalled { + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) { + # It is installed + return $true + } + else { + # Module not installed + return $false + } +} + +function Install-SqlServerPowerShellModule { + # Define parameters + param( + $PowerShellModuleName, + $LocalModulesPath + ) + + # Check to see if the package provider has been installed + if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + # Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + } + + # Save the module in the temporary location + Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force + + # Display + Write-Output \"Importing module $PowerShellModuleName ...\" + + # Import the module + Import-Module -Name $PowerShellModuleName +} + +Function Get-SqlServerAssmblies { + # Declare parameters + + # Get the folder where the SqlServer module ended up in + $sqlServerModulePath = [System.IO.Path]::GetDirectoryName((Get-Module -ListAvailable -Name \"SqlServer\").Path) + + # Loop through the assemblies + foreach ($assemblyFile in (Get-ChildItem -Path $sqlServerModulePath -Exclude msv*.dll | Where-Object { $_.Extension -eq \".dll\" })) { + # Load the assembly + [Reflection.Assembly]::LoadFile($assemblyFile.FullName) | Out-Null + } +} + +#endregion SQLPS functions + +#region SQL Server functions +function Get-ConnectionObject { + param + ( + [parameter(ParameterSetName = 'Default', HelpMessage = 'Name of the server', Mandatory = $true)] + [parameter(ParameterSetName = 'SQLAuth', HelpMessage = 'Name of the server', Mandatory = $true)] + [string] + $SQLServerName, + [parameter(ParameterSetName = 'SQLAuth')] + [switch] $UseSQLAuth, + [parameter(ParameterSetName = 'SQLAuth', HelpMessage = 'SQL Server Login', Mandatory = $true)] + [string] $SQLLogin, + [parameter(ParameterSetName = 'SQLAuth', HelpMessage = 'SQL Server Password', Mandatory = $true)] + [string] $Password + + ) + # Create a connection to the server + $sqlConnectionString = \"Data Source=$ServerName;Initial Catalog=SSISDB;\" + + if ($UseSQLAuth) { + # Add username and password to connection string + $sqlConnectionString += \"User ID=$SQLLogin; Password=$Password;\" + } + else { + # Use integrated + $sqlConnectionString += \"Integrated Security=SSPI;\" + } + + # Create new connection object with connection string + return (New-Object System.Data.SqlClient.SqlConnection $sqlConnectionString) +} +#endregion SQL Server functions + +#region SQL SSIS functions +Function Get-Catalog { + # define parameters + Param ($CatalogName) + + # define working varaibles + $Catalog = $null + # check to see if there are any catalogs + if ($integrationServices.Catalogs.Count -eq 0 -or -not $integrationServices.Catalogs[$CatalogName]) { + throw \"SSIS Catalog not found $CatalogName\" + # get reference to catalog + } + + $Catalog = $integrationServices.Catalogs[$CatalogName] + + # return the catalog + return $Catalog +} + +Function Get-Folder { + # parameters + Param($FolderName, $Catalog) + + $Folder = $null + # try to get reference to folder + + if (!($null -eq $Catalog.Folders)) { + $Folder = $Catalog.Folders[$FolderName] + } + + # check to see if $Folder has a value + if ($null -eq $Folder) { + # display + Write-Host \"Folder $FolderName doesn't exist, creating folder...\" + + # create the folder + $Folder = New-Object \"$ISNamespace.CatalogFolder\" ($Catalog, $FolderName, $FolderName) + $Folder.Create() + } + + # return the folde reference + return $Folder +} + +Function Get-Environment { + # define parameters + Param($Folder, $EnvironmentName) + + $Environment = $null + # get reference to Environment + if (!($null -eq $Folder.Environments) -and $Folder.Environments.Count -gt 0) { + $Environment = $Folder.Environments[$EnvironmentName] + } + + # check to see if it's a null reference + if ($null -eq $Environment) { + # display + Write-Host \"Environment $EnvironmentName doesn't exist, creating environment...\" + + # create environment + $Environment = New-Object \"$ISNamespace.EnvironmentInfo\" ($Folder, $EnvironmentName, $EnvironmentName) + $Environment.Create() + } + + # return the environment + return $Environment +} +Function Set-PojectEnvironmentReference { + # define parameters + Param($Project, $Environment, $Folder) + + # get reference + $Reference = $null + + if (!($null -eq $Project.References)) { + $Reference = $Project.References[$Environment.Name, $Folder.Name] + + } + + # check to see if it's a null reference + if ($null -eq $Reference) { + # display + Write-Host \"Removeing old Project reference environment creating reference...\" + foreach ( $Reference in $Project.References) { + $Project.References.Remove($Reference.Name, $Reference.EnvironmentFolderName) + } + Write-Host \"Project does not reference environment $($Environment.Name), creating reference...\" + # create reference + $Project.References.Add($Environment.Name, $Folder.Name) + $Project.Alter() + } +} + +Function Add-EnvironmentVariable { + # define parameters + Param($Environment, $Parameter, $ParameterName, $EnvironmentValue ) + + # display + Write-Host \"- Adding environment variable $($ParameterName)\" + + # add variable with an initial value + $Environment.Variables.Add($ParameterName, $Parameter.DataType, $EnvironmentValue, $Parameter.Sensitive, $Parameter.Description) + $Parameter.Set([Microsoft.SqlServer.Management.IntegrationServices.ParameterInfo+ParameterValueType]::Referenced, $ParameterName) +} + +Function Remove-ReferencedValue { + Param($Project + , $ParameterName + ) + + if ($ParameterName -match \"\\|\") { + $ParameterPath = $ParameterName.Split(\"|\") + $Project.Packages[$ParameterPath[0]].Parameters[$ParameterPath[1]].Clear() + $Project.Packages[$ParameterPath[0]].Alter() + } + else { + $Project.Parameters[$ParameterName].Clear() + } +} + +Function Sync-EnvironmentVariables { + # define parameters + Param($Environment + , $Project + , $ReferencedVariables + , $OctopusParameters + , $OctopusSSISParameterKey ) + + foreach ($VariableToEvaluate in $Environment.Variables) { + $OctopusParameterKey = $OctopusSSISParameterKey -f $VariableToEvaluate.Name +` if (-not $OctopusParameters.Keys.Contains($OctopusParameterKey)) { + Write-Host \"- Removing environment variable: $($VariableToEvaluate.Name)\" + $Environment.Variables.Remove($VariableToEvaluate) | Out-Null + if ($ReferencedVariables.Keys.Contains($VariableToEvaluate.Name)) { + Remove-ReferencedValue -Project $Project -ParameterName $ReferencedVariables[$VariableToEvaluate.Name] + $ReferencedVariables.Remove($VariableToEvaluate.Name) + } + } + } + + foreach ($ReferencedKeyName in $ReferencedVariables.Keys) { +` if (-not $Environment.Variables.Contains($ReferencedKeyName)) { + Write-Host \"- Removing Prodect referenace $($ReferencedVariables[$ReferencedKeyName] -replace \"\\|\", \" \")\" + Remove-ReferencedValue -Project $Project -ParameterName $ReferencedVariables[$ReferencedKeyName] + } + } + + $Project.Alter() + # alter the environment + $Environment.Alter() +} + +Function Set-ProjectParametersToEnvironmentVariablesReference { + # define parameters + Param($Project + , $Environment + , $OctopusParameters + , $OctopusSSISParameterKey + ) + + $ReferencedVariables = @{ } + + if ($null -eq $Project.Parameters) { + Write-Host \"No project parameters exist\" + return + } + + # loop through project parameters + foreach ($Parameter in $Project.Parameters) { + $OctopusParameterKey = $OctopusSSISParameterKey -f $Parameter.Name + # Add variable to list of variable + if ($OctopusParameters.Keys.Contains($OctopusParameterKey)) { + Write-Host $(\"Updating Project / Environment {0} Parameter Value ...\" -f $Parameter.Name) + $EnvironmentValue = $OctopusParameters[$OctopusParameterKey] + $Variable = $null + if (!($null -eq $Environment.Variables)) { + # get reference to variable + $Variable = $Environment.Variables[$Parameter.Name] + } + # check to see if variable exists + if ($null -eq $Variable) { + # add the environment variable + Add-EnvironmentVariable -Environment $Environment -Parameter $Parameter -ParameterName $Parameter.Name -EnvironmentValue $EnvironmentValue + } + else { + $Variable.value = $EnvironmentValue + } + } + if ($Parameter.ValueType -eq [Microsoft.SqlServer.Management.IntegrationServices.ParameterInfo+ParameterValueType]::Referenced) { + $ReferencedVariables.Add($Parameter.ReferencedVariableName, $Parameter.Name) + } + } + + # alter the environment + $Environment.Alter() + $Project.Alter() + return $ReferencedVariables +} + +Function Set-PackageVariablesToEnvironmentVariablesReference { + # define parameters + Param($Project + , $Environment + , $OctopusParameters + , $OctopusSSISParameterKey) + + $ReferencedVariables = @{ } + + # loop through packages in project + foreach ($Package in $Project.Packages) { + # loop through parameters of package + foreach ($Parameter in $Package.Parameters) { + + $ParameterName = $Parameter.ObjectName.Replace(\".dtsx\", \"\") + \".\" + $Parameter.Name + $OctopusParameterKey = $OctopusSSISParameterKey -f $ParameterName + + # Add variable to list of variable + if ($OctopusParameters.Keys.Contains($OctopusParameterKey)) { + Write-Host $(\"Updating {0} Package / Environment {1} Parameter Value ...\" -f $Parameter.ObjectName, $ParameterName) + $EnvironmentValue = $OctopusParameters[$OctopusParameterKey] + $Variable = $null + if (!($null -eq $Environment.Variables)) { + # get reference to variable + $Variable = $Environment.Variables[$ParameterName] + } + # check to see if the parameter exists + if (!$Variable) { + # add the environment variable + Add-EnvironmentVariable -Environment $Environment -Parameter $Parameter -ParameterName $ParameterName -EnvironmentValue $EnvironmentValue + } + else { + $Variable.value = $EnvironmentValue + } + } + if ($Parameter.ValueType -eq [Microsoft.SqlServer.Management.IntegrationServices.ParameterInfo+ParameterValueType]::Referenced) { + $ReferencedVariables.Add($Parameter.ReferencedVariableName, $(\"{0}|{1}\" -f $Package.Name, $Parameter.Name)) + } + } + + # alter the package + $Package.Alter() + } + + # alter the environment + $Environment.Alter() + + return $ReferencedVariables +} + +#endregion SQL SSIS functions + +#endregion Functions + +#region Script Main + +#region Get enviroment configured +if ((Get-SqlModuleInstalled -PowerShellModuleName \"SqlServer\") -ne $true) { + # Display message + Write-Output \"PowerShell module SqlServer not present, downloading temporary copy ...\" + + # Download and install temporary copy + Install-SqlServerPowerShellModule -PowerShellModuleName \"SqlServer\" -LocalModulesPath $LocalModules +} + +# Dependent assemblies +Get-SqlServerAssmblies + + +# add snapins-- applies to sql server 2008 r2, newer version of SQL do not require this. +Add-PSSnapin SqlServerCmdletSnapin100 -ErrorAction SilentlyContinue +Add-PSSnapin SqlServerProviderSnapin100 -ErrorAction SilentlyContinue +#endregion Get enviroment configured + + +$ISNamespace = \"Microsoft.SqlServer.Management.IntegrationServices\" +$OctopusSSISParameterKey = \"SSIS[{0}]\" +$DeployedPath = $OctopusParameters[\"Octopus.Action[$NugetPackageStepName].Output.Package.InstallationDirectoryPath\"] + +Write-Host \"Connecting to server ...\" + +if ([string]::IsNullOrEmpty($sqlAccountUsername) -and [string]::IsNullOrEmpty($sqlAccountPassword)) { + # Add username and password to connection string + $sqlConnection = Get-ConnectionObject -SQLServerName $ServerName +} +else { + # Use integrated + $sqlConnection = Get-ConnectionObject -SQLServerName $ServerName -UseSQLAuth -SQLLogin $sqlAccountUsername -Password=$sqlAccountPassword +} + +$integrationServices = New-Object \"$ISNamespace.IntegrationServices\" $sqlConnection + + +$IsPacFiles = Get-ChildItem -Recurse -Path $DeployedPath | Where-Object { $_.Extension.ToLower() -eq \".ispac\" } +Write-Host \"$($IsPacFiles.Count) .ispac file(s) found.\" + +# get reference to the catalog +Write-Host \"Getting reference to catalog $CatalogName\" +$Catalog = Get-Catalog -CatalogName $CatalogName +$Folder = Get-Folder -FolderName $FolderName -Catalog $Catalog + +$ReferencedVariables = @{ } +foreach ($IsPacFile in $IsPacFiles) { + $ProjectFile = [System.IO.File]::ReadAllBytes($IsPacFile.FullName) + $ProjectName = $IsPacFile.Name -replace $IsPacFile.Extension , \"\" + $EnvironmentName = \"{0}_{1}\" -f $ProjectName , $EnvironmentTag + # deploy project + Write-Host \"Deploying project $($IsPacFile.Name)...\" + $Folder.DeployProject($ProjectName, $ProjectFile) | Out-Null + + # get reference to deployed project + $Project = $Folder.Projects[$ProjectName] + # get environment reference + $Environment = Get-Environment -Folder $Folder -EnvironmentName $EnvironmentName + Set-PojectEnvironmentReference -Project $Project -Environment $Environment -Folder $Folder + Write-Host \"Referencing Project Parameters to Environment Variables...\" + $ReferencedVariables += Set-ProjectParametersToEnvironmentVariablesReference -Project $Project -Environment $Environment -OctopusParameters $OctopusParameters -OctopusSSISParameterKey $OctopusSSISParameterKey + Write-Host \"Referencing Project Parameters to Environment Variables...\" + $ReferencedVariables += Set-PackageVariablesToEnvironmentVariablesReference -Project $Project -Environment $Environment -OctopusParameters $OctopusParameters -OctopusSSISParameterKey $OctopusSSISParameterKey + Write-Host \"Sync package environment variables...\" + Sync-EnvironmentVariables -Environment $Environment -Project $Project -ReferencedVariables $ReferencedVariables -OctopusParameters $OctopusParameters -OctopusSSISParameterKey $OctopusSSISParameterKey +} " + }, + "Parameters": [ + { + "Id": "4d49a1f7-f7bf-40e2-b845-8c9c8e4deda9", + "Name": "NugetPackageStepName", + "Label": "NuGet package step", + "HelpText": "The step that uploaded the NuGet package to the server.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Id": "6e97bd67-aeb5-4a92-b4d8-9e5b9a13ad93", + "Name": "ServerName", + "Label": "SSIS SQL Server Name", + "HelpText": "The name of SSIS SQL Server ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6b0007eb-81eb-459f-b8c6-2f8c9465beb2", + "Name": "CatalogName", + "Label": "SSISDB Catlog Name", + "HelpText": "The name of the SSISDB Catlog", + "DefaultValue": "SSISDB", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "00cadc4e-17bd-41bb-b4bd-87ca01b483eb", + "Name": "FolderName", + "Label": "SSIS Folder", + "HelpText": "The SSIS folder to deploy to project to", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5e88fe40-d502-4e2e-8c96-c7ed8f2d82c4", + "Name": "EnvironmentTag", + "Label": "Environment Tag", + "HelpText": "The text to add at the end Environment configuration name. Currently default to append the Environment Value begin deploy to.", + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "Zogamorph", + "$Meta": { + "ExportedAt": "2019-10-29T18:58:17.799Z", + "OctopusVersion": "2019.10.0", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/ssis-deploy-sqlagentjob.json.human b/step-templates/ssis-deploy-sqlagentjob.json.human new file mode 100644 index 000000000..db04bffd1 --- /dev/null +++ b/step-templates/ssis-deploy-sqlagentjob.json.human @@ -0,0 +1,494 @@ +{ +"Id" : "f69ea863-ab61-4c58-bcbe-a133450113c9", + "Name": "SSIS - Deploy SQL Agent Job", + "Description": "Deploy a SQL Agent Job for SSIS Ispac Deployment. Requires SMO to be installed on the machine where this step will be run. ", + "ActionType": "Octopus.Script", + "Version": 10, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "Function Format-OctopusArgument\r +{\r +\r + <#\r + .SYNOPSIS\r + Converts boolean values to boolean types\r +\r + .DESCRIPTION\r + Converts boolean values to boolean types\r +\r + .PARAMETER Value\r + The value to convert\r +\r + .EXAMPLE\r + Format-OctopusArgument \"true\"\r + #>\r + Param(\r + [string]$Value\r + )\r +\r + $Value = $Value.Trim()\r +\r + # There must be a better way to do this\r + Switch -Wildcard ($Value)\r + {\r +\r + \"True\"\r + { Return $True\r + }\r + \"False\"\r + { Return $False\r + }\r + \"#{*}\"\r + { Return $null\r + }\r + Default\r + { Return $Value\r + }\r + }\r +}\r +\r +\r +Function Get-SSISCommand\r +{\r +\r + <#\r + .SYNOPSIS\r + Format the SSIS Command that execute the package.\r + Only valid for now for Project Deployment ($typecommand = '/ISSERVER \"\\')\r +\r + .DESCRIPTION\r + Return a string with the correct format to execute a Project Package Deploy (Step).\r + The SSIS Command can be built by paramaters (ServerName, CatalogName, ProjectName, FolderName, Package and Environment)\r + @TO-DO:\r + But most of the cases need an ending string that could be the same for most of the deployments.\r + I will keep this as a Octopus Parameter by now.\r +\r +\r + #>\r +\r + Param($ServerName, $CatalogName, $FolderName, $ProjectName, $PackageName, $EnvironmentName)\r + process {\r + $environmentid = Get-EnvironmentId -ServerName $ServerName -EnvironmentName $EnvironmentName -PackageName $PackageName -ProjectName $ProjectName\r + write-host \"The Environmemnt Id found for $EnvironmentName is $environmentid\"\r + $slash = '\\'\r + $quotes = '\"'\r + $typecommand = '/ISSERVER \"\\'\r + $environmentCommand = '\\\"\" /ENVREFERENCE ' + $environmentid.ToString()\r + $packagep = $slash + $CatalogName + $slash + $FolderName + $slash + $ProjectName + $slash + $PackageName + '.dtsx' + $slash\r + $servertype = '\" /SERVER \"\\\"'\r + $commandoptions = ' /Par \"\\\"$ServerOption::LOGGING_LEVEL(Int16)\\\"\";1 /Par \"\\\"$ServerOption::SYNCHRONIZED(Boolean)\\\"\";True /CALLERINFO SQLAGENT /REPORTING E'\r + }\r + end {\r + return $typecommand + $quotes + $packagep + $quotes + $servertype + $ServerName + $environmentCommand + $commandoptions\r + }\r +\r +}\r +\r +Function Get-EnvironmentId\r +{\r + <#\r + .SYNOPSIS\r + Get the ID of the Environment by Name of Environment\r +\r + .DESCRIPTION\r + ProjectDeploy Packages use Enviroments for variables previously deployed for the package.\r + To be able to format the SSIS Command, we will need the Environment ID\r +\r + .PARAMETER ServerName\r + .PARAMETER CatalogName\r + .PARAMETER FolderName\r + .PARAMETER ProjectName\r + .PARAMETER PackageName\r + .PARAMETER EnvironmentName\r + #>\r +\r + Param($ServerName, $EnvironmentName,$PackageName, $ProjectName)\r +\r +\r + $query = \"SELECT er.reference_id\r + FROM [SSISDB].[internal].[folders] AS f\r + JOIN [SSISDB].[internal].[projects] AS p\r + ON f.folder_id = p.folder_id\r + JOIN [SSISDB].[internal].[environment_references] AS er\r + ON p.project_id = er.project_id\r + where f.name = '$FolderName'\r +\t\t\t\t\t\tand er.environment_name = '$EnvironmentName'\r +\t\t\t\t\t\t and p.name = '$ProjectName'\"\r +\r + $EnvironmentId = Invoke-Sqlcmd -Query $query -ServerInstance $ServerName -Verbose\r +\r + return $EnvironmentId.reference_id\r +}\r +\r +Function Add-Job\r +{\r + <#\r + .SYNOPSIS\r + Add a new type of Job in SQL Agent Job\r +\r + .DESCRIPTION\r + The function will remove an existing Job with same name (SQL SMO Job doesnt contains update function)\r + and it will return as GlobalVariable the JOb Object.\r +\r + .PARAMETER JobName\r + .PARAMETER ServerName\r + .PARAMETER EnvironmentName\r + .PARAMETER JobStepName\r + .PARAMETER JobStepCommand\r + #>\r +\r + param($JobName, $ServerName, $EnvironmentName, $JobsStepName, $JobStepCommand, $JobCategory )\r + try\r + {\r + $server = New-Object Microsoft.SqlServer.Management.Smo.Server($ServerName)\r + $server.exe\r + $server.JobServer.HostLoginName\r + $existingjob = $server.Jobserver.Jobs|where-object {$_.Name -like $JobName}\r + if ($existingjob)\r + {\r + Write-Host \"|- Dropping Job [$JobName]...\" -NoNewline\r + $existingjob.drop()\r + Write-Host \"|- Done\" -ForegroundColor Green\r + }\r +\r + $job = New-Object Microsoft.SqlServer.Management.SMO.Agent.Job($server.JobServer, $jobName)\r + #$job.DropIfExists() only for sqlserver 2016\r + $job.Create()\r + $job.OwnerLoginName = \"sa\"\r + $job.Category = $JobCategory\r + $job.ApplyToTargetServer($server.Name)\r +\r +\r + }\r + catch\r + {\r + write-host \"####### Error Adding a job\" -ForegroundColor Red\r + write-host $_.Exception.Message\r + }\r + #Instead of creating a class and return a Job, lets settup a global variable. Return statament doenst return all script output\r + $Global:newjob = $job\r +\r +}\r +\r +Function Add-JobStep\r +{\r + <#\r + .SYNOPSIS\r + Add a job step to a Job\r +\r + .DESCRIPTION\r + The function will remove an existing Job with same name (SQL SMO Job doesnt contains update function)\r + and it will return as GlobalVariable t\r + he JOb Object.\r + .PARAMETER Job\r + .PARAMETER JobStepName\r + .PARAMETER JobStepCommand\r + #>\r + param($job, $JobStepName, $CommandJob )\r + try\r + {\r + $jobStep = New-Object Microsoft.SqlServer.Management.Smo.Agent.JobStep($job, $JobStepName)\r + $jobStep.Subsystem = [Microsoft.SqlServer.Management.Smo.Agent.AgentSubSystem]::SSIS\r +\r + $jobStep.Command = $CommandJob\r + $jobStep.Create()\r + }\r + catch\r + {\r + write-host \"######### Error adding a job step\" -ForegroundColor Red\r + write-host $_.Exception.Message\r + }\r +\r +\r +}\r +\r +Function Add-JobSchedule\r +{\r + # ToDO: Add more types of frequenct: Weekly, Monthly\r + param($job, $JobScheduleName,$JobExecutionFrequency, $FrecuencyInterval, $startHour, $startMinutes)\r + try\r + {\r + $name = $job.Name\r + $SQLJobSchedule = New-Object -TypeName Microsoft.SqlServer.Management.SMO.Agent.JobSchedule($job, $JobScheduleName)\r +\r + switch ($JobExecutionFrequency) {\r + \"Daily\" {\r + $result = [Microsoft.SqlServer.Management.SMO.Agent.FrequencyTypes]::Daily\r + $subdayTypes = [Microsoft.SqlServer.Management.SMO.Agent.FrequencySubDayTypes]::Hour\r + }\r + \"OneTime\" {\r + $result = [Microsoft.SqlServer.Management.SMO.Agent.FrequencyTypes]::OneTime\r + $subdayTypes = [Microsoft.SqlServer.Management.SMO.Agent.FrequencySubDayTypes]::Once\r + }\r + \"AutoStart\" {\r + $result = [Microsoft.SqlServer.Management.SMO.Agent.FrequencyTypes]::AutoStart\r + }\r + default {\r + $result = [Microsoft.SqlServer.Management.SMO.Agent.FrequencyTypes]::Daily\r + }\r + }\r +\r +\r + $SQLJobSchedule.FrequencyTypes = $result\r + # Setup Frequency Interval\r + $SQLJobSchedule.FrequencyInterval = $FrecuencyInterval\r +\r +\r +\r + # Job Start\r + $timeofday = New-TimeSpan -hours $startHour -minutes $startMinutes\r + $SQLJobSchedule.ActiveStartTimeOfDay = $timeofday\r + #Activate the Job\r + $SQLJobSchedule.ActiveStartDate = Get-Date\r + $SQLJobSchedule.Create()\r + }\r + catch\r + {\r + Write-Host \"Error\" -ForegroundColor Red\r + write-host \"Exception Type: $($_.Exception.GetType().FullName)\" -ForegroundColor Red\r + write-host \"Exception Message: $($_.Exception.Message)\" -ForegroundColor Red\r + $error[0]|format-list -force\r +\r + }\r +\r +}\r +\r +Function Set-SQLJob\r +{\r + #Get-Octopus Variables\r + Write-Host \"Collecting Octopus Variables\"\r +\r + $ServerName = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_ServerName\"]\r + $FolderName = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_FolderName\"]\r + $ProjectName = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_ProjectName\"]\r + $CatalogName = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_CatalogName\"]\r + $EnvironmentName = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_EnvironmentName\"]\r + $PackageName = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_PackageName\"]\r + $JobName = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_JobName\"]\r + $JobCategory = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_JobCategory\"]\r + $JobStepName = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_JobStepName\"]\r + $JobScheduleName = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_JobScheduleName\"]\r + $JobExecutionFrequency = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_JobExecutionFrequency\"]\r + $JobFrequencyInterval = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_JobFrequencyInterval\"]\r + $JobExecutionTimeHour = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_JobExecutionTimeHour\"]\r + $JobExecutionTimeMinute = Format-OctopusArgument -Value $OctopusParameters[\"SSIS_JobExecutionTimeMinute\"]\r +\r +\r + # FrecuencyType is hardcoded\r +\r + #Getting Module sqlserver if possible.\r + $Module = get-module -ListAvailable -name sqlserver\r + if ($Module.Name -eq 'sqlserver') {\r + write-host \"Importing Module SqlServer\"\r + Import-Module sqlserver -DisableNameChecking\r + } else {\r + write-host \"Importing Module sqlps\"\r + import-module sqlps -Verbose -DisableNameChecking\r + }\r +\r + #First step is to generate the command execution for Job Step.\r + $JobStepCommand = Get-SSISCommand -ServerName $ServerName -CatalogName $CatalogName -FolderName $FolderName -ProjectName $ProjectName -PackageName $PackageName -EnvironmentName $EnvironmentName -Verbose\r +\r + write-Host \"Command found to deploy Step is \"\r + write-Host $JobStepCommand\r + write-Host \"STARTING DEPLOYMENT \"\r + write-Host \"|- Start Adding the Job $JobName\"\r + Add-Job -JobName $JobName -ServerName $ServerName -EnvironmentName $EnvironmentName -JobsStepName $JobStepName -JobStepCommand $JobStepCommandmand\r + write-Host \"|- $JobName Added to $ServerName\"\r +\r + write-Host \"|--- Start Adding the JobStep $JobStepName to Job $JobName\"\r + Add-JobStep -JobStepName $JobStepName -CommandJob $JobStepCommand -Job $Global:newjob\r + write-Host \"|--- $JobStepName added to Job $JobName\"\r +\r + write-Host \"|---- Start Adding JobShedule $JobScheduleName JobStep $JobName\"\r + Add-JobSchedule -job $Global:newjob -JobScheduleName $JobScheduleName -JobExecutionFrequency $JobExecutionFrequency -FrecuencyInterval $JobFrequencyInterval -startHour $JobExecutionTimeHour -startMinutes $JobExecutionTimeMinute\r + write-Host \"|---- $JobStepName added to Job $JobName\"\r +}\r +\r +Write-Host \"Starting deployment of SQL Job\"\r +\r +\r +Set-SQLJob\r +\r +\r +Write-Host \"Finishing Install\"\r +" + }, + "Parameters": [ + { + "Id": "44a2fa3c-3117-450b-8004-ebdbfd2362e9", + "Name": "SSIS_ServerName", + "Label": "Server Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "ae040e98-f686-4f44-ad80-8939fbd902e4", + "Name": "SSIS_FolderName", + "Label": "Folder Name", + "HelpText": "Folder Name of the deployed package", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "3e755e91-afb5-46c3-ad61-47bf80ac88b8", + "Name": "SSIS_ProjectName", + "Label": "Project Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "21dc0b13-7db0-4c99-b512-53dbfad55893", + "Name": "SSIS_CatalogName", + "Label": "Catalog Name", + "HelpText": "By default SSISDB", + "DefaultValue": "SSISDB", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "eb197ae7-99e1-41a2-aee4-5b379fdb349c", + "Name": "SSIS_EnvironmentName", + "Label": "Environment Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "94f97e01-d04c-4696-ad0a-e170a21ecd87", + "Name": "SSIS_PackageName", + "Label": "Package Name", + "HelpText": "Complete name of the package to execute, without dtsx extension", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "cb138aa6-2f51-4ebd-b744-9b0d17535ea1", + "Name": "SSIS_JobName", + "Label": "Job Name", + "HelpText": "Name of the SQL Job", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "458225af-7d11-4f98-b984-2fe02e66996e", + "Name": "SSIS_JobStepName", + "Label": "Job Step Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "07d85895-e548-4f54-a403-25cc53fa1a88", + "Name": "SSIS_JobSheduleName", + "Label": "Job Schedule Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "023f21b1-7757-42f2-8b69-e9a95deb7bae", + "Name": "SSIS_JobExecutionFrequency", + "Label": "Job Execution Frequency", + "HelpText": "Frequency of execution +Only supported: +OneTime +Daily +AutoStart", + "DefaultValue": "Daily", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "AutoStart +Daily +OneTime" + }, + "Links": {} + }, + { + "Id": "25efa439-5490-4ed4-a70f-7b514bd18446", + "Name": "SSIS_JobFrequencyInterval", + "Label": "Job Frequency Interval", + "HelpText": "Depends of Job Execution Frequency. +For now only works with Days +Example: +1 -> Recurs every 1 Day +2 -> Recurs every 2 Weeks", + "DefaultValue": "1", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "50b3ab13-8635-4631-a247-092792364902", + "Name": "SSIS_JobExecutionTimeHour", + "Label": "Job Execution Time Hour", + "HelpText": "Hour of the day to execute the job", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "4d57dc4e-8717-4f05-bdad-0f16cc9b566d", + "Name": "SSIS_JobExecutionTimeMinute", + "Label": "Job Execution Time Minute", + "HelpText": "Minutes of the day to execute the job", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "7a4c18db-51c3-48e7-ab04-b4f084dde3de", + "Name": "SSIS_JobCategory", + "Label": "Job Category", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2018-17-04T10:59:08.819Z", + "LastModifiedBy": "andrescolodrero", + "$Meta": { + "ExportedAt": "2018-17-04T10:59:08.819Z", + "OctopusVersion": "3.14.15", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/ssl-certificate-install.json.human b/step-templates/ssl-certificate-install.json.human new file mode 100644 index 000000000..60ce0dd0e --- /dev/null +++ b/step-templates/ssl-certificate-install.json.human @@ -0,0 +1,80 @@ +{ + "Id": "2a939210-3f1c-4a66-a535-40ba7cd709fb", + "Name": "SSL Certificate - Install", + "Description": "Installs an SSL certificate on the target machine", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$base64Certificate = $OctopusParameters['Base64Certificate'] +$password = $OctopusParameters['Password'] +$location = $OctopusParameters['StoreLocation'] +$name = $OctopusParameters['StoreName'] + +Write-Host \"Adding/updating certificate in store\" + +$certBytes = [System.Convert]::FromBase64String($base64Certificate) +$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certBytes, $password, \"MachineKeySet,PersistKeySet\") +$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($name, $location) +$store.Open(\"ReadWrite\") +$store.Add($cert) +$store.Close()", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Base64Certificate", + "Label": "Base64 certificate", + "HelpText": "The certificate, encoded as a base64 string", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "Password", + "Label": "Password", + "HelpText": "The certificate password", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "StoreName", + "Label": "StoreName", + "HelpText": "The name of the certificate store", + "DefaultValue": "My", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "AddressBook +AuthRoot +CA +Disallowed +My +Root +TrustedPeople +TrustedPublisher" + } + }, + { + "Name": "StoreLocation", + "Label": "StoreLocation", + "HelpText": "The location of the certificate store", + "DefaultValue": "LocalMachine", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "CurrentUser +LocalMachine" + } + } + ], + "LastModifiedOn": "2016-10-14T03:01:44.952+00:00", + "LastModifiedBy": "kp-tseng", + "$Meta": { + "ExportedAt": "2014-10-01T07:43:57.503+00:00", + "OctopusVersion": "2.5.8.447", + "Type": "ActionTemplate" + }, + "Category": "ssl" +} diff --git a/step-templates/ssl-disable-sslv2.json.human b/step-templates/ssl-disable-sslv2.json.human new file mode 100644 index 000000000..9c6d3bc72 --- /dev/null +++ b/step-templates/ssl-disable-sslv2.json.human @@ -0,0 +1,90 @@ +{ + "Id": "ea274d21-80ca-4c1b-aa82-f0d124c6a707", + "Name": "SSL - Disable SSLv2", + "Description": "Disables SSL v2, requires restart.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Write-host \"Server : $Server\"\r +\t$ClientEnabled = $false\r +\t$ServerEnabled = $false\r + $reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $Server)\r + $regkey = $reg.OpenSubkey(\"SYSTEM\\\\CurrentControlSet\\\\Control\\\\SecurityProviders\\\\SCHANNEL\\\\Protocols\\\\SSL 2.0\",$true)\r +\t$regkeyC = $reg.OpenSubkey(\"SYSTEM\\\\CurrentControlSet\\\\Control\\\\SecurityProviders\\\\SCHANNEL\\\\Protocols\\\\SSL 2.0\\\\Client\",$true)\r +\t$regkeyS = $reg.OpenSubkey(\"SYSTEM\\\\CurrentControlSet\\\\Control\\\\SecurityProviders\\\\SCHANNEL\\\\Protocols\\\\SSL 2.0\\\\Server\",$true)\r +\t\r +\tforeach($subkeyName in $regkey.GetSubKeyNames())\r +\t{\r +#CLIENT\r +\t\t# Check for Client SubKey\r +\t\tif (!$regkeyC)\t\t\t\r +\t\t{\r +\t\t\t$regkey.CreateSubKey('Client')\r +\t\t\t#reload\r +\t\t\t$regkeyC = $reg.OpenSubkey(\"SYSTEM\\\\CurrentControlSet\\\\Control\\\\SecurityProviders\\\\SCHANNEL\\\\Protocols\\\\SSL 2.0\\\\Client\",$true)\r +\t\t\t$regkeyC.SetValue('DisabledByDefault','1','DWORD')\r +\t\t}\t\t\r +\t\tforeach($subkeyNameC in $regkeyC.GetValueNames())\r +\t\t{\t\t\t\t\t\r +\t\t\tif ($subkeyNameC)\r +\t\t\t{\r +\t\t\t\tif ($subkeyNameC -eq 'Enabled')\r +\t\t\t\t{\r +\t\t\t\t\t$ClientEnabled = $true\r +\t\t\t\t}\r +\t\t\t}\r +\t\t}\r +\t\t# Check to see if the Enabled Key was found\r +\t\tif (!$ClientEnabled)\r +\t\t{\r +\t\t\t#Add enabled SubKey with DWORD value\r +\t\t\t$regkeyC.SetValue('Enabled','0','DWORD')\t\t\t\t\r +\t\t}\r +#SERVER\r +\t\t# Check for Server SubKey\r +\t\tif (!$regkeyS)\r +\t\t{\r +\t\t\t$regkey.CreateSubKey('Server')\r +\t\t\t#reload\r +\t\t\t$regkeyS = $reg.OpenSubkey(\"SYSTEM\\\\CurrentControlSet\\\\Control\\\\SecurityProviders\\\\SCHANNEL\\\\Protocols\\\\SSL 2.0\\\\Server\",$true)\r +\t\t}\t\t\r +\t\tforeach($subkeyNameS in $regkeyS.GetValueNames())\r +\t\t{\r +\t\t\tif ($subkeyNameS)\r +\t\t\t{\r +\t\t\t\tif ($subkeyNameS -eq 'Enabled')\r +\t\t\t\t{\r +\t\t\t\t\t$ServerEnabled = $true\r +\t\t\t\t}\r +\t\t\t}\r +\t\t}\t\t\r +\t\tif (!$ServerEnabled)\r +\t\t{\r +\t\t\t#Add enabled SubKey with DWORD value\r +\t\t\t$regkeyS.SetValue('Enabled','0','DWORD')\r +\t\t}\t\t\t\r +\t} \r +\tWrite-host \"Server : $Server : Complete\"", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Server", + "Label": "Server name", + "HelpText": "Server name to disable SSL v2 on.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-30T20:48:41.172+00:00", + "OctopusVersion": "2.4.7.85", + "Type": "ActionTemplate" + }, + "Category": "ssl" +} diff --git a/step-templates/ssl-write-certificate-pem-and-key.json.human b/step-templates/ssl-write-certificate-pem-and-key.json.human new file mode 100644 index 000000000..238603c1e --- /dev/null +++ b/step-templates/ssl-write-certificate-pem-and-key.json.human @@ -0,0 +1,80 @@ +{ + "Id": "9e7f1836-c7c5-4d52-bd9e-0948114b2a0e", + "Name": "SSL - Write Certificate Pem and Key to the Filesystem", + "Description": "Export the PEM and Key from an SSL certificate to the File System. This is useful in Linux for securing websites or Docker containers", + "ActionType": "Octopus.Script", + "Version": 7, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": " +$CertName = $OctopusParameters[\"sslCertificate.Name\"] +Write-Host \"Writing PEM and Key files for $Certname\" + +New-Item -ItemType directory $sslExportPath -Force -Verbose + +\"Certificate Pem:\" +$pemPath = join-path $sslExportPath $sslPemFile +$OctopusParameters[\"sslCertificate.CertificatePem\"] | out-file $pemPath -Force -Verbose + +\"-\" * 30 + +\"Certificate Key: \" +$keypath = join-path $sslExportPath $sslKeyFile +$OctopusParameters[\"sslCertificate.PrivateKeyPem\"] | out-file $keyPath -Force -Verbose +" + }, + "Parameters": [ + { + "Id": "c9cdd622-1a9a-4e5e-9eaa-ad738b76b356", + "Name": "sslExportPath", + "Label": "Root SSL Export Path", + "HelpText": "The base directory the key and pem file will be saved to", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9211d56c-068e-47c5-9613-0f77eda72dca", + "Name": "sslPemFile", + "Label": "PEM File Name", + "HelpText": "The filename to write the PEM to", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "39dbe4a6-60c5-4bd2-8ad9-03a28f7148f0", + "Name": "sslKeyFile", + "Label": "Key File Name", + "HelpText": "The filename to write the Key to", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "65d6687e-badf-4772-ac14-70bd2790cd4d", + "Name": "sslCertificate", + "Label": "SSL Certificate Variable", + "HelpText": "The name of the SSL certificate Variable", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Certificate" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2020-08-13T07:41:35.949Z", + "OctopusVersion": "2020.1.18", + "Type": "ActionTemplate" + }, + "Category": "ssl" + } diff --git a/step-templates/ssrs-deploy-from-package-parameter.json.human b/step-templates/ssrs-deploy-from-package-parameter.json.human new file mode 100644 index 000000000..1b2a6ddd7 --- /dev/null +++ b/step-templates/ssrs-deploy-from-package-parameter.json.human @@ -0,0 +1,1302 @@ +{ + "Id": "2c118ec9-4c3e-45a4-85b7-9f3c8ae99ca9", + "Name": "Deploy SSRS Reports from a package parameter", + "Description": "Uploads SSRS reports to an SSRS server from a package. + +The following Datasource properties can be overidden: ConnectionString, Username, Password, and CredentialRetrieval (valid values are: Integrated, Prompt, Store, or None). To override the property, create a Variable using the syntax of DatasourceName.PropertyName. For example: MyDatasource.Username + +To specify the Username and Password are Windows Credentials, create a variable called DatasourceName.WindowsCredentials and set the value to the string value 'true' (minus the quotes).", + "ActionType": "Octopus.Script", + "Version": 5, + "Author": "twerthi", + "Packages": [ + { + "Name": "SSRSPackage", + "Id": "5f4c7059-081e-44d7-bfea-6b9a90c4e77c", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "SSRSPackage" + } + } + ], + "Properties": { + "Octopus.Action.Script.ScriptBody": "$DeployedPath = $OctopusParameters[\"Octopus.Action.Package[SSRSPackage].ExtractedPath\"] +$ReleaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$UseArchiveStructure = [Convert]::ToBoolean($OctopusParameters[\"UseArchiveStructure\"]) + +#region Upload-Item +Function Upload-Item +{ + # parameters + param ([string] $Item, [string]$ItemType, [string] $ItemFolder) + + Write-Host \"Loading data from $Item\" + $ItemData = [System.IO.File]::ReadAllBytes($Item) + + # Create local variables + $Warnings = $null + $ItemName = $Item.SubString($Item.LastIndexOf(\"\\\") + 1) + #$ItemName = $ItemName.SubString(0, $ItemName.IndexOf(\".\")) + $ItemName = $ItemName.SubString(0, $ItemName.LastIndexOf(\".\")) + +\t# upload item + if ($IsReportService2005) { + if($ItemType -eq \"Report\") + { +\t [void]$ReportServerProxy.CreateReport($ItemName, $ItemFolder, $true, $ItemData, $null) + } + else + { + # error + Write-Error \"$ItemType is not supported in ReportService2005\" + } +\t} +\telseif ($IsReportService2010) { +\t\t[void]$ReportServerProxy.CreateCatalogItem($ItemType, $ItemName, $ItemFolder, $true, $ItemData, $null, [ref] $Warnings); +\t} +\telse { Write-Warning 'Report Service Unknown in Upload-Item method. Use ReportService2005 or ReportService2010.' } +} +#endregion + +#region Get-ItemDataSourceNames() +Function Get-ItemDataSourceNames +{ + # Parameters + Param ($ItemFile, $DataSourceName) + + # declare working variables + $DataSourceNames = @() + + # load the xml + [xml]$Xml = Get-Content $ItemFile + + # retrieve the datasource nodes + $DataSourceReferenceNodes = $Xml.GetElementsByTagName(\"DataSource\") + + # loop through returned results + foreach($Node in $DataSourceReferenceNodes) + { + # check to see if we're looking for a specific one + if($DataSourceName -ne $null) + { + # check to see if it's the current node + if($DataSourceName -eq $Node.Name) + { + # add + $DataSourceNames += $Node.DataSourceReference + } + } + else + { + # store the name + $DataSourceNames += $Node.DataSourceReference + } + } + + # return the results + return ,$DataSourceNames # Apparently using the , in front of the variable is how you return explicit arrays in PowerShell ... could you be more obsure? +} +#endregion + +#region Get-ItemDataSources() +Function Get-ItemDataSources +{ + # Parameters + Param ($ItemFile) + + # declare working variables + $DataSourceNames = @() + + # load the xml + [xml]$Xml = Get-Content $ItemFile + + # retrieve the datasource nodes + $DataSourceReferenceNodes = $Xml.GetElementsByTagName(\"DataSource\") + + # loop through returned results + foreach($Node in $DataSourceReferenceNodes) + { + # store the name + $DataSourceNames += $Node.Name + } + + # return the results + return ,$DataSourceNames # Apparently using the , in front of the variable is how you return explicit arrays in PowerShell ... could you be more obsure? +} +#endregion + +#region Get-ItemDataSourceReferenceNames +Function Get-ItemDataSourceReferenceNames +{ + # Parameters + Param ($ItemFile) + + # declare working variables + $DataSourceNames = @() + + # load the xml + [xml]$Xml = Get-Content $ItemFile + + # retrieve the datasource nodes + $DataSourceReferenceNodes = $Xml.GetElementsByTagName(\"DataSourceReference\") + + # loop through returned results + foreach($Node in $DataSourceReferenceNodes) + { + # get the data + $DataSourceNames += $Node.InnerText + } + + # return the results + return ,$DataSourceNames # Apparently using the , in front of the variable is how you return explicit arrays in PowerShell ... could you be more obsure? +} +#endregion + +#region Get-DataSetSharedReferenceName +Function Get-DataSetSharedReferenceName +{ + # parameters + param($ReportFile, $DataSetName) + + # load the xml + [xml]$ReportXml = Get-Content $ReportFile + + # Get the DataSet nodes + $DataSetNode = $ReportXml.GetElementsByTagName(\"DataSet\") | Where-Object {$_.Name -eq $DataSetName} + + # return the name + $DataSetNode.SharedDataSet.SharedDataSetReference +} +#endregion + +#region Item-Exists() +Function Item-Exists($ItemFolderPath, $ItemName) +{ + # declare search condition + $SearchCondition = New-Object \"$ReportServerProxyNamespace.SearchCondition\"; + + # fill in properties + $SearchCondition.Condition = Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.ConditionEnum\" -EnumName \"Equals\" + $SearchCondition.ConditionSpecified = $true + $SearchCondition.Name = \"Name\" + $SearchPath = $(if ([string]::IsNullOrWhitespace($ItemFolderPath)) { \"/\"} else { $ItemFolderPath } ) + +\tif ($IsReportService2005) { +\t\t$SearchCondition.Value = $ItemName +\t\t# search +\t $items = $ReportServerProxy.FindItems($SearchPath, (Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.BooleanOperatorEnum\" -EnumName \"And\"), $SearchCondition) +\t} +\telseif ($IsReportService2010) { +\t\t$SearchCondition.Values = @($ItemName) +\t\t# search +\t $items = $ReportServerProxy.FindItems($SearchPath, (Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.BooleanOperatorEnum\" -EnumName \"And\"), $null, $SearchCondition) +\t} +\telse { Write-Warning 'Report Service Unknown in Item-Exists method. Use ReportService2005 or ReportService2010.' } + + + # check to see if anything was returned + if($items.Length -gt 0) + { + # loop through returned items + foreach($item in $items) + { + # check the path + if($item.Path -eq \"$ItemFolderPath/$ItemName\") + { + # return true + return $true + } + else + { + # warn + Write-Warning \"Unexpected path for $($item.Name); path is $($item.Path) exepected $ItemFolderPath/$ItemName\" + } + } + + # items were found, but the path doesn't match + + return $false + } + else + { + return $false + } +} +#endregion + +Function Get-ItemPath +{ + # Define parameters + param( + $ItemName, + $StartFolder, + $CompareFolderPath) + + # declare search condition + $SearchCondition = New-Object \"$ReportServerProxyNamespace.SearchCondition\"; + + # fill in properties + $SearchCondition.Condition = Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.ConditionEnum\" -EnumName \"Equals\" + $SearchCondition.ConditionSpecified = $true + $SearchCondition.Name = \"Name\" + +\tif ($IsReportService2005) { +\t\t$SearchCondition.Value = $ItemName +\t\t# search +\t $items = $ReportServerProxy.FindItems($StartFolder, (Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.BooleanOperatorEnum\" -EnumName \"And\"), $SearchCondition) +\t} +\telseif ($IsReportService2010) { +\t\t$SearchCondition.Values = @($ItemName) +\t\t# search +\t $items = $ReportServerProxy.FindItems($StartFolder, (Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.BooleanOperatorEnum\" -EnumName \"And\"), $null, $SearchCondition) +\t} +\telse { Write-Warning 'Report Service Unknown in Item-Exists method. Use ReportService2005 or ReportService2010.' } + + # Check how many items were returned + if ($items.Length -eq 1) + { + return $items[0].Path + } + else + { + # Loop through returned items + foreach ($item in $items) + { + # compare folders + if ($CompareFolderPath -eq ($item.Path.SubString(0, $item.Path.LastIndexOf(\"/\")))) + { + # Display message we're guessing + Write-Host \"Multiple items were found with name $ItemName, assuming location is same folder as reference, $CompareFolderPath.\" + return $item.Path + } + } + + # Display warning + Write-Warning \"Multiple items were returned for $ItemName, unable to determine which one to return.\" + return [string]::Empty + } +} + +#region Set-ItemDataSources() +Function Set-ItemDataSources +{ + # parameters + Param($ItemFile, $ItemFolder) + + # declare local variables + $ItemName = $ItemFile.SubString($ItemFile.LastIndexOf(\"\\\") + 1) + $ItemName = $ItemName.SubString(0, $ItemName.LastIndexOf(\".\")) + $AllDataSourcesFound = $true + + # get the datasources + $DataSources = $ReportServerProxy.GetItemDataSources([string]::Format(\"{0}/{1}\", $ItemFolder, $ItemName)) + + #loop through retrieved datasources + foreach($DataSource in $DataSources) + { + # check to see if it's a dataset + if([System.IO.Path]::GetExtension($ItemFile).ToLower() -eq \".rsd\") + { + # datasets can have one and only one datasource + # The method GetItemDataSources does not return the name of the datasource for datasets like it does for reports + # instead, it alaways returns DataSetDataSource. This made the call to Get-ItemDataSourceNames necessary, + # otherwise it would not link correctly + $DataSourceName = (Get-ItemDataSourceReferenceNames -ItemFile $ItemFile)[0] + } + else + { + # get the anme from teh source itself + $DataSourceName = (Get-ItemDataSourceNames -ItemFile $ItemFile -DataSourceName $DataSource.Name)[0] + } + + if ([string]::IsNullOrWhiteSpace($DataSourceName)) + { + Write-Host \"Datasource $($DataSource.Name) is not a shared datasource, skipping.\" + $AllDataSourcesFound = $false + continue + } + + # Check to see if datasourcename contains the folder -- this can happen if the report was created by Report Builder + if((![string]::IsNullOrEmpty($ReportDataSource)) -and ($DataSourceName.ToLower().Contains($ReportDatasourceFolder.ToLower()))) + { + # Remove teh path from the item name + $DataSourceName = $DataSourceName.ToLower().Replace(\"$($ReportDatasourceFolder.ToLower())/\",\"\") + } + + $DatasourcePath = \"\" + + if ($UseArchiveStructure -eq $true) + { + $DatasourcePath = Get-ItemPath -ItemName $DataSourceName -StartFolder $RootFolder -CompareFolderPath $ItemFolder + $DatasourcePath = $DatasourcePath.SubString(0, $DatasourcePath.LastIndexOf(\"/\")) + } + else + { + $DatasourcePath = $ReportDatasourceFolder + } + + # check to make sure the datasource exists in the location specified + if((Item-Exists -ItemFolderPath $DatasourcePath -ItemName $DataSourceName) -eq $true) + { + # create datasource reference variable + $DataSourceReference = New-Object \"$ReportServerProxyNamespace.DataSourceReference\"; + + # assign + $DataSourceReference.Reference = \"$DatasourcePath/\" + $DataSourceName + $DataSource.Item = $DataSourceReference + } + else + { + # display warning + Write-Warning \"Unable to find datasource $($DataSourceName) in $DatasourcePath\" + + # update to false + $AllDataSourcesFound = $false + } + } + + # check to see if found all datasources + if($AllDataSourcesFound -eq $true) + { + Write-Host \"Linking datasources to $ItemFolder/$ItemName\" + + # save the references + $ReportServerProxy.SetItemDataSources(\"$ItemFolder/$ItemName\", $DataSources) + } +} +#endregion + +#region Set-ReportDataSets() +Function Set-ReportDataSets +{ + # parameters + param($ReportFile, $ReportFolderPath) + + # declare local variables + $ReportName = $ReportFile.SubString($ReportFile.LastIndexOf(\"\\\") + 1) + $ReportName = $ReportName.SubString(0, $ReportName.LastIndexOf(\".\")) + $AllDataSetsFound = $true + $DataSetFolder = \"\" + + # get the datasources + $DataSets = $ReportServerProxy.GetItemReferences([string]::Format(\"{0}/{1}\", $ReportFolderPath, $ReportName), \"DataSet\") + + # loop through returned values + foreach($DataSet in $DataSets) + { + # get the name of the shared data set reference + $SharedDataSetReferenceName = Get-DataSetSharedReferenceName -ReportFile $ReportFile -DataSetName $DataSet.Name + + # Check to see if the SharedDataSetReferenceName contains the folder path -- this can happen if the report was built using Report Builder + if((![string]::IsNullOrEmpty($ReportDataSetFolder)) -and ($SharedDataSetReferenceName.ToLower().Contains($ReportDataSetFolder.ToLower()))) + { + # Remove the folder path from the name, it will cause issues when trying to set + $SharedDataSetReferenceName = $SharedDataSetReferenceName.ToLower().Replace(\"$($ReportDataSetFolder.ToLower())/\", \"\") + } + + # Check to see if we're using the archive folder structure + if ($UseArchiveStructure -eq $true) + { + # Set dataset folder + $DataSetFolder = Get-ItemPath -ItemName $SharedDataSetReferenceName -StartFolder $RootFolder -CompareFolderPath $ReportFolderPath + $DataSetFolder = $DataSetFolder.SubString(0, $DataSetFolder.LastIndexOf(\"/\")) + } + else + { + $DataSetFolder = $ReportDataSetFolder + } + + # check to make sure the datasource exists in the location specified + if((Item-Exists -ItemFolderPath $DataSetFolder -ItemName $SharedDataSetReferenceName) -eq $true) + { + # create datasource reference variable + $DataSetReference = New-Object \"$ReportServerProxyNamespace.ItemReference\"; + + # assign + $DataSetReference.Reference = \"$DataSetFolder/\" + $SharedDataSetReferenceName + $DataSetReference.Name = $DataSet.Name + + # log + Write-Host \"Linking Shared Data Set $($DataSet.Name) to $ReportName\" + + # update reference + $ReportServerProxy.SetItemReferences(\"$ReportFolderPath/$ReportName\", @($DataSetReference)) + } + else + { + # get the datasource name to include in warning message -- I know there must be a way to use the property in a string literal, but I wasn't able to figure it out while trying + # to solve a reported bug so I took the easy way. + $DataSetName = $DataSet.Name + + # display warning + Write-Warning \"Unable to find dataset $DataSetName in $ReportDataSetFolder\" + + # update to false + $AllDataSetsFound = $false + } + } + + # check to see if all datsets were found + if($AllDataSetsFound -eq $False) + { + Write-Warning \"Not all datasets found\" + + # save the references + $ReportServerProxy.SetItemReferences(\"$ReportFolder/$ReportName\", @($DataSets)) + } +} + +#endregion + +#region Get-ObjectNamespace() +Function Get-ObjectNamespace($Object) +{ + # return the value + ($Object).GetType().ToString().SubString(0, ($Object).GetType().ToString().LastIndexOf(\".\")) +} +#endregion + +#region Get-SpecificEnumValue() +Function Get-SpecificEnumValue($EnumNamespace, $EnumName) +{ + # get the enum values + $EnumValues = [Enum]::GetValues($EnumNamespace) + + # Loop through to find the specific value + foreach($EnumValue in $EnumValues) + { + # check current + if($EnumValue -eq $EnumName) + { + # return it + return $EnumValue + } + } + + # nothing was found + return $null +} +#endregion + +#region Update-ReportParamters() +Function Update-ReportParameters($ReportFile, $ReportFolderPath) +{ + # declare local variables + $ReportParameters = @(); + + # necessary so that when attempting to use the report execution service, it doesn't puke on you when it can't find the data source + $ReportData = (Remove-SharedReferences -ReportFile $ReportFile) + + # get just the report name + $ReportName = $ReportFile.SubString($ReportFile.LastIndexOf(\"\\\") + 1) + $ReportName = $ReportName.SubString(0, $ReportName.LastIndexOf(\".\")) + + # create warnings object + $ReportExecutionWarnings = $null + + # set the report full path + $ReportPath = \"$ReportFolderPath/$ReportName\" + + # load the report definition + $ExecutionInfo = $ReportExecutionProxy.LoadReportDefinition($ReportData, [ref] $ReportExecutionWarnings); + + # loop through the report execution parameters + foreach($Parameter in $ExecutionInfo.Parameters) + { + # create new item parameter object + $ItemParameter = New-Object \"$ReportServerProxyNamespace.ItemParameter\"; + + # fill in the properties except valid values, that one needs special processing + Copy-ObjectProperties -SourceObject $Parameter -TargetObject $ItemParameter; + + # fill in the valid values + $ItemParameter.ValidValues = Convert-ValidValues -SourceValidValues $Parameter.ValidValues; + + # exclude if it's query based + if($Parameter.DefaultValuesQueryBased -ne $true) + { + # add to list + $ReportParameters += $ItemParameter; + } + } + + # force the parameters to update + Write-Host \"Updating report parameters for $ReportFolderPath/$ReportName\" +\tif ($IsReportService2005) { +\t\t$ReportServerProxy.SetReportParameters(\"$ReportFolderPath/$ReportName\", $ReportParameters); +\t} +\telseif ($IsReportService2010) { +\t\t$ReportServerProxy.SetItemParameters(\"$ReportFolderPath/$ReportName\", $ReportParameters); +\t} +\telse { Write-Warning 'Report Service Unknown in Update-ReportParameters method. Use ReportService2005 or ReportService2010.' } +} +#endregion + +#region Remove-ShareReferences() +Function Remove-SharedReferences($ReportFile) +{ + ###################################################################################################### + #You'll notice that I've used the keyword of [void] in front of some of these method calls, this is so + #that the operation isn't captured as output of the function + ###################################################################################################### + + # read xml + [xml]$ReportXml = Get-Content $ReportFile -Encoding UTF8; + + # create new memory stream object + $MemoryStream = New-Object System.IO.MemoryStream + + try + { + + # declare array of nodes to remove + $NodesToRemove = @(); + + # get datasource names + $DataSourceNames = Get-ItemDataSources -ItemFile $ReportFile + + # check to see if report has datasourcenames + if($DataSourceNames.Count -eq 0) + { + # Get reference to reportnode + $ReportNode = $ReportXml.FirstChild.NextSibling # Kind of a funky way of getting it, but the SelectSingleNode(\"//Report\") wasn't working due to Namespaces in the node + + # create new DataSources node + $DataSourcesNode = $ReportXml.CreateNode($ReportNode.NodeType, \"DataSources\", $null) + + # create new datasource node + $DataSourceNode = $ReportXml.CreateNode($ReportNode.NodeType, \"DataSource\", $null) + + # create new datasourcereference node + $DataSourceReferenceNode = $ReportXml.CreateNode($ReportNode.NodeType, \"DataSourceReference\", $null) + + # create new attribute + $DataSourceNameAttribute = $ReportXml.CreateAttribute(\"Name\") + $DataSourceNameAttribute.Value = \"DataSource1\" + $dataSourceReferenceNode.InnerText = \"DataSource1\" + + # add attribute to datasource node + [void]$DataSourceNode.Attributes.Append($DataSourceNameAttribute) + [void]$DataSourceNode.AppendChild($DataSourceReferenceNode) + + # add nodes + [void]$ReportNode.AppendChild($DataSourcesNode) + [void]$DataSourcesNode.AppendChild($DataSourceNode) + + # add fake datasource name to array + $DataSourceNames += \"DataSource1\" + } + + # get all datasource nodes + $DatasourceNodes = $ReportXml.GetElementsByTagName(\"DataSourceReference\"); + + # loop through returned nodes + foreach($DataSourceNode in $DatasourceNodes) + { + # create a new connection properties node + $ConnectionProperties = $ReportXml.CreateNode($DataSourceNode.NodeType, \"ConnectionProperties\", $null); + + # create a new dataprovider node + $DataProvider = $ReportXml.CreateNode($DataSourceNode.NodeType, \"DataProvider\", $null); + $DataProvider.InnerText = \"SQL\"; + + # create new connection string node + $ConnectString = $ReportXml.CreateNode($DataSourceNode.NodeType, \"ConnectString\", $null); + $ConnectString.InnerText = \"Data Source=Server Name Here;Initial Catalog=database name here\"; + + # add new node to parent node + [void] $DataSourceNode.ParentNode.AppendChild($ConnectionProperties); + + # append childeren + [void] $ConnectionProperties.AppendChild($DataProvider); + [void] $ConnectionProperties.AppendChild($ConnectString); + + # Add to remove list + $NodesToRemove += $DataSourceNode; + } + + # get all shareddataset nodes + $SharedDataSetNodes = $ReportXml.GetElementsByTagName(\"SharedDataSet\") + + #loop through the returned nodes + foreach($SharedDataSetNode in $SharedDataSetNodes) + { + # create holder nodes so it won't error + $QueryNode = $ReportXml.CreateNode($SharedDataSetNode.NodeType, \"Query\", $null); + $DataSourceNameNode = $ReportXml.CreateNode($QueryNode.NodeType, \"DataSourceName\", $null); + $CommandTextNode = $ReportXml.CreateNode($QueryNode.NodeType, \"CommandText\", $null); + + # add valid datasource name, just get the first in the list + $DataSourceNameNode.InnerText = $DataSourceNames[0] + + # add node to parent + [void] $SharedDataSetNode.ParentNode.Appendchild($QueryNode) + + # add datasourcename and commandtext to query node + [void]$QueryNode.AppendChild($DataSourceNameNode) + [void]$QueryNode.AppendChild($CommandTextNode) + + # add node to removelist + $NodesToRemove += $SharedDataSetNode + } + + + # loop through nodes to remove + foreach($Node in $NodesToRemove) + { + # remove from parent + [void] $Node.ParentNode.RemoveChild($Node); + } + + $ReportXml.InnerXml = $ReportXml.InnerXml.Replace(\"xmlns=`\"`\"\", \"\") + + # save altered xml to memory stream + $ReportXml.Save($MemoryStream); + + # return the altered xml as byte array + return $MemoryStream.ToArray(); + } + finally + { + # close and dispose + $MemoryStream.Close(); + $MemoryStream.Dispose(); + } +} +#endregion + + +#region Copy-ObjectProperties() +Function Copy-ObjectProperties($SourceObject, $TargetObject) +{ + # Get source object property array + $SourcePropertyCollection = $SourceObject.GetType().GetProperties(); + + # get the destination + $TargetPropertyCollection = $TargetObject.GetType().GetProperties(); + + # loop through source property collection + for($i = 0; $i -lt $SourcePropertyCollection.Length; $i++) + { + # get the target property + $TargetProperty = $TargetPropertyCollection | Where {$_.Name -eq $SourcePropertyCollection[$i].Name} + + # check to see if it's null + if($TargetProperty -ne $null) + { + # check to see if it's the valid values property + if($TargetProperty.Name -ne \"ValidValues\") + { + # set the value + $TargetProperty.SetValue($TargetObject, $SourcePropertyCollection[$i].GetValue($SourceObject)); + } + } + } +} +#endregion + +#region ConvertValidValues() +Function Convert-ValidValues($SourceValidValues) +{ + # declare local values + $TargetValidValues = @(); + + # loop through source values + foreach($SourceValidValue in $SourceValidValues) + { + # create target valid value object + $TargetValidValue = New-Object \"$ReportServerProxyNamespace.ValidValue\"; + + # copy properties + Copy-ObjectProperties -SourceObject $SourceValidValue -TargetObject $TargetValidValue + + # add to list + $TargetValidValues += $TargetValidValue + } + + # return the values + return ,$TargetValidValues +} +#endregion + +#region Backup-ExistingItem() +Function Backup-ExistingItem +{ + # parameters + Param($ItemFile, $ItemFolder) + + # declare local variables + $ItemName = $ItemFile.SubString($ItemFile.LastIndexOf(\"\\\") + 1) + $ItemName = $ItemName.SubString(0, $ItemName.LastIndexOf(\".\")) + + # check to see if the item exists + if((Item-Exists -ItemFolderPath $ItemFolder -ItemName $ItemName) -eq $true) + { + # get file extension + $FileExtension = [System.IO.Path]::GetExtension($ItemFile) + + # check backuplocation + if($BackupLocation.EndsWith(\"\\\") -ne $true) + { + # append ending slash + $BackupLocation = $BackupLocation + \"\\\" + } +\t\t +\t\t# add the release number to the backup location +\t\t$BackupLocation = $BackupLocation + $ReleaseNumber + \"\\\" + + # ensure the backup location actually exists + if((Test-Path $BackupLocation) -ne $true) + { + # create it + New-Item -ItemType Directory -Path $BackupLocation + } + + # download the item + $Item = $ReportServerProxy.GetItemDefinition(\"$ItemFolder/$ItemName\") + + # form the backup path + $BackupPath = \"{0}{1}{2}\" -f $BackupLocation, $ItemName, $FileExtension; + + # write to disk + [System.IO.File]::WriteAllBytes(\"$BackupPath\", $Item); + + # write to screen + Write-Host \"Backed up $ItemFolder/$ItemName to $BackupPath\"; + } +} +#endregion + +#region Normalize-SSRSFolder() +function Normalize-SSRSFolder ([string]$Folder) { + if (-not $Folder.StartsWith('/')) { + $Folder = '/' + $Folder + } + + return $Folder +} +#endregion + +#region New-SSRSFolder() +function New-SSRSFolder ([string] $Name) { + Write-Verbose \"New-SSRSFolder -Name $Name\" + + $Name = Normalize-SSRSFolder -Folder $Name + + if ($ReportServerProxy.GetItemType($Name) -ne 'Folder') { + $Parts = $Name -split '/' + $Leaf = $Parts[-1] + $Parent = $Parts[0..($Parts.Length-2)] -join '/' + + if ($Parent) { + New-SSRSFolder -Name $Parent + } else { + $Parent = '/' + } + + $ReportServerProxy.CreateFolder($Leaf, $Parent, $null) + } +} +#endregion + +#region Clear-SSRSFolder() +function Clear-SSRSFolder ([string] $Name) { + Write-Verbose \"Clear-SSRSFolder -Name $Name\" + + $Name = Normalize-SSRSFolder -Folder $Name + + if ($ReportServerProxy.GetItemType($Name) -eq 'Folder' -and $ClearReportFolder) { + Write-Host (\"Clearing the {0} folder\" -f $Name) + $ReportServerProxy.ListChildren($Name, $false) | ForEach-Object { + Write-Verbose \"Deleting item: $($_.Path)\" + $ReportServerProxy.DeleteItem($_.Path) + } + } +} +#endregion + +#region New-SSRSDataSource() +function New-SSRSDataSource ([string]$RdsPath, [string]$Folder, [bool]$OverwriteDataSources) { + Write-Verbose \"New-SSRSDataSource -RdsPath $RdsPath -Folder $Folder\" + + $Folder = Normalize-SSRSFolder -Folder $Folder + + [xml]$Rds = Get-Content -Path $RdsPath + $dsName = $Rds.RptDataSource.Name + $ConnProps = $Rds.RptDataSource.ConnectionProperties + +\t$type = $ReportServerProxy.GetType().Namespace #Get proxy type +\t$DSDdatatype = ($type + '.DataSourceDefinition') +\t +\t$Definition = new-object ($DSDdatatype) +\tif($Definition -eq $null){ +\t Write-Error Failed to create data source definition object +\t} +\t +\t$dsConnectionString = $($OctopusParameters[\"$($dsName).ConnectionString\"]) + $dsUsername = $($OctopusParameters[\"$($dsName).Username\"]) + $dsPassword = $($OctopusParameters[\"$($dsName).Password\"]) + $dsCredentialRetrieval = $($OctopusParameters[\"$($dsName).CredentialRetrieval\"]) + +\t# replace the connection string variable that is configured in the octopus project +\tif ($dsConnectionString) { +\t $Definition.ConnectString = $dsConnectionString +\t} else { +\t $Definition.ConnectString = $ConnProps.ConnectString +\t} +\t + $Definition.Extension = $ConnProps.Extension + +\t# Check to see if the credential retrieval is overridden + if ($null -ne $dsCredentialRetrieval) + { + \tWrite-Host \"Forcing CredentialRetrieval property to: $dsCredentialRetrieval.\" + $Definition.CredentialRetrieval = $dsCredentialRetrieval + } + else + { + \t# Set the Credential Retrieval method + \tif ([Convert]::ToBoolean($ConnProps.IntegratedSecurity)) { +\t\t\t$Definition.CredentialRetrieval = 'Integrated' +\t\t} + elseif (![string]::IsNullOrWhitespace($dsUsername) -and ![string]::IsNullOrWhitespace($dsPassword)) + { + \t$Definition.CredentialRetrieval = 'Store' + } + } + +\tif ($Definition.CredentialRetrieval -eq 'Store') + {\t\t +\t\tWrite-Host \"$($dsName).Username = '$dsUsername'\" +\t\tWrite-Host \"$($dsName).Password = '$dsPassword'\" +\t\t +\t\t$Definition.UserName = $dsUsername; + $Definition.Password = $dsPassword; +\t} + + # Check to see if this is supposed to be an Windows Authentication stored account + if ($OctopusParameters[\"$($dsName).WindowsCredentials\"] -eq \"true\") + { +\t # Set the definition to Windows Credentials + \t$Definition.WindowsCredentials = $true + } + + + $DataSource = New-Object -TypeName PSObject -Property @{ + Name = $Rds.RptDataSource.Name + Path = $Folder + '/' + $Rds.RptDataSource.Name + } + + if ($OverwriteDataSources -or $ReportServerProxy.GetItemType($DataSource.Path) -eq 'Unknown') { + Write-Host \"Overwriting datasource $($DataSource.Name)\" + $ReportServerProxy.CreateDataSource($DataSource.Name, $Folder, $OverwriteDataSources, $Definition, $null) + } + + return $DataSource +} +#endregion + +#region Main + +try +{ + # declare array for reports + $ReportFiles = @() +\t$ReportDataSourceFiles = @() + $ReportDataSetFiles = @() + $ReportPartFiles = @() +\t +\t$IsReportService2005 = $false +\t$IsReportService2010 = $false +\t +\tif ($ReportServiceUrl.ToLower().Contains('reportservice2005.asmx')) { +\t\t$IsReportService2005 = $true +\t\tWrite-Host \"2005 Report Service found.\" +\t} +\telseif ($ReportServiceUrl.ToLower().Contains('reportservice2010.asmx')) { +\t\t$IsReportService2010 = $true +\t\tWrite-Host \"2010 Report Service found.\" +\t} +\t +\tWrite-Host \"Deploy Path: $DeployedPath\" +\t + # get all report files for deployment + Write-Host \"Getting all .rdl files\" + Get-ChildItem $DeployedPath -Recurse -Filter \"*.rdl\" | ForEach-Object { If(($ReportFiles -contains $_.FullName) -eq $false) {$ReportFiles += $_.FullName}} + Write-Host \"# of rdl files found: $($ReportFiles.Count)\" + + # get all report datasource files for deployment + Write-Host \"Getting all .rds files\" + Get-ChildItem $DeployedPath -Recurse -Filter \"*.rds\" | ForEach-Object { If(($ReportDataSourceFiles -contains $_.FullName) -eq $false) {$ReportDataSourceFiles += $_.FullName}} + Write-Host \"# of rds files found: $($ReportDataSourceFiles.Count)\" + + # get all report datset files for deployment + Write-Host \"Getting all .rsd files\" + Get-ChildItem $DeployedPath -Recurse -Filter \"*.rsd\" | ForEach-Object { If(($ReportDataSetFiles -contains $_.FullName) -eq $false) {$ReportDataSetFiles += $_.FullName}} + Write-Host \"# of rsd files found: $($ReportDataSetFiles.Count)\" + + # get all report part files for deployment + Write-Host \"Getting all .rsc files\" + Get-ChildItem $DeployedPath -Recurse -Filter \"*.rsc\" | ForEach-Object { If(($ReportPartFiles -contains $_.FullName) -eq $false) {$ReportPartFiles += $_.FullName}} + Write-Host \"# of rsc files found: $($ReportPartFiles.Count)\" + + # set the report proxies + Write-Host \"Creating SSRS Web Service proxies\" + + # check to see if credentials were supplied for the services + if(([string]::IsNullOrEmpty($ServiceUserDomain) -ne $true) -and ([string]::IsNullOrEmpty($ServiceUserName) -ne $true) -and ([string]::IsNullOrEmpty($ServicePassword) -ne $true)) + { + # secure the password + $secpasswd = ConvertTo-SecureString \"$ServicePassword\" -AsPlainText -Force + + # create credential object + $ServiceCredential = New-Object System.Management.Automation.PSCredential (\"$ServiceUserDomain\\$ServiceUserName\", $secpasswd) + + # create proxies + $ReportServerProxy = New-WebServiceProxy -Uri $ReportServiceUrl -Credential $ServiceCredential + $ReportExecutionProxy = New-WebServiceProxy -Uri $ReportExecutionUrl -Credential $ServiceCredential + } + else + { + # create proxies using current identity + $ReportServerProxy = New-WebServiceProxy -Uri $ReportServiceUrl -UseDefaultCredential + $ReportExecutionProxy = New-WebServiceProxy -Uri $ReportExecutionUrl -UseDefaultCredential + } + + + +\t#Create folder information for DataSource and Report + if ($UseArchiveStructure -eq $false) + { +\t New-SSRSFolder -Name $ReportFolder +\t New-SSRSFolder -Name $ReportDatasourceFolder + New-SSRSFolder -Name $ReportDataSetFolder + New-SSRSFolder -Name $ReportPartsFolder + } + else + { + New-SSRSFolder -Name $RootFolder + } + + #Clear destination folder if specified + if([System.Convert]::ToBoolean(\"$ClearReportFolder\")) { + Clear-SSRSFolder -Name $ReportFolder + } +\t +\t#Create DataSource + foreach($RDSFile in $ReportDataSourceFiles) { + Write-Host \"New-SSRSDataSource $RdsFile\" + + $DatasourceFolder = \"\" + + if ($UseArchiveStructure -eq $true) + { + # Adjust report folder to archive path + $DatasourceFolder = $(if ($RootFolder -eq \"/\") { [string]::Empty} else { $RootFolder } ) + $RDSFile.Replace($DeployedPath, '').Replace('\\', '/').Replace((Split-Path $RDSFile -Leaf), '') + + # Remove final slash + $DatasourceFolder = $DatasourceFolder.Substring(0, $DatasourceFolder.LastIndexOf('/')) + + # Check if folder exists + New-SSRSFolder -Name $DatasourceFolder + } + else + { + $DatasourceFolder = $ReportDatasourceFolder + } + +\t\t$DataSource = New-SSRSDataSource -RdsPath $RdsFile -Folder $DatasourceFolder -Overwrite ([System.Convert]::ToBoolean(\"$OverwriteDataSources\")) +\t} + + # get the service proxy namespaces - this is necessary because of a bug documented here http://stackoverflow.com/questions/7921040/error-calling-reportingservice2005-finditems-specifically-concerning-the-bool and http://www.vistax64.com/powershell/273120-bug-when-using-namespace-parameter-new-webserviceproxy.html + $ReportServerProxyNamespace = Get-ObjectNamespace -Object $ReportServerProxy + $ReportExecutionProxyNamespace = Get-ObjectNamespace -Object $ReportExecutionProxy + + # Create dat sets + foreach($DataSet in $ReportDataSetFiles) + { + $DataSetPath = \"\" + + # Check to see if it's set to follow archive structure + if ($UseArchiveStructure -eq $true) + { + # Adjust report folder to archive path + $DataSetPath = $(if ($RootFolder -eq \"/\") { [string]::Empty} else { $RootFolder } ) + $DataSet.Replace($DeployedPath, '').Replace('\\', '/').Replace((Split-Path $DataSet -Leaf), '') + + # Remove final slash + $DataSetPath = $DataSetPath.Substring(0, $DataSetPath.LastIndexOf('/')) + + # Check if folder exists + New-SSRSFolder -Name $DataSetPath + } + else + { + $DataSetPath = $ReportDataSetFolder + } + + # check to see if we need to back up + if($BackupLocation -ne $null -and $BackupLocation -ne \"\") + { + # backup the item + Backup-ExistingItem -ItemFile $DataSet -ItemFolder $DataSetPath + } + + # upload the dataset + Upload-Item -Item $DataSet -ItemType \"DataSet\" -ItemFolder $DataSetPath + + # update the dataset datasource + Set-ItemDataSources -ItemFile $DataSet -ItemFolder $DataSetPath + } + + # get the proxy auto generated name spaces + + # loop through array + foreach($ReportFile in $ReportFiles) + { + $ReportPath = \"\" + + # Check to see if it's set to follow archive structure + if ($UseArchiveStructure -eq $true) + { + # Adjust report folder to archive path + $ReportPath = $(if ($RootFolder -eq \"/\") { [string]::Empty} else { $RootFolder } ) + $ReportFile.Replace($DeployedPath, '').Replace('\\', '/').Replace((Split-Path $ReportFile -Leaf), '') + + # Remove final slash + $ReportPath = $ReportPath.Substring(0, $ReportPath.LastIndexOf('/')) + + # Check if folder exists + New-SSRSFolder -Name $ReportPath + } + else + { + $ReportPath = $ReportFolder + } + + # check to see if we need to back up + if($BackupLocation -ne $null -and $BackupLocation -ne \"\") + { + # backup the item + Backup-ExistingItem -ItemFile $ReportFile -ItemFolder $ReportPath + } + + + # upload report + Upload-Item -Item $ReportFile -ItemType \"Report\" -ItemFolder $ReportPath + + # extract datasources + #Write-Host \"Extracting datasource names for $ReportFile\" + #$ReportDataSourceNames = Get-ReportDataSourceNames $ReportFile + + # set the datasources + Set-ItemDataSources -ItemFile $ReportFile -ItemFolder $ReportPath + + # set the datasets + Set-ReportDataSets -ReportFile $ReportFile -ReportFolderPath $ReportPath + + # update the report parameters + Update-ReportParameters -ReportFile $ReportFile -ReportFolderPath $ReportPath + } + + # loop through rsc files + foreach($ReportPartFile in $ReportPartFiles) + { + # check to see if we need to back up + if($BackupLocation -ne $null -and $BackupLocation -ne \"\") + { + # backup the item + Backup-ExistingItem -ItemFile $ReportPartFile -ItemFolder $ReportPartsFolder + } + +\t\t# upload item + Upload-Item -Item $ReportPartFile -ItemType \"Component\" -ItemFolder $ReportPartsFolder + } + +} +finally +{ + # check to see if the proxies are null + if($ReportServerProxy -ne $null) + { + # dispose + $ReportServerProxy.Dispose(); + } + + if($ReportExecutionProxy -ne $null) + { + # dispose + $ReportExecutionProxy.Dispose(); + } +} + +#endregion +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "e339fdfe-376f-4d6b-9226-9d5fc07c0a62", + "Name": "SSRSPackage", + "Label": "Package", + "HelpText": "Select the package containing the SSRS reports.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "", + "Name": "ReportServiceUrl", + "Label": "Url of SSRS Server service", + "HelpText": "The complete Url to the SSRS server. +Example: http://198.239.216.46/ReportServer_LOCALDEV/reportservice2010.asmx?wsdl", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "", + "Name": "ReportExecutionUrl", + "Label": "Report Execution Url", + "HelpText": "The complete Url to the Report Execution service. +Example: http://198.239.216.46/ReportServer_LOCALDEV/ReportExecution2005.asmx?wsdl", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "", + "Name": "ReportFolder", + "Label": "Report folder", + "HelpText": "Relative Url to the folder in which the reports will be deployed.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "", + "Name": "ReportDatasourceFolder", + "Label": "Report data source folder", + "HelpText": "Relative Url where the data sources for the reports are located, starting with '/'", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "", + "Name": "OverwriteDataSources", + "Label": "Overwrite datasource(s)", + "HelpText": "Tick if the existing report data source file needs to e replaced.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "", + "Name": "BackupLocation", + "Label": "Backup Location", + "HelpText": "Directory path to take a backup of existing reports (.rdl) and data source (.rds) files.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "", + "Name": "ReportDataSetFolder", + "Label": "DataSet folder", + "HelpText": "Relative Url where Shared Data Sets are stored", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "", + "Name": "ReportPartsFolder", + "Label": "Report Parts Folder", + "HelpText": "Relative folder where Report Parts are uploaded to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "", + "Name": "ServiceUserDomain", + "Label": "Service Domain", + "HelpText": "(Optional - leave blank to use Tentacle identity) Name of the domain for the user account to execute as", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "", + "Name": "ServiceUserName", + "Label": "Service Username", + "HelpText": "(Optional - leave blank to use Tentacle identity) Username of the user account to execute as", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "", + "Name": "ServicePassword", + "Label": "Service Password", + "HelpText": "(Optional - leave blank to use Tentacle identity) Password of the user account to execute as", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "", + "Name": "ClearReportFolder", + "Label": "Clear the Report Folder", + "HelpText": "Optional - This will delete all items in the Report Folder before adding items.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "", + "Name": "UseArchiveStructure", + "Label": "Use package folder structure", + "HelpText": "Tick this box to create folders matching the package folder structure and upload items into their respective folders. Ticking this box ignores all other folder specifications except Root folder", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "", + "Name": "RootFolder", + "Label": "Root folder", + "HelpText": "Used only when 'Use package folder structure' is checked. This specifies the root folder on SSRS to start in. Value is relative so it begins with a /", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-03-11T19:29:06.452Z", + "OctopusVersion": "2020.6.4671", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "sql" + } diff --git a/step-templates/ssrs-deploy-from-package.json.human b/step-templates/ssrs-deploy-from-package.json.human new file mode 100644 index 000000000..e73c19f9a --- /dev/null +++ b/step-templates/ssrs-deploy-from-package.json.human @@ -0,0 +1,1287 @@ +{ + "Id": "4e3a1163-e157-4675-a60c-4dc569d14348", + "Name": "Deploy SSRS Reports from a package", + "Description": "Uploads SSRS reports to an SSRS server from a package. + +The following Datasource properties can be overidden: ConnectionString, Username, Password, and CredentialRetrieval (valid values are: Integrated, Prompt, Store, or None). To override the property, create a Variable using the syntax of DatasourceName.PropertyName. For example: MyDatasource.Username + +To specify the Username and Password are Windows Credentials, create a variable called DatasourceName.WindowsCredentials and set the value to the string value 'true' (minus the quotes).", + "ActionType": "Octopus.Script", + "Version": 58, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "$DeployedPath = $OctopusParameters[\"Octopus.Action[$NugetPackageStepName].Output.Package.InstallationDirectoryPath\"] +$ReleaseNumber = $OctopusParameters[\"Octopus.Release.Number\"] +$UseArchiveStructure = [Convert]::ToBoolean($OctopusParameters[\"UseArchiveStructure\"]) + +#region Upload-Item +Function Upload-Item +{ + # parameters + param ([string] $Item, [string]$ItemType, [string] $ItemFolder) + + Write-Host \"Loading data from $Item\" + $ItemData = [System.IO.File]::ReadAllBytes($Item) + + # Create local variables + $Warnings = $null + $ItemName = $Item.SubString($Item.LastIndexOf(\"\\\") + 1) + #$ItemName = $ItemName.SubString(0, $ItemName.IndexOf(\".\")) + $ItemName = $ItemName.SubString(0, $ItemName.LastIndexOf(\".\")) + +\t# upload item + if ($IsReportService2005) { + if($ItemType -eq \"Report\") + { +\t [void]$ReportServerProxy.CreateReport($ItemName, $ItemFolder, $true, $ItemData, $null) + } + else + { + # error + Write-Error \"$ItemType is not supported in ReportService2005\" + } +\t} +\telseif ($IsReportService2010) { +\t\t[void]$ReportServerProxy.CreateCatalogItem($ItemType, $ItemName, $ItemFolder, $true, $ItemData, $null, [ref] $Warnings); +\t} +\telse { Write-Warning 'Report Service Unknown in Upload-Item method. Use ReportService2005 or ReportService2010.' } +} +#endregion + +#region Get-ItemDataSourceNames() +Function Get-ItemDataSourceNames +{ + # Parameters + Param ($ItemFile, $DataSourceName) + + # declare working variables + $DataSourceNames = @() + + # load the xml + [xml]$Xml = Get-Content $ItemFile + + # retrieve the datasource nodes + $DataSourceReferenceNodes = $Xml.GetElementsByTagName(\"DataSource\") + + # loop through returned results + foreach($Node in $DataSourceReferenceNodes) + { + # check to see if we're looking for a specific one + if($DataSourceName -ne $null) + { + # check to see if it's the current node + if($DataSourceName -eq $Node.Name) + { + # add + $DataSourceNames += $Node.DataSourceReference + } + } + else + { + # store the name + $DataSourceNames += $Node.DataSourceReference + } + } + + # return the results + return ,$DataSourceNames # Apparently using the , in front of the variable is how you return explicit arrays in PowerShell ... could you be more obsure? +} +#endregion + +#region Get-ItemDataSources() +Function Get-ItemDataSources +{ + # Parameters + Param ($ItemFile) + + # declare working variables + $DataSourceNames = @() + + # load the xml + [xml]$Xml = Get-Content $ItemFile + + # retrieve the datasource nodes + $DataSourceReferenceNodes = $Xml.GetElementsByTagName(\"DataSource\") + + # loop through returned results + foreach($Node in $DataSourceReferenceNodes) + { + # store the name + $DataSourceNames += $Node.Name + } + + # return the results + return ,$DataSourceNames # Apparently using the , in front of the variable is how you return explicit arrays in PowerShell ... could you be more obsure? +} +#endregion + +#region Get-ItemDataSourceReferenceNames +Function Get-ItemDataSourceReferenceNames +{ + # Parameters + Param ($ItemFile) + + # declare working variables + $DataSourceNames = @() + + # load the xml + [xml]$Xml = Get-Content $ItemFile + + # retrieve the datasource nodes + $DataSourceReferenceNodes = $Xml.GetElementsByTagName(\"DataSourceReference\") + + # loop through returned results + foreach($Node in $DataSourceReferenceNodes) + { + # get the data + $DataSourceNames += $Node.InnerText + } + + # return the results + return ,$DataSourceNames # Apparently using the , in front of the variable is how you return explicit arrays in PowerShell ... could you be more obsure? +} +#endregion + +#region Get-DataSetSharedReferenceName +Function Get-DataSetSharedReferenceName +{ + # parameters + param($ReportFile, $DataSetName) + + # load the xml + [xml]$ReportXml = Get-Content $ReportFile + + # Get the DataSet nodes + $DataSetNode = $ReportXml.GetElementsByTagName(\"DataSet\") | Where-Object {$_.Name -eq $DataSetName} + + # return the name + $DataSetNode.SharedDataSet.SharedDataSetReference +} +#endregion + +#region Item-Exists() +Function Item-Exists($ItemFolderPath, $ItemName) +{ + # declare search condition + $SearchCondition = New-Object \"$ReportServerProxyNamespace.SearchCondition\"; + + # fill in properties + $SearchCondition.Condition = Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.ConditionEnum\" -EnumName \"Equals\" + $SearchCondition.ConditionSpecified = $true + $SearchCondition.Name = \"Name\" + +\tif ($IsReportService2005) { +\t\t$SearchCondition.Value = $ItemName +\t\t# search +\t $items = $ReportServerProxy.FindItems($ItemFolderPath, (Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.BooleanOperatorEnum\" -EnumName \"And\"), $SearchCondition) +\t} +\telseif ($IsReportService2010) { +\t\t$SearchCondition.Values = @($ItemName) +\t\t# search +\t $items = $ReportServerProxy.FindItems($ItemFolderPath, (Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.BooleanOperatorEnum\" -EnumName \"And\"), $null, $SearchCondition) +\t} +\telse { Write-Warning 'Report Service Unknown in Item-Exists method. Use ReportService2005 or ReportService2010.' } + + # check to see if anything was returned + if($items.Length -gt 0) + { + # loop through returned items + foreach($item in $items) + { + # check the path + if($item.Path -eq \"$ItemFolderPath/$ItemName\") + { + # return true + return $true + } + else + { + # warn + Write-Warning \"Unexpected path for $($item.Name); path is $($item.Path) exepected $ItemFolderPath/$ItemName\" + } + } + + # items were found, but the path doesn't match + + return $false + } + else + { + return $false + } +} +#endregion + +Function Get-ItemPath +{ + # Define parameters + param( + $ItemName, + $StartFolder, + $CompareFolderPath) + + # declare search condition + $SearchCondition = New-Object \"$ReportServerProxyNamespace.SearchCondition\"; + + # fill in properties + $SearchCondition.Condition = Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.ConditionEnum\" -EnumName \"Equals\" + $SearchCondition.ConditionSpecified = $true + $SearchCondition.Name = \"Name\" + +\tif ($IsReportService2005) { +\t\t$SearchCondition.Value = $ItemName +\t\t# search +\t $items = $ReportServerProxy.FindItems($StartFolder, (Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.BooleanOperatorEnum\" -EnumName \"And\"), $SearchCondition) +\t} +\telseif ($IsReportService2010) { +\t\t$SearchCondition.Values = @($ItemName) +\t\t# search +\t $items = $ReportServerProxy.FindItems($StartFolder, (Get-SpecificEnumValue -EnumNamespace \"$ReportServerProxyNamespace.BooleanOperatorEnum\" -EnumName \"And\"), $null, $SearchCondition) +\t} +\telse { Write-Warning 'Report Service Unknown in Item-Exists method. Use ReportService2005 or ReportService2010.' } + + # Check how many items were returned + if ($items.Length -eq 1) + { + return $items[0].Path + } + else + { + # Loop through returned items + foreach ($item in $items) + { + # compare folders + if ($CompareFolderPath -eq ($item.Path.SubString(0, $item.Path.LastIndexOf(\"/\")))) + { + # Display message we're guessing + Write-Host \"Multiple items were found with name $ItemName, assuming location is same folder as reference, $CompareFolderPath.\" + return $item.Path + } + } + + # Display warning + Write-Warning \"Multiple items were returned for $ItemName, unable to determine which one to return.\" + return [string]::Empty + } +} + +#region Set-ItemDataSources() +Function Set-ItemDataSources +{ + # parameters + Param($ItemFile, $ItemFolder) + + # declare local variables + $ItemName = $ItemFile.SubString($ItemFile.LastIndexOf(\"\\\") + 1) + $ItemName = $ItemName.SubString(0, $ItemName.LastIndexOf(\".\")) + $AllDataSourcesFound = $true + + # get the datasources + $DataSources = $ReportServerProxy.GetItemDataSources([string]::Format(\"{0}/{1}\", $ItemFolder, $ItemName)) + + #loop through retrieved datasources + foreach($DataSource in $DataSources) + { + # check to see if it's a dataset + if([System.IO.Path]::GetExtension($ItemFile).ToLower() -eq \".rsd\") + { + # datasets can have one and only one datasource + # The method GetItemDataSources does not return the name of the datasource for datasets like it does for reports + # instead, it alaways returns DataSetDataSource. This made the call to Get-ItemDataSourceNames necessary, + # otherwise it would not link correctly + $DataSourceName = (Get-ItemDataSourceReferenceNames -ItemFile $ItemFile)[0] + } + else + { + # get the anme from teh source itself + $DataSourceName = (Get-ItemDataSourceNames -ItemFile $ItemFile -DataSourceName $DataSource.Name)[0] + } + + if ([string]::IsNullOrWhiteSpace($DataSourceName)) + { + Write-Host \"Datasource $($DataSource.Name) is not a shared datasource, skipping.\" + $AllDataSourcesFound = $false + continue + } + + # Check to see if datasourcename contains the folder -- this can happen if the report was created by Report Builder + if((![string]::IsNullOrEmpty($ReportDataSource)) -and ($DataSourceName.ToLower().Contains($ReportDatasourceFolder.ToLower()))) + { + # Remove teh path from the item name + $DataSourceName = $DataSourceName.ToLower().Replace(\"$($ReportDatasourceFolder.ToLower())/\",\"\") + } + + $DatasourcePath = \"\" + + if ($UseArchiveStructure -eq $true) + { + $DatasourcePath = Get-ItemPath -ItemName $DataSourceName -StartFolder $RootFolder -CompareFolderPath $ItemFolder + $DatasourcePath = $DatasourcePath.SubString(0, $DatasourcePath.LastIndexOf(\"/\")) + } + else + { + $DatasourcePath = $ReportDatasourceFolder + } + + # check to make sure the datasource exists in the location specified + if((Item-Exists -ItemFolderPath $DatasourcePath -ItemName $DataSourceName) -eq $true) + { + # create datasource reference variable + $DataSourceReference = New-Object \"$ReportServerProxyNamespace.DataSourceReference\"; + + # assign + $DataSourceReference.Reference = \"$DatasourcePath/\" + $DataSourceName + $DataSource.Item = $DataSourceReference + } + else + { + # display warning + Write-Warning \"Unable to find datasource $($DataSourceName) in $DatasourcePath\" + + # update to false + $AllDataSourcesFound = $false + } + } + + # check to see if found all datasources + if($AllDataSourcesFound -eq $true) + { + Write-Host \"Linking datasources to $ItemFolder/$ItemName\" + + # save the references + $ReportServerProxy.SetItemDataSources(\"$ItemFolder/$ItemName\", $DataSources) + } +} +#endregion + +#region Set-ReportDataSets() +Function Set-ReportDataSets +{ + # parameters + param($ReportFile, $ReportFolderPath) + + # declare local variables + $ReportName = $ReportFile.SubString($ReportFile.LastIndexOf(\"\\\") + 1) + $ReportName = $ReportName.SubString(0, $ReportName.LastIndexOf(\".\")) + $AllDataSetsFound = $true + $DataSetFolder = \"\" + + # get the datasources + $DataSets = $ReportServerProxy.GetItemReferences([string]::Format(\"{0}/{1}\", $ReportFolderPath, $ReportName), \"DataSet\") + + # loop through returned values + foreach($DataSet in $DataSets) + { + # get the name of the shared data set reference + $SharedDataSetReferenceName = Get-DataSetSharedReferenceName -ReportFile $ReportFile -DataSetName $DataSet.Name + + # Check to see if the SharedDataSetReferenceName contains the folder path -- this can happen if the report was built using Report Builder + if((![string]::IsNullOrEmpty($ReportDataSetFolder)) -and ($SharedDataSetReferenceName.ToLower().Contains($ReportDataSetFolder.ToLower()))) + { + # Remove the folder path from the name, it will cause issues when trying to set + $SharedDataSetReferenceName = $SharedDataSetReferenceName.ToLower().Replace(\"$($ReportDataSetFolder.ToLower())/\", \"\") + } + + # Check to see if we're using the archive folder structure + if ($UseArchiveStructure -eq $true) + { + # Set dataset folder + $DataSetFolder = Get-ItemPath -ItemName $SharedDataSetReferenceName -StartFolder $RootFolder -CompareFolderPath $ReportFolderPath + $DataSetFolder = $DataSetFolder.SubString(0, $DataSetFolder.LastIndexOf(\"/\")) + } + else + { + $DataSetFolder = $ReportDataSetFolder + } + + # check to make sure the datasource exists in the location specified + if((Item-Exists -ItemFolderPath $DataSetFolder -ItemName $SharedDataSetReferenceName) -eq $true) + { + # create datasource reference variable + $DataSetReference = New-Object \"$ReportServerProxyNamespace.ItemReference\"; + + # assign + $DataSetReference.Reference = \"$DataSetFolder/\" + $SharedDataSetReferenceName + $DataSetReference.Name = $DataSet.Name + + # log + Write-Host \"Linking Shared Data Set $($DataSet.Name) to $ReportName\" + + # update reference + $ReportServerProxy.SetItemReferences(\"$ReportFolderPath/$ReportName\", @($DataSetReference)) + } + else + { + # get the datasource name to include in warning message -- I know there must be a way to use the property in a string literal, but I wasn't able to figure it out while trying + # to solve a reported bug so I took the easy way. + $DataSetName = $DataSet.Name + + # display warning + Write-Warning \"Unable to find dataset $DataSetName in $ReportDataSetFolder\" + + # update to false + $AllDataSetsFound = $false + } + } + + # check to see if all datsets were found + if($AllDataSetsFound -eq $False) + { + Write-Warning \"Not all datasets found\" + + # save the references + $ReportServerProxy.SetItemReferences(\"$ReportFolder/$ReportName\", @($DataSets)) + } +} + +#endregion + +#region Get-ObjectNamespace() +Function Get-ObjectNamespace($Object) +{ + # return the value + ($Object).GetType().ToString().SubString(0, ($Object).GetType().ToString().LastIndexOf(\".\")) +} +#endregion + +#region Get-SpecificEnumValue() +Function Get-SpecificEnumValue($EnumNamespace, $EnumName) +{ + # get the enum values + $EnumValues = [Enum]::GetValues($EnumNamespace) + + # Loop through to find the specific value + foreach($EnumValue in $EnumValues) + { + # check current + if($EnumValue -eq $EnumName) + { + # return it + return $EnumValue + } + } + + # nothing was found + return $null +} +#endregion + +#region Update-ReportParamters() +Function Update-ReportParameters($ReportFile, $ReportFolderPath) +{ + # declare local variables + $ReportParameters = @(); + + # necessary so that when attempting to use the report execution service, it doesn't puke on you when it can't find the data source + $ReportData = (Remove-SharedReferences -ReportFile $ReportFile) + + # get just the report name + $ReportName = $ReportFile.SubString($ReportFile.LastIndexOf(\"\\\") + 1) + $ReportName = $ReportName.SubString(0, $ReportName.LastIndexOf(\".\")) + + # create warnings object + $ReportExecutionWarnings = $null + + # set the report full path + $ReportPath = \"$ReportFolderPath/$ReportName\" + + # load the report definition + $ExecutionInfo = $ReportExecutionProxy.LoadReportDefinition($ReportData, [ref] $ReportExecutionWarnings); + + # loop through the report execution parameters + foreach($Parameter in $ExecutionInfo.Parameters) + { + # create new item parameter object + $ItemParameter = New-Object \"$ReportServerProxyNamespace.ItemParameter\"; + + # fill in the properties except valid values, that one needs special processing + Copy-ObjectProperties -SourceObject $Parameter -TargetObject $ItemParameter; + + # fill in the valid values + $ItemParameter.ValidValues = Convert-ValidValues -SourceValidValues $Parameter.ValidValues; + + # exclude if it's query based + if($Parameter.DefaultValuesQueryBased -ne $true) + { + # add to list + $ReportParameters += $ItemParameter; + } + } + + # force the parameters to update + Write-Host \"Updating report parameters for $ReportFolderPath/$ReportName\" +\tif ($IsReportService2005) { +\t\t$ReportServerProxy.SetReportParameters(\"$ReportFolderPath/$ReportName\", $ReportParameters); +\t} +\telseif ($IsReportService2010) { +\t\t$ReportServerProxy.SetItemParameters(\"$ReportFolderPath/$ReportName\", $ReportParameters); +\t} +\telse { Write-Warning 'Report Service Unknown in Update-ReportParameters method. Use ReportService2005 or ReportService2010.' } +} +#endregion + +#region Remove-ShareReferences() +Function Remove-SharedReferences($ReportFile) +{ + ###################################################################################################### + #You'll notice that I've used the keyword of [void] in front of some of these method calls, this is so + #that the operation isn't captured as output of the function + ###################################################################################################### + + # read xml + [xml]$ReportXml = Get-Content $ReportFile -Encoding UTF8; + + # create new memory stream object + $MemoryStream = New-Object System.IO.MemoryStream + + try + { + + # declare array of nodes to remove + $NodesToRemove = @(); + + # get datasource names + $DataSourceNames = Get-ItemDataSources -ItemFile $ReportFile + + # check to see if report has datasourcenames + if($DataSourceNames.Count -eq 0) + { + # Get reference to reportnode + $ReportNode = $ReportXml.FirstChild.NextSibling # Kind of a funky way of getting it, but the SelectSingleNode(\"//Report\") wasn't working due to Namespaces in the node + + # create new DataSources node + $DataSourcesNode = $ReportXml.CreateNode($ReportNode.NodeType, \"DataSources\", $null) + + # create new datasource node + $DataSourceNode = $ReportXml.CreateNode($ReportNode.NodeType, \"DataSource\", $null) + + # create new datasourcereference node + $DataSourceReferenceNode = $ReportXml.CreateNode($ReportNode.NodeType, \"DataSourceReference\", $null) + + # create new attribute + $DataSourceNameAttribute = $ReportXml.CreateAttribute(\"Name\") + $DataSourceNameAttribute.Value = \"DataSource1\" + $dataSourceReferenceNode.InnerText = \"DataSource1\" + + # add attribute to datasource node + [void]$DataSourceNode.Attributes.Append($DataSourceNameAttribute) + [void]$DataSourceNode.AppendChild($DataSourceReferenceNode) + + # add nodes + [void]$ReportNode.AppendChild($DataSourcesNode) + [void]$DataSourcesNode.AppendChild($DataSourceNode) + + # add fake datasource name to array + $DataSourceNames += \"DataSource1\" + } + + # get all datasource nodes + $DatasourceNodes = $ReportXml.GetElementsByTagName(\"DataSourceReference\"); + + # loop through returned nodes + foreach($DataSourceNode in $DatasourceNodes) + { + # create a new connection properties node + $ConnectionProperties = $ReportXml.CreateNode($DataSourceNode.NodeType, \"ConnectionProperties\", $null); + + # create a new dataprovider node + $DataProvider = $ReportXml.CreateNode($DataSourceNode.NodeType, \"DataProvider\", $null); + $DataProvider.InnerText = \"SQL\"; + + # create new connection string node + $ConnectString = $ReportXml.CreateNode($DataSourceNode.NodeType, \"ConnectString\", $null); + $ConnectString.InnerText = \"Data Source=Server Name Here;Initial Catalog=database name here\"; + + # add new node to parent node + [void] $DataSourceNode.ParentNode.AppendChild($ConnectionProperties); + + # append childeren + [void] $ConnectionProperties.AppendChild($DataProvider); + [void] $ConnectionProperties.AppendChild($ConnectString); + + # Add to remove list + $NodesToRemove += $DataSourceNode; + } + + # get all shareddataset nodes + $SharedDataSetNodes = $ReportXml.GetElementsByTagName(\"SharedDataSet\") + + #loop through the returned nodes + foreach($SharedDataSetNode in $SharedDataSetNodes) + { + # create holder nodes so it won't error + $QueryNode = $ReportXml.CreateNode($SharedDataSetNode.NodeType, \"Query\", $null); + $DataSourceNameNode = $ReportXml.CreateNode($QueryNode.NodeType, \"DataSourceName\", $null); + $CommandTextNode = $ReportXml.CreateNode($QueryNode.NodeType, \"CommandText\", $null); + + # add valid datasource name, just get the first in the list + $DataSourceNameNode.InnerText = $DataSourceNames[0] + + # add node to parent + [void] $SharedDataSetNode.ParentNode.Appendchild($QueryNode) + + # add datasourcename and commandtext to query node + [void]$QueryNode.AppendChild($DataSourceNameNode) + [void]$QueryNode.AppendChild($CommandTextNode) + + # add node to removelist + $NodesToRemove += $SharedDataSetNode + } + + + # loop through nodes to remove + foreach($Node in $NodesToRemove) + { + # remove from parent + [void] $Node.ParentNode.RemoveChild($Node); + } + + $ReportXml.InnerXml = $ReportXml.InnerXml.Replace(\"xmlns=`\"`\"\", \"\") + + # save altered xml to memory stream + $ReportXml.Save($MemoryStream); + + # return the altered xml as byte array + return $MemoryStream.ToArray(); + } + finally + { + # close and dispose + $MemoryStream.Close(); + $MemoryStream.Dispose(); + } +} +#endregion + + +#region Copy-ObjectProperties() +Function Copy-ObjectProperties($SourceObject, $TargetObject) +{ + # Get source object property array + $SourcePropertyCollection = $SourceObject.GetType().GetProperties(); + + # get the destination + $TargetPropertyCollection = $TargetObject.GetType().GetProperties(); + + # loop through source property collection + for($i = 0; $i -lt $SourcePropertyCollection.Length; $i++) + { + # get the target property + $TargetProperty = $TargetPropertyCollection | Where {$_.Name -eq $SourcePropertyCollection[$i].Name} + + # check to see if it's null + if($TargetProperty -ne $null) + { + # check to see if it's the valid values property + if($TargetProperty.Name -ne \"ValidValues\") + { + # set the value + $TargetProperty.SetValue($TargetObject, $SourcePropertyCollection[$i].GetValue($SourceObject)); + } + } + } +} +#endregion + +#region ConvertValidValues() +Function Convert-ValidValues($SourceValidValues) +{ + # declare local values + $TargetValidValues = @(); + + # loop through source values + foreach($SourceValidValue in $SourceValidValues) + { + # create target valid value object + $TargetValidValue = New-Object \"$ReportServerProxyNamespace.ValidValue\"; + + # copy properties + Copy-ObjectProperties -SourceObject $SourceValidValue -TargetObject $TargetValidValue + + # add to list + $TargetValidValues += $TargetValidValue + } + + # return the values + return ,$TargetValidValues +} +#endregion + +#region Backup-ExistingItem() +Function Backup-ExistingItem +{ + # parameters + Param($ItemFile, $ItemFolder) + + # declare local variables + $ItemName = $ItemFile.SubString($ItemFile.LastIndexOf(\"\\\") + 1) + $ItemName = $ItemName.SubString(0, $ItemName.LastIndexOf(\".\")) + + # check to see if the item exists + if((Item-Exists -ItemFolderPath $ItemFolder -ItemName $ItemName) -eq $true) + { + # get file extension + $FileExtension = [System.IO.Path]::GetExtension($ItemFile) + + # check backuplocation + if($BackupLocation.EndsWith(\"\\\") -ne $true) + { + # append ending slash + $BackupLocation = $BackupLocation + \"\\\" + } +\t\t +\t\t# add the release number to the backup location +\t\t$BackupLocation = $BackupLocation + $ReleaseNumber + \"\\\" + + # ensure the backup location actually exists + if((Test-Path $BackupLocation) -ne $true) + { + # create it + New-Item -ItemType Directory -Path $BackupLocation + } + + # download the item + $Item = $ReportServerProxy.GetItemDefinition(\"$ItemFolder/$ItemName\") + + # form the backup path + $BackupPath = \"{0}{1}{2}\" -f $BackupLocation, $ItemName, $FileExtension; + + # write to disk + [System.IO.File]::WriteAllBytes(\"$BackupPath\", $Item); + + # write to screen + Write-Host \"Backed up $ItemFolder/$ItemName to $BackupPath\"; + } +} +#endregion + +#region Normalize-SSRSFolder() +function Normalize-SSRSFolder ([string]$Folder) { + if (-not $Folder.StartsWith('/')) { + $Folder = '/' + $Folder + } + + return $Folder +} +#endregion + +#region New-SSRSFolder() +function New-SSRSFolder ([string] $Name) { + Write-Verbose \"New-SSRSFolder -Name $Name\" + + $Name = Normalize-SSRSFolder -Folder $Name + + if ($ReportServerProxy.GetItemType($Name) -ne 'Folder') { + $Parts = $Name -split '/' + $Leaf = $Parts[-1] + $Parent = $Parts[0..($Parts.Length-2)] -join '/' + + if ($Parent) { + New-SSRSFolder -Name $Parent + } else { + $Parent = '/' + } + + $ReportServerProxy.CreateFolder($Leaf, $Parent, $null) + } +} +#endregion + +#region Clear-SSRSFolder() +function Clear-SSRSFolder ([string] $Name) { + Write-Verbose \"Clear-SSRSFolder -Name $Name\" + + $Name = Normalize-SSRSFolder -Folder $Name + + if ($ReportServerProxy.GetItemType($Name) -eq 'Folder' -and $ClearReportFolder) { + Write-Host (\"Clearing the {0} folder\" -f $Name) + $ReportServerProxy.ListChildren($Name, $false) | ForEach-Object { + Write-Verbose \"Deleting item: $($_.Path)\" + $ReportServerProxy.DeleteItem($_.Path) + } + } +} +#endregion + +#region New-SSRSDataSource() +function New-SSRSDataSource ([string]$RdsPath, [string]$Folder, [bool]$OverwriteDataSources) { + Write-Verbose \"New-SSRSDataSource -RdsPath $RdsPath -Folder $Folder\" + + $Folder = Normalize-SSRSFolder -Folder $Folder + + [xml]$Rds = Get-Content -Path $RdsPath + $dsName = $Rds.RptDataSource.Name + $ConnProps = $Rds.RptDataSource.ConnectionProperties + +\t$type = $ReportServerProxy.GetType().Namespace #Get proxy type +\t$DSDdatatype = ($type + '.DataSourceDefinition') +\t +\t$Definition = new-object ($DSDdatatype) +\tif($Definition -eq $null){ +\t Write-Error Failed to create data source definition object +\t} +\t +\t$dsConnectionString = $($OctopusParameters[\"$($dsName).ConnectionString\"]) + $dsUsername = $($OctopusParameters[\"$($dsName).Username\"]) + $dsPassword = $($OctopusParameters[\"$($dsName).Password\"]) + $dsCredentialRetrieval = $($OctopusParameters[\"$($dsName).CredentialRetrieval\"]) + +\t# replace the connection string variable that is configured in the octopus project +\tif ($dsConnectionString) { +\t $Definition.ConnectString = $dsConnectionString +\t} else { +\t $Definition.ConnectString = $ConnProps.ConnectString +\t} +\t + $Definition.Extension = $ConnProps.Extension + +\t# Check to see if the credential retrieval is overridden + if ($null -ne $dsCredentialRetrieval) + { + \tWrite-Host \"Forcing CredentialRetrieval property to: $dsCredentialRetrieval.\" + $Definition.CredentialRetrieval = $dsCredentialRetrieval + } + else + { + \t# Set the Credential Retrieval method + \tif ([Convert]::ToBoolean($ConnProps.IntegratedSecurity)) { +\t\t\t$Definition.CredentialRetrieval = 'Integrated' +\t\t} + elseif (![string]::IsNullOrWhitespace($dsUsername) -and ![string]::IsNullOrWhitespace($dsPassword)) + { + \t$Definition.CredentialRetrieval = 'Store' + } + } + +\tif ($Definition.CredentialRetrieval -eq 'Store') + {\t\t +\t\tWrite-Host \"$($dsName).Username = '$dsUsername'\" +\t\tWrite-Host \"$($dsName).Password = '$dsPassword'\" +\t\t +\t\t$Definition.UserName = $dsUsername; + $Definition.Password = $dsPassword; +\t} + + # Check to see if this is supposed to be an Windows Authentication stored account + if ($OctopusParameters[\"$($dsName).WindowsCredentials\"] -eq \"true\") + { +\t # Set the definition to Windows Credentials + \t$Definition.WindowsCredentials = $true + } + + + $DataSource = New-Object -TypeName PSObject -Property @{ + Name = $Rds.RptDataSource.Name + Path = $Folder + '/' + $Rds.RptDataSource.Name + } + + if ($OverwriteDataSources -or $ReportServerProxy.GetItemType($DataSource.Path) -eq 'Unknown') { + Write-Host \"Overwriting datasource $($DataSource.Name)\" + $ReportServerProxy.CreateDataSource($DataSource.Name, $Folder, $OverwriteDataSources, $Definition, $null) + } + + return $DataSource +} +#endregion + +#region Main + +try +{ + # declare array for reports + $ReportFiles = @() +\t$ReportDataSourceFiles = @() + $ReportDataSetFiles = @() + $ReportPartFiles = @() +\t +\t$IsReportService2005 = $false +\t$IsReportService2010 = $false +\t +\tif ($ReportServiceUrl.ToLower().Contains('reportservice2005.asmx')) { +\t\t$IsReportService2005 = $true +\t\tWrite-Host \"2005 Report Service found.\" +\t} +\telseif ($ReportServiceUrl.ToLower().Contains('reportservice2010.asmx')) { +\t\t$IsReportService2010 = $true +\t\tWrite-Host \"2010 Report Service found.\" +\t} +\t +\tWrite-Host \"Deploy Path: $DeployedPath\" +\t + # get all report files for deployment + Write-Host \"Getting all .rdl files\" + Get-ChildItem $DeployedPath -Recurse -Filter \"*.rdl\" | ForEach-Object { If(($ReportFiles -contains $_.FullName) -eq $false) {$ReportFiles += $_.FullName}} + Write-Host \"# of rdl files found: $($ReportFiles.Count)\" + + # get all report datasource files for deployment + Write-Host \"Getting all .rds files\" + Get-ChildItem $DeployedPath -Recurse -Filter \"*.rds\" | ForEach-Object { If(($ReportDataSourceFiles -contains $_.FullName) -eq $false) {$ReportDataSourceFiles += $_.FullName}} + Write-Host \"# of rds files found: $($ReportDataSourceFiles.Count)\" + + # get all report datset files for deployment + Write-Host \"Getting all .rsd files\" + Get-ChildItem $DeployedPath -Recurse -Filter \"*.rsd\" | ForEach-Object { If(($ReportDataSetFiles -contains $_.FullName) -eq $false) {$ReportDataSetFiles += $_.FullName}} + Write-Host \"# of rsd files found: $($ReportDataSetFiles.Count)\" + + # get all report part files for deployment + Write-Host \"Getting all .rsc files\" + Get-ChildItem $DeployedPath -Recurse -Filter \"*.rsc\" | ForEach-Object { If(($ReportPartFiles -contains $_.FullName) -eq $false) {$ReportPartFiles += $_.FullName}} + Write-Host \"# of rsc files found: $($ReportPartFiles.Count)\" + + # set the report proxies + Write-Host \"Creating SSRS Web Service proxies\" + + # check to see if credentials were supplied for the services + if(([string]::IsNullOrEmpty($ServiceUserDomain) -ne $true) -and ([string]::IsNullOrEmpty($ServiceUserName) -ne $true) -and ([string]::IsNullOrEmpty($ServicePassword) -ne $true)) + { + # secure the password + $secpasswd = ConvertTo-SecureString \"$ServicePassword\" -AsPlainText -Force + + # create credential object + $ServiceCredential = New-Object System.Management.Automation.PSCredential (\"$ServiceUserDomain\\$ServiceUserName\", $secpasswd) + + # create proxies + $ReportServerProxy = New-WebServiceProxy -Uri $ReportServiceUrl -Credential $ServiceCredential + $ReportExecutionProxy = New-WebServiceProxy -Uri $ReportExecutionUrl -Credential $ServiceCredential + } + else + { + # create proxies using current identity + $ReportServerProxy = New-WebServiceProxy -Uri $ReportServiceUrl -UseDefaultCredential + $ReportExecutionProxy = New-WebServiceProxy -Uri $ReportExecutionUrl -UseDefaultCredential + } + + + +\t#Create folder information for DataSource and Report + if ($UseArchiveStructure -eq $false) + { +\t New-SSRSFolder -Name $ReportFolder +\t New-SSRSFolder -Name $ReportDatasourceFolder + New-SSRSFolder -Name $ReportDataSetFolder + New-SSRSFolder -Name $ReportPartsFolder + } + else + { + New-SSRSFolder -Name $RootFolder + } + + #Clear destination folder if specified + if([System.Convert]::ToBoolean(\"$ClearReportFolder\")) { + Clear-SSRSFolder -Name $ReportFolder + } +\t +\t#Create DataSource + foreach($RDSFile in $ReportDataSourceFiles) { + Write-Host \"New-SSRSDataSource $RdsFile\" + + $DatasourceFolder = \"\" + + if ($UseArchiveStructure -eq $true) + { + # Adjust report folder to archive path + $DatasourceFolder = $(if ($RootFolder -eq \"/\") { [string]::Empty} else { $RootFolder } ) + $RDSFile.Replace($DeployedPath, '').Replace('\\', '/').Replace((Split-Path $RDSFile -Leaf), '') + + # Remove final slash + $DatasourceFolder = $DatasourceFolder.Substring(0, $DatasourceFolder.LastIndexOf('/')) + + # Check if folder exists + New-SSRSFolder -Name $DatasourceFolder + } + else + { + $DatasourceFolder = $ReportDatasourceFolder + } + +\t\t$DataSource = New-SSRSDataSource -RdsPath $RdsFile -Folder $DatasourceFolder -Overwrite ([System.Convert]::ToBoolean(\"$OverwriteDataSources\")) +\t} + + # get the service proxy namespaces - this is necessary because of a bug documented here http://stackoverflow.com/questions/7921040/error-calling-reportingservice2005-finditems-specifically-concerning-the-bool and http://www.vistax64.com/powershell/273120-bug-when-using-namespace-parameter-new-webserviceproxy.html + $ReportServerProxyNamespace = Get-ObjectNamespace -Object $ReportServerProxy + $ReportExecutionProxyNamespace = Get-ObjectNamespace -Object $ReportExecutionProxy + + # Create dat sets + foreach($DataSet in $ReportDataSetFiles) + { + $DataSetPath = \"\" + + # Check to see if it's set to follow archive structure + if ($UseArchiveStructure -eq $true) + { + # Adjust report folder to archive path + $DataSetPath = $(if ($RootFolder -eq \"/\") { [string]::Empty} else { $RootFolder } ) + $DataSet.Replace($DeployedPath, '').Replace('\\', '/').Replace((Split-Path $DataSet -Leaf), '') + + # Remove final slash + $DataSetPath = $DataSetPath.Substring(0, $DataSetPath.LastIndexOf('/')) + + # Check if folder exists + New-SSRSFolder -Name $DataSetPath + } + else + { + $DataSetPath = $ReportDataSetFolder + } + + # check to see if we need to back up + if($BackupLocation -ne $null -and $BackupLocation -ne \"\") + { + # backup the item + Backup-ExistingItem -ItemFile $DataSet -ItemFolder $DataSetPath + } + + # upload the dataset + Upload-Item -Item $DataSet -ItemType \"DataSet\" -ItemFolder $DataSetPath + + # update the dataset datasource + Set-ItemDataSources -ItemFile $DataSet -ItemFolder $DataSetPath + } + + # get the proxy auto generated name spaces + + # loop through array + foreach($ReportFile in $ReportFiles) + { + $ReportPath = \"\" + + # Check to see if it's set to follow archive structure + if ($UseArchiveStructure -eq $true) + { + # Adjust report folder to archive path + $ReportPath = $(if ($RootFolder -eq \"/\") { [string]::Empty} else { $RootFolder } ) + $ReportFile.Replace($DeployedPath, '').Replace('\\', '/').Replace((Split-Path $ReportFile -Leaf), '') + + # Remove final slash + $ReportPath = $ReportPath.Substring(0, $ReportPath.LastIndexOf('/')) + + # Check if folder exists + New-SSRSFolder -Name $ReportPath + } + else + { + $ReportPath = $ReportFolder + } + + # check to see if we need to back up + if($BackupLocation -ne $null -and $BackupLocation -ne \"\") + { + # backup the item + Backup-ExistingItem -ItemFile $ReportFile -ItemFolder $ReportPath + } + + + # upload report + Upload-Item -Item $ReportFile -ItemType \"Report\" -ItemFolder $ReportPath + + # extract datasources + #Write-Host \"Extracting datasource names for $ReportFile\" + #$ReportDataSourceNames = Get-ReportDataSourceNames $ReportFile + + # set the datasources + Set-ItemDataSources -ItemFile $ReportFile -ItemFolder $ReportPath + + # set the datasets + Set-ReportDataSets -ReportFile $ReportFile -ReportFolderPath $ReportPath + + # update the report parameters + Update-ReportParameters -ReportFile $ReportFile -ReportFolderPath $ReportPath + } + + # loop through rsc files + foreach($ReportPartFile in $ReportPartFiles) + { + # check to see if we need to back up + if($BackupLocation -ne $null -and $BackupLocation -ne \"\") + { + # backup the item + Backup-ExistingItem -ItemFile $ReportPartFile -ItemFolder $ReportPartsFolder + } + +\t\t# upload item + Upload-Item -Item $ReportPartFile -ItemType \"Component\" -ItemFolder $ReportPartsFolder + } + +} +finally +{ + # check to see if the proxies are null + if($ReportServerProxy -ne $null) + { + # dispose + $ReportServerProxy.Dispose(); + } + + if($ReportExecutionProxy -ne $null) + { + # dispose + $ReportExecutionProxy.Dispose(); + } +} + +#endregion +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "92daa94e-73f3-466e-a8db-35149646df2b", + "Name": "NugetPackageStepName", + "Label": "SSRS package step", + "HelpText": "Select the step in this project which downloads the SSRS package.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Id": "0bb2f003-fda9-4571-86aa-3bad13775874", + "Name": "ReportServiceUrl", + "Label": "Url of SSRS Server service", + "HelpText": "The complete Url to the SSRS server. +Example: http://198.239.216.46/ReportServer_LOCALDEV/reportservice2010.asmx?wsdl", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "feca8cfe-fe9e-4e8f-ac7e-b370b5ef3ef6", + "Name": "ReportExecutionUrl", + "Label": "Report Execution Url", + "HelpText": "The complete Url to the Report Execution service. +Example: http://198.239.216.46/ReportServer_LOCALDEV/ReportExecution2005.asmx?wsdl", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1924b3a6-d5bf-4713-8bc7-6b77422a7944", + "Name": "ReportFolder", + "Label": "Report folder", + "HelpText": "Relative Url to the folder in which the reports will be deployed.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "28cc63a0-5a3e-4d22-8406-574bdb332fe1", + "Name": "ReportDatasourceFolder", + "Label": "Report data source folder", + "HelpText": "Relative Url where the data sources for the reports are located, starting with '/'", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b1cdc16e-c2db-41eb-9ae8-8df02b05f03d", + "Name": "OverwriteDataSources", + "Label": "Overwrite datasource(s)", + "HelpText": "Tick if the existing report data source file needs to e replaced.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "b0e69f02-1f99-431a-852e-b0084ed23fd4", + "Name": "BackupLocation", + "Label": "Backup Location", + "HelpText": "Directory path to take a backup of existing reports (.rdl) and data source (.rds) files.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a736f0b8-aa27-45ae-b6a0-43a76e44d85a", + "Name": "ReportDataSetFolder", + "Label": "DataSet folder", + "HelpText": "Relative Url where Shared Data Sets are stored", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c88b150a-9f70-4700-9981-ed1514dc5a2b", + "Name": "ReportPartsFolder", + "Label": "Report Parts Folder", + "HelpText": "Relative folder where Report Parts are uploaded to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "47575f66-9b1b-4cbb-a062-30d811e6159d", + "Name": "ServiceUserDomain", + "Label": "Service Domain", + "HelpText": "(Optional - leave blank to use Tentacle identity) Name of the domain for the user account to execute as", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "98b43d45-5a5a-456a-aac7-1f4cc6fe8e55", + "Name": "ServiceUserName", + "Label": "Service Username", + "HelpText": "(Optional - leave blank to use Tentacle identity) Username of the user account to execute as", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fd48bde1-3874-45ea-ab99-b40c13b91d5a", + "Name": "ServicePassword", + "Label": "Service Password", + "HelpText": "(Optional - leave blank to use Tentacle identity) Password of the user account to execute as", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "1310ffde-1f98-4ebf-bf6a-605bb83ea54e", + "Name": "ClearReportFolder", + "Label": "Clear the Report Folder", + "HelpText": "Optional - This will delete all items in the Report Folder before adding items.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "9d64d192-b645-4aac-ba52-3c86aebe8d72", + "Name": "UseArchiveStructure", + "Label": "Use package folder structure", + "HelpText": "Tick this box to create folders matching the package folder structure and upload items into their respective folders. Ticking this box ignores all other folder specifications except Root folder", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "ad0280e7-25a6-48cc-bc90-4e6ee3c8951b", + "Name": "RootFolder", + "Label": "Root folder", + "HelpText": "Used only when 'Use package folder structure' is checked. This specifies the root folder on SSRS to start in. Value is relative so it begins with a /", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2021-03-11T19:19:00.102Z", + "OctopusVersion": "2020.6.4671", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/ssrs-report-backup.json.human b/step-templates/ssrs-report-backup.json.human new file mode 100644 index 000000000..07426d4fc --- /dev/null +++ b/step-templates/ssrs-report-backup.json.human @@ -0,0 +1,117 @@ +{ + "Id": "69c9eb25-9005-47c3-abb6-032e2ef909e3", + "Name": "SSRS Report Backup", + "Description": "Automatically performs a back up of all SSRS reports on an SSRS Server.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "\r +[void][System.Reflection.Assembly]::LoadWithPartialName(\"System.Xml.XmlDocument\");\r +[void][System.Reflection.Assembly]::LoadWithPartialName(\"System.IO\");\r + \r +$reportServerName = $reportServer;\r +$ReportServiceWebServiceURL = \"http://$reportServerName/ReportServer/\"\r +$ssrsVersion = $version;\r +$exportFolder = $folder;\r +\r +\r +if ($ssrsVersion -gt 2005) \r +\t{\r +\t\t# SSRS 2008/2012\r +\t\t$reportServerUri = \"$ReportServiceWebServiceURL/ReportService2010.asmx\" -f $reportServerName\r +\t} else {\r +\t\t# SSRS 2005\r +\t\t$reportServerUri = \"$ReportServiceWebServiceURL/ReportService2005.asmx\" -f $reportServerName\r +\t}\r +\r +\r +$Proxy = New-WebServiceProxy -Uri $reportServerUri -Namespace SSRS.ReportingService2005 -UseDefaultCredential ;\r +\r +\r +if ($ssrsVersion -gt 2005) {\t\t \r +\t\t$items = $Proxy.ListChildren(\"/\", $true) | `\r +\t\t\t\t Select-Object TypeName, Path, ID, Name | `\r +\t\t\t\t Where-Object {$_.typeName -eq \"Report\"};\r +\t} else {\r +\t\t$items = $Proxy.ListChildren(\"/\", $true) | `\r + Select-Object Type, Path, ID, Name | `\r + Where-Object {$_.type -eq \"Report\"};\r +\t}\r +\r +\r +$folderName = $exportFolder + \"\\\" + (Get-Date -format \"yyyy-MM-dd-hhmmtt\");\r +\r + \r +foreach($item in $items)\r +{\r +\r + $subfolderName = split-path $item.Path;\r + $reportName = split-path $item.Path -Leaf;\r + $fullSubfolderName = $folderName + $fullFolderName + $subfolderName;\r + if(-not(Test-Path $fullSubfolderName))\r + {\r +\r + [System.IO.Directory]::CreateDirectory($fullSubfolderName) | out-null\r + }\r + \r + $rdlFile = New-Object System.Xml.XmlDocument;\r + [byte[]] $reportDefinition = $null;\r + \r + if ($ssrsVersion -gt 2005) {\r +\t\t\t$reportDefinition = $Proxy.GetItemDefinition($item.Path);\r +\t\t} else {\r +\t\t\t$reportDefinition = $Proxy.GetReportDefinition($item.Path);\r +\t\t}\r + \r + [System.IO.MemoryStream] $memStream = New-Object System.IO.MemoryStream(@(,$reportDefinition));\r + $rdlFile.Load($memStream);\r + \r + $fullReportFileName = $fullSubfolderName + \"\\\" + $item.Name + \".rdl\";\r + \r + Write-Output $fullReportFileName;\r + \r + $rdlFile.Save($fullReportFileName);\r + \r + \r +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "reportServer", + "Label": "SSRS URL", + "HelpText": "The URL of the Report Server including any ports.", + "DefaultValue": "localhost", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "version", + "Label": "SSRS Version", + "HelpText": "The version of your SSRS instance e.g. 2005, 2008, 2012", + "DefaultValue": "2008", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "folder", + "Label": "Export Folder", + "HelpText": "The folder on the Deployment machine where the reports should be Saved.", + "DefaultValue": "c:\\temp\\", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2016-02-24T11:44:03.516+00:00", + "LastModifiedBy": "bleep-io", + "$Meta": { + "ExportedAt": "2016-02-24T12:10:04.807+00:00", + "OctopusVersion": "2.6.2.845", + "Type": "ActionTemplate" + }, + "Category": "sql" +} diff --git a/step-templates/stackify-api-template.json.human b/step-templates/stackify-api-template.json.human new file mode 100644 index 000000000..431ff9a0b --- /dev/null +++ b/step-templates/stackify-api-template.json.human @@ -0,0 +1,160 @@ +{ + "Id": "e28b6898-c336-47b0-bfe0-6158c846ef94", + "Name": "Stackify's Retrace API Template", + "Description": "Notify Retrace about the status of your deployment via this script.", + "ActionType": "Octopus.Script", + "Version": 36, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "if (!$hostApi) { $hostApi= 'https://api.stackify.net' } + +if ($OctopusParameters[\"Octopus.Deployment.Error\"] -eq $null) +{ +$post = $hostApi.TrimEnd('/') + '/api/v1/deployments/' + $action +} +else +{ +$post = $hostApi.TrimEnd('/') + '/api/v1/deployments/cancel' +} +# build the authorization header + +$headers = @{'authorization'='ApiKey ' + $apiKey} + +# build the body of the post + +if (!$name) { $name = $version } + +$bodyObj = @{ Version=$version; AppName=$app; EnvironmentName=$env; } + +if ($action -eq \"start\" -or $action -eq \"complete\"){ + + $bodyObj.Name = $name + + if ($uri) { $bodyObj.Uri = $uri } + + if ($branch) { $bodyObj.Branch = $branch } + + if ($commit) { $bodyObj.Commit = $commit } + +} + +$body = ConvertTo-Json $bodyObj + +# send the request +Invoke-WebRequest -Uri $post -Method POST -ContentType \"application/json\" -Headers $headers -Body $body -UseBasicParsing", + "Octopus.Action.Package.DownloadOnTentacle": "False" + }, + "Parameters": [ + { + "Id": "32391872-38c9-4be1-af4f-785629ab6f6e", + "Name": "apiKey", + "Label": "Stackify ApiKey", + "HelpText": "Located inside your Retrace account. To use the Stackify.ApiKey parameter check out support.stackify.com for more info!", + "DefaultValue": "#{Stackify.ApiKey}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "88767335-f5bd-4eae-8c6a-1d66883b95d7", + "Name": "app", + "Label": "Application Name", + "HelpText": "Name of your application. _Note this must match the name of an app in Retrace_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "66237479-d4f0-48b7-82a6-01ea9659c45b", + "Name": "env", + "Label": "Environment", + "HelpText": "Name of your app's environment in Retrace. _Note this must match the application's environment in Retrace_", + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "a7d6da84-6b3f-460c-a8bf-7048304943d5", + "Name": "version", + "Label": "Version", + "HelpText": "What version of your app is this?", + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "911474bd-527e-4b39-90e4-24ef861190e3", + "Name": "action", + "Label": "Deployment Action", + "HelpText": "either start/complete/cancel", + "DefaultValue": "complete", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "start|Start +complete|Complete +cancel|Cancel" + }, + "Links": {} + }, + { + "Id": "56dd8f21-65b8-4dbc-9247-cb5cbb80f86c", + "Name": "uri", + "Label": "Deployment URI", + "HelpText": "**OPTIONAL** A link you would like to associate with this release _e.g release notes or a link to this release_", + "DefaultValue": "#{Octopus.Web.ReleaseLink}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1fc3268f-7c4a-4877-8663-6fe00253af3c", + "Name": "branch", + "Label": "Branch", + "HelpText": "**OPTIONAL** _e.g. master_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "5a4618a6-97a9-42f9-abd3-88234de9a8a1", + "Name": "commit", + "Label": "Commit", + "HelpText": "**OPTIONAL** A unique identifier to assign to this release.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "645dab46-ee31-475b-a5a0-05eb059c0893", + "Name": "name", + "Label": "Release Name", + "HelpText": "**OPTIONAL** The name of this release.", + "DefaultValue": "#{Octopus.Deployment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "mattjbrooksii", + "$Meta": { + "ExportedAt": "2018-01-29T22:12:56.674Z", + "OctopusVersion": "4.1.8", + "Type": "ActionTemplate" + }, + "Category": "stackify" +} diff --git a/step-templates/statuscake-maintenance-window.json.human b/step-templates/statuscake-maintenance-window.json.human new file mode 100644 index 000000000..d7ea65633 --- /dev/null +++ b/step-templates/statuscake-maintenance-window.json.human @@ -0,0 +1,534 @@ +{ + "Id": "48668ad0-898f-482b-a529-90421d8ad459", + "Name": "StatusCake Maintenance Window", + "Description": "Creates a maintenance window in Status Cake for a given set of tests.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$apiKey = $OctopusParameters[\"StatusCake.ApiKey\"] +$username = $OctopusParameters[\"StatusCake.Username\"] +$name = $OctopusParameters[\"StatusCake.Name\"] +$tid = $OctopusParameters[\"StatusCake.TestIds\"] +$timezone = $OctopusParameters[\"StatusCake.Timezone\"] +$length = $OctopusParameters[\"StatusCake.Length\"] + +$now = (Get-Date).ToUniversalTime() +$start = [int64](Get-Date($now) -UFormat %s) +$end = [int64](Get-Date($now.AddMinutes($length)) -UFormat %s) + +$headers = @{ + \"API\" = $apiKey; + \"Username\" = $username +} + +$body = @{ + \"name\" = $name; + \"start_unix\" = $start; + \"end_unix\" = $end; + \"raw_tests\" = $tid; + \"timezone\" = $timezone; +} + +Invoke-WebRequest -Uri https://app.statuscake.com/API/Maintenance/Update -Method POST -Headers $headers -Body $body -UseBasicParsing", + "Octopus.Action.PowerShell.Edition": "Desktop", + "Octopus.Action.EnabledFeatures": "Octopus.Features.SelectPowerShellEditionForWindows" + }, + "Parameters": [ + { + "Id": "c8044bd0-ffa4-405f-8081-3ab4f82785bf", + "Name": "StatusCake.ApiKey", + "Label": "API Key", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "182c44a5-7545-4c26-8597-d2cd3ccc8a0d", + "Name": "StatusCake.Username", + "Label": "Username", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e210df57-ac9a-41bb-8f95-51b2595b190d", + "Name": "StatusCake.Name", + "Label": "Name", + "HelpText": "Description to be shown in StatusCake for the maintenance window.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3a9c7c6b-e07a-471c-91b4-ad827581a770", + "Name": "StatusCake.TestIds", + "Label": "Test IDs", + "HelpText": "Comma separated list of StatusCake Test IDs.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e6c36ebb-73c4-4eda-95f6-cee06d93baad", + "Name": "StatusCake.Timezone", + "Label": "Timezone", + "HelpText": "Timezone the maintenance window is defined in.", + "DefaultValue": "UTC", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "UTC|UTC +Africa/Abidjan|Africa/Abidjan +Africa/Accra|Africa/Accra +Africa/Addis_Ababa|Africa/Addis_Ababa +Africa/Algiers|Africa/Algiers +Africa/Asmara|Africa/Asmara +Africa/Bamako|Africa/Bamako +Africa/Bangui|Africa/Bangui +Africa/Banjul|Africa/Banjul +Africa/Bissau|Africa/Bissau +Africa/Blantyre|Africa/Blantyre +Africa/Brazzaville|Africa/Brazzaville +Africa/Bujumbura|Africa/Bujumbura +Africa/Cairo|Africa/Cairo +Africa/Casablanca|Africa/Casablanca +Africa/Ceuta|Africa/Ceuta +Africa/Conakry|Africa/Conakry +Africa/Dakar|Africa/Dakar +Africa/Dar_es_Salaam|Africa/Dar_es_Salaam +Africa/Djibouti|Africa/Djibouti +Africa/Douala|Africa/Douala +Africa/El_Aaiun|Africa/El_Aaiun +Africa/Freetown|Africa/Freetown +Africa/Gaborone|Africa/Gaborone +Africa/Harare|Africa/Harare +Africa/Johannesburg|Africa/Johannesburg +Africa/Juba|Africa/Juba +Africa/Kampala|Africa/Kampala +Africa/Khartoum|Africa/Khartoum +Africa/Kigali|Africa/Kigali +Africa/Kinshasa|Africa/Kinshasa +Africa/Lagos|Africa/Lagos +Africa/Libreville|Africa/Libreville +Africa/Lome|Africa/Lome +Africa/Luanda|Africa/Luanda +Africa/Lubumbashi|Africa/Lubumbashi +Africa/Lusaka|Africa/Lusaka +Africa/Malabo|Africa/Malabo +Africa/Maputo|Africa/Maputo +Africa/Maseru|Africa/Maseru +Africa/Mbabane|Africa/Mbabane +Africa/Mogadishu|Africa/Mogadishu +Africa/Monrovia|Africa/Monrovia +Africa/Nairobi|Africa/Nairobi +Africa/Ndjamena|Africa/Ndjamena +Africa/Niamey|Africa/Niamey +Africa/Nouakchott|Africa/Nouakchott +Africa/Ouagadougou|Africa/Ouagadougou +Africa/Porto-Novo|Africa/Porto-Novo +Africa/Sao_Tome|Africa/Sao_Tome +Africa/Tripoli|Africa/Tripoli +Africa/Tunis|Africa/Tunis +Africa/Windhoek|Africa/Windhoek +America/Adak|America/Adak +America/Anchorage|America/Anchorage +America/Anguilla|America/Anguilla +America/Antigua|America/Antigua +America/Araguaina|America/Araguaina +America/Argentina/Buenos_Aires|America/Argentina/Buenos_Aires +America/Argentina/Catamarca|America/Argentina/Catamarca +America/Argentina/Cordoba|America/Argentina/Cordoba +America/Argentina/Jujuy|America/Argentina/Jujuy +America/Argentina/La_Rioja|America/Argentina/La_Rioja +America/Argentina/Mendoza|America/Argentina/Mendoza +America/Argentina/Rio_Gallegos|America/Argentina/Rio_Gallegos +America/Argentina/Salta|America/Argentina/Salta +America/Argentina/San_Juan|America/Argentina/San_Juan +America/Argentina/San_Luis|America/Argentina/San_Luis +America/Argentina/Tucuman|America/Argentina/Tucuman +America/Argentina/Ushuaia|America/Argentina/Ushuaia +America/Aruba|America/Aruba +America/Asuncion|America/Asuncion +America/Atikokan|America/Atikokan +America/Bahia|America/Bahia +America/Bahia_Banderas|America/Bahia_Banderas +America/Barbados|America/Barbados +America/Belem|America/Belem +America/Belize|America/Belize +America/Blanc-Sablon|America/Blanc-Sablon +America/Boa_Vista|America/Boa_Vista +America/Bogota|America/Bogota +America/Boise|America/Boise +America/Cambridge_Bay|America/Cambridge_Bay +America/Campo_Grande|America/Campo_Grande +America/Cancun|America/Cancun +America/Caracas|America/Caracas +America/Cayenne|America/Cayenne +America/Cayman|America/Cayman +America/Chicago|America/Chicago +America/Chihuahua|America/Chihuahua +America/Costa_Rica|America/Costa_Rica +America/Creston|America/Creston +America/Cuiaba|America/Cuiaba +America/Curacao|America/Curacao +America/Danmarkshavn|America/Danmarkshavn +America/Dawson|America/Dawson +America/Dawson_Creek|America/Dawson_Creek +America/Denver|America/Denver +America/Detroit|America/Detroit +America/Dominica|America/Dominica +America/Edmonton|America/Edmonton +America/Eirunepe|America/Eirunepe +America/El_Salvador|America/El_Salvador +America/Fort_Nelson|America/Fort_Nelson +America/Fortaleza|America/Fortaleza +America/Glace_Bay|America/Glace_Bay +America/Godthab|America/Godthab +America/Goose_Bay|America/Goose_Bay +America/Grand_Turk|America/Grand_Turk +America/Grenada|America/Grenada +America/Guadeloupe|America/Guadeloupe +America/Guatemala|America/Guatemala +America/Guayaquil|America/Guayaquil +America/Guyana|America/Guyana +America/Halifax|America/Halifax +America/Havana|America/Havana +America/Hermosillo|America/Hermosillo +America/Indiana/Indianapolis|America/Indiana/Indianapolis +America/Indiana/Knox|America/Indiana/Knox +America/Indiana/Marengo|America/Indiana/Marengo +America/Indiana/Petersburg|America/Indiana/Petersburg +America/Indiana/Tell_City|America/Indiana/Tell_City +America/Indiana/Vevay|America/Indiana/Vevay +America/Indiana/Vincennes|America/Indiana/Vincennes +America/Indiana/Winamac|America/Indiana/Winamac +America/Inuvik|America/Inuvik +America/Iqaluit|America/Iqaluit +America/Jamaica|America/Jamaica +America/Juneau|America/Juneau +America/Kentucky/Louisville|America/Kentucky/Louisville +America/Kentucky/Monticello|America/Kentucky/Monticello +America/Kralendijk|America/Kralendijk +America/La_Paz|America/La_Paz +America/Lima|America/Lima +America/Los_Angeles|America/Los_Angeles +America/Lower_Princes|America/Lower_Princes +America/Maceio|America/Maceio +America/Managua|America/Managua +America/Manaus|America/Manaus +America/Marigot|America/Marigot +America/Martinique|America/Martinique +America/Matamoros|America/Matamoros +America/Mazatlan|America/Mazatlan +America/Menominee|America/Menominee +America/Merida|America/Merida +America/Metlakatla|America/Metlakatla +America/Mexico_City|America/Mexico_City +America/Miquelon|America/Miquelon +America/Moncton|America/Moncton +America/Monterrey|America/Monterrey +America/Montevideo|America/Montevideo +America/Montserrat|America/Montserrat +America/Nassau|America/Nassau +America/New_York|America/New_York +America/Nipigon|America/Nipigon +America/Nome|America/Nome +America/Noronha|America/Noronha +America/North_Dakota/Beulah|America/North_Dakota/Beulah +America/North_Dakota/Center|America/North_Dakota/Center +America/North_Dakota/New_Salem|America/North_Dakota/New_Salem +America/Ojinaga|America/Ojinaga +America/Panama|America/Panama +America/Pangnirtung|America/Pangnirtung +America/Paramaribo|America/Paramaribo +America/Phoenix|America/Phoenix +America/Port-au-Prince|America/Port-au-Prince +America/Port_of_Spain|America/Port_of_Spain +America/Porto_Velho|America/Porto_Velho +America/Puerto_Rico|America/Puerto_Rico +America/Rainy_River|America/Rainy_River +America/Rankin_Inlet|America/Rankin_Inlet +America/Recife|America/Recife +America/Regina|America/Regina +America/Resolute|America/Resolute +America/Rio_Branco|America/Rio_Branco +America/Santarem|America/Santarem +America/Santiago|America/Santiago +America/Santo_Domingo|America/Santo_Domingo +America/Sao_Paulo|America/Sao_Paulo +America/Scoresbysund|America/Scoresbysund +America/Sitka|America/Sitka +America/St_Barthelemy|America/St_Barthelemy +America/St_Johns|America/St_Johns +America/St_Kitts|America/St_Kitts +America/St_Lucia|America/St_Lucia +America/St_Thomas|America/St_Thomas +America/St_Vincent|America/St_Vincent +America/Swift_Current|America/Swift_Current +America/Tegucigalpa|America/Tegucigalpa +America/Thule|America/Thule +America/Thunder_Bay|America/Thunder_Bay +America/Tijuana|America/Tijuana +America/Toronto|America/Toronto +America/Tortola|America/Tortola +America/Vancouver|America/Vancouver +America/Whitehorse|America/Whitehorse +America/Winnipeg|America/Winnipeg +America/Yakutat|America/Yakutat +America/Yellowknife|America/Yellowknife +Antarctica/Casey|Antarctica/Casey +Antarctica/Davis|Antarctica/Davis +Antarctica/DumontDUrville|Antarctica/DumontDUrville +Antarctica/Macquarie|Antarctica/Macquarie +Antarctica/Mawson|Antarctica/Mawson +Antarctica/McMurdo|Antarctica/McMurdo +Antarctica/Palmer|Antarctica/Palmer +Antarctica/Rothera|Antarctica/Rothera +Antarctica/Syowa|Antarctica/Syowa +Antarctica/Troll|Antarctica/Troll +Antarctica/Vostok|Antarctica/Vostok +Arctic/Longyearbyen|Arctic/Longyearbyen +Asia/Aden|Asia/Aden +Asia/Almaty|Asia/Almaty +Asia/Amman|Asia/Amman +Asia/Anadyr|Asia/Anadyr +Asia/Aqtau|Asia/Aqtau +Asia/Aqtobe|Asia/Aqtobe +Asia/Ashgabat|Asia/Ashgabat +Asia/Atyrau|Asia/Atyrau +Asia/Baghdad|Asia/Baghdad +Asia/Bahrain|Asia/Bahrain +Asia/Baku|Asia/Baku +Asia/Bangkok|Asia/Bangkok +Asia/Barnaul|Asia/Barnaul +Asia/Beirut|Asia/Beirut +Asia/Bishkek|Asia/Bishkek +Asia/Brunei|Asia/Brunei +Asia/Chita|Asia/Chita +Asia/Choibalsan|Asia/Choibalsan +Asia/Colombo|Asia/Colombo +Asia/Damascus|Asia/Damascus +Asia/Dhaka|Asia/Dhaka +Asia/Dili|Asia/Dili +Asia/Dubai|Asia/Dubai +Asia/Dushanbe|Asia/Dushanbe +Asia/Famagusta|Asia/Famagusta +Asia/Gaza|Asia/Gaza +Asia/Hebron|Asia/Hebron +Asia/Ho_Chi_Minh|Asia/Ho_Chi_Minh +Asia/Hong_Kong|Asia/Hong_Kong +Asia/Hovd|Asia/Hovd +Asia/Irkutsk|Asia/Irkutsk +Asia/Jakarta|Asia/Jakarta +Asia/Jayapura|Asia/Jayapura +Asia/Jerusalem|Asia/Jerusalem +Asia/Kabul|Asia/Kabul +Asia/Kamchatka|Asia/Kamchatka +Asia/Karachi|Asia/Karachi +Asia/Kathmandu|Asia/Kathmandu +Asia/Khandyga|Asia/Khandyga +Asia/Kolkata|Asia/Kolkata +Asia/Krasnoyarsk|Asia/Krasnoyarsk +Asia/Kuala_Lumpur|Asia/Kuala_Lumpur +Asia/Kuching|Asia/Kuching +Asia/Kuwait|Asia/Kuwait +Asia/Macau|Asia/Macau +Asia/Magadan|Asia/Magadan +Asia/Makassar|Asia/Makassar +Asia/Manila|Asia/Manila +Asia/Muscat|Asia/Muscat +Asia/Nicosia|Asia/Nicosia +Asia/Novokuznetsk|Asia/Novokuznetsk +Asia/Novosibirsk|Asia/Novosibirsk +Asia/Omsk|Asia/Omsk +Asia/Oral|Asia/Oral +Asia/Phnom_Penh|Asia/Phnom_Penh +Asia/Pontianak|Asia/Pontianak +Asia/Pyongyang|Asia/Pyongyang +Asia/Qatar|Asia/Qatar +Asia/Qyzylorda|Asia/Qyzylorda +Asia/Riyadh|Asia/Riyadh +Asia/Sakhalin|Asia/Sakhalin +Asia/Samarkand|Asia/Samarkand +Asia/Seoul|Asia/Seoul +Asia/Shanghai|Asia/Shanghai +Asia/Singapore|Asia/Singapore +Asia/Srednekolymsk|Asia/Srednekolymsk +Asia/Taipei|Asia/Taipei +Asia/Tashkent|Asia/Tashkent +Asia/Tbilisi|Asia/Tbilisi +Asia/Tehran|Asia/Tehran +Asia/Thimphu|Asia/Thimphu +Asia/Tokyo|Asia/Tokyo +Asia/Tomsk|Asia/Tomsk +Asia/Ulaanbaatar|Asia/Ulaanbaatar +Asia/Urumqi|Asia/Urumqi +Asia/Ust-Nera|Asia/Ust-Nera +Asia/Vientiane|Asia/Vientiane +Asia/Vladivostok|Asia/Vladivostok +Asia/Yakutsk|Asia/Yakutsk +Asia/Yangon|Asia/Yangon +Asia/Yekaterinburg|Asia/Yekaterinburg +Asia/Yerevan|Asia/Yerevan +Atlantic/Azores|Atlantic/Azores +Atlantic/Bermuda|Atlantic/Bermuda +Atlantic/Canary|Atlantic/Canary +Atlantic/Cape_Verde|Atlantic/Cape_Verde +Atlantic/Faroe|Atlantic/Faroe +Atlantic/Madeira|Atlantic/Madeira +Atlantic/Reykjavik|Atlantic/Reykjavik +Atlantic/South_Georgia|Atlantic/South_Georgia +Atlantic/St_Helena|Atlantic/St_Helena +Atlantic/Stanley|Atlantic/Stanley +Australia/Adelaide|Australia/Adelaide +Australia/Brisbane|Australia/Brisbane +Australia/Broken_Hill|Australia/Broken_Hill +Australia/Currie|Australia/Currie +Australia/Darwin|Australia/Darwin +Australia/Eucla|Australia/Eucla +Australia/Hobart|Australia/Hobart +Australia/Lindeman|Australia/Lindeman +Australia/Lord_Howe|Australia/Lord_Howe +Australia/Melbourne|Australia/Melbourne +Australia/Perth|Australia/Perth +Australia/Sydney|Australia/Sydney +Europe/Amsterdam|Europe/Amsterdam +Europe/Andorra|Europe/Andorra +Europe/Astrakhan|Europe/Astrakhan +Europe/Athens|Europe/Athens +Europe/Belgrade|Europe/Belgrade +Europe/Berlin|Europe/Berlin +Europe/Bratislava|Europe/Bratislava +Europe/Brussels|Europe/Brussels +Europe/Bucharest|Europe/Bucharest +Europe/Budapest|Europe/Budapest +Europe/Busingen|Europe/Busingen +Europe/Chisinau|Europe/Chisinau +Europe/Copenhagen|Europe/Copenhagen +Europe/Dublin|Europe/Dublin +Europe/Gibraltar|Europe/Gibraltar +Europe/Guernsey|Europe/Guernsey +Europe/Helsinki|Europe/Helsinki +Europe/Isle_of_Man|Europe/Isle_of_Man +Europe/Istanbul|Europe/Istanbul +Europe/Jersey|Europe/Jersey +Europe/Kaliningrad|Europe/Kaliningrad +Europe/Kiev|Europe/Kiev +Europe/Kirov|Europe/Kirov +Europe/Lisbon|Europe/Lisbon +Europe/Ljubljana|Europe/Ljubljana +Europe/London|Europe/London +Europe/Luxembourg|Europe/Luxembourg +Europe/Madrid|Europe/Madrid +Europe/Malta|Europe/Malta +Europe/Mariehamn|Europe/Mariehamn +Europe/Minsk|Europe/Minsk +Europe/Monaco|Europe/Monaco +Europe/Moscow|Europe/Moscow +Europe/Oslo|Europe/Oslo +Europe/Paris|Europe/Paris +Europe/Podgorica|Europe/Podgorica +Europe/Prague|Europe/Prague +Europe/Riga|Europe/Riga +Europe/Rome|Europe/Rome +Europe/Samara|Europe/Samara +Europe/San_Marino|Europe/San_Marino +Europe/Sarajevo|Europe/Sarajevo +Europe/Saratov|Europe/Saratov +Europe/Simferopol|Europe/Simferopol +Europe/Skopje|Europe/Skopje +Europe/Sofia|Europe/Sofia +Europe/Stockholm|Europe/Stockholm +Europe/Tallinn|Europe/Tallinn +Europe/Tirane|Europe/Tirane +Europe/Ulyanovsk|Europe/Ulyanovsk +Europe/Uzhgorod|Europe/Uzhgorod +Europe/Vaduz|Europe/Vaduz +Europe/Vatican|Europe/Vatican +Europe/Vienna|Europe/Vienna +Europe/Vilnius|Europe/Vilnius +Europe/Volgograd|Europe/Volgograd +Europe/Warsaw|Europe/Warsaw +Europe/Zagreb|Europe/Zagreb +Europe/Zaporozhye|Europe/Zaporozhye +Europe/Zurich|Europe/Zurich +Indian/Antananarivo|Indian/Antananarivo +Indian/Chagos|Indian/Chagos +Indian/Christmas|Indian/Christmas +Indian/Cocos|Indian/Cocos +Indian/Comoro|Indian/Comoro +Indian/Kerguelen|Indian/Kerguelen +Indian/Mahe|Indian/Mahe +Indian/Maldives|Indian/Maldives +Indian/Mauritius|Indian/Mauritius +Indian/Mayotte|Indian/Mayotte +Indian/Reunion|Indian/Reunion +Pacific/Apia|Pacific/Apia +Pacific/Auckland|Pacific/Auckland +Pacific/Bougainville|Pacific/Bougainville +Pacific/Chatham|Pacific/Chatham +Pacific/Chuuk|Pacific/Chuuk +Pacific/Easter|Pacific/Easter +Pacific/Efate|Pacific/Efate +Pacific/Enderbury|Pacific/Enderbury +Pacific/Fakaofo|Pacific/Fakaofo +Pacific/Fiji|Pacific/Fiji +Pacific/Funafuti|Pacific/Funafuti +Pacific/Galapagos|Pacific/Galapagos +Pacific/Gambier|Pacific/Gambier +Pacific/Guadalcanal|Pacific/Guadalcanal +Pacific/Guam|Pacific/Guam +Pacific/Honolulu|Pacific/Honolulu +Pacific/Johnston|Pacific/Johnston +Pacific/Kiritimati|Pacific/Kiritimati +Pacific/Kosrae|Pacific/Kosrae +Pacific/Kwajalein|Pacific/Kwajalein +Pacific/Majuro|Pacific/Majuro +Pacific/Marquesas|Pacific/Marquesas +Pacific/Midway|Pacific/Midway +Pacific/Nauru|Pacific/Nauru +Pacific/Niue|Pacific/Niue +Pacific/Norfolk|Pacific/Norfolk +Pacific/Noumea|Pacific/Noumea +Pacific/Pago_Pago|Pacific/Pago_Pago +Pacific/Palau|Pacific/Palau +Pacific/Pitcairn|Pacific/Pitcairn +Pacific/Pohnpei|Pacific/Pohnpei +Pacific/Port_Moresby|Pacific/Port_Moresby +Pacific/Rarotonga|Pacific/Rarotonga +Pacific/Saipan|Pacific/Saipan +Pacific/Tahiti|Pacific/Tahiti +Pacific/Tarawa|Pacific/Tarawa +Pacific/Tongatapu|Pacific/Tongatapu +Pacific/Wake|Pacific/Wake +Pacific/Wallis|Pacific/Wallis" + } + }, + { + "Id": "e6b909fa-80f5-440c-9c9e-6851455dd3ff", + "Name": "StatusCake.Length", + "Label": "Length", + "HelpText": "Amount of time in minutes that the maintenance window should remain active.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-03-15T06:00:28.045Z", + "OctopusVersion": "2020.6.4688", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "svenkle", + "Category": "statuscake" +} diff --git a/step-templates/statuspageio-create-scheduled-maintenance-incident.json.human b/step-templates/statuspageio-create-scheduled-maintenance-incident.json.human new file mode 100644 index 000000000..192ec2542 --- /dev/null +++ b/step-templates/statuspageio-create-scheduled-maintenance-incident.json.human @@ -0,0 +1,160 @@ +{ + "Id": "5f8dbdd9-e309-4aa7-b1d3-b090876a959a", + "Name": "StatusPage.io - Create Scheduled Maintenance Incident", + "Description": "Creates or updates a scheduled maintenance incident on StatusPage.io", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "## --------------------------------------------------------------------------------------\r +## Input\r +## --------------------------------------------------------------------------------------\r +$pageId = $OctopusParameters['PageId']\r +$apiKey = $OctopusParameters['ApiKey']\r +$incidentName = $OctopusParameters['IncidentName']\r +$incidentStatus = $OctopusParameters['IncidentStatus']\r +$incidentMessage = $OctopusParameters['IncidentMessage']\r +$componentId = $OctopusParameters['ComponentId']\r +\r +function Validate-Parameter($parameterValue, $parameterName) {\r + if(!$parameterName -contains \"Key\") {\r + Write-Host \"${parameterName}: ${parameterValue}\"\r + }\r +\r + if (! $parameterValue) {\r + throw \"$parameterName cannot be empty, please specify a value\"\r + }\r +}\r +\r +function New-ScheduledIncident\r +{\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory=$true)]\r + [string]$PageId,\r +\r + [Parameter(Mandatory=$true)]\r + [string]$ApiKey,\r +\r + [Parameter(Mandatory=$true)]\r + [string]$Name,\r +\r + [Parameter(Mandatory=$true)]\r + [ValidateSet(\"scheduled\", \"in_progress\", \"verifying\", \"completed\")]\r + [string]$Status,\r + \r + [Parameter(Mandatory=$false)]\r + [string]$Message,\r +\r + [Parameter(Mandatory=$false)]\r + [string]$Componentid\r + )\r +\r + $date = [System.DateTime]::Now.ToString(\"o\")\r + $url = \"https://api.statuspage.io/v1/pages/$PageId/incidents.json\"\r + $headers = @{\"Authorization\"=\"OAuth $ApiKey\"}\r + $body = \"incident[name]=$Name&incident[status]=$Status&incident[scheduled_for]=$date&incident[scheduled_until]=$date\"\r +\r + if($Message)\r + {\r + $body += \"&incident[message]=$Message\"\r + }\r +\r + if($Componentid)\r + {\r + $body += \"&incident[component_ids][]=$Componentid\"\r + }\r +\r + $response = iwr -UseBasicParsing -Uri $url -Headers $headers -Method POST -Body $body\r + $content = ConvertFrom-Json $response\r + $content.id\r +}\r +\r +Validate-Parameter $pageId -parameterName 'PageId'\r +Validate-Parameter $apiKey = -parameterName 'ApiKey'\r +Validate-Parameter $incidentName = -parameterName 'IncidentName'\r +Validate-Parameter $incidentStatus -parameterName 'IncidentStatus'\r +\r +Write-Output \"Creating new scheduled maintenance incident `\"$incidentName`\" ...\"\r +New-ScheduledIncident -PageId $pageId -ApiKey $apiKey -Name $incidentName -Status $incidentStatus -Message $incidentMessage -ComponentId $componentId\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "587dd594-5141-4c84-b204-0803935e2a5e", + "Name": "IncidentName", + "Label": "Scheduled Maintenance Name", + "HelpText": "The name of the scheduled maintenance incident.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "a6de9c6a-4777-413e-a679-75af62846a95", + "Name": "IncidentStatus", + "Label": "Status", + "HelpText": "The status of the incident, one of scheduled|in_progress|verifying|completed", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "scheduled|Scheduled +in_progress|In Progress +verifying|Verifying +completed|Completed" + } + }, + { + "Id": "49c1d937-7632-46f3-afc5-376d893deff9", + "Name": "IncidentMessage", + "Label": "Message", + "HelpText": "Optional message to add to scheduled maintenance incident.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b9a01157-aa89-4ae1-95ee-5b05b53b042a", + "Name": "ComponentId", + "Label": "ComponentId", + "HelpText": "Optional component id to reference for the scheduled maintenance incident. Talk to your StatusPage.io administrator for details.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "29694b56-739a-4623-9040-0b2865fb15a4", + "Name": "PageId", + "Label": "StatusPage.io Page Id", + "HelpText": "StatusPage.io Organization or \"Page ID\". Visit the [API authentication docs](http://doers.statuspage.io/api/authentication/) for details.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c7d00d1c-e963-4be5-b088-83a945ed4540", + "Name": "ApiKey", + "Label": "StatusPage.io API Key", + "HelpText": "StatusPage.io API key for the organization. Visit the [API Authentication docs](http://doers.statuspage.io/api/authentication/) for details.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedBy": "nshenoy", + "$Meta": { + "ExportedAt": "2016-10-26T18:49:13.955+00:00", + "OctopusVersion": "3.4.10", + "Type": "ActionTemplate" + }, + "Category": "statusPage" +} diff --git a/step-templates/statuspageio-update-scheduled-maintenance-incident.json.human b/step-templates/statuspageio-update-scheduled-maintenance-incident.json.human new file mode 100644 index 000000000..00b3ebc8e --- /dev/null +++ b/step-templates/statuspageio-update-scheduled-maintenance-incident.json.human @@ -0,0 +1,163 @@ +{ + "Id": "7bc83a07-8927-467e-a2eb-922609212e3a", + "Name": "StatusPage.io - Update Scheduled Maintenance Incident", + "Description": "Updates an existing scheduled maintenance incident on StatusPage.io", + "ActionType": "Octopus.Script", + "Version": 0, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "## --------------------------------------------------------------------------------------\r +## Input\r +## --------------------------------------------------------------------------------------\r +$pageId = $OctopusParameters['PageId']\r +$apiKey = $OctopusParameters['ApiKey']\r +$incidentName = $OctopusParameters['IncidentName']\r +$incidentStatus = $OctopusParameters['IncidentStatus']\r +$incidentMessage = $OctopusParameters['IncidentMessage']\r +\r +function Validate-Parameter($parameterValue, $parameterName) {\r + if(!$parameterName -contains \"Key\") {\r + Write-Host \"${parameterName}: ${parameterValue}\"\r + }\r +\r + if (! $parameterValue) {\r + throw \"$parameterName cannot be empty, please specify a value\"\r + }\r +}\r +\r +function Get-InProgressScheduledIncidentByName\r +{\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory=$true)]\r + [string]$PageId,\r +\r + [Parameter(Mandatory=$true)]\r + [string]$ApiKey,\r +\r + [Parameter(Mandatory=$true)]\r + [string]$Name\r + )\r +\r + $url = \"https://api.statuspage.io/v1/pages/$PageId/incidents/unresolved.json\"\r + $headers = @{\"Authorization\"=\"OAuth $ApiKey\"}\r +\r + $response = iwr -UseBasicParsing -Uri $url -Headers $headers -Method GET\r + $content = ConvertFrom-Json $response\r + $incident = $content | where {$_.name -eq $Name}\r + $incident.id\r +}\r +\r +function Update-ScheduledIncident\r +{\r + [CmdletBinding()]\r + Param(\r + [Parameter(Mandatory=$true)]\r + [string]$PageId,\r +\r + [Parameter(Mandatory=$true)]\r + [string]$ApiKey,\r +\r + [Parameter(Mandatory=$true)]\r + [string]$IncidentId,\r +\r + [Parameter(Mandatory=$true)]\r + [ValidateSet(\"scheduled\", \"in_progress\", \"verifying\", \"completed\")]\r + [string]$Status,\r + \r + [Parameter(Mandatory=$false)]\r + [string]$Message\r + )\r +\r + $url = \"https://api.statuspage.io/v1/pages/$PageId/incidents/$IncidentId.json\"\r + $headers = @{\"Authorization\"=\"OAuth $ApiKey\"}\r + $body = \"incident[status]=$Status\"\r +\r + if($Message)\r + {\r + $body += \"&incident[message]=$Message\"\r + }\r +\r + $response = iwr -UseBasicParsing -Uri $url -Headers $headers -Method PATCH -Body $body -ContentType application/x-www-form-urlencoded\r +}\r +\r +Validate-Parameter $pageId -parameterName 'PageId'\r +Validate-Parameter $apiKey = -parameterName 'ApiKey'\r +Validate-Parameter $incidentName = -parameterName 'IncidentName'\r +Validate-Parameter $incidentStatus -parameterName 'IncidentStatus'\r +\r +$incidentId = Get-InProgressScheduledIncidentByName -PageId $pageId -ApiKey $apiKey -Name $incidentName\r +Write-Verbose \"Found incident id: $incidentId\"\r +Write-Output \"Updating scheduled maintenance incident `\"$incidentName`\" [IncidentId: $incidentId]\"\r +Update-ScheduledIncident -PageId $pageId -ApiKey $apiKey -IncidentId $incidentId -Status $incidentStatus -Message $incidentMessage \r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "b1044b12-8cdc-4b55-b429-a08711a66339", + "Name": "IncidentName", + "Label": "Scheduled Maintenance Name", + "HelpText": "The name of the scheduled maintenance incident.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "6d9277b0-3cab-43ea-9946-d772c9d3aebf", + "Name": "IncidentStatus", + "Label": "Status", + "HelpText": "The status of the incident, one of scheduled|in_progress|verifying|completed", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "scheduled|Scheduled +in_progress|In Progress +verifying|Verifying +completed|Completed" + } + }, + { + "Id": "a2121f55-09ea-4200-ae4f-0131f7db06b1", + "Name": "IncidentMessage", + "Label": "Message", + "HelpText": "Optional message to add to scheduled maintenance incident.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d0504e1f-150d-46b2-8127-449a708e55c6", + "Name": "PageId", + "Label": "StatusPage.io Page Id", + "HelpText": "StatusPage.io Organization or \"Page ID\". Visit the [API authentication docs](http://doers.statuspage.io/api/authentication/) for details.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "48c90b4b-d9ca-419a-8c8f-98d301db2015", + "Name": "ApiKey", + "Label": "StatusPage.io API Key", + "HelpText": "StatusPage.io API key for the organization. Visit the [API Authentication docs](http://doers.statuspage.io/api/authentication/) for details.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedBy": "nshenoy", + "$Meta": { + "ExportedAt": "2016-10-26T18:51:37.967+00:00", + "OctopusVersion": "3.4.10", + "Type": "ActionTemplate" + }, + "Category": "statusPage" +} diff --git a/step-templates/swaggerhub-post-api.json.human b/step-templates/swaggerhub-post-api.json.human new file mode 100644 index 000000000..a0e876656 --- /dev/null +++ b/step-templates/swaggerhub-post-api.json.human @@ -0,0 +1,189 @@ +{ + "Id": "0f60f13c-bc00-4035-a0ba-5aeef6a2c1e5", + "Author": "waxtell", + "Name": "SwaggerHub Post Api", + "Description": "Post your Swagger/OAS to SwaggerHub", + "ActionType": "Octopus.Script", + "Version": 1, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "function Coalesce($a, $b) { + if ($null -ne $a) { + $a + } else { + $b + } +} + +function Validate([string]$parameterValue, [string[]]$validInput, $parameterName) { + Write-Host \"${parameterName}: $parameterValue\" + + if (!$parameterValue) { + throw \"Parameter $parameterName is required!\" + } + + if ($validInput) { + if (! $validInput -contains $parameterValue) { + throw \"'$input' is not a valid value for '$parameterName'\" + } + } +} + +$apiKey = $OctopusParameters['shpApiKey'] +$owner = $OctopusParameters['shpOwner'] +$apiName = $OctopusParameters['shpApi'] +$definition = $OctopusParameters['shpDefinition'] +$contentType = Coalesce $OctopusParameters['shpContentType'] \"application/json\" +$oas = Coalesce $OctopusParameters['shpOas'] \"2.0\" +$isPrivate = (Coalesce $OctopusParameters['shpIsPrivate'] \"False\").ToLower() +$force = (Coalesce $OctopusParameters['shpForce'] \"False\").ToLower() +$version = $OctopusParameters['shpVersion'] + +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 + +Validate $apiKey -parameterName \"Api Key\" +Validate $owner -parameterName \"Owner\" +Validate $apiName -parameterName \"Api Name\" +Validate $definition -parameterName \"Definition\" + +try { + Write-Host \"Updating $($apiName)...\" + + $headers = @{ + 'Authorization' = $apiKey + 'Accept' = 'application/json' + } + + $query = \"https://api.swaggerhub.com/apis/$($owner)/$($apiName)?isPrivate=$($isPrivate.ToLower())&oas=$($oas)&force=$($force.ToLower())\" + + if($version) { + $query = $query+\"&version=$($version)\" + } + + $specification = $definition + + # If $definition contains a file path, load the content of the provided value + if((Test-Path $definition -ErrorAction SilentlyContinue)[0]) { + $specification = get-content $definition + } + + Invoke-RestMethod $query -Headers $headers -ContentType $contentType -Method Post -Body $specification + + Write-Host \"SwaggerHub post successful\" +} catch { + Write-Host $_.Exception.Message + Write-Host \"SwaggerHub post failed!\" + Write-Host \" HttpStatus: $($_.Exception.Response.StatusCode.value__)\" + + exit 1 +} +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "c71e96a7-5e25-4624-9e57-976cc18eb348", + "Name": "shpApiKey", + "Label": "SwaggerHub API Key", + "HelpText": "Your SwaggerHub API key", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "7047922F-8AA3-476F-B2E6-89E9C6F0DB3F", + "Name": "shpOwner", + "Label": "Owner", + "HelpText": "API owner (user or organization, case-sensitive)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "BBE8AB7A-2BFE-4413-ACFF-384F6B6F953C", + "Name": "shpApi", + "Label": "Api Name", + "HelpText": "API name (case-sensitive)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "409EAF93-FF98-4430-89CD-19C30E1CC792", + "Name": "shpIsPrivate", + "Label": "Is Private", + "HelpText": "Defines whether the API has to be private", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "3643C071-B51F-4611-900C-FAB22B4C5698", + "Name": "shpVersion", + "Label": "Version", + "HelpText": "API version", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "25550589-4208-476C-B581-20EF0A4F1453", + "Name": "shpOas", + "Label": "OAS Version", + "HelpText": "The OpenApi Specification (OAS) version.", + "DefaultValue": "2.0", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "2.0|2.0 +3.0.0|3.0.0 +3.0.1|3.0.1" + } + }, + { + "Id": "AF1EF8F1-A829-4D40-86B1-9D18D909037E", + "Name": "shpDefinition", + "Label": "Definition", + "HelpText": "The Swagger definition, or the path to the file that contains the definition, of this API", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "F99E5B74-55BF-4C91-83E6-E6AC17284F4C", + "Name": "shpContentType", + "Label": "Definition Content Type", + "HelpText": "Definition Content Type", + "DefaultValue": "application/json", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "application/json|application/json +application/yaml|application/yaml" + } + }, + { + "Id": "3790F1C7-3FA8-4908-B4C3-F96DCD02B96D", + "Name": "shpForce", + "Label": "Force", + "HelpText": "Force update", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedBy": "waxtell", + "$Meta": { + "ExportedAt": "2020-01-29T17:02:59.155Z", + "OctopusVersion": "2019.12.0", + "Type": "ActionTemplate" + }, + "Category": "SwaggerHub" + } diff --git a/step-templates/teamcity-download-artifact.json.human b/step-templates/teamcity-download-artifact.json.human new file mode 100644 index 000000000..267080c46 --- /dev/null +++ b/step-templates/teamcity-download-artifact.json.human @@ -0,0 +1,103 @@ +{ + "Id": "55a172cc-de3d-45f9-90b1-be51042754b0", + "Name": "TeamCity - Download Artifact", + "Description": "Downloads the TeamCity artifact from the most recent build of the specified branch.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Expected parameters:\r +# $TeamCityHost - The domain name and optional port (teamcity.mycompany.com:8080) of the TeamCity build server.\r +# $TeamCityUsername - The TeamCity username.\r +# $TeamCityPassword - The TeamCity password.\r +# $BuildType - The unique identifier of the TeamCity build configuration.\r +# $BranchName - The name of the branch.\r +# $ArtifactName - The filename of the artifact.\r +# $OutputLocation - The name of the folder where the artifact will be downloaded.\r +\r +$secure_password = ConvertTo-SecureString $TeamCityPassword -AsPlainText -Force\r +$credential = New-Object System.Management.Automation.PSCredential($TeamCityUsername, $secure_password)\r +\r +$resource_identifier = \"buildType:$BuildType,branch:$BranchName\"\r +\r +$source = \"http://$TeamCityHost/httpAuth/app/rest/builds/$resource_identifier/artifacts/content/$ArtifactName\"\r +$destination = \"$OutputLocation\\$Artifactname\"\r +\r +Invoke-WebRequest $source -OutFile $destination -Credential $credential", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "TeamCityHost", + "Label": "TeamCity Host", + "HelpText": "The domain name and optional port of the TeamCity build server. +(ex. teamcity.mycompany.com:8080)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityUsername", + "Label": "TeamCity Username", + "HelpText": "The TeamCity username that is used to authenticate the request. This user must have permissions to download the specified artifact.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityPassword", + "Label": "TeamCity Password", + "HelpText": "The TeamCity password that is used to authenticate the request.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BuildType", + "Label": "Build Type ID", + "HelpText": "The unique identifier of the TeamCity build configuration.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BranchName", + "Label": "Branch Name", + "HelpText": "The name of the branch whose artifact is being download.", + "DefaultValue": "master", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ArtifactName", + "Label": "Artifact Name", + "HelpText": "The filename of the artifact being downloaded.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "OutputLocation", + "Label": "Output Location", + "HelpText": "The name of the folder where the artifact will be downloaded. The resulting file will match the filename of the artifact being downloaded.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-01-23T04:15:51.361+00:00", + "OctopusVersion": "2.6.0.778", + "Type": "ActionTemplate" + }, + "Category": "teamcity" +} diff --git a/step-templates/teamcity-pin-build-and-dependencies.json.human b/step-templates/teamcity-pin-build-and-dependencies.json.human new file mode 100644 index 000000000..4c7b82964 --- /dev/null +++ b/step-templates/teamcity-pin-build-and-dependencies.json.human @@ -0,0 +1,120 @@ +{ + "Id": "35d1aae8-950a-466e-99f7-afd8fa9d5dff", + "Name": "Pin TeamCity Build Version and Dependencies", + "Description": "Try to pin the TeamCity build version and dependencies +(Requires Octopus version to match TeamCity version)", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$buildNumber = $OctopusParameters['buildNumber'] +$buildTypeId = $OctopusParameters['buildTypeId'] + +$tcUrl = $OctopusParameters['TeamCityUrl'] +$tcUser = $OctopusParameters['TeamCityUser'] +$tcPass = $OctopusParameters['TeamCityPassword'] +$tcComments = $OctopusParameters['TeamCityPinComment'] +$tcTags = $OctopusParameters['TeamCityTags'] + +$credentials = [System.Text.Encoding]::UTF8.GetBytes(\"$($tcUser):$($tcPass)\") +$headers = @{ \"Authorization\" = \"Basic $([System.Convert]::ToBase64String($credentials))\" } + +[string]$restUri = $tcUrl + (\"/httpAuth/app/rest/builds/?locator=buildType:{1},branch:default:any,number:{0}\" -f $buildNumber,$buildTypeId) + +$response = Invoke-RestMethod -Headers $headers -DisableKeepAlive -Method GET -Uri $restUri + +if ($response -ne $null -and $response.builds.count -eq 1) { + $id = $response.builds.build.id + + [string]$pinUri = $tcUrl + (\"/ajax.html?pinComment={1}&pin=true&buildId={0}&buildTagsInfo={2}&applyToChainBuilds=true\" -f $id,$tcComments,$tcTags) + + Write-Output \"Pinning Build with ID $($id)\" + + try { + Invoke-RestMethod -Headers $headers -DisableKeepAlive -Method POST -Uri $pinUri + Write-Output \"Build ID $($id) pinned successfully\" + } catch { + Write-Output \"Build ID $($id) not pinned: $($_.Exception.Message)\" + } +} else { + Write-Warning \"Build not found, unable to pin\" +} +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "buildNumber", + "Label": "Build Number", + "HelpText": null, + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "buildTypeId", + "Label": "Build Configuration ID", + "HelpText": "The build configuration id to look for the build to pin. + +General Settings of the Build Configuration", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityUrl", + "Label": "Url of TeamCity Server", + "HelpText": "The url to the TeamCity server.", + "DefaultValue": "http://localhost:8082", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityUser", + "Label": "TeamCity User", + "HelpText": "The TeamCity user used for pinning the build", + "DefaultValue": "teamcity", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityPassword", + "Label": "TeamCity User Password", + "HelpText": "The password for the TeamCity user.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "TeamCityPinComment", + "Label": "TeamCity Pin Comment", + "HelpText": "Comments for the TeamCity Pin", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityTags", + "Label": "TeamCity Tags", + "HelpText": "Tags to add to the TeamCity Build, space separated list of values.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2016-05-17T09:18:00.000+00:00", + "LastModifiedBy": "roberto-mardeni", + "$Meta": { + "ExportedAt": "2016-05-06T17:36:39.661+00:00", + "OctopusVersion": "3.2.19", + "Type": "ActionTemplate" + }, + "Category": "teamcity" +} diff --git a/step-templates/teamcity-run-build.json.human b/step-templates/teamcity-run-build.json.human new file mode 100644 index 000000000..1529b637e --- /dev/null +++ b/step-templates/teamcity-run-build.json.human @@ -0,0 +1,176 @@ +{ + "Id": "a7fa3e51-14aa-4bb9-8686-781adc9bf93e", + "Name": "TeamCity - Run Build", + "Description": "Trigger a specific Team City build from an Octopus Deploy process and wait for the result. The step will fail if the build fails.", + "ActionType": "Octopus.Script", + "Version": 3, + "Packages": [], + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Set TLS 1.2 +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +$teamCityBuildConfigId = $OctopusParameters['tcrb_TeamCityBuildConfigurationId']\r +$teamCityUrl = $OctopusParameters['tcrb_TeamCityUrl']\r +$teamCityUsername = $OctopusParameters['tcrb_TeamCityUsername']\r +$teamCityPassword = $OctopusParameters['tcrb_TeamCityPassword']\r +$teamCityInterval = [int]::Parse($OctopusParameters['tcrb_TeamCityInterval'])\r +$teamCityBuildParams = $OctopusParameters['tcrb_BuildParams']\r +\r +function Start-TeamCityBuild($Url, $Username, $Password, $BuildConfigId, $BuildParams) {\r + $endpoint = \"${Url}/httpAuth/app/rest/buildQueue\"\r + $content = \"\"\r + if (-not [String]::IsNullOrEmpty($BuildParams)) {\r + foreach ($param in (ConvertFrom-Csv -Delimiter '=' -Header Name,Value -InputObject $BuildParams)) {\r + $name = $param.Name.Replace('\"', '"')\r + $value = $param.Value.Replace('\"', '"')\r + $content += \"\"\r + }\r + }\r + $content += \"\" \r + $encodedContent = [System.Text.Encoding]::UTF8.GetBytes($content)\r +\r + Write-Host \"Triggering build with Id ${BuildConfigId} in TeamCity. Server: ${Url}\"\r +\r + $req = [System.Net.WebRequest]::Create($endpoint)\r + $req.Credentials = New-Object System.Net.NetworkCredential($Username, $Password)\r + $req.Method = \"POST\"\r + $req.ContentType = \"application/xml\"\r +\r + $req.ContentLength = $encodedContent.length\r + $requestStream = $req.GetRequestStream()\r + $requestStream.Write($encodedContent, 0, $encodedContent.length)\r + $requestStream.Close()\r +\r + $resp = $req.GetResponse()\r + $reader = New-Object System.IO.StreamReader($resp.GetResponseStream())\r + $result = [xml]$reader.ReadToEnd()\r + $buildUrl = $result.build.webUrl\r +\r + Write-Host $buildUrl\r + Write-Host \"================================================================================\"\r +\r + return $result\r +}\r +\r +function Get-TeamCityBuildState($Url, $Username, $Password, $BuildInfo) {\r + $href = $BuildInfo.href\r + $buildId = $BuildInfo.id\r + $endpoint = \"${Url}${href}\"\r +\r + Write-Host \"Getting state of build ${buildId} in TeamCity. Server: ${Url}\"\r +\r + $req = [System.Net.WebRequest]::Create($endpoint)\r + $req.Credentials = New-Object System.Net.NetworkCredential($Username, $Password)\r + $req.Method = \"GET\"\r +\r + $resp = $req.GetResponse()\r + $reader = New-Object System.IO.StreamReader($resp.GetResponseStream())\r + return [xml]$reader.ReadToEnd();\r +}\r +\r +function Invoke-TeamCityBuild ($Url, $Username, $Password, $BuildConfigId, $Interval, $BuildParams) {\r + $build = Start-TeamCityBuild -Url $Url -Username $Username -Password $Password -BuildConfigId $BuildConfigId -BuildParams $teamCityBuildParams\r + $buildInfo = $build.build\r +\r + while ($true) {\r + $buildState = Get-TeamCityBuildState -Url $teamCityUrl -Username $teamCityUsername -Password $teamCityPassword -BuildInfo $buildInfo\r + Write-Host $buildState.build.state\r + if ($buildState.build.state -eq 'finished') {\r + return $buildState.build\r + }\r + \r + Start-Sleep -Seconds $Interval\r + }\r +}\r +\r +$buildResult = Invoke-TeamCityBuild -Url $teamCityUrl -Username $teamCityUsername -Password $teamCityPassword -BuildConfigId $teamCityBuildConfigId -Interval $teamCityInterval -BuildParams $teamCityBuildParams\r +$message = $buildResult.statusText\r +Write-Host \"================================================================================\"\r +Write-Host $buildResult.webUrl\r +if ($buildResult.status -eq 'FAILURE') {\r + Write-Host \"Build failed: ${message}\"\r + exit 1\r +}\r +elseif ($message -eq 'Canceled') {\r + Write-Host \"Build canceled: ${message}\"\r + exit 2\r +}\r +else {\r + Write-Host \"Build successful: ${message}\"\r + exit 0\r +}\r +", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptFileName": null + }, + "Parameters": [ + { + "Name": "tcrb_TeamCityBuildConfigurationId", + "Label": "BuildConfigurationId", + "HelpText": "The Id of the build configuration to trigger.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "tcrb_TeamCityUrl", + "Label": "TeamCityUrl", + "HelpText": "The URL of the Team City server. +E.g. `http://teamcity.example.com`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "tcrb_TeamCityUsername", + "Label": "TeamCityUsername", + "HelpText": "The username to use for accessing TeamCity.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "tcrb_TeamCityPassword", + "Label": "TeamCityPassword", + "HelpText": "The password for the TeamCity user.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "tcrb_TeamCityInterval", + "Label": "TeamCityInterval", + "HelpText": "Number of seconds to wait between each check of the build's state.", + "DefaultValue": "5", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "tcrb_BuildParams", + "Label": "BuildParameters", + "HelpText": "Line-delimited list of parameters to add to the build in the form = + and can be contained in quotes. +E.g. +param1=param_value1 +env.param2=\"env_param2\" +system.param3=sys_param2", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-11-03T09:42:34.506Z", + "OctopusVersion": "2020.4.10", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "teamcity" +} diff --git a/step-templates/teamcity-tag-build.json.human b/step-templates/teamcity-tag-build.json.human new file mode 100644 index 000000000..9937bdc14 --- /dev/null +++ b/step-templates/teamcity-tag-build.json.human @@ -0,0 +1,117 @@ +{ + "Id": "aabf7d20-579f-4e6f-af5f-14e0d87b1258", + "Name": "Tag TeamCity Build Version", + "Description": "Try to tag the TeamCity build version and dependencies +(Requires Octopus version to match TeamCity version)", + "ActionType": "Octopus.Script", + "Author": "BOR1K", + "Version": 2, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$buildNumber = $OctopusParameters['buildNumber'] +$buildTypeId = $OctopusParameters['buildTypeId'] + +$tcUrl = $OctopusParameters['TeamCityUrl'] +$tcUser = $OctopusParameters['TeamCityUser'] +$tcPass = $OctopusParameters['TeamCityPassword'] +$tcTags = $OctopusParameters['TeamCityTags'] + +$credentials = [System.Text.Encoding]::UTF8.GetBytes(\"$($tcUser):$($tcPass)\") +$headers = @{ \"Authorization\" = \"Basic $([System.Convert]::ToBase64String($credentials))\" } + +[string]$tagUri = $tcUrl + (\"/app/rest/builds/buildType:{0},number:{1}/tags/\" -f $buildTypeId,$buildNumber) + +Write-Output \"Tagging Build with ID $($id)\" + +try { + Invoke-RestMethod -Headers $headers -DisableKeepAlive -Method POST -Uri $tagUri -Body $tcTags -ContentType \"text/plain\" + Write-Output \"Build ID $($id) tagged successfully\" +} catch { + Write-Output \"Build ID $($id) not tagged: $($_.Exception.Message)\" +} + +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "5fa8e5cb-0b93-4d23-8ef3-322b2bbb5e4c", + "Name": "buildNumber", + "Label": "Build Number", + "HelpText": null, + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "6086a3f2-ed02-4066-a497-1b0066178ed0", + "Name": "buildTypeId", + "Label": "Build Configuration ID", + "HelpText": "The build configuration id to look for the build to pin. + +General Settings of the Build Configuration", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "2b248891-64ec-48ae-94ed-b0e2d32b4ade", + "Name": "TeamCityUrl", + "Label": "Url of TeamCity Server", + "HelpText": "The url to the TeamCity server.", + "DefaultValue": "http://localhost:88", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f736c827-0429-4db5-9f9e-ddfa97d364a8", + "Name": "TeamCityUser", + "Label": "TeamCity User", + "HelpText": "The TeamCity user used for pinning the build", + "DefaultValue": "teamcity", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "202383fe-c464-4d5c-bad5-7069ee8037fd", + "Name": "TeamCityPassword", + "Label": "TeamCity User Password", + "HelpText": "The password for the TeamCity user.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "518b4bdf-84e6-4abd-b88a-c1aa457364e0", + "Name": "TeamCityTags", + "Label": "TeamCity Tags", + "HelpText": "Tags to add to the TeamCity Build, space separated list of values.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "B0R1K", + "$Meta": { + "ExportedAt": "2018-01-19T17:39:44.741Z", + "OctopusVersion": "3.12.0", + "Type": "ActionTemplate" + } +} diff --git a/step-templates/teamcity-trigger-build.json.human b/step-templates/teamcity-trigger-build.json.human new file mode 100644 index 000000000..2f8bd937b --- /dev/null +++ b/step-templates/teamcity-trigger-build.json.human @@ -0,0 +1,87 @@ +{ + "Id": "22483ce2-1444-4c02-861c-fb9533959e16", + "Name": "Trigger TeamCity Build", + "Description": "Trigger a specific Team City build from an Octopus Deploy process.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$buildConfId = $OctopusParameters['BuildConfigurationId']\r +\r +$teamCityUrl = $OctopusParameters['TeamCityUrl']\r +$teamCityUsername = $OctopusParameters['TeamCityUsername']\r +$teamCityPassword = $OctopusParameters['TeamCityPassword']\r +\r +$url = $teamCityUrl + '/httpAuth/app/rest/buildQueue'\r +$contentTemplate = ''\r +$content = $contentTemplate -f $buildConfId\r +$encodedContent = [System.Text.Encoding]::UTF8.GetBytes($content)\r +\r +Write-Host \"================================================================================\"\r +Write-Host \"Triggering build with Id $buildConfId in TeamCity. Server:\" $teamCityUrl \r +Write-Host \"================================================================================\"\r +\r +$req = [System.Net.WebRequest]::Create($url)\r +$req.Credentials = new-object System.Net.NetworkCredential($teamCityUsername, $teamCityPassword)\r +$req.Method =\"POST\"\r +$req.ContentType = \"application/xml\"\r +\r +$req.ContentLength = $encodedContent.length\r +$requestStream = $req.GetRequestStream()\r +$requestStream.Write($encodedContent, 0, $encodedContent.length)\r +$requestStream.Close()\r +\r +$resp = $req.GetResponse()\r +$reader = new-object System.IO.StreamReader($resp.GetResponseStream())\r +$reader.ReadToEnd() | Write-Host\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "BuildConfigurationId", + "Label": null, + "HelpText": "The Id of the build configuration to trigger.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityUrl", + "Label": null, + "HelpText": "The URL of the Team City server. +E.g. `http://teamcity.example.com`.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityUsername", + "Label": null, + "HelpText": "The username to use for accessing TeamCity.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TeamCityPassword", + "Label": null, + "HelpText": "The password for the TeamCity user.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "LastModifiedOn": "2015-03-16T10:45:58.588+00:00", + "LastModifiedBy": "bjorgvino", + "$Meta": { + "ExportedAt": "2015-03-16T10:47:31.328+00:00", + "OctopusVersion": "2.6.3.886", + "Type": "ActionTemplate" + }, + "Category": "teamcity" +} diff --git a/step-templates/terraform-custom-script.json.human b/step-templates/terraform-custom-script.json.human new file mode 100644 index 000000000..94b937a8c --- /dev/null +++ b/step-templates/terraform-custom-script.json.human @@ -0,0 +1,230 @@ +{ + "Id": "1d2a62d0-1f12-4417-8566-4ca1b8e6a69c", + "Name": "Execute Custom Terraform Script with Package", + "Description": "Run a custom terraform script using a package and an Azure Account. + +E.g. run \"terraform taint some-resource\" in the context of your terraform package files and authenticated against Azure using credentials in an Octopus Azure Account variable. This step operates very similarly to the built-in Terraform Apply step but allowing you the control to call terraform commands rather than using the octopus runner. Very useful for runbooks doing terraform import.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [ + { + "Name": "InfrastructurePackage", + "Id": "582b5176-aa5d-4166-a5fa-9b760a3b2d7d", + "PackageId": null, + "FeedId": null, + "AcquisitionLocation": "Server", + "Properties": { + "Extract": "True", + "SelectionMode": "deferred", + "PackageParameterName": "TerraformPackageId" + } + } + ], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.EnabledFeatures": "Octopus.Features.SubstituteInFiles", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$DynamicPackageName = \"InfrastructurePackage\" +$AccountVariableName = \"TerraformScript.AzureAccount\" +# Version of the terrform package - used to substitute Octopus.Release.Number in tfvars file +$packageVersion = $OctopusParameters[\"Octopus.Action.Package[$DynamicPackageName].PackageVersion\"] +# Override the package version with a custom version string - used to substitute Octopus.Release.Number in tfvars file +$userOverridenVersionNumber = $OctopusParameters[\"TerraformScript.Octopus.Release.Number\"] +# Should we call terraform init before the script? +$runInit = [System.Convert]::ToBoolean($OctopusParameters[\"TerraformScript.RunInitBeforeScript\"]) + +$versionNumberToSubstitute = If ($userOverridenVersionNumber) { $userOverridenVersionNumber } else { $packageVersion } +$versionNumberSubstitutionFilesPattern = if ( $OctopusParameters[\"TerraformScript.versionNumberSubstitutionFilesPattern\"]) { + $OctopusParameters[\"TerraformScript.VersionNumberSubstitutionFilesPattern\"] +} +else { + \"*.tf*\" +} + +# Should Octopus collect all terraform files from the package after execution as artefacts? +$collectArtefacts = if ($OctopusParameters[\"TerraformScript.CollectArtefacts\"]) { + [System.Convert]::ToBoolean($OctopusParameters[\"TerraformScript.CollectArtefacts\"]) +} +else { $false } +# override pattern to match (terraform) files like *.t* when collecting artefacts +$artefactNameLikePattern = if ($OctopusParameters[\"TerraformScript.ArtefactNameLikePattern\"]) { + $OctopusParameters[\"TerraformScript.ArtefactNameLikePattern\"] +} +else { + \"*.tf*\" +} +# Detect if we have Azure Account credentials from a variable and log in to azure +$HasAzureAccount = if ($OctopusParameters[\"$AccountVariableName.Client\"]) { $true } else { $false } +# The path where the package containing terraform files is extracted to disk - usually just ./InfrastructurePackage +$terraformPackageFolder = $OctopusParameters[\"Octopus.Action.Package[$DynamicPackageName].ExtractedPath\"]; +# Override the location of the terraform exe - otherwise assume terraform is available on PATH +$CustomTerraformExe = $OctopusParameters[\"Octopus.Action.Terraform.CustomTerraformExecutable\"] +# The terraform script to be executed +$terraformCliScriptToExecute = [Scriptblock]::Create($OctopusParameters[\"TerraformScript.CliScript\"]) + +# If we have service principal creds to Azure then set ENV variables so that azurerm can authenticate +if ($HasAzureAccount) { + Write-host \"Selecting azure subscription $($OctopusParameters[\"$AccountVariableName.SubscriptionNumber\"]) using $AccountVariableName variable\" + + $ENV:ARM_CLIENT_ID = $OctopusParameters[\"$AccountVariableName.Client\"] + $ENV:ARM_CLIENT_SECRET = $OctopusParameters[\"$AccountVariableName.Password\"] + $ENV:ARM_SUBSCRIPTION_ID = $OctopusParameters[\"$AccountVariableName.SubscriptionNumber\"] + $ENV:ARM_TENANT_ID = $OctopusParameters[\"$AccountVariableName.TenantId\"] +} + +Function Invoke-Exec { + [CmdletBinding()] + param( + [Parameter(Position = 0, Mandatory = 1)][scriptblock]$cmd + ) + $scriptExpanded = $ExecutionContext.InvokeCommand.ExpandString($cmd).Trim().Trim(\"&\") + Write-Verbose \"Executing command: $scriptExpanded\" + + & $cmd | Out-Default + + if ($lastexitcode -ne 0) { + throw (\"Non-zero exit code '$lastexitcode' detected from command: '$scriptExpanded'\") + } +} + +#================= Prep to run terraform ======================== + +# List the contents of the package - useful debugging +Write-Verbose \"Using package contents:\" +Get-ChildItem $terraformPackageFolder -Verbose + +# Use a custom version of terraform? If so add it to the path for this sesssion. Otherwise assume terraform already on PATH +if ($CustomTerraformExe) { + $terraformfolder = Split-Path $CustomTerraformExe; + # add custom terraform to path if required + if ($ENV:PATH -notcontains $terraformfolder) { + $ENV:PATH += \";$terraformfolder\"; + } + Write-Verbose \"PATH: $ENV:PATH\" + Write-Host \"`nUsing terraform.exe from $terraformExePath\" +} + +Write-Host \"Running in $terraformPackageFolder\" +Set-Location $terraformPackageFolder + +# Substitute #{Octopus.Release.Number} in *.tf files because that variable is not availe during runbook execution +Get-ChildItem . -file | Where-Object { $_.Name -like $versionNumberSubstitutionFilesPattern } | ForEach-Object { + Write-Host \"Replacing #{Octopus.Release.Number} in $($_.FullName) with $versionNumberToSubstitute\" + (Get-Content $_.FullName) -replace \"#{Octopus.Release.Number}\", $versionNumberToSubstitute | Set-Content $_.FullName +} + +#================= terraform init ======================== + +# optionally initialise terraform +if($runInit) { + Write-Host \"`n terraform init`n\" + terraform init -no-color +} +#================= Run terraform script ======================== + +try { + # Execute the provided script + Invoke-Exec $terraformCliScriptToExecute +} +finally { + # optionally collect all terraform files as artefacts + if ($collectArtefacts) { + Get-ChildItem . -File | Where-Object { $_.name -like $artefactNameLikePattern } | New-OctopusArtifact + } +} +", + "Octopus.Action.SubstituteInFiles.TargetFiles": "InfrastructurePackage\\*.tf +InfrastructurePackage\\*.tfvars" + }, + "Parameters": [ + { + "Id": "0170e094-b282-4924-b1ed-5103015e366f", + "Name": "TerraformPackageId", + "Label": "Package Id (Required)", + "HelpText": "The ID of the nupkg/zip that contains the terraform files", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Package" + } + }, + { + "Id": "3b7abc1b-49d9-4302-86e8-a85cc6eb2a75", + "Name": "TerraformScript.AzureAccount", + "Label": "Azure Account", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "AzureAccount" + } + }, + { + "Id": "53195dc5-dcc4-4a35-b3af-b63a0958ae46", + "Name": "TerraformScript.CliScript", + "Label": "Terraform Script", + "HelpText": "A terraform CLI script to be executed in the context of the package, such as `terraform show -no-color` or ` terraform import some-resource`", + "DefaultValue": "terraform plan -var-file=\"octopus.tfvars\" -no-color", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "2d907ba2-41f4-47d4-9592-1111a9ab8f93", + "Name": "TerraformScript.RunInitBeforeScript", + "Label": "Run Terraform Init?", + "HelpText": "If true, then script will call terraform init before executing the script", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "d0d1018d-d8aa-4c34-8da9-5c893076f8fe", + "Name": "TerraformScript.CollectArtefacts", + "Label": "Collect terraform files as artefacts?", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "d379e1f0-fac5-48cb-a011-249e0bcb401e", + "Name": "TerraformScript.ArtefactNameLikePattern", + "Label": "Collect terraform files artefact pattern", + "HelpText": "A like pattern for files to be collected as artefacts when 'Collect Terraform Files as Artefacts' is enabled. Example: `*.tf*`", + "DefaultValue": "*.tf*", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7b24c582-de47-4dad-9e37-c300802d1df9", + "Name": "TerraformScript.Octopus.Release.Number", + "Label": "Release Number Override", + "HelpText": "When step is run in a runbook there is no release number, so you may need to provide one here. By default we will use the release number from the package and substitute it into variable Octopus.Release.Number in terraform files, but you may override the value here if required.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "23e0e330-6b6b-4f7a-bd95-e44e69d1bcf6", + "Name": "TerraformScript.VersionNumberSubstitutionFilesPattern", + "Label": "Files to substitute Release Number", + "HelpText": "A like pattern for files that should have #{Octopus.Release.Number} substituted. Example: `*.tf*`", + "DefaultValue": "*.tf*", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-03-15T18:13:18.243Z", + "OctopusVersion": "2020.5.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "alastairtree", + "Category": "terraform" +} diff --git a/step-templates/test-run-nunit.json.human b/step-templates/test-run-nunit.json.human new file mode 100644 index 000000000..060375a08 --- /dev/null +++ b/step-templates/test-run-nunit.json.human @@ -0,0 +1,84 @@ +{ + "Id": "edcacbb5-6776-445a-9555-576ee4c3e2d6", + "Name": "Test - Run NUnit", + "Description": "Runs NUnit tests from a list of assemblies.", + "ActionType": "Octopus.Script", + "Version": 10, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Write-Output \"Running NUnit tests...\" + +$exePath = '\"' + $NUnitExePath + '\"' +if (-not $exePath) { + $exePath = \"nunit-console.exe\" +} + +$runNUnit = \"& $exePath /out:TestStdOut.txt /err:TestStdErr.txt $NUnitAdditionalArgs\" + +$NUnitTestAssemblies.Split(\";\") | ForEach { + $asm = $_.Trim() + Write-Output \"Including assembly $asm\" + $runNUnit += \" $asm\" +} + +cd $NUnitWorkingDirectoryPath + +iex $runNUnit +$nunitExit = $lastExitCode + +New-OctopusArtifact -Path TestResult.xml +New-OctopusArtifact -Path TestStdOut.txt +New-OctopusArtifact -Path TestStdErr.txt + +exit $nunitExit +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "NUnitExePath", + "Label": "NUnit path", + "HelpText": "The path to `nunit-console.exe`. If this is not provided it will be assumed that the executable is on the PATH. Example: _C:\\Program Files (x86)\\NUnit 2.6.3\\bin\ +unit-console.exe_.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NUnitWorkingDirectoryPath", + "Label": "Working directory", + "HelpText": "The folder that contains the test assemblies. Generally this will be bound to an output variable from a previous step. Example: _#{Octopus.Action[Deploy integration tests].Output.Package.InstallationDirectoryPath}_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NUnitTestAssemblies", + "Label": "Test assemblies", + "HelpText": "A semicolon-separated list of assembly names containing tests. Example: _MyCompany.IntegrationTests.dll; MyCompany.SmokeTests.dll_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "NUnitAdditionalArgs", + "Label": "Additional arguments", + "HelpText": "Additional arguments to the NUnit command-line tool. For a full list of supported flags and options see [the NUnit documentation](http://www.nunit.org/index.php?p=consoleCommandLine&r=2.6.3).", + "DefaultValue": "/nodots", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-06T04:23:41.674+00:00", + "OctopusVersion": "2.4.3.0", + "Type": "ActionTemplate" + }, + "Category": "nunit" +} diff --git a/step-templates/testery-create-test-run.json.human b/step-templates/testery-create-test-run.json.human new file mode 100644 index 000000000..536003e64 --- /dev/null +++ b/step-templates/testery-create-test-run.json.human @@ -0,0 +1,162 @@ +{ + "Id": "9aa25d78-a90a-425e-a25d-b1e9f1bf08be", + "Name": "Testery - Create Test Run", + "Description": "Run tests in the Testery platform. For more information, visit https://testery.io/.", + "ActionType": "Octopus.Script", + "Version": 25, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "try {$pipCmd = get-command pip} catch {} +if (!($pipCmd)) { +\tFail-Step \"This step template requires Python 3.6 or greater and pip to be installed. Python is available at https://www.python.org/downloads/\" +} + +pip install testery --upgrade --disable-pip-version-check --no-warn-script-location -qqq + +$TesteryCommand = \"testery create-test-run --token `\"${TesteryToken}`\" --project `\"${TesteryProjectName}`\" --environment `\"${TesteryEnvironment}`\"\" + +if (\"${TesteryIncludeTags}\" -eq \"\") { +\t$TesteryCommand = $TesteryCommand + \" --include-all-tags\" +} else { +\t$TesteryCommand = $TesteryCommand + \" --include-tags `\"${TesteryIncludeTags}`\"\" +} + +if (\"${TesteryLatestDeploy}\" -eq \"True\") { +\t$TesteryCommand = $TesteryCommand + \" --latest-deploy\" +} + +if (\"${TesteryGitReference}\" -ne \"\") { +\t$TesteryCommand = $TesteryCommand + \" --git-ref `\"${TesteryGitReference}`\"\" +} + +if (\"${TesteryBuildId}\" -ne \"\") { +\t$TesteryCommand = $TesteryCommand + \" --build-id `\"${TesteryBuildId}`\"\" +} + +if (\"${TesteryWaitForResults}\" -eq \"True\") { +\t$TesteryCommand = $TesteryCommand + \" --wait-for-results\" +} + +if (\"${TesteryFailOnFailure}\" -eq \"True\") { +\t$TesteryCommand = $TesteryCommand + \" --fail-on-failure\" +} + + +echo $TesteryCommand +Invoke-Expression $TesteryCommand" + }, + "Parameters": [ + { + "Id": "db36b837-9fe6-480f-b4f1-05cfc68bd08b", + "Name": "TesteryToken", + "Label": "Testery Token", + "HelpText": "Your Testery API token (found in Testery -> Settings -> Integrations -> Show API Token)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "54de434f-0fa8-4351-b59c-a790845a0f14", + "Name": "TesteryProjectName", + "Label": "Testery Project Name", + "HelpText": "The project name for this build.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "352f0cf8-da46-4651-b7d0-3c5413d7f3e0", + "Name": "TesteryEnvironment", + "Label": "Testery Environment", + "HelpText": "The environment (defined in Testery) where the tests should be run. It can be convenient to have these line up with tenant names.", + "DefaultValue": "#{Octopus.Deployment.Tenant.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9a9d2a9f-cd24-40a2-96b7-c6f42e489e7e", + "Name": "TesteryIncludeTags", + "Label": "Testery Include Tags", + "HelpText": "Comma separated list of tags for tests that should be included in the test run.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b9d78e94-20fb-4fa5-9a9a-34cf173c8911", + "Name": "TesteryBuildId", + "Label": "Build ID", + "HelpText": "The build id of the project being tested, typically coming from your CI/CD. If uploading test artifacts to Testery, this build id must match.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "96cec59e-cf55-4867-87ec-bb4af80a86e3", + "Name": "TesteryGitReference", + "Label": "Git Refererence", + "HelpText": "This is the git reference to the tests you want to run. This value can be extracted from the packages when build info is reported to Octopus.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c8af7536-23d8-44c1-82e0-13325d9b3d06", + "Name": "TesteryLatestDeploy", + "Label": "Latest Deploy", + "HelpText": "When set, Testery will run the latest version of the tests that has been deployed using the Create Deploy command. Optionally use this instead of Git Ref.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "7ad3f9db-398a-470e-861a-1c93e4f4ea46", + "Name": "TesteryFailOnFailure", + "Label": "Fail on Failure", + "HelpText": "When set, this step will fail if there are any failing tests.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "59391562-5601-4c8b-a117-1fc29d71d91d", + "Name": "TesteryWaitForResults", + "Label": "Wait for Results", + "HelpText": "When set, this step will wait for the test run to be complete before continuing.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "ae843e0f-a189-411e-94e8-cc8d92c7373a", + "Name": "TesteryApiUrl", + "Label": "Testery API URL", + "HelpText": "Override the default API URL for development and testing purposes. Most users of this step template will not need to modify this parameter.", + "DefaultValue": "https://api.testery.io/api", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2022-02-14T18:39:12.429Z", + "OctopusVersion": "2022.1.890", + "Type": "ActionTemplate" + }, + "LastModifiedOn": "2022-02-14T18:39:12.429+00:00", + "LastModifiedBy": "harbertc", + "Category": "testery" +} diff --git a/step-templates/testery-report-deployment.json.human b/step-templates/testery-report-deployment.json.human new file mode 100644 index 000000000..163ff6975 --- /dev/null +++ b/step-templates/testery-report-deployment.json.human @@ -0,0 +1,120 @@ +{ + "Id": "9c85f96e-09d3-4814-948a-aef8708740b5", + "Name": "Testery - Report Deployment", + "Description": "Reports a deployment to Testery, enabling you to do post-deployment validation and testing. See https://testery.io for more info.", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.RunOnServer": "true", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "try {$pipCmd = get-command pip} catch {} +if (!($pipCmd)) { +\tFail-Step \"This step template requires Python 3.6 or greater and pip to be installed. Python is available at https://www.python.org/downloads/\" +} + +pip -q install testery --upgrade + +write-output \"Fail on failure: ${TesteryFailOnFailure}\" + + +if (${TesteryFailOnFailure}) { + write-output \"Fail on failure option selected.\" + $failOnFailureSwitch=\"--fail-on-failure\" +} else { + write-output \"Fail on failure option not selected.\" + $failOnFailureSwitch=\"\" +} + +if (${TesteryWaitForResults}) { + $waitForResultsSwitch=\"--wait-for-results\" +} else { + $waitForResultsSwitch=\"\" +} + +echo \"Reporting deployment info to Testery...\" +testery create-deploy --commit \"${TesteryGitReference}\" --token \"${TesteryToken}\" --project \"${TesteryProjectName}\" --environment \"${TesteryEnvironment}\" --build-id \"${TesteryBuildId}\" \"${failOnFailureSwitch}\" \"${waitForResultsSwitch}\" +" + }, + "Parameters": [ + { + "Id": "f96b7529-7ced-4265-8176-972ec30b9bba", + "Name": "TesteryGitReference", + "Label": "Testery Git Reference", + "HelpText": "The git hash of the commit for the version of the tests to be run.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f01f917a-b2c8-4038-be1c-b2355639f57e", + "Name": "TesteryToken", + "Label": "Testery Token", + "HelpText": "Your Testery API token (found in Testery -> Settings -> Integrations -> Show API Token)", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "4873b6f2-694a-463f-928a-9845b044bc8b", + "Name": "TesteryProjectName", + "Label": "Testery Project Name", + "HelpText": "The name of the test project in the Testery platform.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "811352ba-a3e4-4092-99c2-b48499b9a880", + "Name": "TesteryEnvironment", + "Label": "Testery Environment", + "HelpText": "The name of the environment defined in Testery where you want the tests to run. It may be useful to set this to Octopus.Deployment.Tenant.Name.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7a6aa01f-13a1-4fa7-a681-7f0acda63932", + "Name": "TesteryBuildId", + "Label": "Testery Build Id", + "HelpText": "The build ID from your CI/CD. If you have uploaded any test artifacts from your CI/CD, this build id should match the build id used when uploading artifacts.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "09ed4f34-d381-4f85-8869-c52264694b7c", + "Name": "TesteryFailOnFailure", + "Label": "Testery Fail On Failure", + "HelpText": "When checked, the Octopus deployment will fail if any of the test runs associated with the deployment fail. When unchecked, the Octopus deployment will continue even if there are test failures.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "2266652a-258d-447f-aebf-14e9575d6b9d", + "Name": "TesteryWaitForResults", + "Label": "Testery Wait For Results", + "HelpText": "When checked, Octopus Deploy will wait for the any test runs associated with the deployment to complete before proceeding. This is useful for making sure deployments don't run on environments/tenants with an active test run.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2020-10-29T16:37:27.511Z", + "OctopusVersion": "2020.5.0-rc0003", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "bobjwalker", + "Category": "testery" +} diff --git a/step-templates/tomcat-undeploy-application.json.human b/step-templates/tomcat-undeploy-application.json.human new file mode 100644 index 000000000..a0be278bb --- /dev/null +++ b/step-templates/tomcat-undeploy-application.json.human @@ -0,0 +1,140 @@ +{ + "Id": "34f13b4c-64e1-42b4-ad1a-4599f25a850e", + "Name": "Undeploy Tomcat Application via Manager", + "Description": "Undeploys the specified application/version from the Tomcat server.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "# Get variables +tomcatManagerUrl=$(get_octopusvariable \"Tomcat.Undeploy.ManagerUrl\") +tomcatManagementUser=$(get_octopusvariable \"Tomcat.Undeploy.Management.User\") +tomcatManagementPassword=$(get_octopusvariable \"Tomcat.Undeploy.Management.Password\") +contextPath=$(get_octopusvariable \"Tomcat.Undeploy.ContextPath\") +deploymentVersion=$(get_octopusvariable \"Tomcat.Undeploy.DeploymentVersion\") +applicationFound=false +displayMessage=\"$contextPath\" + +# Get list of applications +echo \"Checking Tomcat for $contextPath ...\" +listUrl=\"$tomcatManagerUrl/text/list\" + +results=$(curl $listUrl --user \"$tomcatManagementUser\":\"$tomcatManagementPassword\" 2>&1) + +# Break results into an array +IFS=$'\ +' resultArray=($results) + +# Loop through results +for i in \"${resultArray[@]}\" +do +\t# Check for context path + if [[ \"$i\" == *\"$contextPath\"* ]] + then + \t# Check to see if there was a version specified + if [[ \"$deploymentVersion\" != \"\" ]] + then + \tdisplayMessage=\"$displayMessage $deploymentVersion\" + \t# Check for version + if [[ \"$i\" == *\"$deploymentVersion\"* ]] + then + \techo \"Found $contextPath with version $deploymentVersion ...\" + applicationFound=true + break + fi + else + \tif [[ \"$i\" != *\"##\"* ]] + then + \techo \"Found $contextPath ...\" + \tapplicationFound=true + \tbreak + fi + fi + fi +done + +if [[ \"$applicationFound\" == true ]] +then +\t# Create URL +\tundeployUrl=\"$tomcatManagerUrl/text/undeploy?path=/$contextPath\" + +\t# Check to see if a version was specified +\tif [[ \"$deploymentVersion\" != \"\" ]] +\tthen +\t\tundeployUrl=\"$undeployUrl&version=$deploymentVersion\" +\tfi + +\t# Let user know what's going on +\techo \"Removing $displayMessage ...\" + +\t# Call the undeploy for Tomcat +\tcurl \"$undeployUrl\" --user \"$tomcatManagementUser\":\"$tomcatManagementPassword\" 2>&1 +else +\techo \"Unable to find $displayMessage ...\" +fi" + }, + "Parameters": [ + { + "Id": "a3a0b5b5-067f-40fe-b93d-833bbe901937", + "Name": "Tomcat.Undeploy.ManagerUrl", + "Label": "Tomcat Manager URL", + "HelpText": "This is the URL of the Tomcat Manager instance that the package will be uploaded to. This URL is relative to the host that is executing the step. So if the Tentacle is on the same host as Tomcat, then this value may be something like http://localhost:8080/manager.", + "DefaultValue": "http://localhost:8080/manager", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bb902de7-ad45-4f76-8111-0d080d491c77", + "Name": "Tomcat.Undeploy.Management.User", + "Label": "Management User", + "HelpText": "The username to use with the management interface. This user must be assigned to the manager-script group in the tomcat-users.xml file.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "7be02a9f-bc24-4df3-bf2b-3d4963c1d6ae", + "Name": "Tomcat.Undeploy.Management.Password", + "Label": "Management Password", + "HelpText": "The password to use with the management interface.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "2b07425a-a164-470e-a8b0-20051e91f796", + "Name": "Tomcat.Undeploy.ContextPath", + "Label": "Context path", + "HelpText": "This field defines the context path of the deployed artifact. For example, setting this field to myapp will result in the deployment being having the context path /myapp in Tomcat. Set the value to / to deploy to the root context.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3c8199d2-db7f-473c-aab5-5e5337e645bd", + "Name": "Tomcat.Undeploy.DeploymentVersion", + "Label": "Deployment Version (Optional)", + "HelpText": "Leave this field blank to to leave the Tomcat version undefined, in which case you will overwrite any existing deployment with the same context path, and parallel deployments will not be enabled. + +Alternatively you can define a custom Tomcat version. These versions are compared as plain strings, meaning traditional version strings like 1.0.1 may not be suitable as this format is not guaranteed to correctly identify the latest version using string comparisons. A date string like #{ | NowDateUtc \"yyMMddHHmmss\"} is a good option, as this format produces the correct result when compared as strings.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2021-09-20T18:05:20.249Z", + "OctopusVersion": "2021.2.7462", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "tomcat" +} diff --git a/step-templates/topshelf-install.json.human b/step-templates/topshelf-install.json.human new file mode 100644 index 000000000..a49bae9aa --- /dev/null +++ b/step-templates/topshelf-install.json.human @@ -0,0 +1,97 @@ +{ + "Id": "24189509-ae05-434b-9a28-080b81875e10", + "Name": "Install TopShelf service", + "Description": null, + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$step = $OctopusParameters['Unpackage step'] +$username = $OctopusParameters['Username']; +$password = $OctopusParameters['Password']; +$customExeFilename = $OctopusParameters['Exe filename']; + +$outputPath = $OctopusParameters[\"Octopus.Action[$step].Package.CustomInstallationDirectory\"] +if(!$outputPath) +{ + $outputPath = $OctopusParameters[\"Octopus.Action[$step].Output.Package.InstallationDirectoryPath\"] +} + +$defaultExeFilename = $OctopusParameters[\"Octopus.Action[$step].Package.NuGetPackageId\"] + \".exe\" +$exeFilename = If ($customExeFilename) {$customExeFilename} Else {$defaultExeFilename} +$path = Join-Path $outputPath $exeFilename + +if(-not (Test-Path $path) ) +{ + Throw \"$path was not found\" +} + +Write-Host \"Installing from: $path\" +if(!$username) +{ + Start-Process $path -ArgumentList \"install\" -NoNewWindow -Wait | Write-Host +} +else +{ + Start-Process $path -ArgumentList @(\"install\", \"-username\", $username, \"-password\", $password) -NoNewWindow -Wait | Write-Host +} +Start-Process $path -ArgumentList \"start\" -NoNewWindow -Wait | Write-Host +" + }, + "Parameters": [ + { + "Id": "463159e1-62fa-4150-bdb9-dfb7dac6ecfa", + "Name": "Unpackage step", + "Label": "", + "HelpText": "The step where you unpack the topshelf service", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + }, + "Links": {} + }, + { + "Id": "d5d3e88e-b16a-4864-ac1e-614f11c861ed", + "Name": "Username", + "Label": "Service username", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "0fcca6af-0ba4-495c-b120-b06ac8de2ebd", + "Name": "Password", + "Label": "Service password", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "40da9228-33db-402a-9404-8e06c2817d7d", + "Name": "Exe filename", + "Label": "", + "HelpText": "Name of exe file for service, if empty, package Id+\".exe.\" will be used as default", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "sphinxy", + "$Meta": { + "ExportedAt": "2018-02-22T16:54:25.613Z", + "OctopusVersion": "4.0.10", + "Type": "ActionTemplate" + }, + "Category": "topshelf" +} diff --git a/step-templates/topshelf-uninstall.json.human b/step-templates/topshelf-uninstall.json.human new file mode 100644 index 000000000..1f2d1ccf6 --- /dev/null +++ b/step-templates/topshelf-uninstall.json.human @@ -0,0 +1,70 @@ +{ + "Id": "b5a4ed5a-7fb3-4d0c-b4a5-02616d0ed919", + "Name": "Uninstall TopShelf service", + "Description": "This step can be used before unpacking a package with your TopShelf service to stop and remove the previous installation, if there is one.", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$step = $OctopusParameters['Unpackage step'] +$previous = $OctopusParameters[\"Octopus.Action[$step].Package.CustomInstallationDirectory\"] +$customExeFilename = $OctopusParameters['Exe filename']; + +if(!$previous -or (-not (Test-Path $previous)) ) +{ + Write-Host \"No installation found in: $previous\" +\t + $previous = $OctopusParameters[\"Octopus.Action[$step].Output.Package.InstallationDirectoryPath\"] + if(!$previous -or (-not (Test-Path $previous)) ) + { + Write-Host \"No installation found in: $previous\" + Break + } +} + + +$defaultExeFilename = $OctopusParameters[\"Octopus.Action[$step].Package.NuGetPackageId\"] + \".exe\" +$exeFilename = If ($customExeFilename) {$customExeFilename} Else {$defaultExeFilename} +$path = Join-Path $previous $exeFilename + +Write-Host \"Previous installation: $path\" + +Start-Process $path -ArgumentList \"stop\" -NoNewWindow -Wait | Write-Host +Start-Process $path -ArgumentList \"uninstall\" -NoNewWindow -Wait | Write-Host +" + }, + "Parameters": [ + { + "Id": "14d4b5e5-98ff-48ee-aeba-c062e294a18c", + "Name": "Unpackage step", + "Label": "", + "HelpText": "The step where you unpack the topshelf service", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + }, + "Links": {} + }, + { + "Id": "75418f4b-48fb-4475-93a1-a6d793495693", + "Name": "Exe filename", + "Label": "", + "HelpText": "Name of exe file for service, if empty, package Id+\".exe.\" will be used as default", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "sphinxy", + "$Meta": { + "ExportedAt": "2018-02-22T16:57:20.068Z", + "OctopusVersion": "4.0.10", + "Type": "ActionTemplate" + }, + "Category": "topshelf" +} diff --git a/step-templates/trigger-multitenant-deployment.json.human b/step-templates/trigger-multitenant-deployment.json.human new file mode 100644 index 000000000..79013cfb5 --- /dev/null +++ b/step-templates/trigger-multitenant-deployment.json.human @@ -0,0 +1,206 @@ +{ + "Id": "053c7adc-f31f-42e1-b7c7-671d3df55ab2", + "Name": "Trigger Multi-Tenant Deployment", + "Description": "A step template that triggers Multi-Tenant Deployment. It can be used in not-tenanted projects. The step will lookup most recent release for the specified project if a specific version is not provided. Then it will lookup all tenants that are tagged with the specified tag and create a new deployment for each of the tenants. This will result in multiple tasks scheduled in Octopus. The step is not tracking status of deployment tasks execution. + +Hypothetical use-case: + * A multi-tenanted system deployed (as a monoliths) with a non-tenanted Octopus Deploy process. + * Use this step to start a smoke test for several customers from such a not-tenanted deployment. In this case tenants can be used to represent different customers. This way each customer-specific parameters (access URLs, credentials, etc.) can be configured on tenants level.", + "ActionType": "Octopus.Script", + "Version": 2, + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "Category": "tenants", + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function TriggerMultitenantDeployment { +\t\t### Steps: +\t\t### 1. find most recent release of the Multi-tenanted project by project name if Release Number (2) parameter is empty +\t\t### 2. locate all the tenants for specified tag +\t\t### 3. for each tenant create a new deployment for the found release with the parameters + + param + ( + [ValidateNotNullOrEmpty()] + [Parameter(Mandatory = $true)] + [string]$apiUrl, + [ValidateNotNullOrEmpty()] + [Parameter(Mandatory = $true)] + [string]$apiKey, + [ValidateNotNullOrEmpty()] + [Parameter(Mandatory = $true)] + [string]$projectName, + [Parameter(Mandatory = $false)] + [string]$releaseNumber = '', + [ValidateNotNullOrEmpty()] + [Parameter(Mandatory = $true)] + [string]$environmentName, + [ValidateNotNullOrEmpty()] + [Parameter(Mandatory = $true)] + [string]$tenantsTag + ) +\t\t\t + +\t\t$errors=0 +\t\t$headers = @{\"X-Octopus-ApiKey\"=$apiKey} +\t\t +\t\t$initiator = \"#{Octopus.Deployment.CreatedBy.Username}\" + +\t\tWrite-Host \"### TriggerMultiTenantedDeployment parameters:\" +\t\tWrite-Host \"###`tAPI URL: [$($apiUrl)]\" +\t\tif($debug) { +\t\t\tWrite-Host \"###`tAPI Key: [$($apiKey)]\" +\t\t} +\t\tWrite-Host \"###`tProject Name: [$($projectName)]\" +\t\tWrite-Host \"###`tRelease Number: [$($releaseNumber)]\" +\t\tWrite-Host \"###`tEnvironment: [$($environmentName)]\" +\t\tWrite-Host \"###`tTenants Tag Name: [$($tenantsTag)]\" +\t\tWrite-Host \"###`tInitiator: [$($initiator)]\" + +\t\ttry { +\t\t\tif (-Not ($tenantsTag -match '\\w+/\\w+')) +\t\t\t{ +\t\t\t\tWrite-Error \"### Tenants Tag Name must be in format 'TenantTagSet/TenantTag'\" +\t\t\t\t$errors=1 +\t\t\t} +\t\t\t$projects = Invoke-RestMethod -Method GET -Uri $apiUrl/projects/all -Headers $headers +\t\t\t$project = $projects | where Name -eq $projectName +\t\t\tif (-Not $project) { +\t\t\t\tWrite-Error \"### Could not find project with name [$($projectName)].\" +\t\t\t\t$errors=1 +\t\t\t} + $projectUrl=\"$($apiUrl)/projects/$($project.Id)/releases\" +\t\t\t$releases = Invoke-RestMethod -Method GET -Uri $projectUrl -Headers $headers | select -expand Items +\t\t\t$mostRecentRelease = $releases | Sort-Object -Descending -Property { [int]($_.Version -replace \"\\.\") } | Select -First 1 +\t\t\t$release = $releases | where Version -eq $releaseNumber | Select -First 1 +\t\t\tif (-Not ($release)) { +\t\t\t\t$release=$mostRecentRelease +\t\t\t\tWrite-Host \"### Selected most recent release [$($release.Version)].\" +\t\t\t} +\t\t\tif(-Not $release) { +\t\t\t\tif($releaseNumber) { +\t\t\t\t\tWrite-Error \"### Could not find release [$($releaseNumber)]for project with name [$($projectName)].\" +\t\t\t\t} else { +\t\t\t\t\tWrite-Error \"### Could not find any releases for project with name [$($projectName)].\" +\t\t\t\t} +\t\t\t\t$errors=1 +\t\t\t} +\t\t\t$environments = Invoke-RestMethod -Method GET -Uri $apiUrl/environments/all -Headers $headers +\t\t\t$environment = $environments | where Name -eq $environmentName +\t\t\tif(-Not $environment){ +\t\t\t\tWrite-Error \"### Could not find environment [$($environmentName)] for project with name [$($projectName)].\" +\t\t\t\t$errors=1 +\t\t\t} +\t\t\t$tenants = Invoke-RestMethod -Method GET -Uri $apiUrl/tenants/all -Headers $headers +\t\t\t$selectedTenants = $tenants | where TenantTags -Contains $tenantsTag +\t\t\tif(-Not $selectedTenants){ +\t\t\t\tWrite-Error \"### Could not find any tenants with tag [$($tenantsTag)].\" +\t\t\t\t$errors=1 +\t\t\t} +\t\t\tif($errors) +\t\t\t{ +\t\t\t\tFail-Step \"### Encoutered an error. See log for more details. Interrupting the task.\" +\t\t\t} + +\t\t\tif ($debug){ +\t\t\t\tWrite-Host \"##Project##: \" $project +\t\t\t\tWrite-Host \"##Project.Id##: \" $projectId +\t\t\t\tWrite-Host \"##LifeCycleId##: \" $lifeCycleId +\t\t\t\tWrite-Host \"##Release##: \" $release +\t\t\t\tWrite-Host \"##ReleaseId##: \" $release.Id +\t\t\t\tWrite-Host \"##ChannelId##: \" $release.channelId +\t\t\t\tWrite-Host \"##TenantsTag##: \" $tag.Id $tag.CanonicalTagName +\t\t\t\tWrite-Host \"##SelectedTenants##: \" $selectedTenants.Count +\t\t\t} + +\t\t\tforeach($tenant in $selectedTenants) { +\t\t\t\t$deploymentJson = \"{`\"ProjectId`\":`\"$($projectId)`\",`\"ReleaseId`\":`\"$($release.Id)`\",`\"EnvironmentId`\":`\"$($environment.Id)`\",`\"ChannelId`\":`\"$($release.channelId)`\",`\"TenantId`\":`\"$($tenant.Id)`\",`\"Comments`\":`\"Initiated by $($initiator)`\"}\" +\t\t\t +\t\t\t\tif($debug) { Write-Host \"##DeploymentJson##: $($deploymentJson)\" } +\t\t\t\t +\t\t\t\t$deployment = Invoke-RestMethod -Method POST -Uri $apiUrl/deployments -Headers $headers -Body $deploymentJson +\t\t\t\tWrite-Host \"### Created new deployment for $($tenant.Name): [$($deployment)].\" +\t\t\t\tWrite-Host \"### Deployment for release [$($project.Name) $($release.Version)], tenant [$($tenant.Name)] in [$($environmentName)] environment was created and scheduled successfuly.\" +\t\t\t} +\t\t\texit +\t\t} Catch { +\t\t\tWrite-Error \"### Failed to complete deployment for \" $projectName \"to\" $environmentName \"for\" $tenantsTag +\t\t\tthrow $_ +\t\t} +} + + +\tTriggerMultitenantDeployment -apiUrl $OctopusAPIurl -apiKey $OctopusAPIkey -projectName $MultiTenantProjectName -environmentName $EnvironmentName -releaseNumber $ReleaseNumber -tenantsTag $TenantsTag +", + "Octopus.Action.SubstituteInFiles.Enabled": "True", + "Octopus.Action.EnabledFeatures": "Octopus.Features.SubstituteInFiles" + }, + "Parameters": [ + { + "Id": "f488f5a8-eafc-4da8-97ea-66cc01dfb39e", + "Name": "OctopusAPIurl", + "Label": "Octopus API URL", + "HelpText": null, + "DefaultValue": "#{Octopus.Dashboard.BaseUrl}/api", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9a4af3e2-340e-4e2f-9529-c160a491cc41", + "Name": "OctopusAPIkey", + "Label": "Octopus API Key", + "HelpText": null, + "DefaultValue": "#{Octopus.API.key}", + "DisplaySettings": {"Octopus.ControlType": "Sensitive"} + }, + { + "Id": "dd89222e-662d-4e63-a6a6-260ec6495547", + "Name": "MultiTenantProjectName", + "Label": "Multi-Tenant Project Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "8447f3fc-c8ed-44e3-ab2f-932981160ea8", + "Name": "ReleaseNumber", + "Label": "Release Number", + "HelpText": "Release number of the Multi-tenant project. Leave empty to use most recent release of the project.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "50ece5fe-7650-491f-9e0a-e9e521b14799", + "Name": "EnvironmentName", + "Label": "Environment Name", + "HelpText": null, + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e1657396-4221-4fcf-84a1-9fe130263cc4", + "Name": "TenantsTag", + "Label": "Tenants Selection Tag", + "HelpText": "Required. Expected format: /", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2019-04-10T20:41:22.620Z", + "OctopusVersion": "2018.10.5", + "Type": "ActionTemplate" + } +} diff --git a/step-templates/twilio-send-sms-powershell.json.human b/step-templates/twilio-send-sms-powershell.json.human new file mode 100644 index 000000000..a70d1e36b --- /dev/null +++ b/step-templates/twilio-send-sms-powershell.json.human @@ -0,0 +1,88 @@ +{ + "Id": "3c3904a9-d08c-4f18-b86c-0304800bb541", + "Name": "Twilio - Send SMS (PowerShell)", + "Description": "Send an SMS using Twilio's API. This script step is written in PowerShell.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$url = \"https://api.twilio.com/2010-04-01/Accounts/$Twilio_SendMessage_AccountSID/Messages.json\" +$params = @{ + To = $Twilio_SendMessage_ToNumber; + From = $Twilio_SendMessage_FromNumber; + Body = $Twilio_SendMessage_Message +} + +Write-Verbose \"Creating Twilio credentials\" +$secureToken = $Twilio_SendMessage_AuthToken | ConvertTo-SecureString -asPlainText -Force +$credential = New-Object System.Management.Automation.PSCredential($Twilio_SendMessage_AccountSID, $secureToken) + +Write-Verbose \"Creating Twilio credentials\" +Invoke-WebRequest $url -Method Post -Credential $credential -Body $params -UseBasicParsing +" + }, + "Parameters": [ + { + "Id": "392a1b09-0f79-48c2-95f8-679869383e39", + "Name": "Twilio_SendMessage_AccountSID", + "Label": "SID", + "HelpText": "The Twilio account SID.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "1c10d07a-f2fc-466d-ad2b-0d9800141e60", + "Name": "Twilio_SendMessage_AuthToken", + "Label": "Auth Token", + "HelpText": "The Twilio AuthToken.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "bdbd0b60-9544-41a5-a64d-b8544c6f1958", + "Name": "Twilio_SendMessage_FromNumber", + "Label": "From Number", + "HelpText": "The number to send the message from. This is usually your Twilio number.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fbb8e980-fd54-4c34-a149-848a1eebd560", + "Name": "Twilio_SendMessage_ToNumber", + "Label": "To Number", + "HelpText": "The number to send the message to.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "d3674581-f589-4f9e-998c-f829ee9195f6", + "Name": "Twilio_SendMessage_Message", + "Label": "Message", + "HelpText": "The message to send.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "Author": "ryanrousseau", + "LastModifiedOn": "2020-04-14T15:25:44.139Z", + "LastModifiedBy": "ryanrousseau", + "$Meta": { + "ExportedAt": "2020-04-14T15:25:44.139Z", + "OctopusVersion": "2020.1.9", + "Type": "ActionTemplate" + }, + "Category": "twilio" +} diff --git a/step-templates/umbraco-v7-folder-permissions.json.human b/step-templates/umbraco-v7-folder-permissions.json.human new file mode 100644 index 000000000..c1ecc5a3f --- /dev/null +++ b/step-templates/umbraco-v7-folder-permissions.json.human @@ -0,0 +1,82 @@ +{ + "Id": "c60a8c60-c5bc-4dac-9384-65ec4bcf6c67", + "Name": "Set Umbraco 7 Folder Permissions", + "Description": "To ensure a stable and smoothly running umbraco installation, these permissions need to be set correctly. + +These permissions should be set before or during the insallation of umbraco. The user with the permissions set are the user used by the Application Pool used by the IIS website (usually Network Service or the IIS_IUSRS group). If in doubt, ask your server admin / hosting company + +[Umbraco Wiki Article](http://our.umbraco.org/wiki/reference/files-and-folders/permissions) +", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$PhysicalPath = \"$WebsiteDirectory\"\r +$appPoolAccount = \"IIS APPPOOL\\$ApplicationPoolName\"\r +$readExecute = $appPoolAccount,\"ReadAndExecute\",\"ContainerInherit,ObjectInherit\",\"None\",\"Allow\"\r +$read = $appPoolAccount,\"Read\",\"ContainerInherit,ObjectInherit\",\"None\",\"Allow\"\r +$modify = $appPoolAccount,\"Modify\",\"ContainerInherit,ObjectInherit\",\"None\",\"Allow\"\r +$fileModify = $appPoolAccount,\"Modify\",\"Allow\"\r +$objects = @{}\r +$objects[\"App_Browsers\"] = $readExecute\r +$objects[\"App_Code\"] = $modify\r +$objects[\"App_Data\"] = $modify\r +$objects[\"App_Plugins\"] = $modify\r +$objects[\"bin\"] = $modify\r +$objects[\"Config\"] = $modify\r +$objects[\"Css\"] = $modify\r +$objects[\"MacroScripts\"] = $modify\r +$objects[\"Masterpages\"] = $modify\r +$objects[\"Media\"] = $modify\r +$objects[\"Scripts\"] = $modify\r +$objects[\"Umbraco\"] = $modify\r +$objects[\"Umbraco_Client\"] = $modify\r +$objects[\"UserControls\"] = $modify\r +$objects[\"Views\"] = $modify\r +$objects[\"Web.config\"] = $fileModify\r +$objects[\"Xslt\"] = $modify\r +foreach($object in $objects.Keys){\r + try {\r + $path = Join-Path $PhysicalPath $object\r + $acl = Get-ACL $path\r + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($objects[$object])\r + $acl.AddAccessRule($rule)\r + Set-Acl $path $acl\r + Get-Acl $path | Format-List\r + }\r + catch [System.Exception]\r + {\r + Write-Host \"Unable to set ACL on\" Join-Path $PhysicalPath $object\r + }\r +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "WebsiteDirectory", + "Label": "Website Directory", + "HelpText": "This is the location where the website is configured to run from in IIS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ApplicationPoolName", + "Label": "Application Pool Name", + "HelpText": "This is the name of the application pool the website is configured to run under in IIS", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-01-23T12:31:26.603+00:00", + "OctopusVersion": "2.6.0.778", + "Type": "ActionTemplate" + }, + "Category": "umbraco" +} diff --git a/step-templates/unicorn-sync-configuration.json.human b/step-templates/unicorn-sync-configuration.json.human new file mode 100644 index 000000000..67d911730 --- /dev/null +++ b/step-templates/unicorn-sync-configuration.json.human @@ -0,0 +1,88 @@ +{ + "Id": "993b2b9a-05a4-4dbd-b5b7-3dc0358acac8", + "Name": "Unicorn - Sync Configuration", + "Description": "Sync Unicorn items", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$url = \"$BaseUrl/unicorn.aspx?verb=Sync&configuration=$ConfigName\"\r +Write-Host \"Syncing configuration: $ConfigName\"\r +Write-Host \"Attempting to invoke: $url\"\r +$deploymentToolAuthToken = $DeploymentAuthToken\r +$timeout = $Timeout\r +$basicAuthUser = $BasicAuthUsername\r +$basicAuthPass = $BasicAuthPassword\r +$secpasswd = ConvertTo-SecureString $basicAuthPass -AsPlainText -Force\r +$credential = New-Object System.Management.Automation.PSCredential($basicAuthUser, $secpasswd)\r +$result = Invoke-WebRequest -Uri $url -Headers @{ \"Authenticate\" = $deploymentToolAuthToken } -TimeoutSec $timeout -UseBasicParsing \r +\r +Write-Host $result.Content", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "BaseUrl", + "Label": "Base Url", + "HelpText": "e.g. http://mywebsite.com.au/ - This should be where you'd normally manually sync Unicorn", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ConfigName", + "Label": "Configuration Name", + "HelpText": "e.g. Default Configuration", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Timeout", + "Label": "Timeout", + "HelpText": "The timeout for the Web-Invoke method", + "DefaultValue": "10800", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "DeploymentAuthToken", + "Label": "Deployment Auth Token", + "HelpText": "The auth token used to authenticate this request. +This should be in your web.config appSettings as DeploymentToolAuthToken.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BasicAuthUsername", + "Label": "Basic Auth Username", + "HelpText": "Fill this in if the target site is behind basic auth", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "BasicAuthPassword", + "Label": "Basic Auth Password", + "HelpText": "Fill this in if the target site is behind basic auth", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-09-04T05:09:05.695+00:00", + "LastModifiedBy": "elkaz", + "$Meta": { + "ExportedAt": "2015-09-07T02:20:43.772+00:00", + "OctopusVersion": "2.6.3.886", + "Type": "ActionTemplate" + }, + "Category": "sitecore" +} diff --git a/step-templates/unzip.json.human b/step-templates/unzip.json.human new file mode 100644 index 000000000..b14918e83 --- /dev/null +++ b/step-templates/unzip.json.human @@ -0,0 +1,56 @@ +{ + "Id": "d2f144e7-e998-4229-ab7c-1013865070d5", + "Name": "Unzip", + "Description": "Unzip file", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$shell_app=new-object -com shell.application\r +\r +$FOF_SILENT_FLAG = 4\r +$FOF_NOCONFIRMATION_FLAG = 16\r +\r +if (Test-Path $filename)\r +{\r + Write-Host Unzipping $filename\r + $zip_file = $shell_app.namespace(\"$filename\")\r + $destination = $shell_app.namespace(\"$dest\")\r + $destination.Copyhere($zip_file.items(), $FOF_SILENT_FLAG + $FOF_NOCONFIRMATION_FLAG)\r +}\r +else\r +{\r + Write-Host File $filename does not exist\r +}\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "filename", + "Label": "FIle to unzip", + "HelpText": "Zip filename path", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "dest", + "Label": "Destination", + "HelpText": "Destination folder.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-08-03T13:45:04.765+00:00", + "LastModifiedBy": "pitrew", + "$Meta": { + "ExportedAt": "2015-08-03T13:45:19.309+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "unzip" +} diff --git a/step-templates/update-hosts-file.json.human b/step-templates/update-hosts-file.json.human new file mode 100644 index 000000000..8e7d60ced --- /dev/null +++ b/step-templates/update-hosts-file.json.human @@ -0,0 +1,67 @@ +{ + "Id": "79d6ce26-71bc-44c8-9228-d90cc7de9991", + "Name": "Update Hosts File", + "Description": "This template will update the value of your HOSTS file to what you define in this step. +It will skip all lines that start with \"#\" (comments), and all other lines will be deleted. +Then the values defined in this step will be added. You can define as many entry lines as you want.", + "ActionType": "Octopus.Script", + "Version": 9, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$hostsPath = \"$env:windir\\system32\\drivers\\etc\\hosts\" +Write-Host \"Opening HOSTS file:$hostsPath\" + +$hostEntries = $OctopusParameters[\"uhf_Hosts\"] +Write-Verbose \"hostEntries:$hostEntries\" + +$lines = (Get-Content $hostsPath) + +for ($i = 0; $i -lt $lines.Length; $i++) { + $line = $lines[$i] + if ($line -match \"^#\" -or $line -match \"^[\\s\\t]*$\") { + continue + } + + $line = \"\" + + $lines[$i] = $line +} + +foreach ($hostEntry in $hostEntries.Split(\"`n\")) { + Write-Verbose $hostEntry + $parts = $hostEntry.Split(\",\") + $ip = $parts[0] + Write-Verbose $ip + $hostname = $parts[1] + Write-Verbose $hostname + $line = \"$ip`t`t`t$hostname\" + Write-Host \"Adding entry:$line\" + $lines += $line +} + +Out-File -FilePath $hostsPath -Encoding ascii -InputObject $lines.Where({ $_ -ne \"\"}) -Force" + }, + "Parameters": [ + { + "Id": "a815bb21-5ca4-4645-9076-e0e9c9042006", + "Name": "uhf_Hosts", + "Label": "Hosts Entries", + "HelpText": "A comma delimited list of IP's and Hostnames", + "DefaultValue": "127.0.0.1,hostname.xyz +127.0.0.1,hostname2.xyz", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedBy": "BlueWombat", + "$Meta": { + "ExportedAt": "2019-12-02T05:53:03.714Z", + "OctopusVersion": "2019.10.8", + "Type": "ActionTemplate" + }, + "Category": "hosts-file" +} diff --git a/step-templates/upgrade-octopus-server.json.human b/step-templates/upgrade-octopus-server.json.human new file mode 100644 index 000000000..6505b3fc2 --- /dev/null +++ b/step-templates/upgrade-octopus-server.json.human @@ -0,0 +1,165 @@ +{ + "Id": "4b3a1f09-1827-41bb-88a4-894c6317922b", + "Name": "Upgrade Octopus Server", + "Description": "This step downloads the latest version of Octopus Server and upgrades an existing instance. Run this step on a tentacle that has privileges to install software and start/stop services on the target server. + +**Run this after a database backup step** + +To Use: +- Install a tentacle on the Octopus Server machine with privileges to install software and start/stop services +- Add that tentacle to an environment and with a unique role +- Setup a project for the upgrade process +- Add a database backup step for your Octopus Server database +- Add this step, selecting it to run on just the role previously configured +- Create a release +- Deploy that release whenever an upgrade is needed + +NB: The deployment will show as \"Timed Out\" when the server comes back online", + "ActionType": "Octopus.Script", + "Version": 10, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +Function GetLatestVersionOrSpecificVersionOfOctopusServer() +{ + if([string]::IsNullOrEmpty($SpecificOctopusServerVersionToInstall)) + { + Write-Host \"No specific version has been selected, getting latest version from octopusdeploy.com\" + $versions = Invoke-WebRequest https://octopusdeploy.com/download/upgrade/v3 -UseBasicParsing | ConvertFrom-Json + $version = $versions[-1].Version + return $version + } + else + { + \tWrite-Host \"Specific version has been selected\" + return $SpecificOctopusServerVersionToInstall + } +} + +Function GetCurrentlyInstalledVersionOfOctopusServer() +{ + $InstalledVersion = (get-item \"$InstallPath\\Octopus.Server.exe\").VersionInfo.fileversion + return $installedVersion +} + +Function DownloadOctopusServer([string] $versionNumber) +{ + Write-Host \"Downloading Octopus Server version $versionNumber\" + $tempFile = [System.IO.Path]::GetTempFileName() + \ttry + { + (New-Object System.Net.WebClient).DownloadFile(\"https://download.octopusdeploy.com/octopus/Octopus.$versionNumber-x64.msi\", $tempFile) + } + catch + { + Write-Host \"Exception occurred\" + echo $_.Exception|format-list -force + } + Write-Host \"Download completed\" + return $tempFile +} + +Function StopOctopusServer() +{ + Write-Host \"Stopping Server\" + . \"$InstallPath\\Octopus.Server.exe\" service --stop --console --instance $InstanceName +} + +Function InstallOctopusServer([object] $tempFile) +{ + Write-Host \"Installing ...\" + msiexec /i $tempFile /quiet | Out-Null +} + +Function RemoveTempFile([object] $temporaryFile) +{ + Write-Host \"Deleting downloaded installer\" + Remove-Item $temporaryFile +} + +Function StartOctopusServer() +{ + Write-Host \"Starting Octopus Server\" + . \"$InstallPath\\Octopus.Server.exe\" service --start --console --instance $InstanceName +} + +Function InstallSetVersionOnOctopusServer([string] $currentlyInstalledVersion, [string] $selectedVersionToInstall){ + + Write-Host \"Currently installed version: $currentlyInstalledVersion\" + Write-Host \"Selected version to install: $selectedVersionToInstall\" + + $tempFile = DownloadOctopusServer $selectedVersionToInstall + StopOctopusServer + InstallOctopusServer $tempFile + RemoveTempFile $tempFile + StartOctopusServer +} + +Function StartInstallationOfOctopusServer () { + + ## Get the current state of Octopus Server + $selectedVersionToInstall = GetLatestVersionOrSpecificVersionOfOctopusServer + $currentlyInstalledVersion = GetCurrentlyInstalledVersionOfOctopusServer + + + if([version]$selectedVersionToInstall -eq [version]$currentlyInstalledVersion) + { + Write-host \"Octopus Server has already been installed with version $currentlyInstalledVersion\" + } + else + { + InstallSetVersionOnOctopusServer $currentlyInstalledVersion $selectedVersionToInstall + } +} + +StartInstallationOfOctopusServer" + }, + "Parameters": [ + { + "Id": "41442ee8-d56a-4a03-8c4b-af4c02245d9e", + "Name": "InstanceName", + "Label": "Instance Name", + "HelpText": "The name of your octopus instance. For the default instance use `OctopusServer`.", + "DefaultValue": "OctopusServer", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "18ede5ad-24a8-4796-a6e2-1b49a4cd7df2", + "Name": "InstallPath", + "Label": "InstallPath", + "HelpText": "The installation path to Octopus Deploy Server. Not the instance directory", + "DefaultValue": "C:\\Program Files\\Octopus Deploy\\Octopus", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "098375bf-ff27-47c3-93ce-0191aade1b21", + "Name": "SpecificOctopusServerVersionToInstall", + "Label": "Version number of Octopus Server which is going to be installed", + "HelpText": "Selects a specific version of Octopus Server to install. This is especially handy for a downgrade. The version number must be valid though.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "Verzada", + "Website": "/step-templates/4b3a1f09-1827-41bb-88a4-894c6317922b", + "Logo": "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAC1QTFRFT6Tl////L5Pg8vj9Y67omsvwPJrisdfzfbzs5fL7y+T32Ov5isLucLXqvt31CJPHWwAABMJJREFUeNrs3deW4jAMAFDF3U75/89dlp0ZhiU4blJEjvQ8hYubLJsA00UCBCIQgQhEIAIRiEAEIhCBCEQgAhGIQAQiEIEIhD8kJm+t+QprfdKfB9HbYpx6CWfspj8HMi+gMgHL/AmQA8W3JTKH+ALFvzCeL0RbpyoCPE9IJeNOSQwh5Z3qd6yRGWQ2qi2cZQWxqj1WzQYSjeoJmJlAklOd4VlArOqPhQEkqBERToeMcfRJBkC0Uep8CfBpjz4JsHJ0zF3dkEWNje0kiB/sUC6eApndaIiCMyAa1PiwJ0AWhRGJHJJQHG2dC7h1rNbO1QOxSA7lNCkkKrQIpJCAB1GREILYIC1NAiwbpKFJgGWDNExcwGstfExcZBCHC6nOglshHtmhViLIig1RNBCN7qjtW8C0Z1UvJcC1Z9XmwMBzzvobmgAyEzgq91dtEEsBsQSQQAFZCSBAATEEEApHZbrVBIkkEIUPSVeB+KtALA0kXQUSrwKZBCIQBnk8Y4i5CsReBeKvkqLM+BCSDWJlrZFvGk9SRTHshkgjZCGAaArIxm3H3grhVzFlW2msfl1ca79UJ1bofYvsDHHlNdTZnlh5MghuPd5NdBDUNZHyCkfktIh03XzALGRPlBDPac7qgWjHZzWcmF5zmmkhidMQ6boKiDXcDTUEaylZqCGJ0Vjvu/fLJtHqhSANEvqb2OYqkOUqEHuVMbJcZdZCGiPhKhC4yjqiIjEE7XThMp8fAWII3mY3kUIQD+AMKQTzPiBhgQ63HlT/KSvgtoi0dq5mCPah1UIE0eh3sT0NhOByvKeAkFzi8PgQomumFhsyOxpIzZN4gLOj5plVwNpR0b2AuePWKBEHQu24pSsJA+LVCeHHQxZ1SiyDIdqok8IOhSSnTottHEQTdyt4ettAj4KkzA4dMikk2Dht2S5ptm1vswnPDxn0YyDZ5oDM3iToo2T5voWaYe+Q+vdjH80QyAzZhCgcDtLMI1Tmtz9w++XHgziHQHJJu/OZ3bs9Xn8gQ72NcP3dKqEfkp10F51xhoIi2I91R+LurXV/5q7pH+wx061CzO16oSQleMyr8fXvwMA0Pro8432DPD/ySx8XrHfSuDAM8n6UhnjQabaiXf5Bq/lREHvEeNtn1rJ08+C/uXkQZHeguxAPC3UvtcJYUogLzZX5hhZZvS6onG5lxXtzWGaygwb79vT/IXhdlNibwlKYOR6T8xjI7W8n+xV7T+GH4tMzWwR+lZhRkJYSsC0thpmCYqyngOz3rN2FLBZ2wZflBCggUHF0Vnp88JKienzIXLSEZCZqU7IKr/gQW9yx3pzV7Y9kvWZWTRRIqDmTtRUnU7b2lLcTYmoqHqnmiO1poER0SPkAeZMAZxaJx0Y3TCdAclsIqDz03ALcyxfTCZBsthoGXWmigGyVhWPLFJJfuuKQWycoEFdXbH4dJJoJxNR1eD/kshz6yn48cF8yW8sFoitflB1w6Q8n+/15Za7oA17/pYNmYgP5fmWm8L1NOHPWgK8kuFew1/JXtOA0yJCv7ah7X8ObUuT5kObU30+fDZm8+zqP+HTIpK0xQ796b5Kv2hSIQAQiEIEIRCACEYhABCIQgQhEIAIRiEAEIpBf8UeAAQAEjtYmlDTcCgAAAABJRU5ErkJggg==", + "$Meta": { + "ExportedAt": "2018-03-09T15:07:55.181Z", + "OctopusVersion": "2018.3.2", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} diff --git a/step-templates/upgrade_to_latest_tentacle.json.human b/step-templates/upgrade_to_latest_tentacle.json.human new file mode 100644 index 000000000..e9902b1ff --- /dev/null +++ b/step-templates/upgrade_to_latest_tentacle.json.human @@ -0,0 +1,221 @@ +{ + "Id": "349c64f5-c9bd-4b55-bff7-cc497a1f898e", + "Name": "Upgrade To Latest Tentacle", + "Description": "Upgrades a list of machines to the latest version of Tentacle. Machines can be targeted by Environment and/or Role or Machine Name. + +This needs to be run directly on a worker or the octopus server itself.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Define working variables +$OctopusUrl = [string]$OctopusParameters['UpdateTentacles.OctopusUrl'].Trim() +$ApiKey = [string]$OctopusParameters['UpdateTentacles.ApiKey'].Trim() +$SpaceName = [string]$OctopusParameters['UpdateTentacles.SpaceName'].Trim() +$EnvironmentNames = [string[]]$OctopusParameters['UpdateTentacles.EnvironmentNames'] +$RoleNames = [string[]]$OctopusParameters['UpdateTentacles.RoleNames'] +$MachineNames = [string[]]$OctopusParameters['UpdateTentacles.MachineNames'] +$WhatIf = [bool]::Parse($OctopusParameters['UpdateTentacles.WhatIf']) +$Wait = [bool]::Parse($OctopusParameters['UpdateTentacles.Wait']) + +# Remove white space and blank lines. +if ($null -ne $EnvironmentNames) { + $EnvironmentNames = $EnvironmentNames.Split(\"`n\").Trim().Where({$_}) # Trim white space and blank lines. +} +if ($null -ne $RoleNames) { + $RoleNames = $RoleNames.Split(\"`n\").Trim().Where({$_}) # Trim white space and blank lines. +} +if ($null -ne $MachineNames) { + $MachineNames = $MachineNames.Split(\"`n\").Trim().Where({$_}) # Trim white space and blank lines. +} + +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +$ErrorActionPreference = \"Stop\" + +$header = @{ \"X-Octopus-ApiKey\" = $ApiKey } + +if ($null -eq $SpaceName) { + $baseUri = \"$OctopusUrl/api\" +} else { + $space = (Invoke-RestMethod -Method Get -Uri \"$OctopusUrl/api/spaces/all\" -Headers $header) | Where-Object { $_.Name -eq $SpaceName } + + if ($null -eq $space) { + throw \"Space Name $SpaceName does not exist.\" + } else { + Write-Verbose \"Using Space $SpaceName.\" + } + + $baseUri = \"$OctopusUrl/api/$($space.Id)\" +} + +# Start with no machines +$allMachines = @() + +# Add machines for each requested environment. +foreach ($environmentName in $EnvironmentNames) { + $environment = (Invoke-RestMethod -Method Get -Uri \"$baseUri/environments/all\" -Headers $header) | Where-Object { $_.Name -eq $environmentName } + + if ($null -eq $environment) { + throw \"Environment $environmentName does not exist.\" + } else { + Write-Verbose \"Adding machines from Environment $environmentName.\" + } + + $allMachines += (Invoke-RestMethod -Method Get -Uri \"$baseUri/environments/$($environment.Id)/machines?take=$([int32]::MaxValue)\" -Headers $header).Items +} + +# If roles are specifed, include only machines in the specicied roles. Otherwise don't filter. +if ($null -eq $RoleNames) { + $roleFilteredMachines += $allMachines +} else { + $roleFilteredMachines = @() + foreach ($roleName in $RoleNames) { + $roleFilteredMachines += $allMachines | Where-Object { $_.Roles -contains $roleName } + } +} + +# Add each specific machine requested. +$roleFilteredMachines += (Invoke-RestMethod -Method Get -Uri \"$baseUri/machines/all\" -Headers $header) | Where-Object { $_.Name -in $MachineNames } + +# Create array of unique IDs to target. +$uniqueIDs = [array]($roleFilteredMachines.Id | Sort-Object -Unique) + +if (-not $uniqueIDs) { + Write-Highlight \"No machines were targeted. Exiting...\" + exit +} + +# Build json payload, targeting unique machine IDs. +$jsonPayload = @{ + Arguments = @{ + MachineIds = $uniqueIDs + } + Description = \"Upgrade Tentacle version.\" + Name = \"Upgrade\" +} + +if ($WhatIf) { + Write-Host \"Upgrading tentacles on:\" + Write-Host $(($roleFilteredMachines.Name | Sort-Object -Unique) -join \"`r\") +} else { + Write-Verbose \"Upgrading tentacles on:\" + Write-Verbose $(($roleFilteredMachines.Name | Sort-Object -Unique) -join \"`r\") + $task = Invoke-RestMethod -Method Post -Uri \"$baseUri/tasks\" -Headers $header -Body ($jsonPayload | ConvertTo-Json -Depth 10) + Write-Highlight \"$($task.Id) started. Progress can be monitored [here]($OctopusUrl$($task.Links.Web)?activeTab=taskLog)\" + + if ($Wait) { + do { + \t# Output the current state of the task every five seconds. + $task = Invoke-RestMethod -Method Get -Uri \"$baseUri/tasks/$($task.Id)\" -Headers $header + $task + Start-Sleep -Seconds 5 + } while ($task.IsCompleted -eq $false) + } +}" + }, + "Parameters": [ + { + "Id": "51ea8dde-8df6-4621-8c8a-f5cacf485c5e", + "Name": "UpdateTentacles.OctopusUrl", + "Label": "Octopus Url", + "HelpText": "**Required** + +The url of the server where the upgrades will be performed. + +e.g. `https://octopus.server.com`", + "DefaultValue": "#{Octopus.Web.ServerUri}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b8d147e0-de55-488e-a69f-9b212fd99559", + "Name": "UpdateTentacles.ApiKey", + "Label": "API Key", + "HelpText": "**Required** + +The api key of a user permitted to perform tentacle upgrades.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e0d7450e-01de-47b2-abd2-3b22aa12a295", + "Name": "UpdateTentacles.SpaceName", + "Label": "Space", + "HelpText": "The space to use when targeting machines.", + "DefaultValue": "#{Octopus.Space.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fb2c3110-d597-4729-9c86-d6dc5314dfb7", + "Name": "UpdateTentacles.EnvironmentNames", + "Label": "Environments", + "HelpText": "A list of environments to be used for targeting machines. If no environments are specified, only `Machines Names` will be targeted. + +Multiple environments can be specified, one per line.", + "DefaultValue": "#{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "2ec7ff8e-6dd3-46a8-9ef6-5869d14b6a03", + "Name": "UpdateTentacles.RoleNames", + "Label": "Roles", + "HelpText": "A list of roles to filter the machines found in the specified `Environments`. Only machines that are members of a specified role will be targeted. + +Multiple roles can be specified, one per line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "e68ee3f0-bf4b-47ce-bba2-9a2e50c80595", + "Name": "UpdateTentacles.MachineNames", + "Label": "Machine Names", + "HelpText": "Additional machine names to upgrade. These will be added to the list after `Environments` and `Roles` have been processed. + +Multiple machines can be specified, one per line.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "9e742501-e215-4217-8e32-a6fb5ac16b6d", + "Name": "UpdateTentacles.WhatIf", + "Label": "What If", + "HelpText": "This can be used to test the targeting parameters. When selected, a list of target machines will be listed, but no machines will be upgraded.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "8e3f8d65-c117-40b6-b654-93f8ab65776d", + "Name": "UpdateTentacles.Wait", + "Label": "Wait", + "HelpText": "Wait until the upgrade is finished.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2023-11-21T00:42:08.257Z", + "OctopusVersion": "2023.2.13113", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "REOScotte", + "Category": "octopus" +} diff --git a/step-templates/variables-find-unreplaced.json.human b/step-templates/variables-find-unreplaced.json.human new file mode 100644 index 000000000..65a3abcb2 --- /dev/null +++ b/step-templates/variables-find-unreplaced.json.human @@ -0,0 +1,166 @@ +{ + "Id": "0b753c94-c12b-46f3-bb82-459e27bbe812", + "Name": "Variables - Find Unreplaced", + "Description": "Searches `Web.config` or `App.config` files looking for Octopus Deploy variables that have not been replaced. Alternatively, any arbitrary file can be checked.", + "ActionType": "Octopus.Script", + "Version": 26, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "function Find-Unreplaced { + <# + .SYNOPSIS + Looks for Octopus Deploy variables + .DESCRIPTION + Analyses `Web/App.Release.configs`, etc... looking for Octopus Deploy + variables that have not been replaced. + .EXAMPLE + Find-Unreplaced C:\\Folder *.config, *.ps1 + .PARAMETER Path + Root folder to search in + .PARAMETER Files + An array of all the files or globs to search in. Defaults to *.config + .PARAMETER Exclude + Files to ignore + .PARAMETER Recurse + Should the cmdlet look for the file types recursively + .PARAMETER TreatAsError + Will cause the script to write an Error instead of a warning if variables are found + #> + [CmdletBinding()] + param + ( + [Parameter( + Position=0, + Mandatory=$true, + ValueFromPipeline=$True)] + [string] $Path, + + [Parameter( + Position=1, + Mandatory=$false)] + [string[]] $Files = @('*.config'), + + [Parameter(Mandatory=$false)] + [string[]] $Exclude, + + [Parameter(Mandatory=$false)] + [switch] $Recurse, + + [Parameter(Mandatory=$false)] + [switch] $TreatAsError + ) + + process { + Write-Host \"Searching for files in '$Path'\" + if (-not (Test-Path $Path -PathType container)) { + Write-Error \"The path '$Path' does not exist or is not a folder.\" + return + } + + if (-not $Recurse) { + # For some reason, a splat is required when not recursing + if ($Path.EndsWith(\"\\\")) { $Path += \"*\" } else { $Path += \"\\*\" } + } + + $clean = $true + + $found = Get-ChildItem -Path $Path -Recurse:$Recurse -Include $Files -Exclude $Exclude -File + foreach ($file in $found) { + Write-Host \"Found '$file'.`nSearching for Octopus variables...\" -NoNewline + $matches = Select-String -Path $file -Pattern \"#\\{([^}]*)\\}\" -AllMatches + $clean = $clean -and ($matches.Count -eq 0) + if ($clean) { + Write-Host \"clean\" + } else { + Write-Host \"done`n$matches\" + } + } + + if (-not $clean) { + $msg = \"Unreplaced Octopus Variables were found.\" + if ($TreatAsError) { + Write-Error $msg + } else { + Write-Warning $msg + } + } + } +} + +if (-not $Path) { throw \"A Path must be specified\" } +if (-not $Files) { throw \"At least one File must be specified\" } + +$spPaths = $Path -split \"`n\" | Foreach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrEmpty($_) } +$spFiles = $Files -split \";\" | Foreach-Object { $_.Trim() } +$spExcludes = $Exclude -split \";\" | Foreach-Object { $_.Trim() } +$bRecurse = $Recurse -eq 'True' +$bTreatAsError = $TreatAsError -eq 'True' + +$spPaths | Find-Unreplaced -Files $spFiles -Exclude $spExcludes -Recurse:$bRecurse -TreatAsError:$bTreatAsError + +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "7809f4f9-7388-4067-ae0f-e5d858ad9395", + "Name": "Path", + "Label": "Path", + "HelpText": "The folders to search for files in. Enter multiple paths on separate lines.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "f9389ec3-1602-48b1-9f03-c95af304a3c2", + "Name": "Files", + "Label": "Files", + "HelpText": "An array of all the files or globs to search in. Defaults to `*.config`. Multiple items can be separated with a semicolon `;`.", + "DefaultValue": "*.config", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "0b54fdbd-12ac-4fc8-aa26-410cd1a3bd77", + "Name": "Exclude", + "Label": "Exclude", + "HelpText": "Files or globs to ignore. Multiple items can be separated with a semicolon `;`.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "29f81ee9-529b-45f6-8452-8e8f464c3225", + "Name": "Recurse", + "Label": "Recurse", + "HelpText": "Determines whether or not the step should look through the items in `Path` recursively.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "fd64a155-abe9-483e-b954-e182fbb777b0", + "Name": "TreatAsError", + "Label": "Treat as an error?", + "HelpText": "Determines if the step will cause the build to fail or issue a warning. By default it will only warn of a problem.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2021-09-14T10:40:15.430Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2021-09-14T10:40:15.430Z", + "OctopusVersion": "2021.2.7428", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} diff --git a/step-templates/variables-substitute-in-files.json.human b/step-templates/variables-substitute-in-files.json.human new file mode 100644 index 000000000..d22e17d77 --- /dev/null +++ b/step-templates/variables-substitute-in-files.json.human @@ -0,0 +1,297 @@ +{ + "Id": "a3078bc0-0615-4220-84e8-bb1f271a90f9", + "Name": "Variables - Substitute in Files", + "Description": "This step template attempts to replace Octopus variable placeholders in one or more file(s) with their values from the `$OctopusParameters` Dictionary. + +--- + +### Version 18+: + +Version 18 and higher pins the version of Octostache and Sprache (an Octostache dependency) to the following versions: +- Octostache: [version 3.2.1](https://www.nuget.org/packages/Octostache/3.2.1) +- Sprache: [version 2.3.1](https://www.nuget.org/packages/Sprache/2.3.1) + +This is to resolve issues when running the step template on Windows, where the .NET 4.0 (`net40`) version is no longer present in newer versions of these libraries. + +**As a result, no new functionality available in Octostache, such as new commands/functions, will be present** + +If you need later functionality, use the built-in Octopus [variable substitution feature](https://octopus.com/docs/projects/variables/variable-substitutions). + +### Breaking Change - version 15+: + +Previous versions of this step template would attempt to find the Octostache library bundled with [Calamari](https://github.com/OctopusDeploy/Calamari) as part of a deployment or runbook. Due to compatibility issues running the `netstandard` version of Octostache bundled with newer versions of Calamari on Windows PowerShell, **this functionality has now been removed**. + +As a result, **version 15** and higher of this step template now look for Octostache on the deployment target or worker in the NuGet cache. If it's not found, it will attempt to download two required binaries: + +1. Octostache from [nuget.org](https://www.nuget.org/packages/Octostache) +1. Sprache (an Octostache dependency) from [nuget.org](https://www.nuget.org/packages/Sprache) + +If access to [nuget.org](https://www.nuget.org) is restricted on your deployment target or worker, this step template may fail to execute successfully. + +In that case, it's recommended to use the built-in Octopus [variable substitution feature](https://octopus.com/docs/projects/variables/variable-substitutions). + +--- + +### Notes: + +- Tested on Windows PowerShell only running Octopus **2023.2.4954**. +", + "ActionType": "Octopus.Script", + "Version": 19, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\"; +function Get-Exceptions { + param ($ExceptionObject) + + if ($null -ne $ExceptionObject.InnerException) { + Get-Exceptions -ExceptionObject $ExceptionObject.InnerException + } + + Write-Warning \"Exception is: $($ExceptionObject.Message)\" +} + +function Resolve-OctopusVariablesInTemplate { + <# +.SYNOPSIS +\tResolves Octopus variables in files with their values from a OctopusParameters + +.DESCRIPTION +\tLooks for files using Get-ChildItem and in each of the files replaces ${Variable} with the value from $OctopusParameters. +\tFiles are written back using UTF-8. +\tRequires PowerShell 3.0 or higher. + +.PARAMETER Path +\tPassed to Get-ChildItem to find the files you want to process + +.PARAMETER Filter +\tPassed to Get-ChildItem to find the files you want to process + +.PARAMETER Include +\tPassed to Get-ChildItem to find the files you want to process + +.PARAMETER Exclude +\tPassed to Get-ChildItem to find the files you want to process +\t +.PARAMETER Recurse +\tPassed to Get-ChildItem to find the files you want to process + +#> + Param( + [string]$Path, + [string]$Filter = \"*.config\", + [string[]]$Include, + [string[]]$Exclude, + [switch]$Recurse, + [string]$OctostacheLocation + ) +\t + if (-not $OctopusParameters) { throw \"No OctopusParameters found\" } +\t + Write-Output \"Tentacle Version: $env:TentacleVersion\" + Write-Output \"PowerShell version...\" + Write-Output $PSVersionTable + Write-Output \"Path = $Path\" +\t + Write-Output \"Getting target files...\" + $TargetFiles = Get-ChildItem -File -Path $Path -Filter $Filter -Include $Include -Exclude $Exclude -Recurse:$Recurse + if ($TargetFiles.Count -eq 0) { + Write-Warning \"`tDid not find any files to process!\" + return + } + else { + Write-Output \"`tFound $($TargetFiles.Count) file(s)\" + } +\t + Import-Octostache -OctostacheLocation $OctostacheLocation + + foreach ($File in $TargetFiles) { + Resolve-VariablesUsingOctostache $File.FullName + } +} + + +function Import-Octostache { + Param( + [string]$OctostacheLocation + ) + + $OctostachePath = $null + $SprachePath = $null + + Write-Output \"Searching for installed version of Octostache.\" + if (-not [string]::IsNullOrWhiteSpace($OctostacheLocation)) { + if (-not (Test-Path $OctostacheLocation)) { + Write-Error \"Octostache path: $OctostacheLocation doesnt exist.\" + Exit 1 + } + Write-Verbose \"Searching in $OctostacheLocation for Octostache.dll\" + $OctostacheLibraryLocations = Get-ChildItem -File -Path $OctostacheLocation -Filter \"OctoStache.dll\" -Recurse + $OctostachePath = ($OctostacheLibraryLocations | Select-Object -First 1).FullName + Write-Verbose \"Searching in $OctostacheLocation for Sprache.dll\" + $SpracheLibraryLocations = Get-ChildItem -File -Path $OctostacheLocation -Filter \"Sprache.dll\" -Recurse + $SprachePath = ($SpracheLibraryLocations | Select-Object -First 1).FullName + } + else { + try { + $OctostachePackage = (Get-Package Octostache -ErrorAction Stop) | Select-Object -First 1 + } + catch { + $OctostachePackage = $null + } + + if ($null -eq $OctostachePackage) { + Write-Output \"Downloading Octostache (v3.2.1) from nuget.org.\" + Install-Package Octostache -MaximumVersion \"3.2.1\" -source https://www.nuget.org/api/v2 -Force -SkipDependencies + $OctostachePackage = (Get-Package Octostache) | Select-Object -First 1 + } + + $OctostachePath = Join-Path (Get-Item $OctostachePackage.source).Directory.FullName \"lib/net40/Octostache.dll\" + + try { + $SprachePackage = (Get-Package Sprache -ErrorAction Stop) | Select-Object -First 1 + } + catch { + $SprachePackage = $null + } + + if ($null -eq $SprachePackage) { + Write-Output \"Downloading Sprache (v2.3.1) from nuget.org.\" + Install-Package Sprache -MaximumVersion \"2.3.1\" -source https://www.nuget.org/api/v2 -Force -SkipDependencies + $SprachePackage = @(Get-Package Sprache) | Select-Object -First 1 + } + + $SprachePath = Join-Path (Get-Item $SprachePackage.source).Directory.FullName \"lib/net40/Sprache.dll\" + } + + Write-Verbose \"Octostache path: $OctostachePath\" + Write-Verbose \"Sprache path: $SprachePath\" + + if ([string]::IsNullOrWhiteSpace($OctostachePath) -or [string]::IsNullOrWhiteSpace($SprachePath)) { + Write-Error \"Couldnt locate either the Octostache or Sprache library.\" + Exit 1 + } + + Write-Output \"Adding type $SprachePath\" + Add-Type -Path $SprachePath + + Write-Output \"Adding type $OctostachePath\" + Add-Type -Path $OctostachePath +} + +function Resolve-VariablesUsingOctostache { + Param( + [string]$TemplateFile + ) +\t\t + Write-Output \"Loading template file $TemplateFile...\" + $TemplateContent = Get-Content -Raw $TemplateFile + Write-Output \"`tRead $($TemplateContent.Length) bytes\" +\t + $Dictionary = New-Object -TypeName Octostache.VariableDictionary +\t + # Load the hastable into the dictionary + Write-Output \"Loading `$OctopusParameters...\" + foreach ($Variable in $OctopusParameters.GetEnumerator()) { + Write-Verbose \"#{$($Variable.Key)} = $($Variable.Value)\" + $Dictionary.Set($Variable.Key, $Variable.Value) + } +\t + Write-Output \"Resolving variables...\" + + try { + $EvaluatedTemplate = $Dictionary.Evaluate($TemplateContent) + } + catch { + Get-Exceptions -ExceptionObject $Error.Exception + throw + } +\t + Write-Output \"Writing the resolved template to $($TemplateFile) (UTF8 encoding)\" + #$EvaluatedTemplate | Out-File $TemplateFile -Force\t-Encoding UTF8 + $EvaluatedTemplate -join \"rn\" | Set-Content $TemplateFile -NoNewLine -Encoding UTF8 -Force + Write-Output \"Done!\" +} + +$FunctionParameters = @{} + +if ($null -ne $OctopusParameters['Path']) { $FunctionParameters.Add('Path', $OctopusParameters['Path']) } +if ($null -ne $OctopusParameters['Filter']) { $FunctionParameters.Add('Filter', $OctopusParameters['Filter']) } +if ($null -ne $OctopusParameters['Include']) { $FunctionParameters.Add('Include', $($OctopusParameters['Include'] -split \"`n\")) } +if ($null -ne $OctopusParameters['Exclude']) { $FunctionParameters.Add('Exclude', $($OctopusParameters['Exclude'] -split \"`n\")) } +if ($null -ne $OctopusParameters['Recurse']) { $FunctionParameters.Add('Recurse', [System.Convert]::ToBoolean($OctopusParameters['Recurse'])) } +if (-not [string]::IsNullOrWhiteSpace($OctopusParameters['Variables.SubstituteInFiles.OctostacheLocation'])) { $FunctionParameters.Add('OctostacheLocation', $OctopusParameters['Variables.SubstituteInFiles.OctostacheLocation']) } + +Resolve-OctopusVariablesInTemplate @FunctionParameters", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Name": "Path", + "Label": "Path", + "HelpText": "Specifies a path to one or more locations. Wildcards are permitted. The default location is the current directory (.).", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Filter", + "Label": "Filter", + "HelpText": "Specifies, as a string array, a filter for an item or items that are included in the operation. Wildcards are permitted.", + "DefaultValue": "*.config", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Include", + "Label": "Include", + "HelpText": "Specifies, as a string array, an item or items that are included in the operation. Enter a path element or pattern, such as *.txt. Wildcards are permitted. Leave this empty if you have specified a Filter.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "Exclude", + "Label": "Exclude", + "HelpText": "Specifies, as a string array, an item or items that are excluded in the operation. Enter a path element or pattern, such as *.txt. Wildcards are permitted. Leave this empty if you have specified a Filter.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "Recurse", + "Label": "Recurse", + "HelpText": "Search the specified locations and in all sub-directories of those locations.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "Variables.SubstituteInFiles.OctostacheLocation", + "Label": "Octostache library location", + "HelpText": "*Optional* - Provide the location that contains both of the required `Octostache.dll` and `Sprache.dll` libraries. + +- *If this value is not provided, the libraries will be searched for on the target or worker, and if not found will be downloaded from [nuget.org](https://www.nuget.org).* +- If more than one matching file is found, the first one will be used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedAt": "2023-04-04T10:37:47.270Z", + "LastModifiedBy": "twerthi", + "$Meta": { + "ExportedAt": "2024-02-05T21:47:52.063Z", + "OctopusVersion": "2023.4.8290", + "Type": "ActionTemplate" + }, + "Category": "octopus" +} diff --git a/step-templates/variables-substitute-in-json-file.json.human b/step-templates/variables-substitute-in-json-file.json.human new file mode 100644 index 000000000..682008442 --- /dev/null +++ b/step-templates/variables-substitute-in-json-file.json.human @@ -0,0 +1,173 @@ +{ + "Id": "9E85B3EB-7C19-4F3A-9E9E-71923198EE09", + "Name": "Variables - Substitute in Json file", + "Description": "Use this step template after the Deploy API step template to substitute variables in a target json file like appsettings.json with Octopus scoped variables. Currently supported data types are string, boolean, interger, decimal and non-empty string arrays and can replace these types inside nested types as well as long as you name them like prop:subprop:subsubprop (like AppMetrics:GlobalTags:env) in octopus like we do today...this should be enough for most of the needs for files like appsettings.json", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if ($result -eq $null) { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +$ErrorActionPreference = \"Stop\" + +function UpdateJsonFile ([hashtable]$variables, [string]$fullpath) { + Write-Host 'Starting the json file variable substitution' $variables.Count + if ($variables -eq $null) { + throw \"Missing parameter value $variables\" + } + +\t$pathExists = Test-Path $fullpath +\tif(!$pathExists) { +\t\tWrite-Host 'ERROR: Path '$fullpath ' does not exist' +\t\tExit 1 +\t} + +\t$json = Get-Content $fullpath -Raw | ConvertFrom-Json + Write-Host 'Json content read from file' + +\tforeach($variable in $variables.GetEnumerator()) { +\t\t$key = $variable.Key + Write-Host 'Processing' $key + $keys = $key.Split(':') +\t\t$sub = $json +\t\t$pre = $json +\t\t$found = $true +\t\t$lastKey = '' +\t\tforeach($k in $keys) { +\t\t\tif($sub | Get-Member -name $k -Membertype Properties){ +\t\t\t\t$pre = $sub +\t\t\t\t$sub = $sub.$k +\t\t\t} +\t\t\telse +\t\t\t{ +\t\t\t\t$found = $false +\t\t\t\tbreak +\t\t\t} + +\t\t\t$lastKey = $k +\t\t} + +\t\tif($found) { + Write-Host $key 'found in Json content' + if($pre.$lastKey -eq $null) { + Write-Host $key 'is null in the original source json...values CANNOT be null on the source json file...exiting with 1' + Exit 1 + } + +\t\t\t$typeName = $pre.$lastKey.GetType().Name +\t\t\t[bool]$b = $true +\t\t\t[int]$i = 0 +\t\t\t[decimal]$d = 0.0 +\t\t\tif($typeName -eq 'String'){ +\t\t\t\t$pre.$lastKey = $variable.Value +\t\t\t} +\t\t\telseif($typeName -eq 'Boolean' -and [bool]::TryParse($variable.Value, [ref]$b)) { +\t\t\t\t$pre.$lastKey = $b +\t\t\t} +\t\t\telseif($typeName -eq 'Int32' -and [int]::TryParse($variable.Value, [ref]$i)){ +\t\t\t\t$pre.$lastKey = $i +\t\t\t} +\t\t\telseif($typeName -eq 'Decimal' -and [decimal]::TryParse($variable.Value, [ref]$d)){ +\t\t\t\t$pre.$lastKey = $d +\t\t\t} +\t\t\telseif($typeName -eq 'Object[]') { + if($pre.$lastKey.Length -ne 0 -and $pre.$lastKey[0].GetType().Name -eq 'String') { +\t\t\t\t $pre.$lastKey = $variable.Value.TrimStart('[').TrimEnd(']').Split(',') + } + else { + Write-Host 'ERROR: Cannot handle ' $key ' with type ' $typeName +\t\t\t\t 'Only nonempty string arrays are supported at the moment meaning that it has to be a +\t\t\t\t string array with atleast one element in it in the original source appsettings.json +\t\t\t\t file...Skipping update and exiting with 1' +\t\t\t\t Exit 1 + } +\t\t\t} +\t\t\telse { +\t\t\t\tWrite-Host 'ERROR: Cannot handle ' $key ' with type ' $typeName +\t\t\t\t'Only string, boolean, interger, decimal and non-empty string arrays are supported at the moment + ...Skipping update and exiting with 1' +\t\t\t\tExit 1 +\t\t\t} + + Write-Host $key 'updated in json content with value' $pre.$lastKey + +\t\t} + else { + Write-Host $key 'not found in Json content...skipping it' + } +\t} + +\t$json | ConvertTo-Json -depth 99 | Set-Content $fullpath + + + Write-Host $fullpath 'file variables updated successfully...Done' +} + + +if($OctopusParameters -eq $null) { + Write-Host 'OctopusParameters is null...exiting with 1' + Exit 1 +} + +function ConvertListToHashtable([object[]]$list) { + $h = @{} + foreach ($element in $list) { +\t $h.Add($element.Key, $element.Value) + } + return $h +} + +$oParams = $OctopusParameters.getenumerator() | where-object {$_.key -notlike \"Octopus*\" -and $_.key -notlike \"env:*\"} + +UpdateJsonFile ` +(ConvertListToHashtable $oParams) ` +(Get-Param \"JsonFilePath\" -Required) +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "JsonFilePath", + "Label": "Path to the json file", + "HelpText": "For example if your appsettings.json file is in +c:\\web\\site1\\appsettings.json then set this value to +**c:\\web\\site1\\appsettings.json**. You can use Octopus +variables here like +**#{Deployment_InstallationPath}\\\\#{Octopus.Release.Number}\\appsettings.json** as the value of this variable. A good idea is to copy the value from the Install to variable in the Deploy API step template and add \\appsettings.json if that is your target json file", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "mysterio2465", + "$Meta": { + "ExportedAt": "2017-05-29T11:51:18.310Z", + "OctopusVersion": "3.2.11", + "Type": "ActionTemplate" + }, + "Category": "Octopus" +} diff --git a/step-templates/venafi-tpp-create-provision-certificate.json.human b/step-templates/venafi-tpp-create-provision-certificate.json.human new file mode 100644 index 000000000..862cd73c6 --- /dev/null +++ b/step-templates/venafi-tpp-create-provision-certificate.json.human @@ -0,0 +1,385 @@ +{ + "Id": "dd4dfa66-e632-4c6a-bae6-156a7a105023", + "Name": "Venafi TPP - Create and Provision Certificate", + "Description": "This step template will authenticate against a Venafi TPP instance using an existing OAuth access token, and create a new certificate as well as optionally associate and push the new certificate to specified existing application(s). This is achieved using a combination of two functions from the VenafiPS PowerShell module: + +1. `New-TppCertificate` which is an alias of the [New-VdcCertificate](https://venafips.readthedocs.io/en/latest/functions/New-VdcCertificate/) function. +2. `Add-TppCertificateAssociation` which is an alias of the [Add-VdcCertificateAssociation](https://venafips.readthedocs.io/en/latest/functions/Add-VdcCertificateAssociation/) function. + +--- + +**Options:** + +- Provide a distinguished name (DN) path for the new certificate. +- Provide a name for the new certificate. +- Provide a common name (CN) for the new certificate. +- *Optional* - Provide the distinguished name (DN) path to a certificate authority template to be used for the new certificate. +- *Optional* - Choose from the following certificate types: + - `Code Signing` + - `Device` + - `Server` + - `User` +- *Optional* - Choose from the following certificate management types: + - `Enrollment` + - `Provisioning` + - `Monitoring` + - `Unassigned` +- *Optional* - Provide subject alternate names for the new certificate using the following acceptable SAN types: + - `OtherName` + - `Email` + - `DNS` + - `URI` + - `IPAdress` +- *Optional* - Choose if you would like the step to wait for the certificate to finishing provisioning before moving on. +- *Optional* - Choose the maximum time in seconds that you would like the step to wait for provisioning to finish. +- *Optional* - Provide the application(s) path to associate the new certificate to. +- *Optional* - Choose to push the new certificate to the specified application(s). +- *Optional* - Choose to revoke the access token used on successful completion. + +--- + +**Required:** +- The `VenafiPS` PowerShell module installed on the deployment target or worker. If the module can't be found, the step will attempt to download a version from the [PowerShell gallery](https://www.powershellgallery.com/packages/VenafiPS). + +Notes: + +- Tested on Octopus `2021.2`. +- Tested with VenafiPS `3.1.5`. +- Tested with both Windows PowerShell and PowerShell Core on Linux. +", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ErrorActionPreference = 'Stop' +# Variables +$Server = $OctopusParameters[\"Venafi.TPP.CreateCert.Server\"] +$Token = $OctopusParameters[\"Venafi.TPP.CreateCert.AccessToken\"] +$CertPath = $OctopusParameters[\"Venafi.TPP.CreateCert.DNPath\"] +$CertName = $OctopusParameters[\"Venafi.TPP.CreateCert.Name\"] +$CertCommonName = $OctopusParameters[\"Venafi.Tpp.CreateCert.SubjectCN\"] +# Optional +$CertCAPath = $OctopusParameters[\"Venafi.Tpp.CreateCert.CertificateAuthorityDN\"] +$CertType = $OctopusParameters[\"Venafi.Tpp.CreateCert.Type\"] +$CertManagementType = $OctopusParameters[\"Venafi.Tpp.CreateCert.ManagementType\"] +$CertSubjectAltNames = $OctopusParameters[\"Venafi.Tpp.CreateCert.SubjectAltNames\"] +$CertProvisionWait = $OctopusParameters[\"Venafi.TPP.CreateCert.ProvisioningWait\"] +$CertProvisionTimeout = $OctopusParameters[\"Venafi.TPP.CreateCert.ProvisioningTimeout\"] +$ApplicationPath = $OctopusParameters[\"Venafi.TPP.CreateCert.ApplicationPath\"] +$ApplicationPush = $OctopusParameters[\"Venafi.TPP.CreateCert.PushCertificate\"] +$RevokeToken = $OctopusParameters[\"Venafi.TPP.CreateCert.RevokeTokenOnCompletion\"] +# Validation +if ([string]::IsNullOrWhiteSpace($Server)) { + throw \"Required parameter Venafi.TPP.CreateCert.Server not specified\" +} +if ([string]::IsNullOrWhiteSpace($Token)) { + throw \"Required parameter Venafi.TPP.CreateCert.AccessToken not specified\" +} +if ([string]::IsNullOrWhiteSpace($CertPath)) { + throw \"Required parameter Venafi.TPP.CreateCert.DNPath not specified\" +} +if ([string]::IsNullOrWhiteSpace($CertName)) { + throw \"Required parameter Venafi.TPP.CreateCert.Name not specified\" +} +if ([string]::IsNullOrWhiteSpace($CertCommonName)) { + throw \"Required parameter Venafi.TPP.CreateCert.SubjectCN not specified\" +} + +$SecureToken = ConvertTo-SecureString $Token -AsPlainText -Force +[PSCredential]$AccessToken = New-Object System.Management.Automation.PsCredential(\"token\", $SecureToken) +# Clean-up +$Server = $Server.TrimEnd('/') +# Required Modules +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} +# Check to see if the package provider has been installed +if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + Write-Host \"Nuget package provider not found, installing ...\" + Install-PackageProvider -Name Nuget -Force -Scope CurrentUser +} +Write-Host \"Checking for required VenafiPS module ...\" +$required_venafips_version = 3.1.5 +$module_available = Get-Module -ListAvailable -Name VenafiPS | Where-Object { $_.Version -ge $required_venafips_version } +if (-not ($module_available)) { + Write-Host \"Installing VenafiPS module ...\" + Install-Module -Name VenafiPS -MinimumVersion 3.1.5 -Scope CurrentUser -Force +} +else { + $first_match = $module_available | Select-Object -First 1 + Write-Host \"Found version: $($first_match.Version)\" +} +Write-Host \"Importing VenafiPS module ...\" +Import-Module VenafiPS +Write-Host \"Requesting new session from $Server\" +New-VenafiSession -Server $Server -AccessToken $AccessToken +# New certificate +$NewCert_Params = @{ + Path = $CertPath; + Name = $CertName; + CommonName = $CertCommonName +} +# Optional CertificateType field +if (-not [string]::IsNullOrWhiteSpace($CertType)) { + $NewCert_Params.CertificateType = $CertType +} +# Optional CertificateAuthorityPath field +if (-not [string]::IsNullOrWhiteSpace($CertCAPath)) { + $NewCert_Params.CertificateAuthorityPath = $CertCAPath +} +# Optional ManagementType field +if (-not [string]::IsNullOrWhiteSpace($CertManagementType)) { + $NewCert_Params.ManagementType = $CertManagementType +} +# Optional SubjectAltName field +if (-not [string]::IsNullOrWhiteSpace($CertSubjectAltNames)) { + $SubjectAltNames = @() + $SubjectAltNameStrings = $CertSubjectAltNames -split \"`n\" + foreach ($SubjectAltNameString in $SubjectAltNameStrings) { + if (-not [string]::IsNullOrWhiteSpace($SubjectAltNameString)) { + $ReplacedString = $SubjectAltNameString.Trim().Replace(\";\", \"`n\") + $StringAsHash = $ReplacedString | ConvertFrom-StringData + $SubjectAltNames += $StringAsHash + } + } + $NewCert_Params.SubjectAltName = $SubjectAltNames +} +# Generate New Certificate +Write-Host \"Creating certificate '$CertName' ($CertPath)...\" +$NewCertificate = New-TppCertificate @NewCert_Params -PassThru +$count = 0 +$Continue = $True +# Wait for certificate provisioning +if ($CertProvisionWait -eq $true -and $CertManagementType -eq \"Provisioning\") { + $EndWait = (Get-Date).AddSeconds($CertProvisionTimeout) + do { + if ($count -gt 0) { + Write-Host \"Waiting 30 seconds for certificate to provision...\" + Start-Sleep -Seconds 30 + } + $count++ + Write-Host \"Checking certificate provisioning status.\" + $CertDetails = Get-VenafiCertificate -CertificateId $NewCertificate.Path + Write-Verbose \"ProcessingDetails: $($CertDetails.ProcessingDetails)\" + if (-not \"$($CertDetails.ProcessingDetails)\") { + $Continue = $False + Write-Host \"Successful certificate provisioning detected.\" + } + elseif ($CertDetails.ProcessingDetails.InError -eq $True -or $CertDetails.ProcessingDetails.Status -eq \"Failure\") { + $Continue = $False + Write-Error \"Certificate failed to provision at Stage: $($CertDetails.ProcessingDetails.Stage), Status: $($CertDetails.ProcessingDetails.Status)\" + } + } until ($Continue -eq $False -or (Get-Date) -ge $EndWait) +} +# Associate Certificate with application +if (-not [string]::IsNullOrWhiteSpace($ApplicationPath)) { + $ApplicationPathArray = @() + if ($ApplicationPath.Contains(\",\")) { + $ApplicationPathArray = $ApplicationPath.Split(\",\") + } + else { + $ApplicationPathArray += $ApplicationPath + } + + if ($CertProvisionWait -eq $false) { + \tWrite-Warning \"Associating the certificate $CertName with application(s) at path(s) ($ApplicationPath) may be ongoing as waiting for provisioning is set to False. This could result in a failed association.\" + } + + if ($ApplicationPush -eq $true) { + Write-Host \"Associating and pushing certificate to application at $ApplicationPath\" + Add-TppCertificateAssociation -CertificatePath $NewCertificate.Path -ApplicationPath $ApplicationPathArray -PushCertificate + } + else { + Write-Host \"Associating certificate to application at $ApplicationPath\" + Add-TppCertificateAssociation -CertificatePath $NewCertificate.Path -ApplicationPath $ApplicationPathArray + } +} +if ($RevokeToken -eq $true) { + # Revoke TPP access token + Write-Host \"Revoking access token with $Server\" + Revoke-TppToken -AuthServer $Server -AccessToken $AccessToken -Force +}" + }, + "Parameters": [ + { + "Id": "28b7cc35-41dc-4759-afc0-3ee8ded24a4b", + "Name": "Venafi.TPP.CreateCert.Server", + "Label": "Venafi TPP Server", + "HelpText": "*Required*: The URL of the Venafi TPP instance you want to create a certificate in. + +For example : `https://mytppserver.example.com`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "58bf5279-d9a6-4129-81fc-9806a836e0a0", + "Name": "Venafi.TPP.CreateCert.AccessToken", + "Label": "Venafi TPP Access Token", + "HelpText": "*Required*: The access token to authenticate against the TPP instance.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "52140bce-2c49-46df-9858-145183a76614", + "Name": "Venafi.TPP.CreateCert.DNPath", + "Label": "Venafi TPP Certificate Path", + "HelpText": "*Required*: The Distinguished Name (DN) of the certificate you wish to create. This is the absolute path to the certificate in the TPP instance, separated by `\\`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "caf96139-f59d-4d79-b076-7cba155e5da6", + "Name": "Venafi.TPP.CreateCert.Name", + "Label": "Certificate Name", + "HelpText": "*Required*: Name of the certificate to be created.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2dcab218-b4e3-4076-a05d-6d4b649fda80", + "Name": "Venafi.TPP.CreateCert.SubjectCN", + "Label": "Certificate Subject Common Name", + "HelpText": "*Required*: Subject common name (CN) of the certificate to be created. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "040d9226-c782-47d6-b085-977f43b3f0cc", + "Name": "Venafi.TPP.CreateCert.CertificateAuthorityDN", + "Label": "Venafi TPP Certificate Authority Path (Optional)", + "HelpText": "*Optional*: The Distinguished Name (DN) of the certificate authority you wish to use. This is the absolute path to a certificate authority template in the TPP instance, separated by `\\`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c8863e1f-0e3b-49a4-aafc-d765b103c89a", + "Name": "Venafi.TPP.CreateCert.Type", + "Label": "Venafi TPP Certificate Type (Optional)", + "HelpText": "*Optional*: Type of certificate to be created. Valid options are: + +- `Code Signing` +- `Device` +- `Server` (**Default**) +- `User`", + "DefaultValue": "Server", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Code Signing|Code Signing +Device|Device +Server|Server +User|User" + } + }, + { + "Id": "53b52bb0-64cf-4d11-bc66-f9e22b54c2a0", + "Name": "Venafi.TPP.CreateCert.ManagementType", + "Label": "Venafi TPP Certificate Management Type (Optional)", + "HelpText": "*Optional*: The level of management that Trust Protection Platform applies to the certificate. Valid options are: + +- `Enrollment`: (**Default**) Issue a new certificate, renewed certificate, or key generation request to a CA for enrollment. Do not automatically provision the certificate. +- `Provisioning`: Issue a new certificate, renewed certificate, or key generation request to a CA for enrollment. Automatically install or provision the certificate. +- `Monitoring`: Allow Trust Protection Platform to monitor the certificate for expiration and renewal. +- `Unassigned`: Certificates are neither enrolled or monitored by Trust Protection Platform.", + "DefaultValue": "Enrollment", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Enrollment|Enrollment +Provisioning|Provisioning +Monitoring|Monitoring +Unassigned|Unassigned" + } + }, + { + "Id": "a3306c46-a69d-4bdf-816b-0bac12276b6c", + "Name": "Venafi.TPP.CreateCert.SubjectAltNames", + "Label": "Venafi TPP Certificate Subject Alternate Names (Optional)", + "HelpText": "*Optional*: A list of Subject Alternate Names. The value must be 1 or more lines with the SAN type and value. Each SAN type needs to be separated by a `;`. Acceptable SAN types are `OtherName`, `Email`, `DNS`, `URI`, and `IPAddress`. You can provide more than 1 of the same SAN type with multiple lines. + +**For example**: +```md +DNS=octopus.local.samples;Email=octopus@email.com +DNS=octopus.samples;IPAddress=0.0.0.0 +```", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "3014d2f1-789c-4640-91e1-ee7ddb5661e7", + "Name": "Venafi.TPP.CreateCert.ProvisioningWait", + "Label": "Wait for certificate provisioning?", + "HelpText": "If the new certificate has a management type of `Provisioning` should the step wait for the provisioning to complete?", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "88386d92-8536-4be4-8a8f-2f5fd8d3242b", + "Name": "Venafi.TPP.CreateCert.ProvisioningTimeout", + "Label": "Max wait time for certificate provisioning.", + "HelpText": "The max about of time in seconds you want the step to wait for the certificate to be provisioned.", + "DefaultValue": "600", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "e9e84efe-28e1-437c-9c79-845a59a7e89e", + "Name": "Venafi.TPP.CreateCert.ApplicationPath", + "Label": "Venafi TPP Application Path (Optional)", + "HelpText": "*Optional*: A comma separated list of application paths to associate with the new certificate. Each value in the list is the absolute path to an application in the TPP instance, separated by `\\`. + +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3bfc02d2-4711-4a21-821d-9add5de603d7", + "Name": "Venafi.TPP.CreateCert.PushCertificate", + "Label": "Push certificate to associated application?", + "HelpText": "Push the newly created certificate to the applications", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "38eaa04c-6e4c-4d7d-b119-0631dfb7a490", + "Name": "Venafi.TPP.CreateCert.RevokeTokenOnCompletion", + "Label": "Revoke access token on completion?", + "HelpText": "Should the access token used be revoked once the step has been completed successfully? Default: `False`.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2021-08-23T21:42:40.319Z", + "OctopusVersion": "2021.1.7236", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "harrisonmeister", + "Category": "venafi" + } diff --git a/step-templates/venafi-tpp-export-certificate.json.human b/step-templates/venafi-tpp-export-certificate.json.human new file mode 100644 index 000000000..9191c7b2a --- /dev/null +++ b/step-templates/venafi-tpp-export-certificate.json.human @@ -0,0 +1,373 @@ +{ + "Id": "2417aab5-6d84-4e0d-bc86-b2255bd4028a", + "Name": "Venafi TPP - Export Certificate", + "Description": "This step template will authenticate against a Venafi TPP instance using an existing OAuth access token, and export a certificate using its Distinguished Name (DN). This is the absolute path to the certificate in the TPP instance. + +This is achieved using the VenafiPS PowerShell module's [Export-VenafiCertificate](https://venafips.readthedocs.io/en/latest/functions/Export-VenafiCertificate/) function. + +--- + +**Options:** + +- Provide the distinguished name (DN) path to the certificate. +- Choose from the following export formats: + - `Base64` + - `Base64 (PKCS #8)` + - `DER` + - `JKS` + - `PKCS #7` + - `PKCS #12` +- *Optional* - Provide a custom output path. +- *Optional* - Provide a custom output filename. If not supplied, the filename will automatically be taken from the response. +- *Optional* - Include the full certificate chain in the export. +- *Optional* - Friendly name (Label or alias) to use. Permitted with `Base64` and `PKCS #12` formats. Required when format is `JKS`. +- *Optional* - Include the private key in the export. +- *Optional* - Provide a password to be used for the exported private key. +- *Optional* - store the export certificate response in `JSON` format in an [Octopus sensitive output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables). This output variable can then be used in additional deployment or runbook steps. +- *Optional* - on successful completion, you can revoke the access token used. + +--- + +**Required:** +- The `VenafiPS` PowerShell module installed on the deployment target or worker. If the module can't be found, the step will attempt to download a version from the [PowerShell gallery](https://www.powershellgallery.com/packages/VenafiPS). +- PowerShell `5` or greater. + +Notes: + +- Tested on Octopus `2021.2`. +- Tested with VenafiPS `3.1.5`. +- Tested with both Windows PowerShell and PowerShell Core on Linux.", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ErrorActionPreference = 'Stop' + +# Variables +$Server = $OctopusParameters[\"Venafi.TPP.ExportCert.Server\"] +$Token = $OctopusParameters[\"Venafi.TPP.ExportCert.AccessToken\"] +$Path = $OctopusParameters[\"Venafi.TPP.ExportCert.DNPath\"] +$Format = $OctopusParameters[\"Venafi.TPP.ExportCert.Format\"] +$OutPath = $OctopusParameters[\"Venafi.TPP.ExportCert.OutPath\"] +$OutFileName = $OctopusParameters[\"Venafi.TPP.ExportCert.OutFileName\"] + +# Optional +$IncludeChain = $OctopusParameters[\"Venafi.TPP.ExportCert.IncludeChain\"] +$FriendlyName = $OctopusParameters[\"Venafi.TPP.ExportCert.FriendlyName\"] +$IncludePrivateKey = $OctopusParameters[\"Venafi.TPP.ExportCert.IncludePrivateKey\"] +$PrivateKeyPassword = $OctopusParameters[\"Venafi.TPP.ExportCert.PrivateKeyPassword\"] +$OutputVariableName = $OctopusParameters[\"Venafi.TPP.ExportCert.OutputVariableName\"] +$RevokeToken = $OctopusParameters[\"Venafi.TPP.ExportCert.RevokeTokenOnCompletion\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($Server)) { + throw \"Required parameter Venafi.TPP.ExportCert.Server not specified\" +} +if ([string]::IsNullOrWhiteSpace($Token)) { + throw \"Required parameter Venafi.TPP.ExportCert.AccessToken not specified\" +} +if ([string]::IsNullOrWhiteSpace($Path)) { + throw \"Required parameter Venafi.TPP.ExportCert.DNPath not specified\" +} +else { + if ($Path.Contains(\"\\\") -eq $False) { + throw \"At least one '\\' is required for the Venafi.TPP.ExportCert.DNPath value\" + } +} +if ([string]::IsNullOrWhiteSpace($Format)) { + throw \"Required parameter Venafi.TPP.ExportCert.Format not specified\" +} +else { + if ($Format -eq \"JKS\") { + if ([string]::IsNullOrWhiteSpace($PrivateKeyPassword)) { + throw \"Export format is JKS, and parameter Venafi.TPP.ExportCert.PrivateKeyPassword required but not set!\" + } + } +} +# Conditional validation +if (-not [string]::IsNullOrWhiteSpace($OutPath)) { + if (-not (Test-Path $OutPath -PathType Container)) { + throw \"Optional parameter Venafi.TPP.ExportCert.OutPath specified but does not exist!\" + } +} +if ($IncludePrivateKey -eq $True) { + if ([string]::IsNullOrWhiteSpace($PrivateKeyPassword)) { + throw \"IncludePrivateKey set to true, but parameter Venafi.TPP.ExportCert.PrivateKeyPassword not specified\" + } +} + +$SecureToken = ConvertTo-SecureString $Token -AsPlainText -Force +[PSCredential]$AccessToken = New-Object System.Management.Automation.PsCredential(\"token\", $SecureToken) + +# Clean-up +$Server = $Server.TrimEnd('/') + +# Required Modules +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +# Check to see if the package provider has been installed +if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + Write-Host \"Nuget package provider not found, installing ...\" + Install-PackageProvider -Name Nuget -Force -Scope CurrentUser +} + +Write-Host \"Checking for required VenafiPS module ...\" +$required_venafips_version = 3.1.5 +$module_available = Get-Module -ListAvailable -Name VenafiPS | Where-Object { $_.Version -ge $required_venafips_version } +if (-not ($module_available)) { + Write-Host \"Installing VenafiPS module ...\" + Install-Module -Name VenafiPS -MinimumVersion 3.1.5 -Scope CurrentUser -Force +} +else { + $first_match = $module_available | Select-Object -First 1 + Write-Host \"Found version: $($first_match.Version)\" +} + +Write-Host \"Importing VenafiPS module ...\" +Import-Module VenafiPS + +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +Write-Verbose \"Venafi.TPP.ExportCert.Server: $Server\" +Write-Verbose \"Venafi.TPP.ExportCert.AccessToken: ********\" +Write-Verbose \"Venafi.TPP.ExportCert.DNPath: $Path\" +Write-Verbose \"Venafi.TPP.ExportCert.Format: $Format\" +Write-Verbose \"Venafi.TPP.ExportCert.OutPath: $OutPath\" +Write-Verbose \"Venafi.TPP.ExportCert.OutFileName: $OutFileName\" +Write-Verbose \"Venafi.TPP.ExportCert.IncludeChain: $IncludeChain\" +Write-Verbose \"Venafi.TPP.ExportCert.FriendlyName: $FriendlyName\" +Write-Verbose \"Venafi.TPP.ExportCert.IncludePrivateKey: $IncludePrivateKey\" +Write-Verbose \"Venafi.TPP.ExportCert.PrivateKeyPassword: ********\" +Write-Verbose \"Venafi.TPP.ExportCert.CertDetails.OutputVariableName: $OutputVariableName\" +Write-Verbose \"Venafi.TPP.ExportCert.RevokeTokenOnCompletion: $RevokeTokenOnCompletion\" +Write-Verbose \"Step Name: $StepName\" + +Write-Host \"Requesting new session from $Server\" +New-VenafiSession -Server $Server -AccessToken $AccessToken + +# Export certificate +$ExportCert_Params = @{ + CertificateId = $Path; + Format = $Format; +} + +# Optional IncludeChain field +if ($IncludeChain -eq $True) { + if ($Format -eq \"JKS\") { + Write-Warning \"The IncludeChain parameter is not supported with JKS export format, ignoring.\" + } + else { + $ExportCert_Params.IncludeChain = $True + } +} + +# Optional FriendlyName field +if (-not [string]::IsNullOrWhiteSpace($FriendlyName)) { + $ExportCert_Params.FriendlyName = $FriendlyName +} + +if (-not [string]::IsNullOrWhiteSpace($PrivateKeyPassword)) { + $SecurePrivateKeyPassword = ConvertTo-SecureString $PrivateKeyPassword -AsPlainText -Force + if ($Format -eq \"JKS\") { + $ExportCert_Params.KeystorePassword = $SecurePrivateKeyPassword + } + elseif ($IncludePrivateKey -eq $True) { + $ExportCert_Params.PrivateKeyPassword = $SecurePrivateKeyPassword + $ExportCert_Params.IncludePrivateKey = $True + } +} + +$ExportCertificateResponse = ((Export-VenafiCertificate @ExportCert_Params) 6> $null) + +if ($null -eq $ExportCertificateResponse -or $null -eq $ExportCertificateResponse.CertificateData) { + Write-Warning \"No certificate data returned for path: $Path`nCheck the path value represents a certificate, and not a folder.\" +} +else { + Write-Highlight \"Successfully retrieved certificate data to export for path: $Path\" + + if ([string]::IsNullOrWhiteSpace($OutPath) -eq $False) { + $Filename = $ExportCertificateResponse.Filename + if ([string]::IsNullOrWhiteSpace($OutFileName) -eq $False) { + $Filename = $OutFileName + } + $outFile = Join-Path -Path $OutPath -ChildPath ($Filename.Trim('\"')) + $bytes = [Convert]::FromBase64String($ExportCertificateResponse.CertificateData) + [IO.File]::WriteAllBytes($outFile, $bytes) + Write-Host ('Saved {0} with format {1}' -f $outFile, $ExportCertificateResponse.Format) + } + if ([string]::IsNullOrWhiteSpace($OutputVariableName) -eq $False) { + $CertificateJson = $ExportCertificateResponse | ConvertTo-Json -Compress -Depth 10 + Set-OctopusVariable -Name $OutputVariableName -Value $CertificateJson -Sensitive + Write-Highlight \"Created sensitive output variable: ##{Octopus.Action[$StepName].Output.$OutputVariableName}\" + } +} + +if ($RevokeToken -eq $true) { + # Revoke TPP access token + Write-Host \"Revoking access token with $Server\" + Revoke-TppToken -AuthServer $Server -AccessToken $AccessToken -Force +}" + }, + "Parameters": [ + { + "Id": "56ef4967-37f5-40a0-a66e-f3fa589b6467", + "Name": "Venafi.TPP.ExportCert.Server", + "Label": "Venafi TPP Server", + "HelpText": "*Required*: The URL of the Venafi TPP instance you want to export a certificate from. + +For example: `https://mytppserver.example.com`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "49bcdbbb-3674-4901-8bf6-164e5e4bc395", + "Name": "Venafi.TPP.ExportCert.AccessToken", + "Label": "Venafi TPP Access Token", + "HelpText": "*Required*: The access token to authenticate against the TPP instance.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e3156852-4ba9-4dc0-8d39-5a93c52b1910", + "Name": "Venafi.TPP.ExportCert.DNPath", + "Label": "Venafi TPP Certificate Path", + "HelpText": "*Required*: The Distinguished Name (DN) of the certificate you wish to export. This is the absolute path to the certificate in the TPP instance, separated by `\\`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4f9f4d4b-d686-4d00-aa93-af35b7df320b", + "Name": "Venafi.TPP.ExportCert.Format", + "Label": "Certificate Export Format", + "HelpText": "*Required*: The certificate export format. Valid options are: + +- `Base64` +- `Base64 (PKCS #8)` +- `DER` +- `JKS` +- `PKCS #7` +- `PKCS #12` ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Base64|Base64 +Base64 (PKCS #8)|Base64 (PKCS #8) +DER|DER +JKS|JKS +PKCS #7|PKCS #7 +PKCS #12|PKCS #12" + } + }, + { + "Id": "7f7dc0f5-275e-4d32-a758-c942c9535bbc", + "Name": "Venafi.TPP.ExportCert.OutPath", + "Label": "Certificate output folder (Optional)", + "HelpText": "*Optional*: The folder path to save the certificate to. The folder must exist if this value is specified.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "48df6311-3eba-49b6-8adb-03b7d9eac8b4", + "Name": "Venafi.TPP.ExportCert.OutFileName", + "Label": "Certificate output filename (Optional)", + "HelpText": "*Optional*: The filename to save the exported certificate as. This value is used when the `Venafi.TPP.ExportCert.OutPath` parameter is set. + +If not specified, the TPP filename will be used.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "309d30de-79b6-4461-8a54-1698aedd5822", + "Name": "Venafi.TPP.ExportCert.IncludeChain", + "Label": "Include certificate chain (Optional)", + "HelpText": "*Optional*: Include the certificate chain with the exported certificate. Not supported with `DER` or `JKS` format. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "71fecac3-25c4-4161-9135-94815a485f03", + "Name": "Venafi.TPP.ExportCert.FriendlyName", + "Label": "Friendly Name (Optional)", + "HelpText": "*Optional*: Label or alias to use. Permitted with `Base64` and `PKCS #12` formats. Required when Format is `JKS`. ", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2aaedf1d-be93-4df4-856c-c69650db452a", + "Name": "Venafi.TPP.ExportCert.IncludePrivateKey", + "Label": "Include Private Key (Optional)", + "HelpText": "*Optional*: Include the private key in the certificate export. If this is selected, the `Venafi.TPP.Export.PrivateKeyPassword` must also be provided. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "2d168360-bcbf-4bdc-833d-a9c182e98a47", + "Name": "Venafi.TPP.ExportCert.PrivateKeyPassword", + "Label": "Private Key password (Optional)", + "HelpText": "*Optional*: The password required to include the private key. Not supported with `DER` or `PKCS #7` formats. You must adhere to the following rules: + +- Password is at least 12 characters. +- Comprised of at least three of the following: + - Uppercase alphabetic letters + - Lowercase alphabetic letters + - Numeric characters + - Special characters", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "84f92dd5-064b-47e5-bb11-3dd0faacfeb4", + "Name": "Venafi.TPP.ExportCert.OutputVariableName", + "Label": "Certificate output variable name (Optional)", + "HelpText": "*Optional*: Create an output variable with the certificate details returned from the export call. The certificate details will be stored in `JSON` format.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "606acdfe-641a-47f2-a4ea-56559477ea0c", + "Name": "Venafi.TPP.ExportCert.RevokeTokenOnCompletion", + "Label": "Revoke access token on completion?", + "HelpText": "Should the access token used be revoked once the step has been completed successfully? Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedAt": "2021-08-18T15:22:55.551Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2021-08-18T15:22:55.551Z", + "OctopusVersion": "2021.3.1432", + "Type": "ActionTemplate" + }, + + "Category": "venafi" +} diff --git a/step-templates/venafi-tpp-find-certificate-details.json.human b/step-templates/venafi-tpp-find-certificate-details.json.human new file mode 100644 index 000000000..ee71d5d13 --- /dev/null +++ b/step-templates/venafi-tpp-find-certificate-details.json.human @@ -0,0 +1,267 @@ +{ + "Id": "91ea41ae-5b12-4854-8994-90c06ba4b7f1", + "Name": "Venafi TPP - Find Certificate details", + "Description": "This step template will authenticate against a Venafi TPP instance using an existing OAuth access token, and find a matching certificate based on the certificate subject common name. This is achieved using a combination of two functions from the VenafiPS PowerShell module: + +1. [Find-TppCertificate](https://venafips.readthedocs.io/en/latest/functions/Find-TppCertificate/) function. +2. [Get-VenafiCertificate](https://venafips.readthedocs.io/en/latest/functions/Get-VenafiCertificate/) function. + +If multiple certificate matches are found, additional (optional) search criteria can be provided to further filter the results: + +- Certificate serial number +- Full Issuer Distinguished Name (DN) +- Expires before + +After any filtering is complete, if multiple matches are found, only the first certificate will be returned, and a warning will be logged that multiple matches were found. + +You can also store the entire certificate result in `JSON` format in an [Octopus output variable](https://octopus.com/docs/projects/variables/output-variables) + +This output variable can then be used in additional deployment or runbook steps. + +On successful completion, you can also *optionally* revoke the access token used. + +--- + +**Required:** +- The `VenafiPS` PowerShell module installed on the deployment target or worker. If the module can't be found, the step will attempt to download a version from the [PowerShell gallery](https://www.powershellgallery.com/packages/VenafiPS). + +Notes: + +- Tested on Octopus `2021.2`. +- Tested with VenafiPS `3.1.5`. +- Tested with both Windows PowerShell and PowerShell Core on Linux.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ErrorActionPreference = 'Stop' + +# Variables +$Server = $OctopusParameters[\"Venafi.TPP.FindCert.Server\"] +$Token = $OctopusParameters[\"Venafi.TPP.FindCert.AccessToken\"] +$SubjectCommonName = $OctopusParameters[\"Venafi.TPP.FindCert.SubjectCN\"] + +# Optional +$CertSerialNumber = $OctopusParameters[\"Venafi.TPP.FindCert.SerialNumber\"] +$Issuer = $OctopusParameters[\"Venafi.TPP.FindCert.Issuer\"] +$ExpireBefore = $OctopusParameters[\"Venafi.TPP.FindCert.ExpireBefore\"] +$OutputVariableName = $OctopusParameters[\"Venafi.TPP.FindCert.CertDetails.OutputVariableName\"] +$RevokeTokenOnCompletion = $OctopusParameters[\"Venafi.TPP.FindCert.RevokeTokenOnCompletion\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($Server)) { + throw \"Required parameter Venafi.TPP.FindCert.Server not specified\" +} +if ([string]::IsNullOrWhiteSpace($Token)) { + throw \"Required parameter Venafi.TPP.FindCert.AccessToken not specified\" +} +if ([string]::IsNullOrWhiteSpace($SubjectCommonName)) { + throw \"Required parameter Venafi.TPP.FindCert.SubjectCN not specified\" +} + +$SecureToken = ConvertTo-SecureString $Token -AsPlainText -Force +[PSCredential]$AccessToken = New-Object System.Management.Automation.PsCredential(\"token\", $SecureToken) + +# Clean-up +$Server = $Server.TrimEnd('/') + +# Required Modules +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +# Check to see if the package provider has been installed +if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + Write-Host \"Nuget package provider not found, installing ...\" + Install-PackageProvider -Name Nuget -Force -Scope CurrentUser +} + +Write-Host \"Checking for required VenafiPS module ...\" +$required_venafips_version = 3.1.5 +$module_available = Get-Module -ListAvailable -Name VenafiPS | Where-Object { $_.Version -ge $required_venafips_version } +if (-not ($module_available)) { + Write-Host \"Installing VenafiPS module ...\" + Install-Module -Name VenafiPS -MinimumVersion 3.1.5 -Scope CurrentUser -Force +} +else { + $first_match = $module_available | Select-Object -First 1 + Write-Host \"Found version: $($first_match.Version)\" +} + +Write-Host \"Importing VenafiPS module ...\" +Import-Module VenafiPS + +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +Write-Verbose \"Venafi.TPP.FindCert.Server: $Server\" +Write-Verbose \"Venafi.TPP.FindCert.AccessToken: ********\" +Write-Verbose \"Venafi.TPP.FindCert.SubjectCN: $SubjectCommonName\" +Write-Verbose \"Venafi.TPP.FindCert.SerialNumber: $CertSerialNumber\" +Write-Verbose \"Venafi.TPP.FindCert.Issuer: $Issuer\" +Write-Verbose \"Venafi.TPP.FindCert.ExpireBefore: $ExpireBefore\" +Write-Verbose \"Venafi.TPP.FindCert.CertDetails.OutputVariableName: $OutputVariableName\" +Write-Verbose \"Venafi.TPP.FindCert.RevokeTokenOnCompletion: $RevokeTokenOnCompletion\" +Write-Verbose \"Step Name: $StepName\" + +Write-Host \"Requesting new session from $Server\" +New-VenafiSession -Server $Server -AccessToken $AccessToken + +$FindCert_Params = @{ + First = 5; + CommonName = $SubjectCommonName; +} + +# Optional SerialNumber field +if ([string]::IsNullOrWhiteSpace($CertSerialNumber) -eq $False) { + $FindCert_Params += @{ SerialNumber = $CertSerialNumber } +} +# Optional Issuer field +if ([string]::IsNullOrWhiteSpace($Issuer) -eq $False) { + # Issuer DN should be the complete DN enclosed in double quotes. e.g. \"CN=Example Root CA, O=Venafi,Inc., L=Salt Lake City, S=Utah, C=US\" + # If a value DN already contains double quotes, the string should be enclosed in a second set of double quotes. + if ($Issuer.StartsWith(\"`\"\") -or $Issuer.EndsWith(\"`\"\")) { + Write-Verbose \"Removing double quotes from start and end of Issuer DN.\" + $Issuer = $Issuer.Trim(\"`\"\") + } + $FindCert_Params += @{ Issuer = \"`\"$Issuer`\"\" } +} +# Optional ExpireBefore field +if ([string]::IsNullOrWhiteSpace($ExpireBefore) -eq $False) { + $FindCert_Params += @{ ExpireBefore = $ExpireBefore } +} + +Write-Host \"Searching for certificates matching Subject CN: $SubjectCommonName.\" +$MatchingCertificates = @(Find-TppCertificate @FindCert_Params) +$MatchingCount = $MatchingCertificates.Length +if ($null -eq $MatchingCertificates -or $MatchingCount -eq 0) { + Write-Warning \"No matching certificates found for Subject CN: $SubjectCommonName. Check any additional search criteria and try again.\" +} +else { + $MatchingCertificate = $MatchingCertificates | Select-Object -First 1 + if ($MatchingCount -gt 1) { + Write-Warning \"Multiple matching certificates found ($MatchingCount) for Subject CN: $SubjectCommonName, retrieving details for first match.\" + } + + Write-Highlight \"Retrieving certificate details for Subject CN: $SubjectCommonName ($($MatchingCertificate.Path))\" + $Certificate = Get-VenafiCertificate -CertificateId $MatchingCertificate.Path + if ($null -eq $Certificate) { + Write-Warning \"No certificate details returned for Subject CN: $SubjectCommonName ($($MatchingCertificate.Path))\" + } + else { + Write-Host \"Retrieved certificate details for Subject CN: $SubjectCommonName ($($MatchingCertificate.Path))\" + $Certificate | Format-List + + if ([string]::IsNullOrWhiteSpace($OutputVariableName) -eq $False) { + $CertificateJson = $Certificate | ConvertTo-Json -Compress -Depth 10 + Set-OctopusVariable -Name $OutputVariableName -Value $CertificateJson + Write-Highlight \"Created output variable: ##{Octopus.Action[$StepName].Output.$OutputVariableName}\" + } + } +} + +if ($RevokeTokenOnCompletion -eq $True) { + # Revoke TPP access token + Write-Host \"Revoking access token with $Server\" + Revoke-TppToken -AuthServer $Server -AccessToken $AccessToken -Force +}" + }, + "Parameters": [ + { + "Id": "cf0b3c21-9249-4aa9-bf19-1fffdaa58939", + "Name": "Venafi.TPP.FindCert.Server", + "Label": "Venafi TPP Server", + "HelpText": "*Required*: The URL of the Venafi TPP instance you want to find a certificate from. + +For example: `https://mytppserver.example.com`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "f0d9bb77-7980-45f5-97c6-eefe6ccb3398", + "Name": "Venafi.TPP.FindCert.AccessToken", + "Label": "Venafi TPP Access Token", + "HelpText": "*Required*: The access token to authenticate against the TPP instance.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "0b7565f5-1ca1-4738-b13f-4b8a4ab99cf0", + "Name": "Venafi.TPP.FindCert.SubjectCN", + "Label": "Certificate Subject Common Name", + "HelpText": "*Required*: Enter the certificate subject common name (CN) to search for.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "44bb491d-1249-4893-a9fb-903daf2df6b4", + "Name": "Venafi.TPP.FindCert.SerialNumber", + "Label": "Certificate serial number (Optional)", + "HelpText": "*Optional*: In case of multiple certificates matched on the subject common name (CN), provide the certificate serial number to filter to a single certificate match.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "fcac21e8-34b6-48c6-b40f-2448c0c598a5", + "Name": "Venafi.TPP.FindCert.Issuer", + "Label": "Certificate Issuer Distinguished Name (Optional)", + "HelpText": "*Optional*: In case of multiple certificates matched on the subject common name (CN), provide the full Issuer distinguished name (DN) to filter to a single certificate match. + +For example: `CN=Example Root CA, O=Venafi,Inc., L=Salt Lake City, S=Utah, C=US`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "c15020cd-b722-49c2-89c2-a2a2349d41eb", + "Name": "Venafi.TPP.FindCert.ExpireBefore", + "Label": "Certificate Expires Before (Optional)", + "HelpText": "*Optional*: In case of multiple certificates matched on the subject common name (CN), provide the expires before date in the format `yyyy-MM-dd` e.g. `2021-08-15`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "19f60dd0-887e-4379-94b3-b3227b3073be", + "Name": "Venafi.TPP.FindCert.CertDetails.OutputVariableName", + "Label": "Certificate output variable name (Optional)", + "HelpText": "*Optional*: Create an output variable with the certificate details found from the search. The certificate details will be stored in `JSON` format.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "74dbacbe-6192-45e6-8a10-e714966c4150", + "Name": "Venafi.TPP.FindCert.RevokeTokenOnCompletion", + "Label": "Revoke access token on completion?", + "HelpText": "Should the access token used be revoked once the step has been completed successfully? Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedAt": "2021-08-16T09:51:20.800Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2021-08-16T09:51:20.800Z", + "OctopusVersion": "2021.2.7207", + "Type": "ActionTemplate" + }, + "Category": "venafi" + } diff --git a/step-templates/venafi-tpp-generate-oauth-token.json.human b/step-templates/venafi-tpp-generate-oauth-token.json.human new file mode 100644 index 000000000..e74c1d310 --- /dev/null +++ b/step-templates/venafi-tpp-generate-oauth-token.json.human @@ -0,0 +1,226 @@ +{ + "Id": "7e6f7f03-260a-4b52-9377-66f1c69b77d8", + "Name": "Venafi TPP - Generate OAuth Token", + "Description": "This step template will authenticate against a Venafi TPP instance and generate an OAuth token for specified scope/privileges using the VenafiPS PowerShell module's `New-TppToken` (an alias of the [VdcToken](https://venafips.readthedocs.io/en/latest/functions/New-VdcToken/) function). + +The following properties from the resulting OAuth token will be created as [Octopus sensitive variables](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables): + +- `access_token` created with the name `AccessToken` +- `Expires` created with the name `AccessTokenExpires` in the format `yyyy-MM-ddTHH:mm:ss` +- `refresh_token` created with the name `RefreshToken` +- `refresh_until` created with the name `RefreshTokenExpires` in the format `yyyy-MM-ddTHH:mm:ss`. *Note: This value can be empty*. + +These output variables can be used in additional deployment or runbook steps. + +--- + +**Required:** +- The `VenafiPS` PowerShell module installed on the deployment target or worker. If the module can't be found, the step will attempt to download a version from the [PowerShell gallery](https://www.powershellgallery.com/packages/VenafiPS). + +Notes: + +- Tested on Octopus `2021.2`. +- Tested with VenafiPS `3.1.5`. +- Tested with both Windows PowerShell and PowerShell Core on Linux.", + "ActionType": "Octopus.Script", + "Version": 3, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ErrorActionPreference = 'Stop' + +# Variables +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] +$Server = $OctopusParameters[\"Venafi.TPP.OAuthToken.Server\"] +$ClientID = $OctopusParameters[\"Venafi.TPP.OAuthToken.ClientID\"] +$Username = $OctopusParameters[\"Venafi.TPP.OAuthToken.Username\"] +$Password = $OctopusParameters[\"Venafi.TPP.OAuthToken.Password\"] +$Scopes = $OctopusParameters[\"Venafi.TPP.OAuthToken.Scope\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($Server)) { + throw \"Required parameter Venafi.TPP.OAuthToken.Server not specified\" +} +if ([string]::IsNullOrWhiteSpace($ClientID)) { + throw \"Required parameter Venafi.TPP.OAuthToken.ClientID not specified\" +} +if ([string]::IsNullOrWhiteSpace($Username)) { + throw \"Required parameter Venafi.TPP.OAuthToken.Username not specified\" +} +if ([string]::IsNullOrWhiteSpace($Password)) { + throw \"Required parameter Venafi.TPP.OAuthToken.Password not specified\" +} +if ([string]::IsNullOrWhiteSpace($Scopes)) { + throw \"Required parameter Venafi.TPP.OAuthToken.Scope not specified\" +} + +# Clean-up +$Server = $Server.TrimEnd('/') + +# Required Modules +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +# Check to see if the package provider has been installed +if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + Write-Host \"Nuget package provider not found, installing ...\" + Install-PackageProvider -Name Nuget -Force -Scope CurrentUser +} + +Write-Host \"Checking for required VenafiPS module ...\" +$required_venafips_version = 3.1.5 +$module_available = Get-Module -ListAvailable -Name VenafiPS | Where-Object { $_.Version -ge $required_venafips_version } +if (-not ($module_available)) { + Write-Host \"Installing VenafiPS module ...\" + Install-Module -Name VenafiPS -MinimumVersion 3.1.5 -Scope CurrentUser -Force +} +else { + $first_match = $module_available | Select-Object -First 1 + Write-Host \"Found version: $($first_match.Version)\" +} + +Write-Host \"Importing VenafiPS module ...\" +Import-Module VenafiPS + +$AccessTokenScope = @{} + +$Scopes -Split \";\" | ForEach-Object { + $Scope = ($_ -Split \":\") + $Type = $Scope[0] + $Privileges = $null + + if ($Scope.Length -gt 1) { + $Privileges = $Scope[1].TrimEnd(\",\") + } + + if ($AccessTokenScope.ContainsKey($Type)) { + $CurrentPrivileges = $AccessTokenScope[$Type] + # If no privilege, set to $null + if ([string]::IsNullOrWhiteSpace($Privileges)) { + $AccessTokenScope[$Type] = $null + } + else { + $AccessTokenScope[$Type] = if ([string]::IsNullOrWhiteSpace($CurrentPrivileges)) { $Privileges } else { \"$($CurrentPrivileges),$Privileges\" } + } + } + else { + $AccessTokenScope.Add($Type, $Privileges) + } +} + +if ($AccessTokenScope.Keys.Count -lt 1) { + throw \"No scopes could be determined!\" +} + +$scopeString = @($AccessTokenScope.GetEnumerator() | ForEach-Object { if ($_.Value) { '{0}:{1}' -f $_.Key, $_.Value } else { $_.Key } }) -join ';' + +# Get TPP access token +[PSCredential]$Credential = New-Object System.Management.Automation.PSCredential ($Username, (ConvertTo-SecureString $Password -AsPlainText -Force)) + +Write-Host \"Requesting new OAuth token from: $Server for ClientId: $ClientID with scope '$scopeString' ...\" +$tppTokenResponse = New-TppToken -AuthServer $Server -ClientId $ClientID -Scope $AccessTokenScope -Credential $Credential + +$AccessToken = $tppTokenResponse.AccessToken.GetNetworkCredential().Password +$Expiry = $tppTokenResponse.Expires.ToString(\"s\") +$RefreshToken = $tppTokenResponse.RefreshToken.GetNetworkCredential().Password +$RefreshExpires = $tppTokenResponse.RefreshExpires + +# Refresh Expiry can be $null +if ($null -ne $RefreshExpires) { + $RefreshExpires = $RefreshExpires.ToString(\"s\") +} + +Set-OctopusVariable -Name \"AccessToken\" -Value $AccessToken -Sensitive +Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.AccessToken}\" +Set-OctopusVariable -Name \"AccessTokenExpires\" -Value $Expiry -Sensitive +Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.AccessTokenExpires}\" +Set-OctopusVariable -Name \"RefreshToken\" -Value $RefreshToken -Sensitive +Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.RefreshToken}\" +Set-OctopusVariable -Name \"RefreshTokenExpires\" -Value $RefreshExpires -Sensitive +Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.RefreshTokenExpires}\"" + }, + "Parameters": [ + { + "Id": "7ca2062d-9153-4606-bbab-441c893f9739", + "Name": "Venafi.TPP.OAuthToken.Server", + "Label": "Venafi TPP Server", + "HelpText": "The URL of the Venafi TPP instance you are connecting to. + +For example: `https://mytppserver.example.com`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "57631dfd-965b-4bfa-9f34-5e9147cdb702", + "Name": "Venafi.TPP.OAuthToken.ClientID", + "Label": "Venafi Application ClientID", + "HelpText": "Application Id (also known as `ClientId`) configured in Venafi for token-based authentication.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "83bfad97-50d1-49b9-9d9b-dc2ba87d9281", + "Name": "Venafi.TPP.OAuthToken.Username", + "Label": "Venafi Username", + "HelpText": "Username to request API token.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "05280b11-3dd6-41d7-a970-2328478a4b52", + "Name": "Venafi.TPP.OAuthToken.Password", + "Label": "Venafi Password", + "HelpText": "Password to request API token.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "b7edd6a4-20bb-47fe-8181-42ca5af9adb2", + "Name": "Venafi.TPP.OAuthToken.Scope", + "Label": "Access token scope", + "HelpText": "Scopes and privilege restrictions for the access token. Scopes can include: + +- `agent` +- `certificate` +- `code signing` +- `configuration` +- `restricted` +- `security` +- `ssh` +- `statistics` + +See the [Venafi Auth SDK](https://docs.venafi.com/Docs/21.1/TopNav/Content/SDK/AuthSDK/t-SDKa-Setup-OAuth.php) for more info. + +Multiple values can be supplied separated by `;`. + +For a privilege restriction of none or read, just use the scope as the value. For example, to include management of certificates and include ssh with no privilege restriction, use: `certificate:manage;ssh`. + +If multiple values for the same scope are provided they will be concatenated, *unless* a value of no privilege is found. If that occurs, then the \"last entry\" wins.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedAt": "2024-04-10T17:28:31.301Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2021-07-30T10:36:31.301Z", + "OctopusVersion": "2021.2.6775", + "Type": "ActionTemplate" + }, + "Category": "venafi" + } diff --git a/step-templates/venafi-tpp-import-certificate-into-octopus.json.human b/step-templates/venafi-tpp-import-certificate-into-octopus.json.human new file mode 100644 index 000000000..722126280 --- /dev/null +++ b/step-templates/venafi-tpp-import-certificate-into-octopus.json.human @@ -0,0 +1,638 @@ +{ + "Id": "e10820c2-ae6d-4030-8a8a-b73ed60a81fc", + "Name": "Venafi TPP - Import Certificate into Octopus", + "Description": "This step template will authenticate against a Venafi TPP instance using an existing OAuth access token, export a certificate by its Distinguished Name (DN), and then import the certificate into the Octopus certificate store. + +The export is achieved using the VenafiPS PowerShell module's [Export-VenafiCertificate](https://venafips.readthedocs.io/en/latest/functions/Export-VenafiCertificate/) function. + +--- + +**Export options:** + +- Provide the distinguished name (DN) path to the certificate. +- Choose from the following export formats: + - `Base64` + - `Base64 (PKCS #8)` + - `DER` + - `PKCS #12` +- *Optional* - Include the full certificate chain in the export. +- *Optional* - Friendly name (Label or alias) to use. Permitted with `Base64` and `PKCS #12` formats. +- *Optional* - Include the private key in the export. Not supported with `DER` format. +- *Optional* - Provide a password to be used for the exported private key. +- *Optional* - Also store the export certificate response in `JSON` format in an [Octopus sensitive output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables). This output variable can then be used in additional deployment or runbook steps. +- *Optional* - on successful completion, you can revoke the access token used. + +--- + +**Octopus Import options:** + +- Octopus URL +- Octopus API Key +- Octopus Space name +- Certificate name +- *Optional* - replace the existing certificate if it already exists + +--- + +**Required:** +- The `VenafiPS` PowerShell module installed on the deployment target or worker. If the module can't be found, the step will attempt to download a version from the [PowerShell gallery](https://www.powershellgallery.com/packages/VenafiPS). +- PowerShell `5` or greater. +- Octopus API key with permission to save the certificate. + +Notes: + +- Tested on Octopus `2021.2`. +- Tested with VenafiPS `3.1.5`. +- Tested with both Windows PowerShell and PowerShell Core on Linux.", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ErrorActionPreference = 'Stop' + +# TPP required variables +$Server = $OctopusParameters[\"Venafi.TPP.ImportCert.Server\"] +$Token = $OctopusParameters[\"Venafi.TPP.ImportCert.AccessToken\"] +$Path = $OctopusParameters[\"Venafi.TPP.ImportCert.DNPath\"] +$Format = $OctopusParameters[\"Venafi.TPP.ImportCert.Format\"] + +# TPP optional variables +$IncludeChain = $OctopusParameters[\"Venafi.TPP.ImportCert.IncludeChain\"] +$FriendlyName = $OctopusParameters[\"Venafi.TPP.ImportCert.FriendlyName\"] +$IncludePrivateKey = $OctopusParameters[\"Venafi.TPP.ImportCert.IncludePrivateKey\"] +$PrivateKeyPassword = $OctopusParameters[\"Venafi.TPP.ImportCert.PrivateKeyPassword\"] +$OutputVariableName = $OctopusParameters[\"Venafi.TPP.ImportCert.OutputVariableName\"] +$RevokeToken = $OctopusParameters[\"Venafi.TPP.ImportCert.RevokeTokenOnCompletion\"] + +# Octopus required variables +$OctopusServerUri = $OctopusParameters[\"Venafi.TPP.ImportCert.OctopusServerUri\"] +$OctopusApiKey = $OctopusParameters[\"Venafi.TPP.ImportCert.OctopusApiKey\"] +$OctopusSpaceName = $OctopusParameters[\"Venafi.TPP.ImportCert.OctopusSpaceName\"] +$OctopusCertificateName = $OctopusParameters[\"Venafi.TPP.ImportCert.OctopusCertificateName\"] +$OctopusReplaceExistingCertificate = $OctopusParameters[\"Venafi.TPP.ImportCert.OctopusReplaceExistingCertificate\"] + +# TPP validation +if ([string]::IsNullOrWhiteSpace($Server)) { + throw \"Required parameter Venafi.TPP.ImportCert.Server not specified\" +} +if ([string]::IsNullOrWhiteSpace($Token)) { + throw \"Required parameter Venafi.TPP.ImportCert.AccessToken not specified\" +} +if ([string]::IsNullOrWhiteSpace($Path)) { + throw \"Required parameter Venafi.TPP.ImportCert.DNPath not specified\" +} +else { + if ($Path.Contains(\"\\\") -eq $False) { + throw \"At least one '\\' is required for the Venafi.TPP.ImportCert.DNPath value\" + } +} +if ([string]::IsNullOrWhiteSpace($Format)) { + throw \"Required parameter Venafi.TPP.ImportCert.Format not specified\" +} + +# TPP conditional validation +if ($IncludePrivateKey -eq $True) { + if ([string]::IsNullOrWhiteSpace($PrivateKeyPassword)) { + throw \"IncludePrivateKey set to true, but parameter Venafi.TPP.ImportCert.PrivateKeyPassword not specified\" + } +} +else { + $PrivateKeyPassword = $null +} + +# Octopus validation +if ([string]::IsNullOrWhiteSpace($OctopusServerUri)) { + throw \"Required parameter Venafi.TPP.ImportCert.OctopusServerUri not specified\" +} +if ([string]::IsNullOrWhiteSpace($OctopusApiKey)) { + throw \"Required parameter Venafi.TPP.ImportCert.OctopusApiKey not specified\" +} +if ([string]::IsNullOrWhiteSpace($OctopusSpaceName)) { + throw \"Required parameter Venafi.TPP.ImportCert.OctopusSpaceName not specified\" +} +if ([string]::IsNullOrWhiteSpace($OctopusCertificateName)) { + throw \"Required parameter Venafi.TPP.ImportCert.OctopusCertificateName not specified\" +} +if ([string]::IsNullOrWhiteSpace($OctopusReplaceExistingCertificate)) { + throw \"Required parameter Venafi.TPP.ImportCert.OctopusReplaceExistingCertificate not specified\" +} + +# Helper functions +############################################################################### +function Get-WebRequestErrorBody { + param ( + $RequestError + ) + + # Powershell < 6 you can read the Exception + if ($PSVersionTable.PSVersion.Major -lt 6) { + if ($RequestError.Exception.Response) { + $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream()) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $rawResponse = $reader.ReadToEnd() + $response = \"\" + try { $response = $rawResponse | ConvertFrom-Json } catch { $response = $rawResponse } + return $response + } + } + else { + return $RequestError.ErrorDetails.Message + } +} + +function Get-MatchingOctopusCertificates { + param ( + [string]$ServerUri, + [string]$ApiKey, + [string]$SpaceId, + [string]$CertificateName + ) + Write-Debug \"Entering: Get-MatchingOctopusCertificates\" + + try { + + $header = @{ \"X-Octopus-ApiKey\" = $ApiKey } + + # Get a list of certificates that match our domain search criteria. + $partial_certificates = (Invoke-RestMethod -Uri \"$ServerUri/api/$SpaceId/certificates?partialName=$([uri]::EscapeDataString($CertificateName))&skip=0&take=2000\" -Headers $header) | Select-Object -ExpandProperty Items + + # return certs that arent archived and havent been replaced. + return $partial_certificates | Where-Object { + $null -eq $_.ReplacedBy -and + $null -eq $_.Archived -and + $CertificateName -eq $_.Name + } + } + catch { + $Detail = (Get-WebRequestErrorBody -RequestError $_) + Write-Error \"Could not retrieve certificates from Octopus. Error: $($_.Exception.Message).`n`t$Detail\" + } +} + +function Replace-OctopusCertificate { + param ( + [string]$ServerUri, + [string]$ApiKey, + [string]$SpaceId, + [string]$CertificateId, + [string]$CertificateName, + [string]$CertificateData, + [string]$CertificatePwd + ) + Write-Debug \"Entering: Replace-OctopusCertificate\" + try { + + $header = @{ \"X-Octopus-ApiKey\" = $ApiKey } + + $replacement_certificate = @{ + CertificateData = $CertificateData + } + + if (![string]::IsNullOrWhiteSpace($CertificatePwd)) { + $replacement_certificate.Password = $CertificatePwd + } + + # Replace the cert + $updated_certificate = Invoke-RestMethod -Method Post -Uri \"$ServerUri/api/$SpaceId/certificates/$CertificateId/replace\" -Headers $header -Body ($replacement_certificate | ConvertTo-Json -Depth 10) + Write-Highlight \"Replaced certificate in Octopus for '$($updated_certificate.Name)' ($($updated_certificate.Id))\" + } + catch { + $Detail = (Get-WebRequestErrorBody -RequestError $_) + Write-Error \"Could not replace certificate in Octopus. Error: $($_.Exception.Message).`n`t$Detail\" + } +} + +function New-OctopusCertificate { + param ( + [string]$ServerUri, + [string]$ApiKey, + [string]$SpaceId, + [string]$CertificateName, + [string]$CertificateData, + [string]$CertificatePwd + ) + Write-Debug \"Entering: New-OctopusCertificate\" + try { + + $header = @{ \"X-Octopus-ApiKey\" = $ApiKey } + + $certificate = @{ + Name = $CertificateName; + CertificateData = @{ + NewValue = $CertificateData; + HasData = $True; + } + Password = @{ + HasValue = $False; + NewValue = $null; + } + } + + if (![string]::IsNullOrWhiteSpace($CertificatePwd)) { + $certificate.Password.NewValue = $CertificatePwd + $certificate.Password.HasData = $True + } + + # Create new certificate + $new_certificate = Invoke-RestMethod -Method Post -Uri \"$ServerUri/api/$SpaceId/certificates\" -Headers $header -Body ($certificate | ConvertTo-Json -Depth 10) + Write-Highlight \"New certificate created in Octopus for '$($new_certificate.Name)' ($($new_certificate.Id))\" + } + catch { + $Detail = (Get-WebRequestErrorBody -RequestError $_) + Write-Error \"Could not create new certificate in Octopus. Error: $($_.Exception.Message).`n`t$Detail\" + } +} + +function Clean-VenafiCertificateForOctopus { + param ( + [string]$CertificateData + ) + Write-Debug \"Entering: Clean-VenafiCertificateForOctopus\" + $PemHeaderFragment = \"-----BEGIN *\" + $PemFooterFragment = \"-----END *\" + + $CertificateBytes = [Convert]::FromBase64String($CertificateData) + $RawCert = [System.Text.Encoding]::UTF8.GetString($CertificateBytes) + + $CleanedCertLines = @() + if (![string]::IsNullOrWhiteSpace($RawCert)) { + $RawCertLines = ($RawCert -Split \"`n\") + $currentLine = 0 + while ($currentLine -lt $RawCertLines.Length) { + Write-Verbose \"Working on line $currentLine\" + $headerPosition = [Array]::FindIndex($RawCertLines, $currentLine, [Predicate[string]] { $args[0] -like $PemHeaderFragment }) + if ($headerPosition -gt -1) { + $footerPosition = [Array]::FindIndex($RawCertLines, $headerPosition, [Predicate[string]] { $args[0] -like $PemFooterFragment }) + if ($footerPosition -lt 0) { + throw \"Unable to find a matching '-----END' PEM fragment!\" + } + else { + Write-Verbose \"Selecting PEM lines: $headerPosition-$footerPosition\" + $pemLines = $RawCertLines[$headerPosition..$footerPosition] + $CleanedCertLines += $pemLines + $currentLine = $footerPosition + } + } + else { + $currentLine++ + } + } + } + if ($CleanedCertLines.Length -le 0) { + throw \"Something went wrong extracting contents from file (no cleansed contents)\" + } + + $CleanedCert = $CleanedCertLines | Out-String + $CleanedCertData = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($CleanedCert)) + + return $CleanedCertData +} +############################################################################### +# MAIN STEP TEMPLATE FLOW +############################################################################### + +# TPP Access token +$SecureToken = ConvertTo-SecureString $Token -AsPlainText -Force +[PSCredential]$AccessToken = New-Object System.Management.Automation.PsCredential(\"token\", $SecureToken) + +# Clean-up +$Server = $Server.TrimEnd('/') +$OctopusServerUri = $OctopusServerUri.TrimEnd('/') +$OctopusSpaceName = $OctopusSpaceName.Trim(\" \") +$OctopusCertificateName = $OctopusCertificateName.Trim(\" \") + +# Required Venafi Module +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +# Check to see if the package provider has been installed +if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + Write-Host \"Nuget package provider not found, installing ...\" + Install-PackageProvider -Name Nuget -Force -Scope CurrentUser +} + +Write-Host \"Checking for required VenafiPS module ...\" +$required_venafips_version = 3.1.5 +$module_available = Get-Module -ListAvailable -Name VenafiPS | Where-Object { $_.Version -ge $required_venafips_version } +if (-not ($module_available)) { + Write-Host \"Installing VenafiPS module ...\" + Install-Module -Name VenafiPS -MinimumVersion 3.1.5 -Scope CurrentUser -Force +} +else { + $first_match = $module_available | Select-Object -First 1 + Write-Host \"Found version: $($first_match.Version)\" +} + +Write-Host \"Importing VenafiPS module ...\" +Import-Module VenafiPS + +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] +$ExportFormatsIncompatibleWithOctopusCertificateStore = @(\"Base64\", \"Base64 (PKCS #8)\") + +Write-Verbose \"Venafi.TPP.ImportCert.Server: $Server\" +Write-Verbose \"Venafi.TPP.ImportCert.AccessToken: ********\" +Write-Verbose \"Venafi.TPP.ImportCert.DNPath: $Path\" +Write-Verbose \"Venafi.TPP.ImportCert.Format: $Format\" +Write-Verbose \"Venafi.TPP.ImportCert.IncludeChain: $IncludeChain\" +Write-Verbose \"Venafi.TPP.ImportCert.FriendlyName: $FriendlyName\" +Write-Verbose \"Venafi.TPP.ImportCert.IncludePrivateKey: $IncludePrivateKey\" +Write-Verbose \"Venafi.TPP.ImportCert.PrivateKeyPassword: ********\" +Write-Verbose \"Venafi.TPP.ImportCert.CertDetails.OutputVariableName: $OutputVariableName\" +Write-Verbose \"Venafi.TPP.ImportCert.RevokeTokenOnCompletion: $RevokeTokenOnCompletion\" +Write-Verbose \"Venafi.TPP.ImportCert.OctopusServerUri: $OctopusServerUri\" +Write-Verbose \"Venafi.TPP.ImportCert.OctopusApiKey: ********\" +Write-Verbose \"Venafi.TPP.ImportCert.OctopusSpaceName: $OctopusSpaceName\" +Write-Verbose \"Venafi.TPP.ImportCert.OctopusCertificateName: $OctopusCertificateName\" +Write-Verbose \"Venafi.TPP.ImportCert.OctopusReplaceExistingCertificate: $OctopusReplaceExistingCertificate\" +Write-Verbose \"Step Name: $StepName\" + +Write-Host \"Requesting new session from $Server\" +New-VenafiSession -Server $Server -AccessToken $AccessToken + +# Export certificate +$ExportCert_Params = @{ + CertificateId = $Path; + Format = $Format; +} + +# Optional IncludeChain field +if ($IncludeChain -eq $True) { + $ExportCert_Params.IncludeChain = $True +} + +# Optional FriendlyName field +if (-not [string]::IsNullOrWhiteSpace($FriendlyName)) { + $ExportCert_Params.FriendlyName = $FriendlyName +} + +# Optional Private key field +if (-not [string]::IsNullOrWhiteSpace($PrivateKeyPassword) -and $IncludePrivateKey -eq $True) { + $SecurePrivateKeyPassword = ConvertTo-SecureString $PrivateKeyPassword -AsPlainText -Force + $ExportCert_Params.PrivateKeyPassword = $SecurePrivateKeyPassword + $ExportCert_Params.IncludePrivateKey = $True +} + +# Do the export +$ExportCertificateResponse = ((Export-VenafiCertificate @ExportCert_Params) 6> $null) + +if ($null -eq $ExportCertificateResponse -or $null -eq $ExportCertificateResponse.CertificateData) { + Write-Warning \"No certificate data returned for path: $Path`nCheck the path value represents a certificate, and not a folder.\" +} +else { + Write-Host \"Successfully retrieved certificate data to export for path: $Path\" + + # Get octopus space Id + $header = @{ \"X-Octopus-ApiKey\" = $OctopusApiKey } + $spaces = Invoke-RestMethod -Uri \"$OctopusServerUri/api/spaces?partialName=$([uri]::EscapeDataString($OctopusSpaceName))&skip=0&take=500\" -Headers $header + $OctopusSpace = @($spaces.Items | Where-Object { $_.Name -eq $OctopusSpaceName }) | Select-Object -First 1 + + if ($null -eq $OctopusSpace) { + throw \"Couldnt find Octopus space with name '$OctopusSpaceName'.\" + } + + # Check for certificate based on name + $CertificateMatches = @(Get-MatchingOctopusCertificates -ServerUri $OctopusServerUri -ApiKey $OctopusApiKey -SpaceId $($OctopusSpace.Id) -CertificateName $OctopusCertificateName) + Write-Host \"Found $($CertificateMatches.Length) certificates matching '$OctopusCertificateName'\" + + $FirstCertificateMatch = $CertificateMatches | Select-Object -First 1 + $CertificateData = $ExportCertificateResponse.CertificateData + + if ($ExportFormatsIncompatibleWithOctopusCertificateStore -icontains $Format) { + Write-Host \"Requested export format $Format needs to be cleaned before import to Octopus.\" + $CertificateData = Clean-VenafiCertificateForOctopus -CertificateData $CertificateData + if ([string]::IsNullOrWhiteSpace($CertificateData)) { + throw \"Cleaned certificate data empty!\" + } + } + + switch ($CertificateMatches.Length) { + 0 { + # New cert + Write-Host \"Creating a new certificate '$OctopusCertificateName'\" + New-OctopusCertificate -ServerUri $OctopusServerUri -ApiKey $OctopusApiKey -SpaceId $($OctopusSpace.Id) -CertificateName $OctopusCertificateName -CertificateData $($CertificateData) -CertificatePwd $PrivateKeyPassword + } + 1 { + # One cert to replace + if ($OctopusReplaceExistingCertificate -eq $False) { + Write-Host \"Replace existing certificate set to False, nothing to do.\" + } + else { + Write-Host \"Replacing existing certificate '$OctopusCertificateName' ($($FirstCertificateMatch.Id))\" + Replace-OctopusCertificate -ServerUri $OctopusServerUri -ApiKey $OctopusApiKey -SpaceId $($OctopusSpace.Id) -CertificateId $($FirstCertificateMatch.Id) -CertificateName $OctopusCertificateName -CertificateData $($CertificateData) -CertificatePwd $PrivateKeyPassword + } + } + default { + Write-Warning \"Multiple certs matching name '$OctopusCertificateName' found, nothing to do.\" + return + } + } + + if ([string]::IsNullOrWhiteSpace($OutputVariableName) -eq $False) { + $CertificateJson = $ExportCertificateResponse | ConvertTo-Json -Compress -Depth 10 + Set-OctopusVariable -Name $OutputVariableName -Value $CertificateJson -Sensitive + Write-Highlight \"Created sensitive output variable: ##{Octopus.Action[$StepName].Output.$OutputVariableName}\" + } +} + +if ($RevokeToken -eq $true) { + # Revoke TPP access token + Write-Host \"Revoking access token with $Server\" + Revoke-TppToken -AuthServer $Server -AccessToken $AccessToken -Force +}" + }, + "Parameters": [ + { + "Id": "56ef4967-37f5-40a0-a66e-f3fa589b6467", + "Name": "Venafi.TPP.ImportCert.Server", + "Label": "Venafi TPP Server", + "HelpText": "*Required*: The URL of the Venafi TPP instance you want to export the certificate from. + +For example: `https://mytppserver.example.com`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "49bcdbbb-3674-4901-8bf6-164e5e4bc395", + "Name": "Venafi.TPP.ImportCert.AccessToken", + "Label": "Venafi TPP Access Token", + "HelpText": "*Required*: The access token to authenticate against the TPP instance.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "e3156852-4ba9-4dc0-8d39-5a93c52b1910", + "Name": "Venafi.TPP.ImportCert.DNPath", + "Label": "Venafi TPP Certificate Path", + "HelpText": "*Required*: The Distinguished Name (DN) of the certificate you wish to export. This is the absolute path to the certificate in the TPP instance, separated by `\\`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "4f9f4d4b-d686-4d00-aa93-af35b7df320b", + "Name": "Venafi.TPP.ImportCert.Format", + "Label": "Certificate Export Format", + "HelpText": "*Required*: The certificate export format. Valid options are: + +- `Base64` +- `Base64 (PKCS #8)` +- `DER` +- `PKCS #12`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Base64|Base64 +Base64 (PKCS #8)|Base64 (PKCS #8) +DER|DER +PKCS #12|PKCS #12" + } + }, + { + "Id": "309d30de-79b6-4461-8a54-1698aedd5822", + "Name": "Venafi.TPP.ImportCert.IncludeChain", + "Label": "Include certificate chain (Optional)", + "HelpText": "*Optional*: Include the certificate chain with the exported certificate. Not supported with `DER` format. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "71fecac3-25c4-4161-9135-94815a485f03", + "Name": "Venafi.TPP.ImportCert.FriendlyName", + "Label": "Friendly Name (Optional)", + "HelpText": "*Optional*: Label or alias to use. Permitted with `Base64` and `PKCS #12` formats.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2aaedf1d-be93-4df4-856c-c69650db452a", + "Name": "Venafi.TPP.ImportCert.IncludePrivateKey", + "Label": "Include Private Key (Optional)", + "HelpText": "*Optional*: Include the private key in the certificate export. If this is selected, the `Venafi.TPP.Export.PrivateKeyPassword` must also be provided. Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "2d168360-bcbf-4bdc-833d-a9c182e98a47", + "Name": "Venafi.TPP.ImportCert.PrivateKeyPassword", + "Label": "Private Key password (Optional)", + "HelpText": "*Optional*: The password required to include the private key. Not supported with `DER` format. You must adhere to the following rules: + +- Password is at least 12 characters. +- Comprised of at least three of the following: + - Uppercase alphabetic letters + - Lowercase alphabetic letters + - Numeric characters + - Special characters", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "84f92dd5-064b-47e5-bb11-3dd0faacfeb4", + "Name": "Venafi.TPP.ImportCert.OutputVariableName", + "Label": "Certificate output variable name (Optional)", + "HelpText": "*Optional*: Create an output variable with the certificate details returned from the export call. The certificate details will be stored in `JSON` format.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "606acdfe-641a-47f2-a4ea-56559477ea0c", + "Name": "Venafi.TPP.ImportCert.RevokeTokenOnCompletion", + "Label": "Revoke access token on completion?", + "HelpText": "*Optional*: Should the access token used be revoked once the step has been completed successfully? Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "7813080c-d6d6-4bd1-8c38-d6d03921f541", + "Name": "Venafi.TPP.ImportCert.OctopusServerUri", + "Label": "Octopus Server Url", + "HelpText": "*Required*: Provide the base URL of your Octopus Server. There are two built-in Octopus variables you can use: + +- `#{if Octopus.Web.ServerUri}#{Octopus.Web.ServerUri}#{else}#{Octopus.Web.BaseUrl}#{/if}` +- `Octopus.Web.ServerUri` + +See our [system variables](https://octopus.com/docs/projects/variables/system-variables#Systemvariables-Server) page for further details.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "5be3cf08-43ef-44a3-bd89-3ec3b4928b01", + "Name": "Venafi.TPP.ImportCert.OctopusApiKey", + "Label": "Octopus API Key", + "HelpText": "*Required*: Provide an Octopus API Key with appropriate permissions to save the certificate. +", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "d901f398-a035-4ba6-a546-553360eed283", + "Name": "Venafi.TPP.ImportCert.OctopusSpaceName", + "Label": "Octopus Space Name", + "HelpText": "*Required*: Provide the Space name for the certificate to be saved in. The default is the current space the step is running within: `#{Octopus.Space.Name}`.", + "DefaultValue": "#{Octopus.Space.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "56f8e1a8-fb6c-4b91-a216-eadc2f5cd673", + "Name": "Venafi.TPP.ImportCert.OctopusCertificateName", + "Label": "Octopus Certificate Name", + "HelpText": "*Required*: A short, memorable, unique name for the imported certificate.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3a85993f-8844-49aa-951a-804471f53b23", + "Name": "Venafi.TPP.ImportCert.OctopusReplaceExistingCertificate", + "Label": "Replace existing Octopus certificate?", + "HelpText": "*Optional*: If a certificate exists in Octopus with the same name as the one to be imported, should the one stored in Octopus be replaced? Default: `True`. + +**Note**: If multiple matches are found, the step template will not replace any, and will log a warning instead. + +See [replacing certificates](https://octopus.com/docs/deployments/certificates/replace-certificate) for further information.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedAt": "2021-08-18T15:47:39.557Z", + "LastModifiedBy": "benjimac93", + "$Meta": { + "ExportedAt": "2021-08-23T12:40:10.975Z", + "OctopusVersion": "2021.1.7687", + "Type": "ActionTemplate" + }, + "Category": "venafi" + } diff --git a/step-templates/venafi-tpp-retrieve-certificate-details.json.human b/step-templates/venafi-tpp-retrieve-certificate-details.json.human new file mode 100644 index 000000000..eec188d89 --- /dev/null +++ b/step-templates/venafi-tpp-retrieve-certificate-details.json.human @@ -0,0 +1,181 @@ +{ + "Id": "b893cae4-e280-4686-a60b-38da876528ec", + "Name": "Venafi TPP - Retrieve Certificate Details", + "Description": "This step template will authenticate against a Venafi TPP instance using an existing OAuth access token, and retrieve a certificate's details using its Distinguished Name (DN). This is the absolute path to the certificate in the TPP instance. + +This is achieved using the VenafiPS PowerShell module's [Get-VenafiCertificate](https://venafips.readthedocs.io/en/latest/functions/Get-VenafiCertificate/) function. + +You can also store the entire certificate result in `JSON` format in an [Octopus output variable](https://octopus.com/docs/projects/variables/output-variables) + +This output variable can then be used in additional deployment or runbook steps. + +On successful completion, you can also *optionally* revoke the access token used. + +--- + +**Required:** +- The `VenafiPS` PowerShell module installed on the deployment target or worker. If the module can't be found, the step will attempt to download a version from the [PowerShell gallery](https://www.powershellgallery.com/packages/VenafiPS). + +Notes: + +- Tested on Octopus `2021.2`. +- Tested with VenafiPS `3.1.5`. +- Tested with both Windows PowerShell and PowerShell Core on Linux.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ErrorActionPreference = 'Stop' + +# Variables +$Server = $OctopusParameters[\"Venafi.TPP.GetCert.Server\"] +$Token = $OctopusParameters[\"Venafi.TPP.GetCert.AccessToken\"] +$Path = $OctopusParameters[\"Venafi.TPP.GetCert.DNPath\"] + +# Optional +$OutputVariableName = $OctopusParameters[\"Venafi.TPP.GetCert.OutputVariableName\"] +$RevokeToken = $OctopusParameters[\"Venafi.TPP.GetCert.RevokeTokenOnCompletion\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($Server)) { + throw \"Required parameter Venafi.TPP.GetCert.Server not specified\" +} +if ([string]::IsNullOrWhiteSpace($Token)) { + throw \"Required parameter Venafi.TPP.GetCert.AccessToken not specified\" +} +if ([string]::IsNullOrWhiteSpace($Path)) { + throw \"Required parameter Venafi.TPP.GetCert.DNPath not specified\" +} +else { + if ($Path.Contains(\"\\\") -eq $False) { + throw \"At least one '\\' is required for the Venafi.TPP.GetCert.DNPath value\" + } +} + +$SecureToken = ConvertTo-SecureString $Token -AsPlainText -Force +[PSCredential]$AccessToken = New-Object System.Management.Automation.PsCredential(\"token\", $SecureToken) + +# Clean-up +$Server = $Server.TrimEnd('/') + +# Required Modules +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +# Check to see if the package provider has been installed +if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + Write-Host \"Nuget package provider not found, installing ...\" + Install-PackageProvider -Name Nuget -Force -Scope CurrentUser +} + +Write-Host \"Checking for required VenafiPS module ...\" +$required_venafips_version = 3.1.5 +$module_available = Get-Module -ListAvailable -Name VenafiPS | Where-Object { $_.Version -ge $required_venafips_version } +if (-not ($module_available)) { + Write-Host \"Installing VenafiPS module ...\" + Install-Module -Name VenafiPS -MinimumVersion 3.1.5 -Scope CurrentUser -Force +} +else { + $first_match = $module_available | Select-Object -First 1 + Write-Host \"Found version: $($first_match.Version)\" +} + +Write-Host \"Importing VenafiPS module ...\" +Import-Module VenafiPS + +$StepName = $OctopusParameters[\"Octopus.Step.Name\"] + +# Create Venafi session +New-VenafiSession -Server $Server -AccessToken $AccessToken + +# Retrieve certificate details +$CertificateDetails = Get-VenafiCertificate -CertificateId $Path | Select-Object -First 1 + +if ($null -eq $CertificateDetails -or $null -eq $CertificateDetails.Path) { + Write-Warning \"No certificate details returned for path: $Path`nCheck the path value represents a certificate, and not a folder.\" +} +else { + Write-Highlight \"Retrieved certificate details for path: $Path\" + $CertificateDetails | Format-List + + if ([string]::IsNullOrWhiteSpace($OutputVariableName) -eq $False) { + $CertificateJson = $CertificateDetails | ConvertTo-Json -Compress -Depth 10 + Set-OctopusVariable -Name $OutputVariableName -Value $CertificateJson + Write-Highlight \"Created output variable: ##{Octopus.Action[$StepName].Output.$OutputVariableName}\" + } +} + +if ($RevokeToken -eq $true) { + # Revoke TPP access token + Write-Host \"Revoking access token with $Server\" + Revoke-TppToken -AuthServer $Server -AccessToken $AccessToken -Force +}" + }, + "Parameters": [ + { + "Id": "9873f05e-4a09-47aa-88c4-6bf63aad5cbc", + "Name": "Venafi.TPP.GetCert.Server", + "Label": "Venafi TPP Server", + "HelpText": "*Required*: The URL of the Venafi TPP instance you want to find certificate details from. + +For example: `https://mytppserver.example.com`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "81a075af-2ce7-4f0d-ae97-968dacebc15f", + "Name": "Venafi.TPP.GetCert.AccessToken", + "Label": "Venafi TPP Access Token", + "HelpText": "*Required*: The access token to authenticate against the TPP instance.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "ffdde857-7af9-4fc3-9b74-76789fa1dd9b", + "Name": "Venafi.TPP.GetCert.DNPath", + "Label": "Venafi TPP Certificate Path", + "HelpText": "*Required*: The certificate's Distinguished Name (DN). This is the absolute path to the certificate in the TPP instance, separated by `\\`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "9af1fa28-325c-411d-8e0e-7aa676f05da1", + "Name": "Venafi.TPP.GetCert.OutputVariableName", + "Label": "Certificate output variable name (Optional)", + "HelpText": "*Optional*: Create an output variable with the certificate details found from the search. The certificate details will be stored in `JSON` format.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "1d8421eb-ed22-4513-9d5e-99bba1f8296f", + "Name": "Venafi.TPP.GetCert.RevokeTokenOnCompletion", + "Label": "Revoke access token on completion?", + "HelpText": "Should the access token used be revoked once the step has been completed successfully? Default: `False`.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2021-08-16T13:51:19.689Z", + "OctopusVersion": "2021.2.7207", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mark-the-butler", + "Category": "venafi" + } diff --git a/step-templates/venafi-tpp-revoke-oauth-token.json.human b/step-templates/venafi-tpp-revoke-oauth-token.json.human new file mode 100644 index 000000000..1ed39b0e1 --- /dev/null +++ b/step-templates/venafi-tpp-revoke-oauth-token.json.human @@ -0,0 +1,106 @@ +{ + "Id": "d3e20a46-8119-4f4f-9d6f-52c30bcc6c59", + "Name": "Venafi TPP - Revoke OAuth Token", + "Description": "This step template will revoke an access token obtained through a Venafi TPP instance using the VenafiPS PowerShell module's [Revoke-TppToken](https://venafips.readthedocs.io/en/latest/functions/Revoke-TppToken/) function. + +--- + +**Required:** +- The `VenafiPS` PowerShell module installed on the deployment target or worker. If the module can't be found, the step will attempt to download a version from the [PowerShell gallery](https://www.powershellgallery.com/packages/VenafiPS). + +Notes: + +- Tested on Octopus `2021.2`. +- Tested with VenafiPS `3.1.5`. +- Tested with both Windows PowerShell and PowerShell Core on Linux.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ErrorActionPreference = 'Stop' + +# Variables +$Server = $OctopusParameters[\"Venafi.TPP.RevokeOAuth.Server\"] +$Token = $OctopusParameters[\"Venafi.TPP.RevokeOAuth.AccessToken\"] + +# Validation +if ([string]::IsNullOrWhiteSpace($Server)) { + throw \"Required parameter Venafi.TPP.RevokeOAuth.Server not specified\" +} +if ([string]::IsNullOrWhiteSpace($Token)) { + throw \"Required parameter Venafi.TPP.RevokeOAuth.AccessToken not specified\" +} + +$SecureToken = ConvertTo-SecureString $Token -AsPlainText -Force +[PSCredential]$AccessToken = New-Object System.Management.Automation.PsCredential(\"token\", $SecureToken) + +# Clean-up +$Server = $Server.TrimEnd('/') + +# Required Modules +function Get-NugetPackageProviderNotInstalled { + # See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +# Check to see if the package provider has been installed +if ((Get-NugetPackageProviderNotInstalled) -ne $false) { + Write-Host \"Nuget package provider not found, installing ...\" + Install-PackageProvider -Name Nuget -Force -Scope CurrentUser +} + +Write-Host \"Checking for required VenafiPS module ...\" +$required_venafips_version = 3.1.5 +$module_available = Get-Module -ListAvailable -Name VenafiPS | Where-Object { $_.Version -ge $required_venafips_version } +if (-not ($module_available)) { + Write-Host \"Installing VenafiPS module ...\" + Install-Module -Name VenafiPS -MinimumVersion 3.1.5 -Scope CurrentUser -Force +} +else { + $first_match = $module_available | Select-Object -First 1 + Write-Host \"Found version: $($first_match.Version)\" +} + +Write-Host \"Importing VenafiPS module ...\" +Import-Module VenafiPS + +# Revoke TPP access token +Write-Host \"Revoking access token with $Server\" +Revoke-TppToken -AuthServer $Server -AccessToken $AccessToken -Force" + }, + "Parameters": [ + { + "Id": "7f3400a2-ae73-4299-93a4-4309d38312ff", + "Name": "Venafi.TPP.RevokeOAuth.Server", + "Label": "Venafi TPP Server", + "HelpText": "The URL of the Venafi TPP instance you want to revoke the access token against. + +For example: `https://mytppserver.example.com`.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "27abe7fc-b82a-4d80-a591-f53d8154414c", + "Name": "Venafi.TPP.RevokeOAuth.AccessToken", + "Label": "Venafi TPP Access Token", + "HelpText": "The access token obtained from the TPP server you want to revoke.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + } + ], + "$Meta": { + "ExportedAt": "2021-08-11T10:08:47.547Z", + "OctopusVersion": "2021.2.7127", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "mark-the-butler", + "Category": "venafi" + } diff --git a/step-templates/victorops-create-alert.json.human b/step-templates/victorops-create-alert.json.human new file mode 100644 index 000000000..83ca2c53f --- /dev/null +++ b/step-templates/victorops-create-alert.json.human @@ -0,0 +1,143 @@ +{ + "Id": "2ecb9ec9-2c81-4e75-8093-175d2557ca54", + "Name": "VictorOps - Create Alert", + "Description": "Create an alert in VictorOps via the REST integration. See [VictorOps docs](https://help.victorops.com/knowledge-base/victorops-restendpoint-integration/) for details.", + "ActionType": "Octopus.Script", + "Version": 24, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Set-StrictMode -Version Latest + +function Send-VictorOpsAlert($url, $messageType, $entityDisplayName, $stateMessage, $customFields) +{ + $payload = @{ + \"message_type\" = $messageType + \"entity_display_name\" = $entityDisplayName + \"state_message\" = $stateMessage + } + + if (-not ([string]::IsNullOrEmpty($customFields))) { + foreach($line in $customFields -split \"`n\") { + if (-not ([string]::IsNullOrEmpty($line))) { + if ($line -like '*|*') { + $kv = $line.Split('|') + $payload.Add($kv[0], $kv[1]) + } else { + write-verbose \"The line '$line' in 'Custom fields' contained invalid data. Please ensure its a list of key value pairs, separated by '|'.\" + } + } + } + } + + write-verbose \"Submitting payload`n$($payload | ConvertTo-Json)`n to $url\" + + try { + $response = Invoke-Restmethod -Method POST -Uri $url -Body ($payload | ConvertTo-Json) -ContentType \"application/json\" + write-host \"Successfully submitted\" + write-verbose \"Response was `n$($response | ConvertTo-Json)\" + } catch { + Fail-Step \"Failed to submit VictorOps alert - $($_)\" + } + +} + +if (Test-Path variable:OctopusParameters) { + if ([string]::IsNullOrEmpty($OctopusParameters['VictorOpsAlertUrl'])) { + \tWrite-Host \"Please provide the VictorOps Url\" + exit 1 + } + if ([string]::IsNullOrEmpty($OctopusParameters['VictorOpsMessageType'])) { + \tWrite-Host \"Please provide a valid Message Type\" + exit 1 + } + if ([string]::IsNullOrEmpty($OctopusParameters['VictorOpsEntityDisplayName'])) { + \tWrite-Host \"Please provide a valid Title\" + exit 1 + } + if ([string]::IsNullOrEmpty($OctopusParameters['VictorOpsMessage'])) { + \tWrite-Host \"Please provide a valid Message\" + exit 1 + } + Send-VictorOpsAlert -url $OctopusParameters['VictorOpsAlertUrl'] ` + -messageType $OctopusParameters['VictorOpsMessageType'] ` + -entityDisplayName $OctopusParameters['VictorOpsEntityDisplayName'] ` + -stateMessage $OctopusParameters['VictorOpsMessage'] ` + -customFields $OctopusParameters['VictorOpsCustomFields'] +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "01cbb725-a18f-4543-8f35-e9687ab6c63b", + "Name": "VictorOpsAlertUrl", + "Label": "VictorOps Url", + "HelpText": "The URL to notify from the integrations->REST page in VictorOps, eg: `https://alert.victorops.com/integrations/generic/20131114/alert/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXX/MyApp`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "f53f9861-d051-4a30-9e5d-7091f50782fc", + "Name": "VictorOpsMessageType", + "Label": "Message Type", + "HelpText": "", + "DefaultValue": "WARNING", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "CRITICAL|Critical (triggers an incident) +WARNING|Warning (may trigger an incident, depending on your settings) +ACKNOWLEDGEMENT|Acknowledgement (acks an incident) +INFO|Info (creates a timeline event but does not trigger an incident) +RECOVERY|Recovery (resolves an incident)" + }, + "Links": {} + }, + { + "Id": "010734ae-9987-479b-bc41-642adf380eaf", + "Name": "VictorOpsEntityDisplayName", + "Label": "Title", + "HelpText": "Display Name in the UI and Notifications", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "606190a2-02a0-4590-a397-fab876c572af", + "Name": "VictorOpsMessage", + "Label": "Message", + "HelpText": "", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + }, + { + "Id": "525a75a3-db7d-45dd-99c1-d9821d527cbe", + "Name": "VictorOpsCustomFields", + "Label": "Custom fields", + "HelpText": "A list of keyvalue pairs (separated by `|`), one per line. eg: +``` +Environment|Production +Region|us-west-1 +```", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "matt-richardson", + "$Meta": { + "ExportedAt": "2018-05-22T04:33:31.645Z", + "OctopusVersion": "2018.5.1", + "Type": "ActionTemplate" + }, + "Category": "victorops" +} diff --git a/step-templates/wait-for-iis-connections-to-drop-to-zero.json.human b/step-templates/wait-for-iis-connections-to-drop-to-zero.json.human new file mode 100644 index 000000000..a80ac3ac4 --- /dev/null +++ b/step-templates/wait-for-iis-connections-to-drop-to-zero.json.human @@ -0,0 +1,129 @@ +{ + "Id": "2e27acd9-5ab3-4580-b746-7d0a0de23fcc", + "Name": "Wait for IIS connections to drop to 0 or time expires", + "Description": "Wait for IIS connections to drop to 0 or time expires", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "Import-Module WebAdministration\r +\r +$websites = Get-ChildItem IIS:\\Sites\r +$continue = $true\r +\r +#Verify sleepSeconds can be converted to integer.\r +$sleepSecondsString = 4\r +\r +If ($OctopusParameters['wfitdSleepSeconds'])\r +{\r + $sleepSecondsString = $OctopusParameters['wfitdSleepSeconds']\r +}\r +\r +[int]$sleepSeconds = 0\r +[bool]$result = [int]::TryParse($sleepSecondsString, [ref]$sleepSeconds)\r +\r +If ($result)\r +{\r + Write-Host ('Sleep Seconds: ' + $sleepSeconds)\r +}\r +Else\r +{\r + Throw \"Cannot convert Sleep Seconds: '\" + $sleepSecondsString + \"' to integer.\"\r +}\r +\r +#Verify totalWaitMinutes can be converted to integer.\r +$totalWaitMinutesString = 5\r +\r +If ($OctopusParameters['wfitdTotalWaitMinutes'])\r +{\r + $totalWaitMinutesString = $OctopusParameters['wfitdTotalWaitMinutes']\r +}\r +\r +[int]$totalWaitMinutes = 0\r +[bool]$result = [int]::TryParse($totalWaitMinutesString, [ref]$totalWaitMinutes)\r +\r +If ($result)\r +{\r + Write-Host ('Total Wait Minutes: ' + $totalWaitMinutes)\r +}\r +Else\r +{\r + Throw \"Cannot convert Total Wait Minutes: '\" + $totalWaitMinutesString + \"' to integer.\"\r +}\r +\r +Write-Host '***********************************************'\r +\r +$stopWatch = [system.diagnostics.stopwatch]::StartNew()\r +While ($continue)\r +{\r + $connectionsExist = $false\r + \r + Foreach ($website in $websites)\r + {\r + $connections = (Get-Counter ('\\\\' + $env:COMPUTERNAME + '\\web service(' + $website.name + ')\\Current Connections')).CounterSamples.CookedValue\r + Write-Host ($website.Name + ' - ' + $connections + ' connection(s).')\r + If ($connections -gt 0)\r + {\r + $connectionsExist = $true\r + }\r + }\r + \r + If ($connectionsExist)\r + {\r + Start-Sleep -Seconds $sleepSeconds\r + \r + If ($stopWatch.Elapsed.Minutes -ge $totalWaitMinutes)\r + {\r + $continue = $false\r + }\r + }\r + Else\r + {\r + $continue = $false\r + }\r + \r + Write-Host ('Elapsed seconds: ' + $stopWatch.Elapsed.TotalSeconds)\r + Write-Host '==============================================='\r +}\r +\r +$stopWatch.Stop()\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "af43ce76-167e-44f2-a985-eee8597f8e39", + "Name": "wfitdSleepSeconds", + "Label": "Sleep Seconds", + "HelpText": "Number of seconds to sleep between checking connection count.", + "DefaultValue": "4", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "37709659-d95b-4524-a287-771c1b62d112", + "Name": "wfitdTotalWaitMinutes", + "Label": "Total Wait Minutes", + "HelpText": "Maximum wait time in minutes before proceeding, even if connections have not dropped to 0.", + "DefaultValue": "5", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "tbrasch", + "$Meta": { + "ExportedAt": "2017-07-28T18:51:55.519Z", + "OctopusVersion": "3.15.7", + "Type": "ActionTemplate" + }, + "Category": "IIS" +} diff --git a/step-templates/wait.json.human b/step-templates/wait.json.human new file mode 100644 index 000000000..dfe37c918 --- /dev/null +++ b/step-templates/wait.json.human @@ -0,0 +1,40 @@ +{ + "Id": "c69b03c9-bfa1-41ab-a712-ba74dc0512b1", + "Name": "Wait", + "Description": "Pauses the process for a set duration", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "[int]$Wait = $Seconds +Write-Output \"Waiting for $seconds seconds\" +for ($CountDown = $Wait; $CountDown -ge 0; $CountDown--){ +Write-Verbose \"$CountDown seconds remaining\" +Start-Sleep 1 +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "8bcf738c-d7f8-4f4a-b35f-15aedecd24a8", + "Name": "Seconds", + "Label": "Seconds", + "HelpText": "Number of seconds to wait", + "DefaultValue": "120", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2020-01-12T21:05:05.736Z", + "LastModifiedBy": "biohazardhpk", + "$Meta": { + "ExportedAt": "2019-12-06T20:15:05.736Z", + "OctopusVersion": "2018.9.16", + "Type": "ActionTemplate" + }, + "Category": "wait" +} diff --git a/step-templates/web-deploy-azure-convention.json.human b/step-templates/web-deploy-azure-convention.json.human new file mode 100644 index 000000000..e77a624c6 --- /dev/null +++ b/step-templates/web-deploy-azure-convention.json.human @@ -0,0 +1,223 @@ +{ + "Id": "c3ff8c78-c1cd-43ea-aedb-f5ac7dac3352", + "Name": "Web Deploy to Azure by convention", + "Description": "Makes it super simple to deploy websites to different regions inside a single process. + +This script assumes that you want to deploy websites like these to multiple regions: + +- playground.mydomain.com +- status.mydomain.com + +When you deploy to staging and production to 3 regions, this means that each website requires 3 (regions) * 2 (deployment slots) = 6 scripts. If you want this deployed using ms web deploy, you will need 6 * 5 = 30 variables (for a single website). + +With this convention script you only need a few variables, but it requires some convention (mostly done by Azure anyway): + +- [prefix]-[websitename]-[region] +- [prefix]-[websitename]-[region]-staging + +So + +- mydomain-playground-eu-west +- mydomain-playground-eu-west-staging + +The following variables are required: + +- AzurePrefix +- AzureName +- AzurePassword-[region] +- AzurePassword-[region]-staging + +The password is required for each region and deployment slot, the rest is fully determined by convention.", + "ActionType": "Octopus.Script", + "Version": 29, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "[System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.Web.Deployment\")\r +# A collection of functions that can be used by script steps to determine where packages installed\r +# by previous steps are located on the filesystem.\r + \r +function Find-InstallLocations {\r + $result = @()\r + $OctopusParameters.Keys | foreach {\r + if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) {\r + $result += $OctopusParameters[$_]\r + }\r + }\r + return $result\r +}\r + \r +function Find-InstallLocation($stepName) {\r + $result = $OctopusParameters.Keys | where {\r + $_.Equals(\"Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath\", [System.StringComparison]::OrdinalIgnoreCase)\r + } | select -first 1\r + \r + if ($result) {\r + return $OctopusParameters[$result]\r + }\r + \r + throw \"No install location found for step: $stepName\"\r +}\r + \r +function Find-SingleInstallLocation {\r + $all = @(Find-InstallLocations)\r + if ($all.Length -eq 1) {\r + return $all[0]\r + }\r + if ($all.Length -eq 0) {\r + throw \"No package steps found\"\r + }\r + throw \"Multiple package steps have run; please specify a single step\"\r +}\r +\r +function Test-LastExit($cmd) {\r + if ($LastExitCode -ne 0) {\r + Write-Host \"##octopus[stderr-error]\"\r + write-error \"$cmd failed with exit code: $LastExitCode\"\r + }\r +}\r +\r +# Somehow we can only check for exactly 'True'\r +$isStagingText = $OctopusParameters['IsStaging'];\r +$isStaging = $isStagingText -eq \"True\"\r +\r +Write-Host \"Is staging text: $isStagingText\"\r +Write-Host \"Is staging: $isStaging\"\r +\r +$stepName = $OctopusParameters['WebDeployPackageStepName']\r +if ([string]::IsNullOrEmpty($stepName)) {\r +\tWrite-Host \"Defaulting to step name Extract package\"\r +\t$stepName = \"Extract package\"\r +}\r +\r +if ($isStaging) {\r +\t$stepName = $stepName + \" - staging\"\r +}\r +\r +$stepPath = \"\"\r +if (-not [string]::IsNullOrEmpty($stepName)) {\r + Write-Host \"Finding path to package step: $stepName\"\r + $stepPath = Find-InstallLocation $stepName\r +} else {\r + $stepPath = Find-SingleInstallLocation\r +}\r +Write-Host \"Package was installed to: $stepPath\"\r +\r +Write-Host \"##octopus[stderr-progress]\"\r + \r +Write-Host \"Publishing Website\"\r +\r +$prefix = $OctopusParameters['Prefix']\r +if ([string]::IsNullOrEmpty($prefix)) {\r +\tWrite-Host \"Prefix is empty, reading prefix from variable set using AzurePrefix\"\r +\t$prefix = $OctopusParameters['AzurePrefix']\r +}\r +\r +$websiteName = $OctopusParameters['WebsiteName']\r +if ([string]::IsNullOrEmpty($websiteName)) {\r +\tWrite-Host \"WebsiteName is empty, reading website name from variable set using AzureName\"\r +\t$websiteName = $OctopusParameters['AzureName']\r +}\r +\r +$regionName = $OctopusParameters['RegionName']\r +\r +$publishUrl = \"$prefix-$websiteName-$regionName\"\r +if ($isStaging) {\r +\t$publishUrl = $publishUrl + \"-staging\"\r +}\r +$publishUrl = $publishUrl + \".scm.azurewebsites.net:443\"\r +\r +$userName = '$' + \"$prefix-$websiteName-$regionName\"\r +if ($isStaging) {\r +\t$userName = $userName + \"__staging\"\r +}\r +\r +$passwordKey = \"AzurePassword-$regionName\"\r +if ($isStaging) {\r +\t$passwordKey = $passwordKey + \"-staging\"\r +}\r +\r +Write-Host \"Using the following values to publish:\"\r +Write-Host \" * Publish url: $publishUrl\"\r +Write-Host \" * Website name: $websiteName\"\r +Write-Host \" * User name: $userName\"\r +Write-Host \" * Password variable: $passwordKey\"\r +\r +$destBaseOptions = new-object Microsoft.Web.Deployment.DeploymentBaseOptions\r +$destBaseOptions.UserName = $userName\r +$destBaseOptions.Password = $OctopusParameters[$passwordKey]\r +$destBaseOptions.ComputerName = \"https://$publishUrl/msdeploy.axd?site=$websiteName\"\r +$destBaseOptions.AuthenticationType = \"Basic\"\r +\r +$syncOptions = new-object Microsoft.Web.Deployment.DeploymentSyncOptions\r +#$syncOptions.WhatIf = $false\r +$syncOptions.UseChecksum = $true\r +\r +$deploymentObject = [Microsoft.Web.Deployment.DeploymentManager]::CreateObject(\"contentPath\", $stepPath)\r +$deploymentObject.SyncTo(\"contentPath\", $websiteName, $destBaseOptions, $syncOptions)" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "WebsiteName", + "Label": "Website Name", + "HelpText": "The name of the website (e.g. playground) + +If you leave this empty, this field will use the _AzureName_ variable.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "RegionName", + "Label": "Region Name", + "HelpText": "The name of the region (e.g. eu-west)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "IsStaging", + "Label": "Is Staging", + "HelpText": "Determines whether this is staging or not.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "Prefix", + "Label": "Prefix", + "HelpText": "The prefix. If you are smart, your websites look like this: + +[prefix]-[websitename]-[region] + +This allows you to deploy a lot of subdomains for a complete solution to multiple regions. + +If you leave this empty, this field will use the _AzurePrefix_ variable.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "WebDeployPackageStepName", + "Label": "Package step name", + "HelpText": "Name of the previously-deployed package step that contains the files that you want to deploy. + +If you leave this empty, this field will default to 'Extract package'", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "GeertvanHorrik", + "$Meta": { + "ExportedAt": "2015-11-03T00:25:05.516+00:00", + "OctopusVersion": "3.0.20.0", + "Type": "ActionTemplate" + }, + "Category": "webDeploy" +} diff --git a/step-templates/web-deploy-publish-website-msdeploy.json.human b/step-templates/web-deploy-publish-website-msdeploy.json.human new file mode 100644 index 000000000..14bc19661 --- /dev/null +++ b/step-templates/web-deploy-publish-website-msdeploy.json.human @@ -0,0 +1,226 @@ +{ + "Id": "ba8581a6-c463-40fb-ab98-3800e761b6f4", + "Name": "Web Deploy - Publish Website (MSDeploy)", + "Description": "Ensure that Web Deploy 3.5 is installed on the system. The installer is downloaded from http://www.iis.net/downloads/microsoft/web-deploy if required.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.Web.Deployment\")\r +\r +# A collection of functions that can be used by script steps to determine where packages installed\r +# by previous steps are located on the filesystem.\r + \r +function Find-InstallLocations {\r + $result = @()\r + $OctopusParameters.Keys | foreach {\r + if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) {\r + $result += $OctopusParameters[$_]\r + }\r + }\r + return $result\r +}\r + \r +function Find-InstallLocation($stepName) {\r + $result = $OctopusParameters.Keys | where {\r + $_.Equals(\"Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath\", [System.StringComparison]::OrdinalIgnoreCase)\r + } | select -first 1\r + \r + if ($result) {\r + return $OctopusParameters[$result]\r + }\r + \r + throw \"No install location found for step: $stepName\"\r +}\r + \r +function Find-SingleInstallLocation {\r + $all = @(Find-InstallLocations)\r + if ($all.Length -eq 1) {\r + return $all[0]\r + }\r + if ($all.Length -eq 0) {\r + throw \"No package steps found\"\r + }\r + throw \"Multiple package steps have run; please specify a single step\"\r +}\r +\r +function Test-LastExit($cmd) {\r + if ($LastExitCode -ne 0) {\r + Write-Host \"##octopus[stderr-error]\"\r + write-error \"$cmd failed with exit code: $LastExitCode\"\r + }\r +}\r +\r +$stepName = $OctopusParameters['WebDeployPackageStepName']\r +\r +$stepPath = \"\"\r +if (-not [string]::IsNullOrEmpty($stepName)) {\r + Write-Host \"Finding path to package step: $stepName\"\r + $stepPath = Find-InstallLocation $stepName\r +} else {\r + $stepPath = Find-SingleInstallLocation\r +}\r +Write-Host \"Package was installed to: $stepPath\"\r +\r +Write-Host \"##octopus[stderr-progress]\"\r + \r +$websiteName = $OctopusParameters['WebsiteName']\r +$publishUrl = $OctopusParameters['PublishUrl']\r +\r +$destBaseOptions = new-object Microsoft.Web.Deployment.DeploymentBaseOptions\r +$destBaseOptions.UserName = $OctopusParameters['Username']\r +$destBaseOptions.Password = $OctopusParameters['Password']\r +$destBaseOptions.ComputerName = \"https://$publishUrl/msdeploy.axd?site=$websiteName\"\r +$destBaseOptions.AuthenticationType = \"Basic\"\r +\r +$syncOptions = new-object Microsoft.Web.Deployment.DeploymentSyncOptions\r +$syncOptions.WhatIf = $false\r +$syncOptions.UseChecksum = $true\r +\r +$enableAppOfflineRule = $OctopusParameters['EnableAppOfflineRule']\r +if($enableAppOfflineRule -eq $true)\r +{\r + $appOfflineRule = $null\r + $availableRules = [Microsoft.Web.Deployment.DeploymentSyncOptions]::GetAvailableRules()\r + if (!$availableRules.TryGetValue('AppOffline', [ref]$appOfflineRule))\r + {\r + throw \"Failed to find AppOffline Rule\"\r + }\r + else\r + {\r + $syncOptions.Rules.Add($appOfflineRule)\r + Write-Host \"Enabled AppOffline Rule\"\r + }\r +}\r +\r +$preserveAppData = [boolean]::Parse($OctopusParameters['PreserveApp_Data'])\r +\r +if ($preserveAppData -eq $true) {\r + \r + Write-Host \"Skipping delete actions on App_Data\"\r + $skipAppDataFiles = new-object Microsoft.Web.Deployment.DeploymentSkipRule(\"appDataFiles\", \"Delete\", \"filePath\", \"\\App_Data\\.*\", $null)\r + $skipAppDataDirectories = new-object Microsoft.Web.Deployment.DeploymentSkipRule(\"appDataDirectories\", \"Delete\", \"dirPath\", \"\\App_Data(\\.*|$)\", $null)\r +\r + $syncOptions.Rules.Add($skipAppDataFiles);\r + $syncOptions.Rules.Add($skipAppDataDirectories);\r +}\r +\r +$SkipSyncPaths = $OctopusParameters['SkipSyncPaths']\r +if ([string]::IsNullOrEmpty($SkipSyncPaths) -eq $false)\r +{\r + $skipPaths = $SkipSyncPaths.Split(\";\", [System.StringSplitOptions]::RemoveEmptyEntries)\r + foreach($item in $skipPaths) {\r + $index = $skipPaths.IndexOf($item)\r + Write-Host \"Skipping sync of AbsolutePath: $item.\"\r + $name = \"SkipDirective$index\"\r + $value = \"absolutePath=$item\"\r + $skipDirective = new-object Microsoft.Web.Deployment.DeploymentSkipDirective($name, $value)\r + $destBaseOptions.SkipDirectives.Add($skipDirective)\r + }\r +}\r +\r +if ($OctopusParameters['AllowUntrustedCertificate'] -eq $true) {\r + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { return $true; }\r +}\r +\r +Write-Host \"Publishing Website\"\r +$deploymentObject = [Microsoft.Web.Deployment.DeploymentManager]::CreateObject(\"contentPath\", $stepPath)\r +\r +$changes = $deploymentObject.SyncTo(\"contentPath\", $websiteName, $destBaseOptions, $syncOptions)\r +\r +#Write out all the changes.\r +$changes | Select-Object -Property *", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "PublishUrl", + "Label": "Publish Url", + "HelpText": "Publish url to use when publishing the website.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AllowUntrustedCertificate", + "Label": "Allow Untrusted Certificate", + "HelpText": "Allow untrusted certificate at the publish URL.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "WebsiteName", + "Label": "Website Name", + "HelpText": "Website name to use when publishing the website.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Username", + "Label": "Username", + "HelpText": "Username to use when authenticating with the HTTPS server.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Password", + "Label": null, + "HelpText": "Password to use when authenticating with the HTTPS server. You should create a sensitive variable in your project variables, and then bind this value.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "WebDeployPackageStepName", + "Label": "Package Step Name", + "HelpText": "Name of the previously-deployed package step that contains the files that you want to deploy.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "EnableAppOfflineRule", + "Label": "Enable App Offline Rule", + "HelpText": "Enables the App Offline Rule. [See here for more details](http://www.iis.net/learn/publish/deploying-application-packages/taking-an-application-offline-before-publishing)", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "SkipSyncPaths", + "Label": "Skip Paths From Sync", + "HelpText": "Semi-colon separated, absolute reg-ex paths to skip/not-sync during deployment. Handy for folders that you don't want deleted etc. (Escape slashes)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "PreserveApp_Data", + "Label": "Preserve App_Data", + "HelpText": "The App_Data and its files will not be deleted when this option is enabled", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-07-25T22:00:43.204+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "webDeploy" +} diff --git a/step-templates/windows-add-back-connection-host-name.json.human b/step-templates/windows-add-back-connection-host-name.json.human new file mode 100644 index 000000000..7fe075d9b --- /dev/null +++ b/step-templates/windows-add-back-connection-host-name.json.human @@ -0,0 +1,44 @@ +{ + "Id": "d66d091d-39d8-43ba-a7d4-db868cd9f7d5", + "Name": "Windows - Add Back Connection Host Name", + "Description": "Disables loopback checking for a given host header name, allowing an IIS site running with integrated authentication to be accessed from the same machine, e.g. an MVC application referencing a WebAPI application. See below for more information: + +", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$hostName = $OctopusParameters['HostName']\r +\r +$key = 'HKLM:\\System\\CurrentControlSet\\Control\\Lsa\\MSV1_0\\'\r +\r +$hostNames = get-itemproperty $key -Name BackConnectionHostNames -ErrorAction SilentlyContinue\r +\r +If ($hostNames -eq $null) { new-itemproperty $key -Name BackConnectionHostNames -Value $hostName -PropertyType MultiString }\r +\r +ElseIf ($hostNames.BackConnectionHostNames -notcontains $hostName) { set-itemproperty $key -Name BackConnectionHostNames -Value ($hostNames.BackConnectionHostNames + $hostName) }\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "HostName", + "Label": "Host header name", + "HelpText": "The host header of the target application referenced by the client application. For example, an MVC website which requires access to a WebAPI application on the same machine would add a back connection host name for the API's host header, e.g.: + +api.mycompany.com", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-06-30T11:18:08.029+00:00", + "OctopusVersion": "2.6.0.778", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-add-poll-endpoint-task.json.human b/step-templates/windows-add-poll-endpoint-task.json.human new file mode 100644 index 000000000..d0d72a18e --- /dev/null +++ b/step-templates/windows-add-poll-endpoint-task.json.human @@ -0,0 +1,170 @@ +{ + "Id": "0ad0ad00-adad-adad-adad-000000000002", + "Name": "Windows - Add poll rest endpoint scheduled task", + "Description": "Adds a scheduled task that polls a specified endpoint at a specific interval using the provided HTTP method", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +Param( + [string] $AD_PollRestEndpoint_Uri, + [string] $AD_PollRestEndpoint_Name = \"Polling task for endpoint\", + [string] $AD_PollRestEndpoint_HttpMethod = \"GET\", + [string] $AD_PollRestEndpoint_Interval = 60, + [Int16] $AD_PollRestEndpoint_Attempts = 5, + [switch] $WhatIf +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($null -ne $OctopusParameters) { + $result = $OctopusParameters[$Name] + } + + if ($null -eq $result) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($null -ne $variable) { + $result = $variable.Value + } + } + + if ($null -eq $result) { + if ($Required) { + throw \"Missing parameter value $Name\" + } + else { + $result = $Default + } + } + + return $result +} +function Execute( + [Parameter(Mandatory = $true)][string] $Uri, + [Parameter(Mandatory = $false)][string] $Name = \"Polling task for endpoint\", + [Parameter(Mandatory = $false)][string] $HttpMethod = \"GET\", + [Parameter(Mandatory = $false)][string] $Interval = 60, + [Parameter(Mandatory = $false)][Int16] $Attempts = 5 +) { + $attemptCount = 0 + $operationIncomplete = $true + $maxFailures = $Attempts + $sleepBetweenFailures = 1 + + $script = '-noprofile -executionpolicy bypass -command \"& { Invoke-RestMethod -Uri ' + $Uri + ' -Method ' + $HttpMethod + ' }\"' + $repeat = (New-TimeSpan -Seconds $Interval) + + $action = New-ScheduledTaskAction \"powershell.exe\" -Argument \"$script\" + $duration = (New-TimeSpan -Days 9999) + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).Date -RepetitionInterval $repeat -RepetitionDuration $duration + $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -DontStopOnIdleEnd + + while ($operationIncomplete -and $attemptCount -lt $maxFailures) { + $attemptCount = ($attemptCount + 1) + if ($attemptCount -ge 2) { + Write-Output \"Waiting for $sleepBetweenFailures seconds before retrying...\" + Start-Sleep -s $sleepBetweenFailures + Write-Output \"Retrying...\" + $sleepBetweenFailures = ($sleepBetweenFailures * 2) + } + try { + $task = Get-ScheduledTask -TaskName $Name -ErrorAction SilentlyContinue + Write-Output $task + $msg = \"Task '$Name'\" + if ($null -ne $task) { + Write-Output \"$msg already exists - DELETING...\" + if (-Not ($WhatIf)) { + Unregister-ScheduledTask -TaskName $name -Confirm:$false + } + Write-Output \"$msg - DELETED\" + } + Write-Output \"$msg - ADDING...\" + if (-Not ($WhatIf)) { + Register-ScheduledTask -TaskName $Name -Action $action -Trigger $trigger -RunLevel Highest -Settings $settings -User \"System\" + } + Write-Output \"$msg - ADDED\" + $operationIncomplete = $false + } + catch [System.Exception] { + if ($attemptCount -lt ($maxFailures)) { + Write-Host (\"Attempt $attemptCount of $maxFailures failed: \" + $_.Exception.Message) + } + else { + throw + } + } + } +} +& Execute ` +(Get-Param 'AD_PollRestEndpoint_Uri' -Required)` +(Get-Param 'AD_PollRestEndpoint_Name')` +(Get-Param 'AD_PollRestEndpoint_HttpMethod')` +(Get-Param 'AD_PollRestEndpoint_Interval')` +(Get-Param 'AD_PollRestEndpoint_Attempts') +" + }, + "Parameters": [ + { + "Name": "AD_PollRestEndpoint_Uri", + "Label": "Endpoint Uri", + "HelpText": "The endpoint uri to poll", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AD_PollRestEndpoint_Name", + "Label": "Task name", + "HelpText": "Task name without any special characters", + "DefaultValue": "Polling task for endpoint", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AD_PollRestEndpoint_HttpMethod", + "Label": "HTTP Method", + "HelpText": "The HTTP method to be used for calling the endpoint", + "DefaultValue": "GET", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "GET +POST +PUT +DELETE +PATCH" + } + }, + { + "Name": "AD_PollRestEndpoint_Interval", + "Label": "Polling interval", + "HelpText": "Polling interval in seconds", + "DefaultValue": "60", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "AD_PollRestEndpoint_Attempts", + "Label": "Attempts", + "HelpText": "Number of retry attempts in case the scheduling fails", + "DefaultValue": "5", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "anatolie-darii", + "$Meta": { + "ExportedAt": "2018-07-17T10:18:23.003Z", + "OctopusVersion": "2018.2.8", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-add-users-to-local-groups.json.human b/step-templates/windows-add-users-to-local-groups.json.human new file mode 100644 index 000000000..9bbd2158d --- /dev/null +++ b/step-templates/windows-add-users-to-local-groups.json.human @@ -0,0 +1,179 @@ +{ + "Id": "ed204305-3399-4ffa-8566-a62da4e36641", + "Name": "Windows - Add Users to Local Groups", + "Description": "Add a list of users to a list of Local Groups", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# Running outside octopus +param( + [string]$UserNames, + [string]$GroupNames +) + +$ErrorActionPreference = \"Stop\" + +function Get-Param($Name, [switch]$Required, $Default) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + $variable = Get-Variable $Name -EA SilentlyContinue + if ($variable -ne $null) { + $result = $variable.Value + } + } + + if ($result -eq $null) { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +function IsValidUser ( $user ) { + + if($user.Guid -and ($user.SchemaClassName -eq \"User\")){ + return $true + } + + Write-Host \"ERROR - `\"WinNT://$($user.Name)`\" not found\" + return $false +} + +function IsValidGroup ( $group ) { + + if( $group.Guid -and ($group.SchemaClassName -eq \"Group\") ) { + Write-Host \"the group $($group.Guid) was found\" -ForegroundColor green + return $true + } + + Write-Host \"ERROR - `\"WinNT://$($Env:COMPUTERNAME)/$($group.Name)`\" not found\" -ForegroundColor red + return $false +} + +function IsUserInGroup($user, $groupName){ + + Write-Host \"Checking to see if $($user.Name) is in $($group.Name)\" + + if(!(isValidGroup $group)){ + throw \"Could not find group $($group.Name)\" + } + + $members = @($group.psbase.Invoke(\"Members\")) + + Write-Host \"There are $($members.Count) members in $($group.Name)\" + + $isInGroup = ($members | foreach {$_.GetType().InvokeMember(\"Name\", 'GetProperty', $null, $_, $null)}) -contains \"$($user.Name)\" + + if($isInGroup) { + Write-Host \"User $($user.Name) is already a part of `\"$($group.Name)`\"\" -ForegroundColor Yellow + } else { + Write-Host \"User $($user.Name) is not a part of `\"$($group.Name)`\"\" -ForegroundColor Green + } + + return $isInGroup +} + +function FormatUserNameForQuery([string]$userName) +{ + return $userName.Trim().Replace(\"\\\", \"/\") +} + +& { + param( + [string]$UserNames, + [string]$GroupNames + ) + + Write-Host \"Windows - Add Users to Local Groups\" + Write-Host \"UserNames: $UserNames\" + Write-Host \"GroupNames: $GroupNames\" + + $UserNames.Split(\";\") | foreach { + + $userName = FormatUserNameForQuery $_ + $user = [ADSI]\"WinNT://$userName\" + + $userDoesNotExist = !(IsValidUser $user) + + if( $userDoesNotExist ) + { + throw \"User $userName was not found\" + } + + Write-Host \"Current user $userName\" + + $GroupNames.Split(\";\") | foreach { + + $groupName = $_.Trim() + $group = [ADSI]\"WinNT://$Env:COMPUTERNAME/$groupName\" + + $groupDoesNotExist = !(IsValidGroup $group) + + if($groupDoesNotExist) + { + throw \"Group $groupName was not found\" + } + + Write-Host \"Current group $groupName\" + + $isInGroup = IsUserInGroup $user $group + + if( $isInGroup ) { + + Write-Host \"Skipping...\" + + continue + } + + Write-Host \"Adding $userName to $groupName\" -ForegroundColor Cyan + + $group.psbase.Invoke(\"Add\",$user.Path) + + Write-Host \"SUCCESS - added $userName to $groupName\" -ForegroundColor Green + } + } + + } ` + (Get-Param 'UserNames' -Required) ` + (Get-Param 'GroupNames' -Required)", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "UserNames", + "Label": "User Names", + "HelpText": "List of user names separated by ;", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "GroupNames", + "Label": "Group Names", + "HelpText": "list of group names separated by ;", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + } + ], + "LastModifiedOn": "2015-05-19T19:47:40.330+00:00", + "LastModifiedBy": "josh3ennett", + "$Meta": { + "ExportedAt": "2015-05-19T19:57:11.048+00:00", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-apply-windows-updates.json.human b/step-templates/windows-apply-windows-updates.json.human new file mode 100644 index 000000000..f650f51ee --- /dev/null +++ b/step-templates/windows-apply-windows-updates.json.human @@ -0,0 +1,111 @@ +{ + "Id": "3472f207-3934-44db-a4ac-1390167cf7ed", + "Name": "Windows - Apply Windows Updates", + "Description": "Step template to check for and apply Windows Updates with optional automatic reboot.", + "ActionType": "Octopus.Script", + "Version": 1, + "Author": "twerthi", + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "function Get-NugetPackageProviderNotInstalled +{ +\t# See if the nuget package provider has been installed + return ($null -eq (Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)) +} + +function Get-ModuleInstalled +{ + # Define parameters + param( + $PowerShellModuleName + ) + + # Check to see if the module is installed + if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName)) + { + # It is installed + return $true + } + else + { + # Module not installed + return $false + } +} + + +# Force use of TLS 1.2 +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$autoReboot = [System.Convert]::ToBoolean(\"$windowsUpdateAutoReboot\") + +# Check to see if the NuGet package provider is installed +if ((Get-NugetPackageProviderNotInstalled) -ne $false) +{ + # Display that we need the nuget package provider + Write-Host \"Nuget package provider not found, installing ...\" + + # Install Nuget package provider + Install-PackageProvider -Name Nuget -Force + + Write-Output \"Nuget package provider succesfully installed ...\" +} + + +Write-Output \"Checking for PowerShell module PSWindowsUpdate ...\" + +if ((Get-ModuleInstalled -PowerShellModuleName \"PSWindowsUpdate\") -ne $true) +{ +\tWrite-Output \"PSWindowsUpdate not found, installing ...\" + + # Install PSWindowsUpdate + Install-Module PSWindowsUpdate -Force + + Write-Output \"Installation of PSWindowsUpdate complete ...\" +} + +Write-Output \"Checking for updates ...\" + +$windowsUpdates = Get-WindowsUpdate + +# Check to see if there's anything to install +if ($windowsUpdates.Count -gt 0) +{ +\tWrite-Output \"Installing updates ...\" + if ($autoReboot) + { +\t\tInstall-WindowsUpdate -AcceptAll -AutoReboot + } + else + { + \tInstall-WindowsUpdate -AcceptAll + } +} +else +{ +\tWrite-Output \"There are no updates available.\" +}", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "004de404-ec52-47d3-ad49-ea96224182c6", + "Name": "windowsUpdateAutoReboot", + "Label": "Auto reboot", + "HelpText": "Check the box to allow an automatic reboot. **Warning**: using this option will cause the machine to reboot after installing the first update that requires a reboot. If there are multiple updates that require a reboot, the rest of the updates will not be installed.", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "$Meta": { + "ExportedAt": "2020-07-22T23:47:53.859Z", + "OctopusVersion": "2020.3.1", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "twerthi", + "Category": "windows" + } diff --git a/step-templates/windows-certificate-grant-read-access.json.human b/step-templates/windows-certificate-grant-read-access.json.human new file mode 100644 index 000000000..98efb016c --- /dev/null +++ b/step-templates/windows-certificate-grant-read-access.json.human @@ -0,0 +1,75 @@ +{ + "Id": "cf6f35bf-b3e0-4285-98be-dcb509ab2ef9", + "Name": "Windows - Certificate Grant Read Access", + "Description": "Grant read access to certificate for a specific user", + "ActionType": "Octopus.Script", + "Version": 12, + "Properties": { + "Octopus.Action.Script.ScriptBody": "# $certCN is the identifiying CN for the certificate you wish to work with\r +# The selection also sorts on Expiration date, just in case there are old expired certs still in the certificate store.\r +# Make sure we work with the most recent cert\r + \r + Try\r + {\r + $WorkingCert = Get-ChildItem CERT:\\LocalMachine\\My |where {$_.Subject -match $certCN} | sort $_.NotAfter -Descending | select -first 1 -erroraction STOP\r + $TPrint = $WorkingCert.Thumbprint\r + $rsaFile = $WorkingCert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName\r + if($TPrint){\r + Write-Host \"Found certificate named $certCN with thumbprint $TPrint\"\r + }\r + else{\r + throw \"Error: unable to locate certificate for $($CertCN)\"\r + }\r + }\r + Catch\r + {\r + throw \"Error: unable to locate certificate for $($CertCN)\"\r + }\r + $keyPath = \"$env:SystemDrive\\ProgramData\\Microsoft\\Crypto\\RSA\\MachineKeys\\\"\r + $fullPath=$keyPath+$rsaFile\r + $acl=Get-Acl -Path $fullPath\r + $permission=$userName,\"Read\",\"Allow\"\r + $accessRule=new-object System.Security.AccessControl.FileSystemAccessRule $permission\r + $acl.AddAccessRule($accessRule)\r + Try \r + {\r + Write-Host \"Granting read access for user $userName on $certCN\"\r + Set-Acl $fullPath $acl\r + Write-Host \"Success: ACL set on certificate\"\r + }\r + Catch\r + {\r + throw \"Error: unable to set ACL on certificate\"\r + }", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "certCN", + "Label": "Certificate Name", + "HelpText": "The CN of the Certificate", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "userName", + "Label": "User name", + "HelpText": "The Windows user", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-01-30T14:37:16.927+00:00", + "LastModifiedBy": "ARBNIK@skandianet.org", + "$Meta": { + "ExportedAt": "2015-01-30T14:39:14.212+00:00", + "OctopusVersion": "2.6.0.778", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-check-net-core-framework-version.json.human b/step-templates/windows-check-net-core-framework-version.json.human new file mode 100644 index 000000000..e75481ba7 --- /dev/null +++ b/step-templates/windows-check-net-core-framework-version.json.human @@ -0,0 +1,118 @@ +{ + "Id": "929ff903-29de-4217-b6a9-83fbfd477e11", + "Name": ".NET Core - Check .NET Core Framework Version", + "Description": "Check if given .NET Core framework version (or greater) is installed.", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\" +function Get-Parameter($Name, $Default, [switch]$Required) { + $result = $null + + if ($OctopusParameters -ne $null) { + $result = $OctopusParameters[$Name] + } + + if ($result -eq $null) { + if ($Required) { + throw \"Missing parameter value $Name\" + } else { + $result = $Default + } + } + + return $result +} + +function Get-DotNetCoreFrameworkVersions() { + $dotNetCoreVersions = @() + if(Test-Path \"$env:programfiles/dotnet/shared/Microsoft.NETCore.App\") { + $dotNetCoreVersions = (ls \"$env:programfiles/dotnet/shared/Microsoft.NETCore.App\").Name + } + return $dotNetCoreVersions +} + +function Get-AspDotNetCoreRuntimeVersions() { + $aspDotNetCoreRuntimeVersions = @() + $DotNETCoreUpdatesPath = \"Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\Updates\\.NET Core\" + $DotNETUpdatesPath = \"Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\Updates\\.NET\" + + if (Test-Path \"HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Updates\\.NET Core\") { +\t $DotNetCoreItems = (Get-Item -Path $DotNETCoreUpdatesPath).GetSubKeyNames() + } + if (Test-Path \"HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Updates\\.NET\") { + $DotNetItems = (Get-Item -Path $DotNETUpdatesPath).GetSubKeyNames() + } +\t$aspDotNetCoreRuntimeVersions = $DotNetCoreItems + $DotNetItems | where { $_ -match \"^Microsoft (ASP)?\\.NET Core (?[\\d\\.]+(.*?)?) \"} | foreach { $Matches['version'] } + + return $aspDotNetCoreRuntimeVersions +} + +$targetVersion = (Get-Parameter \"TargetVersion\" -Required).Trim() +$exact = [boolean]::Parse((Get-Parameter \"Exact\" -Required)) +$CheckASPdotNETCore = [boolean]::Parse((Get-Parameter \"CheckASPdotNETCore\" -Required)) + +$matchedVersions = Get-DotNetCoreFrameworkVersions | Where-Object { if ($exact) { $_ -eq $targetVersion } else { $_ -ge $targetVersion } } +if (!$matchedVersions) { + throw \"Can't find .NET Core Runtime $targetVersion installed in the machine.\" +} + +$matchedVersions | foreach { Write-Host \"Found .NET Core Runtime $_ installed in the machine.\" } + +if ($CheckASPdotNETCore) { + $matchedAspVersions = Get-AspDotNetCoreRuntimeVersions + if (!$matchedAspVersions) { + throw \"Can't find ASP.NET Core Runtime installed in the machine.\" + } + + $matchedAspVersions | foreach { Write-Host \"Found ASP.NET Core Runtime $_ installed in the machine.\" } +}" + }, + "Parameters": [ + { + "Id": "f386deb4-36fb-4116-ad4a-5bceed89fa5b", + "Name": "TargetVersion", + "Label": "Target .NET Core framework version", + "HelpText": "The target .NET Core framework version you expect to be installed in the machine. For example, 2.0.5.", + "DefaultValue": "2.0.5", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "7312dd89-2174-47d7-bdfd-2745fb812f58", + "Name": "Exact", + "Label": "Exact", + "HelpText": "If you check \"Exact\", it means the installed .NET Core framework version MUST match target version. + +Otherwise, as long as the installed .NET Coreframework version is greater than or equal to target version, the check will pass.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + }, + { + "Id": "4f0b738b-8f06-4c93-a27f-49b3cd06ea9a", + "Name": "CheckASPdotNETCore", + "Label": "Check ASP.NET Core Module", + "HelpText": "If you check \"Check ASP.NET Core Module\", it means this step will check if ASP.NET Core module is installed in the host or not.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "$Meta": { + "ExportedAt": "2018-06-13T08:43:39.365Z", + "OctopusVersion": "2018.4.1", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "FinnianDempsey", + "Category": "dotnetcore" +} diff --git a/step-templates/windows-check-net-framework-version.json.human b/step-templates/windows-check-net-framework-version.json.human new file mode 100644 index 000000000..e659c793d --- /dev/null +++ b/step-templates/windows-check-net-framework-version.json.human @@ -0,0 +1,112 @@ +{ + "Id": "b15c6b3d-20a1-4059-b0e4-75bef1ff41d5", + "Name": ".NET - Check .NET Framework Version", + "Description": "Check if given .NET framework version (or greater) is installed.", + "ActionType": "Octopus.Script", + "Version": 8, + "CommunityActionTemplateId": "CommunityActionTemplates-561", + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "# This script is based on MSDN: https://msdn.microsoft.com/en-us/library/hh925568 + +$releaseVersionMapping = @{ + 378389 = '4.5' # 4.5 + 378675 = '4.5.1' # 4.5.1 installed with Windows 8.1 + 378758 = '4.5.1' # 4.5.1 installed on Windows 8, Windows 7 SP1, or Windows Vista SP2 + 379893 = '4.5.2' # 4.5.2 + 393295 = '4.6' # 4.6 installed with Windows 10 + 393297 = '4.6' # 4.6 installed on all other Windows OS versions + 394254 = '4.6.1' # 4.6.1 installed on Windows 10 + 394271 = '4.6.1' # 4.6.1 installed on all other Windows OS versions + 394802 = '4.6.2' # 4.6.2 installed on Windows 10 + 394806 = '4.6.2' # 4.6.2 installed on all other Windows OS versions + 460798 = '4.7' # 4.7 installed on Windows 10 + 460805 = '4.7' # 4.7 installed on all other Windows OS versions + 461308 = '4.7.1' # 4.7.1 installed on Windows 10 + 461310 = '4.7.1' # 4.7.1 installed on all other Windows OS versions + 461808 = '4.7.2' # 4.7.2 installed on Windows 10 + 461814 = '4.7.2' # 4.7.2 installed on all other Windows OS versions + 528040 = '4.8' #4.8 installed on Windows 10 + 528049 = '4.8' #4.8 installed on all other Windows OS versions1 + 528372 = '4.8' # 4.8 Windows 10 May 2020 Update +} + +function Get-DotNetFrameworkVersions() { + $dotNetVersions = @() + if ($baseKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', '')) { + # To find .NET Framework versions (.NET Framework 1-4)$dotNetVersions + if ($ndpKey = $baseKey.OpenSubKey('SOFTWARE\\Microsoft\\NET Framework Setup\\NDP')) { + foreach ($versionKeyName in $ndpKey.GetSubKeyNames()) { + if ($versionKeyName -match 'v[2|3]') { + $versionKey = $ndpKey.OpenSubKey($versionKeyName) + if ($versionKey.GetValue('Version', '') -ne '') { + $version = [version] ($versionKey.GetValue('Version')) + $dotNetVersions += \"$($version.Major).$($version.Minor)\" + } + } + } + + # for .NET 4.0 + if ($ndp40Key = $ndpKey.OpenSubKey(\"v4.0\")) { + foreach ($subKeyName in $ndp40Key.GetSubKeyNames()) { + $versionKey = $ndp40Key.OpenSubKey($subKeyName) + $version = [version]($versionKey.GetValue('Version', '')) + $dotNetVersions += \"$($version.Major).$($version.Minor)\" + } + } + } + + # To find .NET Framework versions (.NET Framework 4.5 and later) + if ($ndp4Key = $baseKey.OpenSubKey('SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full')) { + $releaseKey = $ndp4Key.GetValue('Release') + $dotNetVersions += $releaseVersionMapping[$releaseKey] + } + } + return $dotNetVersions +} + +$targetVersion = $OctopusParameters['TargetVersion'].Trim() +$exact = [boolean]::Parse($OctopusParameters['Exact']) + +$matchedVersions = Get-DotNetFrameworkVersions | Where-Object { if ($exact) { $_ -eq $targetVersion } else { $_ -ge $targetVersion } } +if (!$matchedVersions) { + throw \"Can't find .NET $targetVersion installed in the machine.\" +} +$matchedVersions | foreach { Write-Host \"Found .NET $_ installed in the machine.\" }", + "Octopus.Action.RunOnServer": "false" + }, + "Parameters": [ + { + "Id": "e902b08b-2fc3-4f62-aec0-5225533cace8", + "Name": "TargetVersion", + "Label": "Target .NET framework version", + "HelpText": "The target .NET framework version you expect to be installed in the machine. For example, 4.5.2.", + "DefaultValue": "4.5.2", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "205e297b-c72c-4d9b-a6fe-9a20ab62d5b0", + "Name": "Exact", + "Label": "Exact", + "HelpText": "If you check \"Exact\", it means the installed .NET framework version MUST match target version. + +Otherwise, as long as the installed .NET framework version is greater than or equal to target version, the check will pass.", + "DefaultValue": "false", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "$Meta": { + "ExportedAt": "2019-06-06T07:39:33.409Z", + "OctopusVersion": "2018.4.1", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "hullscotty1986", + "Category": "aspnet" +} diff --git a/step-templates/windows-create-eventsource.json.human b/step-templates/windows-create-eventsource.json.human new file mode 100644 index 000000000..301fd3205 --- /dev/null +++ b/step-templates/windows-create-eventsource.json.human @@ -0,0 +1,51 @@ +{ + "Id": "6047c2b5-95f7-46db-85b4-d970c1586f94", + "Name": "Windows - Create Event Source", + "Description": "Establishes the specified source name as a valid event source for writing entries to a log on the local computer. + +This method can also create a new custom log on the local computer.", + "ActionType": "Octopus.Script", + "Version": 5, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$sourceName = $OctopusParameters[\"EventSourceName\"]\r +$logName = $OctopusParameters[\"EventLogName\"]\r +\r +$sourceExists = [System.Diagnostics.EventLog]::SourceExists($sourceName)\r +if($sourceExists) {\r +\tWrite-Output \"Event source $sourceName already exist.\"\r +\treturn\r +}\r +\r +[System.Diagnostics.EventLog]::CreateEventSource($sourceName, $logName)", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "EventSourceName", + "Label": "Source name", + "HelpText": "The source name by which the application is registered on the local computer.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "EventLogName", + "Label": "Log name", + "HelpText": "The name of the log the source's entries are written to. Possible values include Application, System, or a custom event log.", + "DefaultValue": "Application", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2017-01-20T08:46:45.849+01:00", + "LastModifiedBy": "HumanPrinter", + "$Meta": { + "ExportedAt": "2015-06-20T10:04:09.578+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-create-shortcut.json.human b/step-templates/windows-create-shortcut.json.human new file mode 100644 index 000000000..142196d0e --- /dev/null +++ b/step-templates/windows-create-shortcut.json.human @@ -0,0 +1,84 @@ +{ + "Id": "95ed2a99-f92a-482e-a625-bbd1fb7c24bc", + "Name": "Windows - Create Shortcut", + "Description": "Creates or updates a Windows shortcut to point to a target file.", + "ActionType": "Octopus.Script", + "Version": 7, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$destination = $OctopusParameters['ShortcutDestination'] +$targetFilePath = $OctopusParameters['TargetFilePath'] +$shortcutName = $OctopusParameters['Shortcutname'] + +#Use Custom or predefined path +$shortcutDestination = + if($destination -eq \"PublicDesktop\") { \"$env:PUBLIC\\Desktop\" } + elseif($destination -eq \"Custom\") { $OctopusParameters['ShortcutPath'] } + else {Write-Error \"Shortcut destination is not set\"} + + +#Create shortcut filename +$shortcut = \"$shortcutDestination\\$shortcutName.lnk\" + +Write-Output \"Shortcut: $shortcut\" +Write-Output \"Target: $targetFilePath\" + +if(!(Test-Path $destination)){ + New-Item -ItemType Directory -Path $destination +} + +$WshShell = New-Object -comObject WScript.Shell +$Shortcut = $WshShell.CreateShortcut(\"$shortcut\") +$Shortcut.TargetPath = $targetFilePath +$Shortcut.Save()", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "TargetFilePath", + "Label": "Target file the shortcut will reference", + "HelpText": "This should be set to the file that the shortcut will link to.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ShortcutDestination", + "Label": "Choose a destination for the shortcut or a Custom path.", + "HelpText": "", + "DefaultValue": "PublicDesktop", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "PublicDesktop|Desktop (All Users) +Custom|Custom" + } + }, + { + "Name": "ShortcutPath", + "Label": "The path the shortcut will be put in if Custom path is chosen", + "HelpText": "This path is only used if Custom is set as destination for the shortcut. This should include a path that the shortcut should live in. If the path does not exist, it will be created.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "ShortcutName", + "Label": "The name of the shortcut", + "HelpText": "This should be the name of the shortcut.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-12-23T09:41:47.577+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-disk-cleanup.json.human b/step-templates/windows-disk-cleanup.json.human new file mode 100644 index 000000000..3b2bb6766 --- /dev/null +++ b/step-templates/windows-disk-cleanup.json.human @@ -0,0 +1,144 @@ +{ + "Id": "0147e912-ff8d-46d9-965a-dee0789ef32b", + "Name": "Windows Disk Cleanup Manager", + "Description": "Installs Windows Disk Cleanup manager on Windows 2008, 2008-R2 and 2012 Server. Should work on Windows 7 and 8", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$driveLetter = $OctopusParameters[\"driveLetter\"]\r +$cleanupSwitch = $OctopusParameters[\"cleanupSwitch\"]\r +\r +#REM ORIGINAL AUTHOR JAMES FOX \r +#REM SOURCE http://technet.microsoft.com/en-us/library/ff630161(WS.10).aspx\r +\r +# DECLARATIONS\r +$SOURCEEXE = \"\"\r +$SOURCEMUI = \"\"\r +# END DECLARATIONS \r +\r +# SETUP TEMPORARY ENVIROMENT VARIABLES FOR COPY PROCESS\r +$DCLEANMGR = \"$env:systemroot\\System32\"\r +$DCLEANMGRMUI = \"$env:systemroot\\System32\\en-US\"\r +\r +# $PATH TO MUI FILE WINDOWS 2008 R2 64bit oR 2012\r +if (Test-Path $env:systemroot\\winsxs\\amd64_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.1.7600.16385_en-us_b9cb6194b257cc63\\cleanmgr.exe.mui)\r +{\r +$SOURCEMUI = \"$env:systemroot\\winsxs\\amd64_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.1.7600.16385_en-us_b9cb6194b257cc63\\cleanmgr.exe.mui\"\r +}\r +\r +# $PATH TO EXE FILE WINDOWS 2008 R2 64bit oR 2012\r +if (Test-Path $env:systemroot\\winsxs\\amd64_microsoft-windows-cleanmgr_31bf3856ad364e35_6.1.7600.16385_none_c9392808773cd7da\\cleanmgr.exe)\r +{\r + $SOURCEEXE = \"$env:systemroot\\winsxs\\amd64_microsoft-windows-cleanmgr_31bf3856ad364e35_6.1.7600.16385_none_c9392808773cd7da\\cleanmgr.exe\"\r +}\r +\r +\r +# $PATH TO MUI FILE WINDOWS 2008 64bit\r +if (Test-Path $env:systemroot\\winsxs\\amd64_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.0.6001.18000_en-us_b9f50b71510436f2\\cleanmgr.exe.mui)\r +{\r +\t$SOURCEMUI = \"$env:systemroot\\winsxs\\amd64_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.0.6001.18000_en-us_b9f50b71510436f2\\cleanmgr.exe.mui\"\r +}\r +# $PATH TO EXE FILE WINDOWS 2008 64bit\r +if (Test-Path $env:systemroot\\winsxs\\amd64_microsoft-windows-cleanmgr_31bf3856ad364e35_6.0.6001.18000_none_c962d1e515e94269\\cleanmgr.exe)\r +{\r +\t$SOURCEEXE = \"$env:systemroot\\winsxs\\amd64_microsoft-windows-cleanmgr_31bf3856ad364e35_6.0.6001.18000_none_c962d1e515e94269\\cleanmgr.exe\"\r +}\r +\r +# $PATH TO MUI FILE WINDOWS 2008 32bit\r +if (Test-Path $env:systemroot\\winsxs\\x86_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.0.6001.18000_en-us_5dd66fed98a6c5bc\\cleanmgr.exe.mui)\r +{\r +\t$SOURCEMUI = \"$env:systemroot\\winsxs\\x86_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.0.6001.18000_en-us_5dd66fed98a6c5bc\\cleanmgr.exe.mui\"\r +}\r +# $PATH TO EXE FILE WINDOWS 2008 32bit\r +if (Test-Path $env:systemroot\\winsxs\\x86_microsoft-windows-cleanmgr_31bf3856ad364e35_6.0.6001.18000_none_6d4436615d8bd133\\cleanmgr.exe)\r +{\r +\t$SOURCEEXE = \"$env:systemroot\\winsxs\\x86_microsoft-windows-cleanmgr_31bf3856ad364e35_6.0.6001.18000_none_6d4436615d8bd133\\cleanmgr.exe\"\r +}\r +\r +# COPY PROCESS \r +# THIS SECTION SHOULD NEVER HAPPEN ON WINDOWS 2003 SERVER BECAUSE CLEANMGR.EXE IS ALWAYS INSTALLED\r +# TEST AND COPY IF CLEANMGR.EXE DOES NOT EXIST IN EXPECTED LOCATION COPY FROM SOURCE EXE AND MUI\r +if (!(Test-Path $env:systemroot\\SYSTEM32\\cleanmgr.exe))\r +{\r + xcopy $SOURCEMUI $DCLEANMGRMUI /y\r +}\r +if (!(Test-Path $env:systemroot\\SYSTEM32\\cleanmgr.exe))\r +{\r + xcopy $SOURCEEXE $DCLEANMGR /y\r +}\r +\r +# RUN EXE AND CLEAN DRIVE MAX CLEANUP\r +$pinfo = New-Object System.Diagnostics.ProcessStartInfo\r +$pinfo.FileName = \"$env:systemroot\\SYSTEM32\\CLEANMGR.EXE\"\r +$pinfo.RedirectStandardError = $true\r +$pinfo.RedirectStandardOutput = $true\r +$pinfo.UseShellExecute = $false\r +$pinfo.Arguments = \"/d$driveLetter /$cleanupSwitch\"\r +$p = New-Object System.Diagnostics.Process\r +$p.StartInfo = $pinfo\r +$p.Start() | Out-Null\r +$p.WaitForExit()\r +$stdout = $p.StandardOutput.ReadToEnd()\r +$stderr = $p.StandardError.ReadToEnd()\r +Write-Host \"stdout: $stdout\"\r +Write-Host \"stderr: $stderr\"\r +Write-Host \"Exit Code: \" + $p.ExitCode\r +" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "driveLetter", + "Label": "Drive Letter", + "HelpText": "The driver letter in which to run the clean up manager on, by default C.", + "DefaultValue": "C", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "C +D +E +F +G +H +I +J +K +L +M +N +O +P +Q +R +S +T +U +V +W +X +Y +Z" + } + }, + { + "Name": "cleanupSwitch", + "Label": "Clean up Level", + "HelpText": "2 options are LOWDISK or VERYLOWDISK, varies the level of cleanup that is done. VERYLOWDISK is the most complete, most cleaned up.", + "DefaultValue": "VERYLOWDISK", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "VERYLOWDISK| Very Low Disk +LOWDISK\\Low Disk" + } + } + ], + "LastModifiedOn": "2015-10-12T18:46:26.067+00:00", + "LastModifiedBy": "dbloch", + "$Meta": { + "ExportedAt": "2015-10-12T19:07:41.676+00:00", + "OctopusVersion": "3.0.21.0", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-ensure-features-installed.json.human b/step-templates/windows-ensure-features-installed.json.human new file mode 100644 index 000000000..ff77bc546 --- /dev/null +++ b/step-templates/windows-ensure-features-installed.json.human @@ -0,0 +1,81 @@ +{ + "Id": "ed837372-165f-4e0e-b755-1df9633d9eb1", + "Name": "Windows - Ensure Features Installed", + "Description": "Ensures that a set of Windows Features are installed on the system.", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$requiredFeatures = $OctopusParameters['WindowsFeatures'].split(\",\") | foreach { $_.trim() } +if(! $requiredFeatures) { + Write-Output \"No required Windows Features specified...\" + exit +} +$requiredFeatures | foreach { $feature = DISM.exe /ONLINE /Get-FeatureInfo /FeatureName:$_; if($feature -like \"*Feature name $_ is unknown*\") { throw $feature } } + +Write-Output \"Retrieving all Windows Features...\" +$allFeatures = DISM.exe /ONLINE /Get-Features /FORMAT:List | Where-Object { $_.StartsWith(\"Feature Name\") -OR $_.StartsWith(\"State\") } +$features = new-object System.Collections.ArrayList +for($i = 0; $i -lt $allFeatures.length; $i=$i+2) { + $feature = $allFeatures[$i] + $state = $allFeatures[$i+1] + $features.add(@{feature=$feature.split(\":\")[1].trim();state=$state.split(\":\")[1].trim()}) | OUT-NULL +} + +Write-Output \"Checking for missing Windows Features...\" +$missingFeatures = new-object System.Collections.ArrayList +$features | foreach { if( $requiredFeatures -contains $_.feature -and $_.state.StartsWith(\"Disabled\")) { $missingFeatures.add($_.feature) | OUT-NULL } } +if(! $missingFeatures) { + Write-Output \"All required Windows Features are installed\" + exit +} +Write-Output \"Installing missing Windows Features...\" +$featureNameArgs = \"\" +$missingFeatures | foreach { $featureNameArgs = $featureNameArgs + \" /FeatureName:\" + $_ } +$dism = \"DISM.exe\" +IF ($SuppressReboot) +{ + $arguments = \"/NoRestart \" +} +ELSE +{ + $arguments = \"\" +} +$arguments = $arguments + \"/ONLINE /Enable-Feature /All $featureNameArgs\" +Write-Output \"Calling DISM with arguments: $arguments\" +start-process -NoNewWindow $dism $arguments", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "WindowsFeatures", + "Label": "Windows features", + "HelpText": "The set of Windows Features that should be installed on the system. This can be either a single Windows Feature or a comma separated list of Windows Features to check. + +Example 1: IIS-WebServer + +Example 2: IIS-WebServer, IIS-WindowsAuthentication", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Name": "SuppressReboot", + "Label": "Suppress Reboot", + "HelpText": "Suppresses reboot. If a reboot is not necessary, then this option does nothing. This option will keep DISM.exe from prompting for a restart, or from restarting automatically).", + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + } + ], + "LastModifiedOn": "2017-10-03T21:58:18.839Z", + "LastModifiedBy": "WesleySSmith", + "$Meta": { + "ExportedAt": "2017-10-03T21:58:18.839Z", + "OctopusVersion": "3.17.1", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-ensure-hosts-file-entry-exists.json.human b/step-templates/windows-ensure-hosts-file-entry-exists.json.human new file mode 100644 index 000000000..4f215e058 --- /dev/null +++ b/step-templates/windows-ensure-hosts-file-entry-exists.json.human @@ -0,0 +1,57 @@ +{ + "Id": "36e828dc-8679-451d-9f3d-6edf3e90babf", + "Name": "Windows - Ensure Hosts File Entry Exists", + "Description": "Ensures that the given IP/host name entry exists in the hosts file.", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$ip = $OctopusParameters['IP']\r +$hostName = $OctopusParameters['HostName']\r +\r +$hostsPath = \"$env:windir\\System32\\drivers\\etc\\hosts\"\r +\r +$hosts = Get-Content $hostsPath\r +\r +$match = $hosts -match (\"^\\s*$ip\\s+$hostName\" -replace '\\.', '\\.')\r +\r +If ($match) { Exit }\r +\r +$hostsEntry = \"$ip`t$hostName\"\r +\r +If ([IO.File]::ReadAllText($hostsPath) -notmatch \"\\r\ +\\z\") { $hostsEntry = [environment]::newline + $hostsEntry }\r +\r +Add-Content $hostsPath $hostsEntry\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "IP", + "Label": "IP Address", + "HelpText": "The IP address which the host name should resolve to, e.g. 127.0.0.1", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "HostName", + "Label": "Host Name", + "HelpText": "The host name which should resolve to the given IP, e.g. www.mydomain.com", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2015-07-15T15:02:15.749+00:00", + "OctopusVersion": "2.6.0.778", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-firewall-configure-rule.json.human b/step-templates/windows-firewall-configure-rule.json.human new file mode 100644 index 000000000..0ab12e70f --- /dev/null +++ b/step-templates/windows-firewall-configure-rule.json.human @@ -0,0 +1,81 @@ +{ + "Id": "e47a9558-e49c-44a6-ae8f-66372b285789", + "Name": "Windows Firewall - Configure Rule", + "Description": "Configures a firewall rule", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$ruleName = $OctopusParameters['RuleName'] +$localPort = $OctopusParameters['LocalPort'] +$remotePort = $OctopusParameters['RemotePort'] +$protocol = $OctopusParameters['Protocol'] +$direction = $OctopusParameters['Direction'] + +# Remove any existing rule + +Write-Host \"Removing existing rule\" +netsh advfirewall firewall delete rule name=\"$ruleName\" dir=$direction + +# Add new rule + +Write-Host \"Adding new rule\" +netsh advfirewall firewall add rule name=\"$ruleName\" dir=$direction action=allow protocol=$protocol localport=$localPort remoteport=$remotePort", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "RuleName", + "Label": "Rule name", + "HelpText": "The name of the rule", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "LocalPort", + "Label": "Local Port", + "HelpText": "The comma separated list of local port values", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "RemotePort", + "Label": "Remote Port", + "HelpText": "The comma separated list of remote port values", + "DefaultValue": "any", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Protocol", + "Label": "Protocol", + "HelpText": "The protocol for the rule. Commonly either 'TCP or 'UDP'", + "DefaultValue": "TCP", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Direction", + "Label": "Direction", + "HelpText": "The rule direction. Either 'in' or 'out'", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-16T10:13:17.207+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-get-pending-reboot.json.human b/step-templates/windows-get-pending-reboot.json.human new file mode 100644 index 000000000..a81ceed24 --- /dev/null +++ b/step-templates/windows-get-pending-reboot.json.human @@ -0,0 +1,102 @@ +{ + "Id": "814c4265-62ec-44d3-9d42-800aef8f7380", + "Name": "Windows - Get Pending Reboot", + "Description": "Get pending reboot status from computer.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$TempErrAct = $ErrorActionPreference + $ErrorActionPreference = \"Stop\" + Foreach ($Computer in $ComputerName) + { + Try + { + # Setting pending values to false to cut down on the number of else statements + $PendFileRename,$Pending,$SCCM = $false,$false,$false +\t\t\t + # Setting CBSRebootPend to null since not all versions of Windows has this value + $CBSRebootPend = $null +\t\t\t + # Querying WMI for build version + $WMI_OS = Get-WmiObject -Class Win32_OperatingSystem -Property BuildNumber, CSName -ComputerName $Computer +\t\t\t + # Making registry connection to the local/remote computer + $RegCon = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]\"LocalMachine\",$Computer) +\t\t\t + # If Vista/2008 & Above query the CBS Reg Key + If ($WMI_OS.BuildNumber -ge 6001) + { + $RegSubKeysCBS = $RegCon.OpenSubKey(\"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\\").GetSubKeyNames() + $CBSRebootPend = $RegSubKeysCBS -contains \"RebootPending\" + }#End If ($WMI_OS.BuildNumber -ge 6001) +\t\t\t + # Query WUAU from the registry + $RegWUAU = $RegCon.OpenSubKey(\"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\\") + $RegWUAURebootReq = $RegWUAU.GetSubKeyNames() + $WUAURebootReq = $RegWUAURebootReq -contains \"RebootRequired\" +\t\t\t + # Query PendingFileRenameOperations from the registry + $RegSubKeySM = $RegCon.OpenSubKey(\"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\\") + $RegValuePFRO = $RegSubKeySM.GetValue(\"PendingFileRenameOperations\",$null) +\t\t\t + # Closing registry connection + $RegCon.Close() +\t\t\t + # If PendingFileRenameOperations has a value set $RegValuePFRO variable to $true + If ($RegValuePFRO) + { + $PendFileRename = $true + }#End If ($RegValuePFRO) + + # If any of the variables are true, set $Pending variable to $true + If ($CBSRebootPend -or $WUAURebootReq -or $PendFileRename) + { + $Pending = $true + }#End If ($CBS -or $WUAU -or $PendFileRename) + # Creating Custom PSObject and Select-Object Splat +\t\t\t$SelectSplat = @{ +\t\t\t Property=('Computer','CBServicing','WindowsUpdate','PendFileRename','PendFileRenVal','RebootPending') +\t\t\t } + New-Object -TypeName PSObject -Property @{ + Computer=$WMI_OS.CSName + CBServicing=$CBSRebootPend + WindowsUpdate=$WUAURebootReq + PendFileRename=$PendFileRename + PendFileRenVal=$RegValuePFRO + RebootPending=$Pending + } | Select-Object @SelectSplat + }#End Try + Catch + { + Write-Warning \"$Computer`: $_\" + # If $ErrorLog, log the file to a user specified location/path + If ($ErrorLog) + { + Out-File -InputObject \"$Computer`,$_\" -FilePath $ErrorLog -Append + }#End If ($ErrorLog) + }#End Catch + }#End Foreach ($Computer in $ComputerName) +\t $ErrorActionPreference = $TempErrAct ", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ComputerName", + "Label": "Computer name", + "HelpText": "Computer Name to get pending reboot status from.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-30T20:34:01.920+00:00", + "OctopusVersion": "2.4.7.85", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-get-processor-load.json.human b/step-templates/windows-get-processor-load.json.human new file mode 100644 index 000000000..2fd3043ba --- /dev/null +++ b/step-templates/windows-get-processor-load.json.human @@ -0,0 +1,41 @@ +{ + "Id": "072eda4e-eb5f-4603-b3af-e742f6431cd0", + "Name": "Windows - Get Processor Load", + "Description": "Get processor load from computer.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$prcInfo = gwmi win32_processor -computer $ComputerName -ErrorAction STOP +Try{ + $Name = \"Proc type: $($prcInfo.Name)\" + $Load = \"Proc load: $($prcInfo.LoadPercentage) %\" + $Freq = \"Proc frequency: $($prcInfo.CurrentClockSpeed) MHz\" + \"$Name `n$Load `n$Freq\" +} +Catch +{ + Write-Host \"Error getting processor load information.\" +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ComputerName", + "Label": "Computer name", + "HelpText": "Name of computer to get processor load from.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-30T20:58:55.916+00:00", + "OctopusVersion": "2.4.7.85", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-get-uptime.json.human b/step-templates/windows-get-uptime.json.human new file mode 100644 index 000000000..b72f8db99 --- /dev/null +++ b/step-templates/windows-get-uptime.json.human @@ -0,0 +1,44 @@ +{ + "Id": "8bd69993-6c6f-4729-8455-0aa8fc07dc3c", + "Name": "Windows - Get Uptime", + "Description": "Get computer up time.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$pc = $computer +$info = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $computer +$diff = ($info.ConvertToDateTime($info.LocalDateTime) - $info.ConvertToDateTime($info.LastBootUpTime)) + +$properties=[ordered]@{ + 'ComputerName'=$pc; + 'UptimeDays'=$diff.Days; + 'UptimeHours'=$diff.Hours; + 'UptimeMinutes'=$diff.Minutes + 'UptimeSeconds'=$diff.Seconds + } + $obj = New-Object -TypeName PSObject -Property $properties + +Write-Output $obj", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Computer", + "Label": "Computer name", + "HelpText": "Computer to get up-time from.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-30T18:33:20.536+00:00", + "OctopusVersion": "2.4.7.85", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-grant-logon-as-service.json.human b/step-templates/windows-grant-logon-as-service.json.human new file mode 100644 index 000000000..d1139dac7 --- /dev/null +++ b/step-templates/windows-grant-logon-as-service.json.human @@ -0,0 +1,77 @@ +{ + "Id": "0e295d12-cc29-4f61-9eb1-dac387697d5c", + "Name": "Grant SeServiceLogonRight To Account", + "Description": "Grants `SeServiceLogonRight` to Windows account", + "ActionType": "Octopus.Script", + "Version": 4, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Code based on Stack Overflow solution https://stackoverflow.com/a/21235462/201382 from @grenade (https://stackoverflow.com/users/68115/grenade) + +$grantLogonAsServiceAccountName = $OctopusParameters['GrantLogonAsServiceAccountName'] + +$tempPath = [System.IO.Path]::GetTempPath() +$import = Join-Path -Path $tempPath -ChildPath \"import.inf\" +if (Test-Path $import) { + Remove-Item -Path $import -Force +} + +$export = Join-Path -Path $tempPath -ChildPath \"export.inf\" +if (Test-Path $export) { + Remove-Item -Path $export -Force +} + +$secedt = Join-Path -Path $tempPath -ChildPath \"secedt.sdb\" +if (Test-Path $secedt) { + Remove-Item -Path $secedt -Force +} + +try { + Write-Output (\"Granting SeServiceLogonRight to user account: $grantLogonAsServiceAccountName.\") + $sid = ((New-Object System.Security.Principal.NTAccount($grantLogonAsServiceAccountName)).Translate([System.Security.Principal.SecurityIdentifier])).Value + secedit /export /cfg $export + $sids = (select-string $export -pattern \"SeServiceLogonRight\").line.Split(\"=\").Trim()[1] + foreach ($line in @(\"[Unicode]\", \"Unicode=yes\", \"[System Access]\", \"[Event Audit]\", \"[Registry Values]\", \"[Version]\", \"signature=`\"`$CHICAGO$`\"\", \"Revision=1\", \"[Profile Description]\", \"Description=GrantLogOnAsAService security template\", \"[Privilege Rights]\", \"SeServiceLogonRight = $sids,*$sid\")) { + Add-Content $import $line + } + + Write-Verbose \"Calling secedit...\" + secedit /import /db $secedt /cfg $import + secedit /configure /db $secedt + Write-Verbose \"Calling gpupdate...\" + gpupdate /force + Write-Verbose \"Cleaning up temp files...\" + Remove-Item -Path $import -Force + Remove-Item -Path $export -Force + Remove-Item -Path $secedt -Force + Write-Output(\"SeServiceLogonRight successfully granted to $grantLogonAsServiceAccountName\") +} +catch { + Write-Error \"Failed to grant SeServiceLogonRight to user account: $grantLogonAsServiceAccountName.\" + $error[0] +} +" + }, + "Parameters": [ + { + "Id": "456ad1ed-286d-4bbf-a096-026d3928e3ef", + "Name": "GrantLogonAsServiceAccountName", + "Label": "Account Name", + "HelpText": "Domain account name to grant `SeServiceLogonRight`. Example: `US\\testAccount`", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "nshenoy", + "$Meta": { + "ExportedAt": "2017-11-30T20:48:26.823Z", + "OctopusVersion": "4.0.10", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-local-user.json.human b/step-templates/windows-local-user.json.human new file mode 100644 index 000000000..6b7a14a65 --- /dev/null +++ b/step-templates/windows-local-user.json.human @@ -0,0 +1,116 @@ +{ + "Id": "6dbe826d-f973-46fe-a897-a0a2cdfd01f4", + "Name": "Windows - Local User", + "Description": "Configures a local user. (If assigning user rights the ntrights.exe file should be installed on the target server and added to the system path variable)", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$username = $OctopusParameters['Username'] +$password = $OctopusParameters['Password'] +$memberOf = $OctopusParameters['MemberOf'] +$userRights = $OctopusParameters['UserRights'] + +# Add/Update User + +$user = Get-WmiObject Win32_UserAccount -filter \"LocalAccount=True AND Name='$username'\" +if($user -eq $null) +{ + Write-Host \"Adding user\" + net user \"$username\" \"$password\" /add /expires:never /passwordchg:no /yes +} +else +{ + Write-Host \"User already exists, updating password\" + net user \"$username\" \"$password\" /expires:never /passwordchg:no +} + +# Ensure password never expires + +write \"Ensuring password never expires\" +$user = Get-WmiObject Win32_UserAccount -filter \"LocalAccount=True AND Name='$username'\" +$user.PasswordExpires = $false; +$user.Put(); + +# Add/Update group membership + +if($memberOf) +{ + $groups = $memberOf.Split(\",\") + foreach($group in $groups) + { + $ntGroup = [ADSI](\"WinNT://./$group\") + $members = $ntGroup.psbase.Invoke(\"Members\") | %{$_.GetType().InvokeMember(\"Name\", 'GetProperty', $null, $_, $null)} + if($members -contains \"$username\") + { + Write-Host \"User already a member of the $group group\" + } + else + { + Write-Host \"Adding to the $group group\" + net localgroup \"$group\" \"$username\" /add + } + } +} + +# Add/Update user rights assignment + +if($userRights) +{ + $userRightsArr = $userRights.Split(\",\") + foreach($userRight in $userRightsArr) + { + Write-Host \"Granting $userRight right\" + & \"ntrights\" +r $userRight -u \"$username\" + } +} +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Username", + "Label": "Username", + "HelpText": "The username", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Password", + "Label": "Password", + "HelpText": "The password", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MemberOf", + "Label": "Group memberships", + "HelpText": "A comma separated list of group memberships", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "UserRights", + "Label": "User rights", + "HelpText": "A comma separated list of user rights to assign (e.g. SeServiceLogonRight). See http://support.microsoft.com/kb/315276 for full list of rights", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-16T10:30:58.052+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-map-network-drive.json.human b/step-templates/windows-map-network-drive.json.human new file mode 100644 index 000000000..49f9edc01 --- /dev/null +++ b/step-templates/windows-map-network-drive.json.human @@ -0,0 +1,101 @@ +{ + "Id": "34a67091-bb67-46fa-90cf-ff2d09f24732", + "Name": "Windows - Map Network Drive", + "Description": "Map a network drive.", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.ScriptBody": "#http://gallery.technet.microsoft.com/scriptcenter/Powershell-Map-utility-to-444c1c95 +function Map ($computer){ + +function GetDriveType($DriveCode) { + switch ($DriveCode) + { 0 {\"Unknown\"} + 1 {\"No root directory\"} + 2 {\"Removable Disk\"} + 3 {\"Local Disk\"} + 4 {\"Network Drive\"} + 5 {\"Compact Disk\"} + 6 {\"RAM Disk\"} + } # end switch + } # end function GetDriveType + +if ($computer -eq $null) {$computer='localhost'} +Get-WmiObject -Class win32_logicaldisk -ComputerName $computer | select DeviceID, VolumeName, ` + @{n='DriveType'; e={GetDriveType($_.driveType)}}, ` + @{n='Size';e={\"{0:F2} GB\" -f ($_.Size / 1gb)}}, ` + @{n='FreeSpace';e={\"{0:F2} GB\" -f ($_.FreeSpace / 1gb)}} | Format-Table + +} + +$map = new-object -ComObject WScript.Network +if (!(Test-Path $DriveLetter)) +{ +\t$map.MapNetworkDrive($DriveLetter, $MapPath, $MapPersist, $MapUser, $MapPass) +\tWrite-Host \"Drive $DriveLetter mapped to $MapPath as user $MapUser.\" +} +else +{ + Write-Host \"Drive $DriveLetter already in use.\" +} + +Map .", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "DriveLetter", + "Label": "Drive letter", + "HelpText": "Enter a drive letter. Example: _H:_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MapPath", + "Label": "Share/server path", + "HelpText": "Enter the share path. Example: _\\\\server\\share_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MapPersist", + "Label": "Persist the mapping?", + "HelpText": "Persist the map. Example: _True_ or _False_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MapUser", + "Label": "User to map drive as", + "HelpText": "Enter a user to map the drive as. Example: _DOMAIN\\user_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "MapPass", + "Label": "Password for the user", + "HelpText": "Enter the password for the user. This should be bound to a secure variable. Example: _#{MappingPassword}_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-19T20:21:10.373+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-restart-computer.json.human b/step-templates/windows-restart-computer.json.human new file mode 100644 index 000000000..7275901e1 --- /dev/null +++ b/step-templates/windows-restart-computer.json.human @@ -0,0 +1,31 @@ +{ + "Id": "3db008be-3fd8-4114-8f5d-0b986babf934", + "Name": "Windows - Restart Computer", + "Description": "Restarts the computer.", + "ActionType": "Octopus.Script", + "Version": 0, + "Properties": { + "Octopus.Action.Script.ScriptBody": "Restart-Computer -ComputerName $ComputerName -force", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ComputerName", + "Label": "Computer name", + "HelpText": "Computer to restart.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-30T20:40:30.083+00:00", + "OctopusVersion": "2.4.7.85", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-restart-if-required.json.human b/step-templates/windows-restart-if-required.json.human new file mode 100644 index 000000000..bf3bc4995 --- /dev/null +++ b/step-templates/windows-restart-if-required.json.human @@ -0,0 +1,95 @@ +{ + "Id": "cb0ca23c-524d-44a9-86e0-2ae05989d6a0", + "Name": "Check for Pending Restart and Restart if required", + "Description": "Checks if Server is pending a reboot, and reboots if it needs it.", + "ActionType": "Octopus.Script", + "Version": 0, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "# Sourced from https://github.com/adbertram/Random-PowerShell-Work/blob/master/Random%20Stuff/Test-PendingReboot.ps1 +# Script will run only on the server with the Tentacle installed + +function Test-RegistryKey($Key) { + if (Get-Item -Path $Key -ErrorAction Ignore) { + return $true + } + return $false +} + +function Test-RegistryValue($Key, $Value) { + if (Get-ItemProperty -Path $Key -Name $Value -ErrorAction Ignore) { + return $true + } + return $false +} + +function Test-RegistryValueNotNull($Key, $Value) { + if (($regVal = Get-ItemProperty -Path $Key -Name $Value -ErrorAction Ignore) -and $regVal.($Value)) { + return $true + } + return $false +} + +$IsPendingReboot = $false + +# Testing Registry keys for Restarts +# an exception is thrown when Get-ItemProperty or Get-ChildItem are passed a nonexistant key path +$tests = @( + { Test-RegistryKey -Key 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending' } + { Test-RegistryKey -Key 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootInProgress' } + { Test-RegistryKey -Key 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired' } + { Test-RegistryKey -Key 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\PackagesPending' } + { Test-RegistryKey -Key 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\PostRebootReporting' } + { Test-RegistryValueNotNull -Key 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager' -Value 'PendingFileRenameOperations' } + { Test-RegistryValueNotNull -Key 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager' -Value 'PendingFileRenameOperations2' } + { +\t\t(Test-RegistryKey -Key 'HKLM:\\SOFTWARE\\Microsoft\\Updates') -and +\t\t(Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Updates' -Name 'UpdateExeVolatile' | Select-Object -ExpandProperty UpdateExeVolatile) -ne 0 +\t} + { Test-RegistryValue -Key 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce' -Value 'DVDRebootSignal' } + { Test-RegistryKey -Key 'HKLM:\\SOFTWARE\\Microsoft\\ServerManager\\CurrentRebootAttemps' } + { Test-RegistryValue -Key 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Netlogon' -Value 'JoinDomain' } + { Test-RegistryValue -Key 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Netlogon' -Value 'AvoidSpnSet' } + { + (Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ActiveComputerName').ComputerName -ne + (Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ComputerName').ComputerName + } + { + if (Get-ChildItem -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Services\\Pending') { + $true + } + } +) + +Write-Debug \"Running tests to checking for reboot pending\" +foreach ($test in $tests) { + if (& $test) { + Write-Debug \"Test found reboot pending: $test\" + $IsPendingReboot = $true + break + } +} + + +if($IsPendingReboot -eq $true) { + Write-Host \"Restarting\" + Restart-Computer -Force +} +if($IsPendingReboot -eq $false) { + Write-Host \"No Restart required\" +} +" + }, + "Parameters": [], + "LastModifiedOn": "2020-01-14T20:40:28.430+00:00", + "LastModifiedBy": "DevOpsDerek", + "$Meta": { + "ExportedAt": "2020-01-14T08:41:32.508Z", + "OctopusVersion": "2019.11.1", + "Type": "ActionTemplate" + }, + "Category": "windows" + } diff --git a/step-templates/windows-scheduled-task-changepath.json.human b/step-templates/windows-scheduled-task-changepath.json.human new file mode 100644 index 000000000..08e25c645 --- /dev/null +++ b/step-templates/windows-scheduled-task-changepath.json.human @@ -0,0 +1,86 @@ +{ + "Id": "406ae0ad-72ce-491f-89a3-22bc2dbbb7ed", + "Name": "Windows Scheduled Task - Change Path", + "Description": "Changes the execution path of a Windows Scheduled Task for both 2008 and 2012.", + "ActionType": "Octopus.Script", + "Version": 4, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$taskName = $OctopusParameters['TaskName'] +$taskFolder = $OctopusParameters['TaskFolder'] +$taskExe = $OctopusParameters['TaskExe'] +$userName = $OctopusParameters['TaskUserName'] +$password = $OctopusParameters['TaskPassword'] + +$taskPath = Join-Path $taskFolder $taskExe +Write-Output \"Changing execution path of $taskName to $taskPath\" + +#Check if 2008 Server +if ((Get-WmiObject Win32_OperatingSystem).Name.Contains(\"2008\")) +{ + $userName = \"`\"$userName`\"\" + schtasks /Change /RU $userName /RP $password /TR $taskPath /TN $taskName +} +else +{ + $action = New-ScheduledTaskAction -Execute $taskPath + Set-ScheduledTask -TaskName $taskName -Action $action -User $userName -Password $password; +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "TaskName", + "Label": "Task Name", + "HelpText": "Name of the Windows Scheduled Task.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TaskFolder", + "Label": "Task Folder", + "HelpText": "Folder path of the command to be executed by the Scheduled Task.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TaskUserName", + "Label": "User Name", + "HelpText": "User name the task will run under.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "TaskPassword", + "Label": "Password", + "HelpText": "Password for the user the task will run under.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "TaskExe", + "Label": "Task Executable", + "HelpText": "Executable name of the task to be run from the Task Folder.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-12-10T18:00:42.218+00:00", + "OctopusVersion": "2.5.12.666", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-scheduled-task-create.json.human b/step-templates/windows-scheduled-task-create.json.human new file mode 100644 index 000000000..7ccf60cc3 --- /dev/null +++ b/step-templates/windows-scheduled-task-create.json.human @@ -0,0 +1,438 @@ +{ + "Id": "17bc51d1-8b88-4aad-b188-24a0904d0bf2", + "Name": "Windows Scheduled Task - Create", + "Description": "Create Windows scheduled task. If the task exists it will be torn down and re-added to ensure consistency", + "ActionType": "Octopus.Script", + "Version": 22, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\"; +Set-StrictMode -Version \"Latest\"; + +# use http://msdn.microsoft.com/en-us/library/windows/desktop/bb736357(v=vs.85).aspx for API reference + +Function Create-ScheduledTask($TaskName,$RunAsUser,$RunAsPassword,$TaskRun,$Arguments,$Schedule,$StartTime,$StartDate,$RunWithElevatedPermissions,$Days,$Interval,$Duration, $Modifier) +{ + + # SCHTASKS /Create [/S system [/U username [/P [password]]]] + # [/RU username [/RP password]] /SC schedule [/MO modifier] [/D day] + # [/M months] [/I idletime] /TN taskname /TR taskrun [/ST starttime] + # [/RI interval] [ {/ET endtime | /DU duration} [/K] [/XML xmlfile] [/V1]] + # [/SD startdate] [/ED enddate] [/IT | /NP] [/Z] [/F] [/HRESULT] [/?] + + # note - /RL and /DELAY appear in the \"Parameter list\" for \"SCHTASKS /Create /?\" but not in the syntax above + + $argumentList = @(); + $argumentList += @( \"/Create\" ); + + $argumentList += @( \"/RU\", \"`\"$RunAsUser`\"\" ); + + if( -not (StringIsNullOrWhiteSpace($RunAsPassword))) + { + \t$RAP = $RunAsPassword -Replace \"`\"\",\"\\`\"\" + $argumentList += @( \"/RP `\"$RAP`\"\" ); + } + + $argumentList += @( \"/SC\", $Schedule ); + + if( -not (StringIsNullOrWhiteSpace($Interval)) ) + { + $argumentList += @( \"/RI\", $Interval ); + } + + if( -not (StringIsNullOrWhiteSpace($Modifier))) + { + switch -Regex ($Schedule) + { + \"MINUTE|HOURLY|DAILY|WEEKLY|MONTHLY|ONEVENT\" { + $argumentList += @( \"/MO\", $Modifier ); + } + \"ONCE|ONSTART|ONLOGON|ONIDLE\" { + $argumentList += @( \"/MO\" ); + } + } + } + + if( -not (StringIsNullOrWhiteSpace($Days))) + { + if($Schedule -ne \"WEEKDAYS\") { + $argumentList += @( \"/D\", $Days ); + } else { + $argumentList += @( \"/D\", \"MON,TUE,WED,THU,FRI\" ); + } + } + + $argumentList += @( \"/TN\", \"`\"$TaskName`\"\" ); + + if( $Arguments ) + { + $argumentList += @( \"/TR\", \"`\"'$TaskRun' $Arguments`\"\" ); + } + else + { + $argumentList += @( \"/TR\", \"`\"'$TaskRun'`\"\" ); + } + + if( -not (StringIsNullOrWhiteSpace($StartTime)) ) + { + $argumentList += @( \"/ST\", $StartTime ); + } + + if( -not (StringIsNullOrWhiteSpace($Duration)) ) + { + $argumentList += @( \"/DU\", $Duration ); + } + + if( -not (StringIsNullOrWhiteSpace($StartDate)) ) + { + $argumentList += @( \"/SD\", $StartDate ); + } + + $argumentList += @( \"/F\" ); + + if( $RunWithElevatedPermissions ) + { + $argumentList += @( \"/RL\", \"HIGHEST\" ); + } + + Invoke-CommandLine -FilePath \"$($env:SystemRoot)\\System32\\schtasks.exe\" ` + -ArgumentList $argumentList; + +} + +Function Delete-ScheduledTask($TaskName) { + # SCHTASKS /Delete [/S system [/U username [/P [password]]]] + # /TN taskname [/F] [/HRESULT] [/?] + Invoke-CommandLine -FilePath \"$($env:SystemRoot)\\System32\\schtasks.exe\" ` + -ArgumentList @( \"/Delete\", \"/S\", \"localhost\", \"/TN\", \"`\"$TaskName`\"\", \"/F\" ); +} + +Function Stop-ScheduledTask($TaskName) { + # SCHTASKS /End [/S system [/U username [/P [password]]]] + # /TN taskname [/HRESULT] [/?] + Invoke-CommandLine -FilePath \"$($env:SystemRoot)\\System32\\schtasks.exe\" ` + -ArgumentList @( \"/End\", \"/S\", \"localhost\", \"/TN\", \"`\"$TaskName`\"\" ); +} + +Function Start-ScheduledTask($TaskName) { + # SCHTASKS /Run [/S system [/U username [/P [password]]]] [/I] + # /TN taskname [/HRESULT] [/?] + Invoke-CommandLine -FilePath \"$($env:SystemRoot)\\System32\\schtasks.exe\" ` + -ArgumentList @( \"/Run\", \"/S\", \"localhost\", \"/TN\", \"`\"$TaskName`\"\" ); +} + +Function Enable-ScheduledTask($TaskName) { + # SCHTASKS /Change [/S system [/U username [/P [password]]]] /TN taskname + # { [/RU runasuser] [/RP runaspassword] [/TR taskrun] [/ST starttime] + # [/RI interval] [ {/ET endtime | /DU duration} [/K] ] + # [/SD startdate] [/ED enddate] [/ENABLE | /DISABLE] [/IT] [/Z] } + # [/HRESULT] [/?] + Invoke-CommandLine -FilePath \"$($env:SystemRoot)\\System32\\schtasks.exe\" ` + -ArgumentList @( \"/Change\", \"/S\", \"localhost\", \"/TN\", \"`\"$TaskName`\"\", \"/ENABLE\" ); +} + +Function Disable-ScheduledTask($TaskName) { + # SCHTASKS /Change [/S system [/U username [/P [password]]]] /TN taskname + # { [/RU runasuser] [/RP runaspassword] [/TR taskrun] [/ST starttime] + # [/RI interval] [ {/ET endtime | /DU duration} [/K] ] + # [/SD startdate] [/ED enddate] [/ENABLE | /DISABLE] [/IT] [/Z] } + # [/HRESULT] [/?] + Invoke-CommandLine -FilePath \"$($env:SystemRoot)\\System32\\schtasks.exe\" ` + -ArgumentList @( \"/Change\", \"/S\", \"localhost\", \"/TN\", \"`\"$TaskName`\"\", \"/DISABLE\" ); +} + +Function ScheduledTask-Exists($taskName) { + $schedule = new-object -com Schedule.Service + $schedule.connect() + $tasks = $schedule.getfolder(\"\\\").gettasks(0) + foreach ($task in ($tasks | select Name)) { + #echo \"TASK: $($task.name)\" + if($task.Name -eq $taskName) { + #write-output \"$task already exists\" + return $true + } + } + return $false +} + +Function StringIsNullOrWhitespace([string] $string) +{ + if ($string -ne $null) { $string = $string.Trim() } + return [string]::IsNullOrEmpty($string) +} + +function Invoke-CommandLine +{ + param + ( + [Parameter(Mandatory=$true)] + [string] $FilePath, + [Parameter(Mandatory=$false)] + [string[]] $ArgumentList = @( ), + [Parameter(Mandatory=$false)] + [string[]] $SuccessCodes = @( 0 ) + ) + $SanitizedArgList = $ArgumentList | ForEach-Object { if($_.StartsWith(\"/RP\")) { \"/RP ********\" } else { $_ } } + Write-Host ($FilePath + \" \" + ($SanitizedArgList -Join \" \")); + + $process = Start-Process -FilePath $FilePath -ArgumentList $ArgumentList -Wait -NoNewWindow -PassThru; + if( $SuccessCodes -notcontains $process.ExitCode ) + { + throw new-object System.InvalidOperationException(\"process terminated with exit code '$($process.ExitCode)'.\"); + } +} + +function Invoke-OctopusStep +{ + param + ( + [Parameter(Mandatory=$true)] + [hashtable] $OctopusParameters + ) + + $taskName = $OctopusParameters['TaskName'] + $runAsUser = $OctopusParameters['RunAsUser'] + $runAsPassword = $OctopusParameters['RunAsPassword'] + $command = $OctopusParameters['Command'] + $arguments = $OctopusParameters['Arguments'] + $schedule = $OctopusParameters['Schedule'] + $startTime = $OctopusParameters['StartTime'] + $startDate = $OctopusParameters['StartDate'] + + if( $OctopusParameters.ContainsKey(\"RunWithElevatedPermissions\") ) + { + $runWithElevatedPermissions = [boolean]::Parse($OctopusParameters['RunWithElevatedPermissions']) + } + else + { + $runWithElevatedPermissions = $false; + } + + $days = $OctopusParameters['Days'] + $interval = $OctopusParameters['Interval'] + $duration = $OctopusParameters['Duration'] + $Modifier = $OctopusParameters['Modifier'] + + if((ScheduledTask-Exists($taskName))){ + Write-Output \"$taskName already exists, Tearing down...\" + Write-Output \"Stopping $taskName...\" + Stop-ScheduledTask($taskName) + Write-Output \"Successfully Stopped $taskName\" + Write-Output \"Deleting $taskName...\" + Delete-ScheduledTask($taskName) + Write-Output \"Successfully Deleted $taskName\" + } + Write-Output \"Creating Scheduled Task - $taskName\" + + Create-ScheduledTask $taskName $runAsUser $runAsPassword $command $arguments $schedule $startTime $startDate $runWithElevatedPermissions $days $interval $duration $Modifier + Write-Output \"Successfully Created $taskName\" + + if( $OctopusParameters.ContainsKey('TaskStatus') ) + { + $taskStatus = $OctopusParameters['TaskStatus'] + if( -not (StringIsNullOrWhiteSpace($taskStatus)) ) + { + if ( $taskStatus -eq \"ENABLE\" ) + { + Enable-ScheduledTask($taskName) + Write-Output \"$taskName enabled\" + } + elseif ( $taskStatus -eq \"DISABLE\" ) + { + Disable-ScheduledTask($taskName) + Write-Output \"$taskName disabled\" + } + else + { + Write-Output \"$taskName status unchanged (on create, will be enabled)\" + } + } + } + + if( $OctopusParameters.ContainsKey(\"StartNewTaskNow\") ) + { + $startNewTaskNow = [boolean]::Parse($OctopusParameters['StartNewTaskNow']) + } + else + { + $startNewTaskNow = $false; + } + + if( $startNewTaskNow ) { + Start-ScheduledTask($taskName) + } +} + + +# only execute the step if it's called from octopus deploy, +# and skip it if we're runnning inside a Pester test +if( Test-Path -Path \"Variable:OctopusParameters\" ) +{ + Invoke-OctopusStep -OctopusParameters $OctopusParameters; +} +", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Name": "TaskName", + "Label": "Name", + "HelpText": "The name of the Scheduled Task", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "RunAsUser", + "Label": "User", + "HelpText": "The User that the task will run as", + "DefaultValue": "System", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "RunAsPassword", + "Label": "Password", + "HelpText": "Specifying a password allows the task to run when the user is not logged on to the server.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "Command", + "Label": "Action", + "HelpText": "The Action that the task executes. Usually a path to the executable", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Arguments", + "Label": "Arguments", + "HelpText": "A value that specifies any arguments to be passed to run the task.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Schedule", + "Label": "Schedule", + "HelpText": "When the Task is triggered", + "DefaultValue": "DAILY", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "MINUTE|Every Minute +HOURLY|Hourly +DAILY|Daily +WEEKLY|Weekly +WEEKDAYS|Weekdays +ONCE|One off +ONSTART|On Start +ONLOGON|On Logon +ONIDLE|On Idle +MONTHLY|Monthly" + } + }, + { + "Name": "StartTime", + "Label": "Start Time", + "HelpText": "The Time the task will run. Use the format HH:mm:ss", + "DefaultValue": "12:00", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "StartDate", + "Label": "Start Date", + "HelpText": "The date the task will start running. use the format MM/dd/yyyy", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Modifier", + "Label": "Modifier", + "HelpText": "A value that specifies how often the task runs within its schedule type. For ONCE, ONSTART, ONLOGON and ONIDLE any value can be used since it will be ignored (They do not use modifiers with MO).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Interval", + "Label": "Interval", + "HelpText": "A value that specifies the repetition interval in minutes.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Duration", + "Label": "Duration", + "HelpText": "A value that specifies the duration to run the task. The time format is HH:mm (24-hour time).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "RunWithElevatedPermissions", + "Label": "RunWithElevatedPermissions", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "Days", + "Label": "Days", + "HelpText": "A value that specifies the day of the week to run the task. Valid values are: MON, TUE, WED, THU, FRI, SAT, SUN and for MONTHLY schedules 1 - 31 (days of the month). The wildcard character (*) specifies all days.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText", + "Octopus.SelectOptions": "" + } + }, + { + "Name": "StartNewTaskNow", + "Label": "StartNewTaskNow", + "HelpText": null, + "DefaultValue": "False", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "TaskStatus", + "Label": "TaskStatus", + "HelpText": "Whether the task is enabled, disabled, or left as-is when this step completes", + "DefaultValue": "ENABLE", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "ENABLE|Enable +DISABLE|Disable +LEAVE|Leave Unchanged" + }, + "Links": {} + } + ], + "LastModifiedOn": "2022-03-29T11:49:45.384Z", + "LastModifiedBy": "harrisonmeister", + "$Meta": { + "ExportedAt": "2022-03-29T11:49:45.384Z", + "OctopusVersion": "2022.1.2152", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-scheduled-task-disable.json.human b/step-templates/windows-scheduled-task-disable.json.human new file mode 100644 index 000000000..adc8e9622 --- /dev/null +++ b/step-templates/windows-scheduled-task-disable.json.human @@ -0,0 +1,120 @@ +{ + "Id": "1be30b21-ba58-4667-bff4-2d0ef9a806af", + "Name": "Windows Scheduled Task - Disable", + "Description": "Disables a Windows Scheduled Task for both 2008 and 2012.", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$taskName = $OctopusParameters['TaskName'] +$maximumWaitTime = $OctopusParameters['MaximumWaitTime'] +$succeedOnTaskNotFound = $OctopusParameters['SucceedOnTaskNotFound'] + +#Check if the PowerShell cmdlets are available +$cmdletSupported = [bool](Get-Command -Name Get-ScheduledTask -ErrorAction SilentlyContinue) + +try { +\tif($cmdletSupported) { +\t\t$taskExists = Get-ScheduledTask | Where-Object { $_.TaskName -eq $taskName } +\t} +\telse { +\t\t$taskService = New-Object -ComObject \"Schedule.Service\" +\t\t$taskService.Connect() +\t\t$taskFolder = $taskService.GetFolder('\\') +\t\t$taskExists = $taskFolder.GetTasks(0) | Select-Object Name, State | Where-Object { $_.Name -eq $taskName } +\t} + +\tif(-not $taskExists) { + if( $succeedOnTaskNotFound){ + Write-Output \"Scheduled task '$taskName' does not exist\" + } + else { +\t\t throw \"Scheduled task '$taskName' does not exist\" + } +\t\treturn +\t} + +\tWrite-Output \"Disabling $taskName...\" +\t$waited = 0 +\tif($cmdletSupported) { +\t\t$task = Disable-ScheduledTask $taskName +\t\tWrite-Output \"Waiting until $taskName is disabled...\" +\t\twhile(($task.State -ne [Microsoft.PowerShell.Cmdletization.GeneratedTypes.ScheduledTask.StateEnum]::Disabled) -and (($maximumWaitTime -eq 0) -or ($waited -lt $maximumWaitTime))) +\t\t{ +\t\t\tStart-Sleep -Milliseconds 200 +\t\t\t$waited += 200 +\t\t\t$task = Get-ScheduledTask $taskName +\t\t} +\t\t +\t\tif($task.State -ne [Microsoft.PowerShell.Cmdletization.GeneratedTypes.ScheduledTask.StateEnum]::Disabled) { +\t\t\tthrow \"The scheduled task $taskName could not be disabled within the specified wait time\" +\t\t} +\t} +\telse { +\t\tschtasks /Change /Disable /TN \"$taskName\" +\t\t#The State property can hold the following values: +\t\t# 0: Unknown +\t\t# 1: Disabled +\t\t# 2: Queued +\t\t# 3: Ready +\t\t# 4: Running +\t\twhile(($taskFolder.GetTask($taskName).State -ne 1) -and (($maximumWaitTime -eq 0) -or ($waited -lt $maximumWaitTime))) { +\t\t\tStart-Sleep -Milliseconds 200 +\t\t\t$waited += 200 +\t\t} +\t\t +\t\tif($taskFolder.GetTask($taskName).State -ne 1) { +\t\t throw \"The scheduled task '$taskName' could not be disabled within the specified wait time\" +\t\t} +\t} +} +finally { + if($taskFolder -ne $NULL) { +\t [System.Runtime.Interopservices.Marshal]::ReleaseComObject($taskFolder) +\t} +\t +\tif($taskService -ne $NULL) { +\t [System.Runtime.Interopservices.Marshal]::ReleaseComObject($taskService) +\t} +}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "TaskName", + "Label": "Task Name", + "HelpText": "Name of the Windows Scheduled Task.", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "DefaultValue": null + }, + { + "Name": "MaximumWaitTime", + "Label": "Maximum Wait Time", + "HelpText": "Maximum time the script must wait before aborting. Use '0' to wait indefinitely.", + "DefaultValue": 0, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "SucceedOnTaskNotFound", + "Label": "Succeed On Task Not Found", + "HelpText": "This setting prevents the script from throwing an error if the task is not found.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + }, + "Links": {} + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2017-03-13T19:50:10.728Z", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-scheduled-task-enable.json.human b/step-templates/windows-scheduled-task-enable.json.human new file mode 100644 index 000000000..f3b149eb2 --- /dev/null +++ b/step-templates/windows-scheduled-task-enable.json.human @@ -0,0 +1,31 @@ +{ + "Id": "c5b985a0-14ed-4364-a1c1-6a1dc65500ed", + "Name": "Windows Scheduled Task - Enable", + "Description": "Enables a Windows Scheduled Task for both 2008 and 2012.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$taskName = $OctopusParameters['TaskName']\r\rWrite-Output \"Enabling $taskName...\"\r\r#Check if 2008 Server\rif ((Get-WmiObject Win32_OperatingSystem).Name.Contains(\"2008\"))\r{\r schtasks /Change /Enable /TN $taskName\r}\relse\r{\r Enable-ScheduledTask $taskName;\r}", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "TaskName", + "Label": "Task Name", + "HelpText": "Name of the Windows Scheduled Task.", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "DefaultValue": null + } + ], + "LastModifiedOn": "2014-05-14T19:45:23.842+00:00", + "LastModifiedBy": "maohde", + "$Meta": { + "ExportedAt": "2014-05-14T19:46:43.760+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-service-change-binary-path.json.human b/step-templates/windows-service-change-binary-path.json.human new file mode 100644 index 000000000..8be210392 --- /dev/null +++ b/step-templates/windows-service-change-binary-path.json.human @@ -0,0 +1,57 @@ +{ + "Id": "b6860fcf-9dee-48a0-afac-85e2098df692", + "Name": "Windows Service - Change binary path", + "Description": "Change binary path of existing windows service (changes will be effective after service start/stop)", + "ActionType": "Octopus.Script", + "Version": 2, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$TheService = Get-Service $ServiceName -ErrorAction SilentlyContinue +if ($TheService) +{ + Write-Host \"Windows Service \"\"$ServiceName\"\" found, changing path.\" + sc.exe config $TheService binPath= $BinaryPath + Write-Host \"Service \"\"$ServiceName\"\" path changed to \"\"$BinaryPath\"\", restart service to use new path.\" +} +else +{ + Write-Host \"Windows Service \"\"$ServiceName\"\" not found.\" +} +", + "Octopus.Action.Package.DownloadOnTentacle": "False" + }, + "Parameters": [ + { + "Id": "b6cfb2c2-7bfd-4d28-ab91-f9f114c2c0dd", + "Name": "ServiceName", + "Label": "Service name to change binary path.", + "HelpText": "Name of the service to change. Example: OctopusService", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "d2e685d0-e244-450f-82eb-c435462cdb0f", + "Name": "BinaryPath", + "Label": "Binary path", + "HelpText": "Path to the new service binary. Example: c:\\services\\octopus.exe", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2018-05-04T08:38:54.450Z", + "LastModifiedBy": "sphinxy", + "$Meta": { + "ExportedAt": "2018-05-04T08:38:54.450Z", + "OctopusVersion": "4.0.10", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-service-check-status.json.human b/step-templates/windows-service-check-status.json.human new file mode 100644 index 000000000..431f95a37 --- /dev/null +++ b/step-templates/windows-service-check-status.json.human @@ -0,0 +1,98 @@ +{ + "Id": "feacd1dd-715b-4176-af87-73a417daeb75", + "Name": "Windows Service - Check status", + "Description": "check Windows service status on target machine or remote host(s). + +_for the remote host(s): the 'OctopusDeploy Tentacle' service installed on target machine (or main tentacle) must have the grants for remote query_ +", + "ActionType": "Octopus.Script", + "Version": 20, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "###----------------------------### +### ### +###check Windows service status### +### ### +###----------------------------### + +$servicename=$OctopusParameters['servicenamewin'] +$desiredstate=$OctopusParameters['servicewinstatus'] +$hostslist=$OctopusParameters['winhostlist'] + +###hosts list comma-separated or single host +$hostl=$hostslist.Split(\",\") +$hostl + +foreach($h in $hostl){ + Write-Output \"Running on $h\" + #check the status of the service on remote host + $remotestatus=invoke-command -computername $h {(Get-Service -Name $servicename).Status} + $status=$remotestatus.Value + if($status){ + try{ + if($status -like $desiredstate){ + Write-Output \"The service $servicename is correctly $desiredstate on $h\" + } + else{ + Write-Error \"The service $servicename is NOT $desiredstate. Currently state is $status\" + + } + } + catch{ + Write-Error \"Is not possible to determinate the status for service $servicename\" + } + + } + else{ + Write-Error \"Error on retrieving the status. Invalid service name or host $h\" + } +} + +###@author:fedele_mattia" + }, + "Parameters": [ + { + "Id": "01a19172-fb06-40e2-8bb1-bcd6b9fe360b", + "Name": "servicenamewin", + "Label": "Windows Service Name", + "HelpText": null, + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "3e9bc02a-ea38-4336-8e81-0252a0a91ff4", + "Name": "servicewinstatus", + "Label": "Desired state", + "HelpText": null, + "DefaultValue": "Running", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "Running|Running +Stopped|Stopped" + } + }, + { + "Id": "828944cb-ff12-47f7-bf4b-fb6d1799235d", + "Name": "winhostlist", + "Label": "Comuputer", + "HelpText": "Host or Hosts list **comma-separated**: +- _machine123_ +- _machine123,machine124,machine125_", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "fedelemattia", + "$Meta": { + "ExportedAt": "2018-09-27T08:29:15.090Z", + "OctopusVersion": "2018.6.10", + "Type": "ActionTemplate" + }, + "Category": "Windows" +} diff --git a/step-templates/windows-service-create.json.human b/step-templates/windows-service-create.json.human new file mode 100644 index 000000000..cddbbab12 --- /dev/null +++ b/step-templates/windows-service-create.json.human @@ -0,0 +1,113 @@ +{ + "Id": "ba50bddb-67ec-4105-a909-134e73905b03", + "Name": "Windows Service - Create", + "Description": "Create Windows Service", + "ActionType": "Octopus.Script", + "Version": 5, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$serviceName = $OctopusParameters['ServiceName']\r +$binaryPath = $OctopusParameters['BinaryPath']\r +$dependsOn = $OctopusParameters['DependsOn']\r +$displayName = $OctopusParameters['DisplayName']\r +$startupType = $OctopusParameters['StartupType']\r +$description = $OctopusParameters['Description']\r +\r +Write-Output \"Creating $serviceName...\"\r +\r +$serviceInstance = Get-Service $serviceName -ErrorAction SilentlyContinue\r +if ($serviceInstance -eq $null)\r +{\r + New-Service -Name $serviceName -BinaryPathName $binaryPath -DependsOn $dependsOn -DisplayName $displayName -StartupType $startupType -Description $description\r + Write-Output \"Service $serviceName created.\"\r +}\r +else\r +{\r + Write-Output \"The $serviceName already exist.\"\r +}\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "df79a6f8-bcd2-49c9-ad3e-e49b7c838a12", + "Name": "ServiceName", + "Label": "Service name", + "HelpText": "Service name", + "DefaultValue": "Required", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "cb9c4a02-0e50-43d8-bca7-f8b006f8b7a1", + "Name": "BinaryPath", + "Label": "Binary path", + "HelpText": "Executable path", + "DefaultValue": "Required", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "d6acf911-8173-460d-be24-b9b31b69a3cd", + "Name": "DependsOn", + "Label": "Depends On", + "HelpText": "Depends On", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "d0b86504-d2d5-4673-9043-2c968055de41", + "Name": "DisplayName", + "Label": "Display name", + "HelpText": "Display name", + "DefaultValue": "Required", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "85315ed9-3b9a-493e-9379-558953105384", + "Name": "StartupType", + "Label": "Startup type", + "HelpText": "Startup type: +Boot, System, Automatic, Manual, Disabled", + "DefaultValue": "Required", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "111791c5-9d42-4ace-8a6c-9f15775db745", + "Name": "Description", + "Label": "Description", + "HelpText": "Description", + "DefaultValue": "Required", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedOn": "2016-12-09T13:30:58.513Z", + "LastModifiedBy": "marekgd", + "$Meta": { + "ExportedAt": "2016-12-09T13:30:58.513Z", + "OctopusVersion": "3.7.4", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-service-remove.json.human b/step-templates/windows-service-remove.json.human new file mode 100644 index 000000000..51dc20bf1 --- /dev/null +++ b/step-templates/windows-service-remove.json.human @@ -0,0 +1,47 @@ +{ + "Id": "c5e85c9f-0408-4b38-b85f-6a225fd3e9d6", + "Name": "Windows Service - Remove", + "Description": "Removes a Windows service.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$TheService = Get-Service $ServiceName -ErrorAction SilentlyContinue\r +if ($TheService)\r +{\r + Write-Host \"Windows Service \"\"$ServiceName\"\" found, removing service.\"\r + if ($TheService.Status -eq \"Running\")\r + {\r + Write-Host \"Stopping $ServiceName ...\"\r + $TheService.Stop()\r + }\r + sc.exe delete $TheService\r + Write-Host \"Service \"\"$ServiceName\"\" removed.\"\r +}\r +else\r +{\r + Write-Host \"Windows Service \"\"$ServiceName\"\" not found.\"\r +}\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ServiceName", + "Label": "Service name to remove.", + "HelpText": "Name of the service to remove. Example: _OctopusDeploy Tentacle_", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-19T21:05:57.107+00:00", + "OctopusVersion": "2.4.5.46", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-service-restart.json.human b/step-templates/windows-service-restart.json.human new file mode 100644 index 000000000..bca7450c7 --- /dev/null +++ b/step-templates/windows-service-restart.json.human @@ -0,0 +1,36 @@ +{ + "Id": "d1df734a-c0da-4022-9e70-8e1931b083da", + "Name": "Windows Service - Restart", + "Description": "Restarts a Windows Service.", + "ActionType": "Octopus.Script", + "Version": 3, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$serviceName = $OctopusParameters['ServiceName'] +Write-Output \"Restarting $serviceName, stopping...\" +$serviceInstance = Get-Service $serviceName +restart-service -InputObject $serviceName -Force +$serviceInstance.WaitForStatus('Running','00:01:00') +Write-Output \"Service $serviceName started.\"\r", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ServiceName", + "Label": "Service Name", + "HelpText": "Name of the Windows Service (this is not always the display name).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-05T04:30:44.943+00:00", + "OctopusVersion": "2.4.2.1412", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-service-set-recovery-on-failure-actions.json.human b/step-templates/windows-service-set-recovery-on-failure-actions.json.human new file mode 100644 index 000000000..5ab8d9e06 --- /dev/null +++ b/step-templates/windows-service-set-recovery-on-failure-actions.json.human @@ -0,0 +1,186 @@ +{ + "Id": "5576faaf-e024-4248-ad98-41717a7c4f43", + "Name": "Windows Service - Set Recovery on Failure Actions", + "Description": "Set the recovery on failure actions for a particular service.", + "ActionType": "Octopus.Script", + "Version": 12, + "CommunityActionTemplateId": null, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "cls +#ignore above + +function main +{ + $serviceName = Get-OctoParameter -parameterName \"ServiceName\" -parameterDescription \"Service Name\" + $firstFailureAction = Get-OctoParameter -parameterName \"FirstFailureAction\" -parameterDescription \"First Failure Action\" -default \"restart\" + $secondFailureAction = Get-OctoParameter -parameterName \"SecondFailureAction\" -parameterDescription \"Second Failure Action\" -default \"restart\" + $thirdFailureAction = Get-OctoParameter -parameterName \"ThirdFailureAction\" -parameterDescription \"Third Failure Action\" -default \"restart\" + $firstFailureDelay = Get-OctoParameter -parameterName \"FirstFailureDelay\" -parameterDescription \"First Failure Delay\" -default 180000 + $secondFailureDelay = Get-OctoParameter -parameterName \"SecondFailureDelay\" -parameterDescription \"Second Failure Delay\" -default 180000 + $thirdFailureDelay = Get-OctoParameter -parameterName \"ThirdFailureDelay\" -parameterDescription \"Third Failure Delay\" -default 180000 + $reset = Get-OctoParameter -parameterName \"Reset\" -parameterDescription \"Reset\" -default 86400 + + $service = Get-Service $serviceName -ErrorAction SilentlyContinue + + if (!$service) + { + Write-Host \"Windows Service '$serviceName' not found, skipping.\" + return + } + + echo \"Updating the '$serviceName' service with recovery options...\" + echo \" On first failure '$firstFailureAction' after '$firstFailureDelay' milliseconds.\" + echo \" On second failure '$secondFailureAction' after '$secondFailureDelay' milliseconds.\" + echo \" On third failure '$thirdFailureAction' after '$thirdFailureDelay' milliseconds.\" + echo \" Reset after '$reset' minutes.\" + + sc.exe failure $service.Name actions= $firstFailureAction/$firstFailureDelay/$secondFailureAction/$secondFailureDelay/$thirdFailureAction/$thirdFailureDelay reset= $reset + + echo \"Done\" +} + +function Get-OctoParameter() +{ + Param + ( + [Parameter(Mandatory=$true)]$parameterName, + [Parameter(Mandatory=$true)]$parameterDescription, + [Parameter(Mandatory=$false)]$default + ) + + $ErrorActionPreference = \"SilentlyContinue\" + $value = $OctopusParameters[$parameterName] + $ErrorActionPreference = \"Stop\" + + if (! $value) + { + if(! $default) + { + throw \"'$parameterDescription' cannot be empty, please specify a value.\" + } + + return $default + } + + return $value +} + +main" + }, + "Parameters": [ + { + "Id": "a4df7a56-844b-4a5b-b233-3aaf5014808e", + "Name": "ServiceName", + "Type": "String", + "Label": "Service Name", + "HelpText": "The name of the service you wish to set the recovery options on.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "600a43c3-e0b3-4fcd-9832-22ebfef110e9", + "Name": "FirstFailureAction", + "Type": "String", + "Label": "First Failure Action", + "HelpText": "The action you wish the service to take after this failure. _Defaults to '**Restart Service**'._", + "DefaultValue": "restart", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "\"\"|Take No Action +restart|Restart Service +reboot|Reboot Computer" + }, + "Links": {} + }, + { + "Id": "a8086832-de84-4ab0-9dd7-2adc3d6a3478", + "Name": "FirstFailureDelay", + "Type": "String", + "Label": "First Failure Delay", + "HelpText": "The time in milliseconds to wait before preforming the failure action. _Defaults to **180000** milliseconds_.", + "DefaultValue": "180000", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "b914c20c-5bb7-4a04-9be5-2090e71be6d7", + "Name": "SecondFailureAction", + "Type": "String", + "Label": "Second Failure Action", + "HelpText": "The action you wish the service to take after this failure. _Defaults to '**Restart Service**'._", + "DefaultValue": "restart", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "\"\"|Take No Action +restart|Restart Service +reboot|Reboot Computer" + }, + "Links": {} + }, + { + "Id": "c24bed04-1f1f-418c-a60e-eba717d0ed2f", + "Name": "SecondFailureDelay", + "Type": "String", + "Label": "Second Failure Delay", + "HelpText": "The time in milliseconds to wait before preforming the failure action. _Defaults to **180000** milliseconds_.", + "DefaultValue": "180000", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "14654da5-dae8-4163-b404-a2f29ea2c518", + "Name": "ThirdFailureAction", + "Type": "String", + "Label": "Third Failure Action", + "HelpText": "The action you wish the service to take after this failure. _Defaults to '**Restart Service**'._", + "DefaultValue": "restart", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "\"\"|Take No Action +restart|Restart Service +reboot|Reboot Computer" + }, + "Links": {} + }, + { + "Id": "957f3937-fba6-4ace-ba79-01441c113173", + "Name": "ThirdFailureDelay", + "Type": "String", + "Label": "Third Failure Delay", + "HelpText": "The time in milliseconds to wait before preforming the failure action. _Defaults to **180000** milliseconds_.", + "DefaultValue": "180000", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "94d29d24-fb84-4621-acda-d7105ea975b6", + "Name": "Reset", + "Type": "String", + "Label": "Reset", + "HelpText": "Specifies the length of time (in seconds) with no failures after which the failure count should be reset to 0. _Defaults to **86400** seconds_.", + "DefaultValue": "86400", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "samcook", + "$Meta": { + "ExportedAt": "2017-04-11T16:38:17.527Z", + "OctopusVersion": "3.12.4", + "Type": "ActionTemplate" + }, + "Category": "Windows" +} diff --git a/step-templates/windows-service-set-startup-type.json.human b/step-templates/windows-service-set-startup-type.json.human new file mode 100644 index 000000000..a214d0d3d --- /dev/null +++ b/step-templates/windows-service-set-startup-type.json.human @@ -0,0 +1,58 @@ +{ + "Id": "361926f3-8c53-4d19-bb3b-337a531e4448", + "Name": "Windows Service - Set Startup Type", + "Description": "Sets the Startup Type of a Windows Service", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$serviceName = $OctopusParameters['ServiceName']\r +$startupType = $OctopusParameters['StartupType']\r +\r +if (!$serviceName)\r +{\r + Write-Error \"No service name was specified. Please specify the name of the service to set the 'Startup Type'.\"\r + exit -2\r +}\r +\r +Write-Output \"Setting Startup Type for $serviceName...\"\r +\r +sc.exe config $serviceName start= $startupType\r +\r +Write-Output \"Startup Type for $serviceName set to $startupType.\"\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ServiceName", + "Label": "Service Name", + "HelpText": "Name of the Windows Service (this is not always the display name).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "StartupType", + "Label": "Startup Type", + "HelpText": "Startup type of the Windows Service.", + "DefaultValue": "auto", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "delayed-auto|Automatic (Delayed Start) +auto|Automatic +demand|Manual +disabled|Disabled" + } + } + ], + "LastModifiedOn": "2014-09-16T00:08:51.570+00:00", + "LastModifiedBy": "caioproiete", + "$Meta": { + "ExportedAt": "2014-09-16T00:19:29.865+00:00", + "OctopusVersion": "2.5.8.447", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-service-start.json.human b/step-templates/windows-service-start.json.human new file mode 100644 index 000000000..8e2de2c15 --- /dev/null +++ b/step-templates/windows-service-start.json.human @@ -0,0 +1,31 @@ +{ + "Id": "60733bf3-1617-4d85-a40f-4b6a0b9289ef", + "Name": "Windows Service - Start", + "Description": "Starts a Windows Service.", + "ActionType": "Octopus.Script", + "Version": 6, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$serviceName = $OctopusParameters['ServiceName']\r\rWrite-Output \"Starting $serviceName...\"\r\r$serviceInstance = Get-Service $serviceName\rif ($serviceInstance.Status -eq \"Running\") {\r Write-Output \"The $serviceName service is already running.\"\r} else {\r start-service $serviceName\r $serviceInstance.WaitForStatus('Running', '00:01:00')\r Write-Output \"Started $serviceName\"\r}\r", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ServiceName", + "Label": "Service Name", + "HelpText": "Name of the Windows Service (this is not always the display name).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-05T04:11:22.101+00:00", + "OctopusVersion": "2.4.2.1412", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-service-stop-or-kill.json.human b/step-templates/windows-service-stop-or-kill.json.human new file mode 100644 index 000000000..75327281e --- /dev/null +++ b/step-templates/windows-service-stop-or-kill.json.human @@ -0,0 +1,93 @@ +{ + "Id": "6fa0fab6-4799-4d81-944d-3c7b54530870", + "Name": "Stop Service With Kill", + "Description": "This steps stops the specified service and in case it does not respond or times out, the service will be killed.", + "ActionType": "Octopus.Script", + "Version": 8, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptBody": "$svcName = $OctopusParameters['ServiceName'] +$svcTimeout = $OctopusParameters['ServiceStopTimeout'] + +function Stop-ServiceWithTimeout ([string] $name, [int] $timeoutSeconds) { + $timespan = New-Object -TypeName System.Timespan -ArgumentList 0,0,$timeoutSeconds + + If ($svc = Get-Service $svcName -ErrorAction SilentlyContinue) { + if ($svc -eq $null) { return $true } + if ($svc.Status -eq [ServiceProcess.ServiceControllerStatus]::Stopped) { return $true } + $svc.Stop() + try { + Write-Host \"Stopping Service\" $svcTimeout \"Timeout\" + $svc.WaitForStatus([ServiceProcess.ServiceControllerStatus]::Stopped, $timespan) + } + catch [ServiceProcess.TimeoutException] { + Write-Host \"Timeout stopping service $($svc.Name)\" + return $false + } + Write-Host \"Service Sucessfully stopped\" + return $true + + } Else { + Write-Host \"Service does not exist, this is acceptable. Probably the first time deploying to this target\" + Exit + } +} + +Write-Host \"Checking service\" + +$svcpid = (get-wmiobject Win32_Service | where{$_.Name -eq $svcName}).ProcessId +Write-Host \"Found PID \" + $svcpid + +Stop-ServiceWithTimeout -name $svcName -timeoutSeconds $svcTimeout + +Write-Host \"Rechecking service\" +$svcpid = (get-wmiobject Win32_Service | where{$_.Name -eq $svcName}).ProcessId +Write-Host \"Found PID \" + $svcpid + +$service = Get-Service -name $svcName | Select -Property Status +if($service.Status -ne \"Stopped\"){\tStart-Sleep -seconds 5 } + +#Check-Service process +if($svcpid){ + #still exists? + $p = get-process -id $svcpid -ErrorAction SilentlyContinue + if($p){ + Write-Host \"Killing Service\" + Stop-Process $p.Id -force + } +}", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline" + }, + "Parameters": [ + { + "Id": "7ad7cec6-c5c8-40a9-8657-793f88ea1c0f", + "Name": "ServiceName", + "Label": "Service Name", + "HelpText": "Name of the service to stop", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "2ece0f23-76b7-490d-808f-64e5612ac0eb", + "Name": "ServiceStopTimeout", + "Label": "Service Stop Timeout", + "HelpText": "Amount of time in seconds to wait before killing the Process", + "DefaultValue": "10", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2018-11-05T03:59:57.028Z", + "LastModifiedBy": "benjimac93", + "$Meta": { + "ExportedAt": "2018-11-05T03:59:57.028Z", + "OctopusVersion": "2018.8.12", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-service-stop.json.human b/step-templates/windows-service-stop.json.human new file mode 100644 index 000000000..41f9c83e5 --- /dev/null +++ b/step-templates/windows-service-stop.json.human @@ -0,0 +1,31 @@ +{ + "Id": "ab3eb4cf-5fc1-4168-be8d-02246d919ca8", + "Name": "Windows Service - Stop", + "Description": "Stops a Windows Service if it is running.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "$serviceName = $OctopusParameters['ServiceName']\r\rWrite-Output \"Stopping $serviceName...\"\r\r$serviceInstance = Get-Service $serviceName -ErrorAction SilentlyContinue\rif ($serviceInstance -ne $null) {\r Stop-Service $serviceName -Force\r $serviceInstance.WaitForStatus('Stopped', '00:01:00')\r Write-Output \"Service $serviceName stopped.\"\r} else {\r Write-Output \"The $serviceName service could not be located.\"\r}\r", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "ServiceName", + "Label": "Service Name", + "HelpText": "Name of the Windows Service (this is not always the display name).", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2021-07-26T16:50:00.000+00:00", + "LastModifiedBy": "bobjwalker", + "$Meta": { + "ExportedAt": "2014-05-05T04:13:42.618+00:00", + "OctopusVersion": "2.4.2.1412", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/windows-wait-for-or-force-close-application.json.human b/step-templates/windows-wait-for-or-force-close-application.json.human new file mode 100644 index 000000000..af8ba1945 --- /dev/null +++ b/step-templates/windows-wait-for-or-force-close-application.json.human @@ -0,0 +1,93 @@ +{ + "Id": "2d59d5b6-1dc3-424e-9279-676393933efd", + "Name": "Process - Wait for or Force close", + "Description": "Waits a set amount of time for a process to close and optionally force closes the process after the timeout expires.", + "ActionType": "Octopus.Script", + "Version": 0, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$seconds = $OctopusParameters['Seconds']\r +$forceCloseOnTimeout = $OctopusParameters['Force']\r +$processName = $OctopusParameters['ProcessName']\r +$timeout = new-timespan -Seconds $seconds\r +$stopwatch = [diagnostics.stopwatch]::StartNew()\r +\r +# Check if the process is even running\r +if (Get-Process $processName -ErrorAction silentlycontinue)\r +{\r + Write-Host \"Waiting $seconds seconds for process '$processName' to terminate\"\r +} \r +else \r +{\r + Write-Host \"Process '$processName' is not running\"\r + return\r +}\r +\r +# Count down waiting for the process to stop gracefully\r +while ($stopwatch.elapsed -lt $timeout)\r +{\r + # Check process is running\r + if (Get-Process $processName -ErrorAction silentlycontinue) \r + {\r + Write-Host \"Waiting...\"\r + }\r + else \r + {\r + Write-Host \"Process '$processName' is no longer running\"\r + return\r + }\r +\r + # Wait for a while\r + Start-Sleep -seconds 1\r +}\r +\r +# Force close the process if set\r +if($forceCloseOnTimeout –eq $TRUE)\r +{\r + Write-Host \"Force closing process $processName\"\r + Stop-Process -processname $processName -Force\r + Write-Host \"Process '$processName' is no longer running\"\r + return\r +}\r +\r +Write-Host \"Process $processName didn't close within the allocated time\"\r +Write-Host \"Continuing anyway\"" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "Seconds", + "Label": "Seconds", + "HelpText": "The number of seconds to wait before forcefully killing the application", + "DefaultValue": "10", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "Force", + "Label": "Force Kill", + "HelpText": "Whether or not the application should be forcefully killed after the timeout has expired.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Name": "ProcessName", + "Label": "Process Name", + "HelpText": "The name of the process to wait for", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "svenkle", + "$Meta": { + "ExportedAt": "2015-11-21T01:21:41.951+00:00", + "OctopusVersion": "3.2.3", + "Type": "ActionTemplate" + }, + "Category": "windows" +} diff --git a/step-templates/winsw-install.json.human b/step-templates/winsw-install.json.human new file mode 100644 index 000000000..cdc639687 --- /dev/null +++ b/step-templates/winsw-install.json.human @@ -0,0 +1,109 @@ +{ + "Id": "b50efca4-ceb6-4e40-b000-d593d7f5ff76", + "Name": "WinSW - Install", + "Description": "Verify that WinSW is installed, if it is not, install it.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptBody": "$winswPath = $OctopusParameters[\"winsw-path\"] +$winswFilename = $OctopusParameters[\"winsw-filename\"] +$winswFullPath = \"$winswPath\\$winswFilename\" +$winswUrl = $OctopusParameters[\"winsw-url\"] +$winswAutoInstall = $OctopusParameters[\"winsw-auto-install\"] + +Write-Host \"Checking to ensure WinSW is installed.\" +Write-Host \"For more information on WinSW check out https://github.com/winsw/winsw\" + +Write-Host \"WinSW should be installed at $winswFullPath\" +Write-Host \"Checking to ensure base path is located at $winswPath\" + +if (-not (Test-Path -LiteralPath \"$winswPath\" -PathType Container)) { +\tWrite-Host \"Path was not found. Creating the directory now.\" + + New-Item -Path \"$winswPath\" -ItemType Directory + + Write-Host \"Directory created.\" +} + + +if (-not (Test-Path -LiteralPath \"$winswFullPath\" -PathType Leaf)) { + +\tWrite-Host \"WinSW was not found at '$winswFullPath'. Check to see if auto install flag is true.\" + + if ($winswAutoInstall -eq $true) { + \t + Write-Host \"Flag is true, installing WinSW from $winswUrl to $winswFullPath\" + + if ($winswUrl -eq $false) { + \t + Write-Error \"WinSW download URL is not set.\" + + } else { + \t + Invoke-WebRequest -Uri $OctopusParameters[\"winsw-url\"] -OutFile \"$winswFullPath\" + + \tWrite-Host \"WinSW installed at $winswPath.\" + } + + } else { + \t + Write-Error \"Flag is false, you will need to install WinSW to $winswFullPath.\" + + } +}", + "Octopus.Action.EnabledFeatures": "" + }, + "Parameters": [ + { + "Id": "edd462d9-3e00-40de-8ce3-cc387938e237", + "Name": "winsw-url", + "Label": "WinSW URL", + "HelpText": "WinSW URL to download.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "47ca8207-301d-41ac-96fc-e3122bdeaa21", + "Name": "winsw-path", + "Label": "WinSW Path", + "HelpText": "Location where WinSW is installed.", + "DefaultValue": "c:\\winsw\\base", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "037be70a-c8f6-479b-a62c-7d77ef7d3a94", + "Name": "winsw-auto-install", + "Label": "WinSW Auto Install", + "HelpText": "Should WinSW automatically install.", + "DefaultValue": "true", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "9221c0e5-e544-43a2-acb4-40f601b46115", + "Name": "winsw-filename", + "Label": "WinSW Filename", + "HelpText": "Name of the base winsw file.", + "DefaultValue": "winsw.exe", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "$Meta": { + "ExportedAt": "2020-08-03T13:53:12.815Z", + "OctopusVersion": "2020.3.2", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "cryptic-ai", + "Category": "Windows" +} diff --git a/step-templates/xamarin-Insights-upload-dysm.json.human b/step-templates/xamarin-Insights-upload-dysm.json.human new file mode 100644 index 000000000..15f3d34ae --- /dev/null +++ b/step-templates/xamarin-Insights-upload-dysm.json.human @@ -0,0 +1,324 @@ +{ + "Id": "a1c51946-abd0-434c-99f3-b7a1b5af74c5", + "Name": "Xamarin Insights dSYM Upload", + "Description": "Uploads a dSYM symbols file to Xamarin Insights, for more readable exceptions.", + "ActionType": "Octopus.Script", + "Version": 0, + "Properties": { + "Octopus.Action.Script.ScriptBody": "#####################################\r +# Xamarin Insights dSYM Upload script\r +#\r +# Uploads a dSYM sybmols file to Xamarin insights from a Nuget file\r +# extracted in a previous Octopus Deploy step. Allows a variety of parameters.\r +#\r +# Uploads to configured application, by API Key.\r +#\r +# API Documentation is available at: https://developer.xamarin.com/guides/insights/user-interface/settings/#Uploading_a_dSYM_File\r +#\r +# The API key involved is provided in the \"Settings\" for the particular app on the Xamarin Insights portal.\r +# https://insights.xamarin.com/\r +# Log in, open the application, and click settings. The general \"settings\" tab has the API Key field.\r +#\r +# Example curl request:\r +# curl -F \"dsym=@YOUR-APPS-DSYM.zip;type=application/zip\" https://xaapi.xamarin.com/api/dsym?apikey=13dd6c82159361ea13ad25a0d9100eb6e228bb17\r +#\r +# v0.1 - Initial version, uploads one dSYM file.\r +# \r +# The nuget package must contain the *.app.dSYM.zip file. \r +#\r +# The following *.nuspec example will package a release IPA and associated *.app.dSYM.zip file.\r +#\r +# The upload script requires a search path (default package root) with exactly one *.app.dSYM.zip file.\r +# \r +# Specify package path relative to the nuspec file location\r +#\r +# https://docs.nuget.org/create/nuspec-reference#file-element-examples\r +#\r +# In some cases the ID, Version, and Description may need manually specified.\r +#\r +\r +<#\r +\r + \r + \r + \r + $id$\r + $id$\r + $version$\r + Mobile project packaged for Octopus deploy. $description$\r + \r + \r + \r + \r + \r + \r +\r + \r + \r +\r + \r + \r +\r +#>\r +\r +#############################\r +# Debug Parameter Overrides #\r +#############################\r +\r +# These values are set explicitly durring debugging so that the script can\r +# be run in the editor.\r +# For local debugging, uncomment these values and fill in appropriately.\r +\r +<#\r +\r +# debug folder with app files\r +$stepPath = \"C:\\Temp\\powershellscript\\\"\r +\r +$OctopusParameters = @{\r +\"InsightsAppSpecificApiToken\" = \"YourApiKeyhere\";\r +# \"NugetSearchPath\" = \"bin\\iPhone\"; # Additional path information, reatlive to the nuget file root, e.g. release\r +}\r +\r +# #>\r +\r +###################################\r +# Octopus Deploy common functions #\r +###################################\r +\r +# A collection of functions that can be used by script steps to determine where packages installed\r +# by previous steps are located on the filesystem.\r + \r +function Find-InstallLocations {\r + $result = @()\r + $OctopusParameters.Keys | foreach {\r + if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) {\r + $result += $OctopusParameters[$_]\r + }\r + }\r + return $result\r +}\r + \r +function Find-InstallLocation($stepName) {\r + $result = $OctopusParameters.Keys | where {\r + $_.Equals(\"Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath\", [System.StringComparison]::OrdinalIgnoreCase)\r + } | select -first 1\r + \r + if ($result) {\r + return $OctopusParameters[$result]\r + }\r + \r + throw \"No install location found for step: $stepName\"\r +}\r +\r +function Find-SingleInstallLocation {\r + $all = @(Find-InstallLocations)\r + if ($all.Length -eq 1) {\r + return $all[0]\r + }\r + if ($all.Length -eq 0) {\r + throw \"No package steps found\"\r + }\r + throw \"Multiple package steps have run; please specify a single step\"\r +}\r +\r +#####################\r +# Utility functions #\r +#####################\r +\r +function Get-ExactlyOneDsymFileInfo($searchPath)\r +{\r + $symbolFiles = Get-ChildItem -Path $searchPath -Recurse -Filter *.app.dSYM.zip\r + \r + $fileCount = $symbolFiles.count\r +\r + if($fileCount -ne 1)\r + {\r + throw \"Did not find exactly one (1) symbols file. Found $fileCount dSYM file(s). Searched under path: $searchPath\"\r + }\r +\r + return $symbolFiles\r +}\r +\r +####################\r +# Basic Parameters #\r +####################\r +\r +$apiToken = $OctopusParameters['InsightsAppSpecificApiToken']\r +\r +$octopusFilePathOverride = $OctopusParameters['NugetSearchPath']\r +\r +$stepName = $OctopusParameters['MobileAppPackageStepName']\r +\r +# set step path, if not already set\r +If([string]::IsNullOrEmpty($stepPath))\r +{\r + if (![string]::IsNullOrEmpty($stepName)) {\r + Write-Host \"Finding path to package step: $stepName\"\r + $stepPath = Find-InstallLocation $stepName\r + } else {\r + $stepPath = Find-SingleInstallLocation\r + }\r +}\r +\r +Write-Host \"##octopus[stderr-progress]\"\r +\r +# if we were not provided a file name, search for a single package file\r +if([string]::IsNullOrWhiteSpace($octopusFilePathOverride))\r +{\r + $appFileInfo = Get-ExactlyOneDsymFileInfo $stepPath\r + $appFullFilePath = $appFileInfo.FullName\r +}\r +else\r +{\r + $searchPathOverride = Join-Path $stepPath $octopusFilePathOverride\r + $appFileInfo = Get-ExactlyOneDsymFileInfo $searchPathOverride\r + $appFullFilePath = $appFileInfo.FullName\r +}\r +\r +$fileName = [System.IO.Path]::GetFileName($appFullFilePath)\r +\r +$apiUploadUri = \"https://xaapi.xamarin.com/api/dsym?apikey=$apiToken\"\r +\r +# Request token details\r +$uniqueBoundaryToken = [Guid]::NewGuid().ToString()\r +\r +$contentType = \"multipart/form-data; boundary=$uniqueBoundaryToken\"\r +\r +\r +Write-Host \"File Location: $appFullFilePath\"\r +\r +################################\r +# Set up Hockey App parameters #\r +################################\r +\r +$formSectionSeparator = @\"\r +\r +--$uniqueBoundaryToken\r +\r +\"@\r +\r +############################\r +# Prepare request wrappers #\r +############################\r +\r +# Standard for multipart form data\r +# http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4\r +\r +$stringEncoding = [System.Text.Encoding]::ASCII\r +\r +# Note the hard-coded \"ipa\" name here is per HockeyApp API documentation\r +# and it applies to ALL platform application files.\r +\r +$preFileBytes = $stringEncoding.GetBytes(\r +$formSectionSeparator +\r +@\"\r +Content-Disposition: form-data; name=\"dsym\"; filename=\"$fileName\"\r +Content-Type: application/zip\r +\r +\r +\"@)\r +\r +# file bytes will go in between\r +\r +$postFileBytes = $stringEncoding.GetBytes(@\"\r +\r +--$uniqueBoundaryToken--\r +\"@)\r +\r +######################\r +# Invoke the request #\r +######################\r +\r +# Note, previous approach was Invoke-RestMethod based. It worked, but was NOT memory\r +# efficient, leading to high memory usage and \"out of memory\" errors.\r +\r +# Based on examples from\r +# http://stackoverflow.com/questions/566462/upload-files-with-httpwebrequest-multipart-form-data\r +# and \r +# https://gist.github.com/nolim1t/271018\r +\r +# Uses a dot net WebRequest and streaming to limit memory usage\r +\r +$WebRequest = [System.Net.WebRequest]::Create(\"$apiUploadUri\")\r +\r +$WebRequest.ContentType = $contentType\r +$WebRequest.Method = \"POST\"\r +$WebRequest.KeepAlive = $true;\r +\r +$RequestStream = $WebRequest.GetRequestStream()\r +\r +# before file bytes\r +$RequestStream.Write($preFileBytes, 0, $preFileBytes.Length);\r +\r +#files bytes\r +\r +$fileMode = [System.IO.FileMode]::Open\r +$fileAccess = [System.IO.FileAccess]::Read\r +\r +$fileStream = New-Object IO.FileStream $appFullFilePath,$fileMode,$fileAccess\r +$bufferSize = 4096 # 4k at a time\r +$byteBuffer = New-Object Byte[] ($bufferSize)\r +\r +# read bytes. While bytes are read...\r +while(($bytesRead = $fileStream.Read($byteBuffer,0,$byteBuffer.Length)) -ne 0)\r +{\r + # write those byes to the request stream\r + $RequestStream.Write($byteBuffer, 0, $bytesRead)\r +}\r +\r +$fileStream.Close()\r +\r +# after file bytes\r +$RequestStream.Write($postFileBytes, 0, $postFileBytes.Length);\r +\r +$RequestStream.Close()\r +\r +$response = $WebRequest.GetResponse();\r +", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "InsightsAppSpecificApiToken", + "Label": "Insights Api Token", + "HelpText": "Your Xamarin API Key for the specific application you are uploading the symbol files to. + +Visit: +https://insights.xamarin.com + +Log in, browse your application, and click Settings. Your application specific API Token should be visible under \"API Key\".", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "PackageStepName", + "Label": "Package Step Name", + "HelpText": "Name of the previously-deployed package step that contains the dSYM symbol file that you want to deploy.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Name": "NugetSearchPath", + "Label": "Nuget Search Path (Optional)", + "HelpText": "This fully optional search path allows you to look in a specific folder path in your nuget file, such as \"bin\\release\". This may be needed in cases when the nuget file has multiple dSYM files in different locations.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2016-01-12T21:54:21.496+00:00", + "LastModifiedBy": "Colin.Dabritz@Viewpoint.com", + "$Meta": { + "ExportedAt": "2016-01-12T23:40:54.635+00:00", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "xamarin" +} diff --git a/step-templates/xamarin-testcloud-execute-testrun.json.human b/step-templates/xamarin-testcloud-execute-testrun.json.human new file mode 100644 index 000000000..aefc57cd4 --- /dev/null +++ b/step-templates/xamarin-testcloud-execute-testrun.json.human @@ -0,0 +1,483 @@ +{ + "Id": "fd79c2f6-8983-4f91-a36d-aa622f44f16f", + "Name": "Xamarin TestCloud execute test run", + "Description": "Executes a Xamarin TestCloud test run for an app contained in a nuget file.", + "ActionType": "Octopus.Script", + "Version": 2, + "Properties": { + "Octopus.Action.Script.ScriptBody": "#####################################\r +# Xamarin TestCloud Start Test Run script\r +# \r +# Kicks off a new test run for an app.\r +# This script uses the test-cloud.exe helper utility included with the Xamarin UITest nuget package. \r +# https://www.nuget.org/packages/Xamarin.UITest\r +# \r +# For use with Xamarin UITests\r +# https://developer.xamarin.com/guides/testcloud/uitest/\r +# being run in Xamarin TestCloud on physical devices\r +# https://developer.xamarin.com/guides/testcloud/introduction-to-test-cloud/\r +#\r +# v0.1 - kicks off configured test run, tested against iOS app only\r +#\r +# The nuget package must contain the *.iap or *.apk file, compiled with calabash included\r +# The nuget package must contain the DLLs from the UITest project\r +# The nuget package may optionally contain a symbols file, *.app.dSYM.zip\r +# The nuget package must contain the test-cloud.exe support utility\r +#\r +# The following *.nuspec example will package:\r +# * a release ipa\r +# * UITest DLLs\r +# * associated *.app.dSYM.zip file\r +# * The test-cloud.exe support utility\r +#\r +# all search paths default to the root of the nuget package,\r +# and may be further qualified relative to the root of the nuget package\r +# The upload script uses default or optionally qualified search paths for the following:\r +# * The .ipa or .apk location\r +# * The UITest project DLLs\r +# * The *.app.dSYM.zip symbols file\r +# * The test-cloud.exe utility\r +#\r +# It also requires the API Key from the application, and the code for the devices desired,\r +# and a valid user accout to run as.\r +#\r +# 1. Visit the testcloud interface: https://testcloud.xamarin.com/\r +# 2. Choose \"New Test Run\" and configure as desired.\r +# 3. In the last step, copy the large hash (app specific API Key), devices parameter code, and username\r +#\r +# The nugetFile below is an example that retrieves the appropriate files from a typical iOS build\r +#\r +# https://docs.nuget.org/create/nuspec-reference#file-element-examples\r +#\r +# In some cases the ID, Version, and Description may need manually specified.\r +#\r +\r +<#\r +\r + \r + \r + \r + $id$\r + $id$\r + $version$\r + Mobile project packaged for Octopus deploy. $description$\r + \r + \r + \r + \r + \r + \r +\r + \r + \r +\r + \r + \r +\r + \r + \r + \r +\r + \r + \r +\r +#>\r +\r +#############################\r +# Debug Parameter Overrides #\r +#############################\r +\r +# These values are set explicitly durring debugging so that the script can\r +# be run in the editor.\r +# For local debugging, uncomment these values and fill in appropriately.\r +\r +<#\r +\r +$OctopusParameters = @{\r +\"appPathOverride\" = \"\" # \"bin\\iPhone\"\r +\"dllPathOverride\" = \"\" # \"bin\\UITest\\Release\"\r +\"testCloudUserName\" = \"your.user@name.com\"\r +\"symbolPathOverride\" = \"\"; # \"bin\\iPhone\"\r +\"apiKey\" = \"YOUR-KEY-HERE\";\r +\"devicesCode\" = \"ae978982\"; # devices code here (example ae978982 is 2 devices)\r +\"series\" = \"master\"; # default is master\r +\"locale\" = \"en_US\"; # default locale is en_US\r +\"testCloudExePathOverride\" = \"\" # \"tools\"\r +}\r +\r +# debug folder with app files\r +$stepPath = \"C:\\Temp\\powershellscript\\\"\r +\r +# #>\r +\r +###################################\r +# Octopus Deploy common functions #\r +###################################\r +\r +# A collection of functions that can be used by script steps to determine where packages installed\r +# by previous steps are located on the filesystem.\r + \r +function Find-InstallLocations {\r + $result = @()\r + $OctopusParameters.Keys | foreach {\r + if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) {\r + $result += $OctopusParameters[$_]\r + }\r + }\r + return $result\r +}\r + \r +function Find-InstallLocation($stepName) {\r + $result = $OctopusParameters.Keys | where {\r + $_.Equals(\"Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath\", [System.StringComparison]::OrdinalIgnoreCase)\r + } | select -first 1\r + \r + if ($result) {\r + return $OctopusParameters[$result]\r + }\r + \r + throw \"No install location found for step: $stepName\"\r +}\r +\r +function Find-SingleInstallLocation {\r + $all = @(Find-InstallLocations)\r + if ($all.Length -eq 1) {\r + return $all[0]\r + }\r + if ($all.Length -eq 0) {\r + throw \"No package steps found\"\r + }\r + throw \"Multiple package steps have run; please specify a single step\"\r +}\r +\r +#####################\r +# Utility functions #\r +#####################\r +\r +function Get-ExactlyOneMobilePackageFileInfo($searchPath)\r +{\r + $apkFiles = Get-ChildItem -Path $searchPath -Recurse -Filter *.apk #Android\r + $ipaFiles = Get-ChildItem -Path $searchPath -Recurse -Filter *.ipa #iOS\r + $appxFiles = Get-ChildItem -Path $searchPath -Recurse -Filter *.appx # windows\r +\r + $apkCount = $apkFiles.count\r +\r + $ipaCount = $ipaFiles.count\r +\r + $appxCount = $appxFiles.count\r +\r + $totalCount = $apkCount + $ipaCount + $appxCount\r +\r + if($totalCount -ne 1)\r + {\r + throw \"Did not find exactly one (1) mobile application package. Found $apkCount APK file(s), $ipaCount IPA file(s), and $appxCount Appx file(s). Searched under path: $searchPath\"\r + }\r +\r + if($apkCount -eq 1)\r + {\r + return $apkFiles\r + }\r +\r + if($ipaCount -eq 1)\r + {\r + return $ipaFiles\r + }\r +\r + if($appxCount -eq 1)\r + {\r + return $appxFiles\r + }\r +\r + throw \"Unable to find mobile application packages (fallback error - not expected)\"\r +}\r +\r +function Get-OneDsymFileInfoOrNull($searchPath)\r +{\r + $symbolFiles = Get-ChildItem -Path $searchPath -Recurse -Filter *.app.dSYM.zip\r + \r + $fileCount = $symbolFiles.count\r +\r + if($fileCount -eq 0)\r + { \r + return $null\r + } \r +\r + if($fileCount -gt 1)\r + {\r + throw \"Found more than one symbols file. Found $fileCount dSYM file(s). Searched under path: $searchPath\"\r + }\r +\r + return $symbolFiles\r +}\r +\r +function Get-ExactlyOneUploadExeFileInfo($searchPath)\r +{\r + $testcloudexefiles = Get-ChildItem -Path $searchPath -Recurse -Filter test-cloud.exe\r + \r + $fileCount = $testcloudexefiles.count\r +\r + if($fileCount -ne 1)\r + {\r + throw \"Did not find exactly one (1) test-cloud.exe. Found $fileCount exe file(s). Searched under path: $searchPath\"\r + }\r +\r + return $testcloudexefiles\r +}\r +\r +function Get-ExactlyOneUITestDllPath($searchPath)\r +{\r + $XamarinUITestdlls = Get-ChildItem -Path $searchPath -Recurse -Filter Xamarin.UITest.dll\r + \r + $fileCount = $XamarinUITestdlls.count\r +\r + if($fileCount -ne 1)\r + {\r + throw \"Did not find exactly one (1) Test DLL location. Found $fileCount DLL location(s), based on finding 'Xamarin.UITest.dll' files. Searched under path: $searchPath\"\r + }\r + \r + $singleXamarinUITestDllFullPath = $XamarinUITestdlls.FullName\r + $UITestDllPath = Split-Path -parent $singleXamarinUITestDllFullPath\r + return $UITestDllPath\r +}\r +\r +####################\r +# Basic Parameters #\r +####################\r +\r +# required\r +$apiKey = $OctopusParameters['apiKey']\r +$devicesCode = $OctopusParameters['devicesCode']\r +$testCloudUserName = $OctopusParameters['testCloudUserName']\r +\r +# optional\r +$series = $OctopusParameters['series'] # default \"master\"\r +$locale = $OctopusParameters['locale'] # default \"en_US\"\r +\r +# optional additional path overrides\r +$appPathOverride = $OctopusParameters['appPathOverride']\r +$dllPathOverride = $OctopusParameters['dllPathOverride']\r +$symbolPathOverride = $OctopusParameters['symbolPathOverride']\r +$testCloudExePathOverride = $OctopusParameters['testCloudExePathOverride']\r +\r +# test cloud user names must be lower case to work around API/Utility issue (until issue is fixed)\r +$testCloudUserName = $testCloudUserName.ToLower()\r +\r +$stepName = $OctopusParameters['MobileAppPackageStepName']\r +\r +# set step path, if not already set\r +If([string]::IsNullOrEmpty($stepPath))\r +{\r + if (![string]::IsNullOrEmpty($stepName)) {\r + Write-Host \"Finding path to package step: $stepName\"\r + $stepPath = Find-InstallLocation $stepName\r + } else {\r + $stepPath = Find-SingleInstallLocation\r + }\r +}\r +\r +Write-Host \"Nuget Package base path : $stepPath\"\r +# Write-Host \"##octopus[stderr-progress]\"\r +\r +# find app\r +\r +# complete search paths, overrides may be blank\r +$appSearchPath = Join-Path $stepPath $appPathOverride\r +$symbolSearchPath = Join-Path $stepPath $symbolPathOverride\r +$dllSearchPath = Join-Path $stepPath $dllPathOverride\r +$testCouldExeSearchPath = Join-Path $stepPath $testCloudExePathOverride\r +\r +$appFileFullPath = (Get-ExactlyOneMobilePackageFileInfo $appSearchPath).FullName\r +$symbolFileFullPath = (Get-OneDsymFileInfoOrNull $symbolSearchPath).FullName\r +$dllDirectoryFullPath = Get-ExactlyOneUITestDllPath $dllSearchPath\r +\r +$testCloudExeFullPath = (Get-ExactlyOneUploadExeFileInfo $testCouldExeSearchPath).FullName\r +\r +# It turns out that the utility exe expects a dsym folder, convert to folder\r +\r +# DIRTY HACKS - the API should accept a *.dSYM.zip like insights does, see\r +# https://testcloud.ideas.aha.io/ideas/XTA-I-50\r +\r +Add-Type -AssemblyName System.IO.Compression.FileSystem\r +\r +function Unzip\r +{\r + param([string]$zipfile, [string]$outpath)\r +\r + [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath)\r +}\r +\r +$symbolFileDirectoryPath = \"\"\r +if($symbolFileFullPath) # has a full zip path\r +{\r + $parentPath = Split-Path -parent $symbolFileFullPath\r + Unzip $symbolFileFullPath $parentPath\r +\r + # get unzipped folder name ending in dSYM\r + $symbolFileDirectoryPath = (Get-ChildItem -Path $parentPath -Recurse -Filter *.dSYM).FullName\r +}\r +elseif ($symbolPathOverride) # no zip, try to find folder\r +{\r + # search for dSYM folder instead\r +\r + $symbolFileDirectorySearchResults = Get-ChildItem -Path $searchPath -Recurse -Filter *.dSYM\r + \r + # if exactly one result\r + if($symbolFileDirectorySearchResults.Count -eq 1)\r + {\r + $symbolFileDirectoryPath = $symbolFileDirectorySearchResults.FullName\r + }\r +}\r +\r +######################\r +# Invoke the request #\r +######################\r + \r +Write-Host \"App path : \" $appFileFullPath\r +Write-Host \"Symbol File path (optional): \" $symbolFileFullPath\r +Write-Host \"Test DLL full path : \" $dllDirectoryFullPath\r +Write-Host \"TestCloud exe path : \" $testCloudExeFullPath\r +Write-Host\r +\r +# run command with optional argument\r +\r +if($symbolFileDirectoryPath) # symbols file present\r +{\r + Write-Host \"Running command: \" \r + Write-Host \"\"\"$testCloudExeFullPath\"\" submit \"\"$appFileFullPath\"\" $apiKey --user $testCloudUserName --devices $devicesCode --series \"\"$series\"\" --locale \"\"$locale\"\" --assembly-dir \"\"$dllDirectoryFullPath\"\" --dsym \"\"$symbolFileDirectoryPath\"\"\"\r + Write-Host \r + cmd /c \"$testCloudExeFullPath\" submit \"$appFileFullPath\" $apiKey --user $testCloudUserName --devices $devicesCode --series \"$series\" --locale \"$locale\" --assembly-dir \"$dllDirectoryFullPath\" --dsym \"$symbolFileDirectoryPath\"\r +}\r +else # no symbols file\r +{\r + Write-Host \"Running command: \" \r + Write-Host \"\"\"$testCloudExeFullPath\"\" submit \"\"$appFileFullPath\"\" $apiKey --user $testCloudUserName --devices $devicesCode --series \"\"$series\"\" --locale \"\"$locale\"\" --assembly-dir \"\"$dllDirectoryFullPath\"\"\"\r + Write-Host\r + cmd /c \"$testCloudExeFullPath\" submit \"$appFileFullPath\" $apiKey --user $testCloudUserName --devices $devicesCode --series \"$series\" --locale \"$locale\" --assembly-dir \"$dllDirectoryFullPath\"\r +}\r +\r +Write-Host\r +Write-Host \"TestCloud upload command complete.\"", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "apiKey", + "Label": "Api Key", + "HelpText": "The app specific API key for your test run. This may be found in the testcloud interface for your application: + +1. Visit the testcloud interface: https://testcloud.xamarin.com/ +2. Choose \"New Test Run\" and configure as desired. +3. In the last step, copy the large hash (app specific API Key), devices parameter code, and username", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Name": "devicesCode", + "Label": "Devices Code", + "HelpText": "The code identifying the devices to test against. + +This may be found in the testcloud interface for your application: + +1. Visit the testcloud interface: https://testcloud.xamarin.com/ +2. Choose \"New Test Run\" and configure as desired. +3. In the last step, copy the large hash (app specific API Key), devices parameter code, and username", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "testCloudUserName", + "Label": "TestCloud user name", + "HelpText": "The TestCloud user name (e.g. email) that should own the test run. This user must have appropriate permissions for test runs for the configured application. + +(Note that this user name will automatically be converted to lower case at this time, to work around an issue with the TestCloud API)", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "PackageStepName", + "Label": "Package Step Name", + "HelpText": "The name of the previously-deployed package step that contains the app files that you want to test.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + } + }, + { + "Name": "series", + "Label": "Series", + "HelpText": "You may specify a test series to run, or use the default of \"master\".", + "DefaultValue": "master", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "locale", + "Label": "Locale", + "HelpText": "You may specify a locate to run the tests under, or use the default of \"en_US\".", + "DefaultValue": "en_US", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "appPathOverride", + "Label": "App path override (optional)", + "HelpText": "This fully optional search path allows you to look for app files in a specific folder path in your nuget file, such as \"bin\\iPhone\". This may be needed in cases when the nuget file has multiple app files in different locations. This path override is a path relative to the root of the nuget file.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "dllPathOverride", + "Label": "UITest DLL path override (optional)", + "HelpText": "This fully optional search path allows you to look for the UITest DLL library files in a specific folder path in your nuget file, such as \"bin\\UITest\\Release\". This may be needed in cases when the nuget file has multiple UITest libraries in different locations. This path override is a path relative to the root of the nuget file.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "symbolPathOverride", + "Label": "Symbol Path Override (optional)", + "HelpText": "!!IMPORTANT!! +A symbols file is NOT REQUIRED for TestCloud. It only provides more information for crashes. + +This optional search path allows you to look in a specific folder path in your nuget file, such as \"bin\\iPhone\", for a *.dSYM.zip symbols file. This path override is a path relative to the root of the nuget file. + +If an override is specified and a .zip file is not found, the script will search for a *.dSYM folder to use instead. This means if you have a dSYM folder and not a zip file, you can specify \".\" as an override to search the entire nuget package for the folder, or specify a more specific search path. + +This may be needed in cases when the nuget file has multiple symbol files in different locations, or when you only have a dSYM folder, not a .zip file available.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "testCloudExePathOverride", + "Label": "test-cloud.exe path override (optional)", + "HelpText": "This fully optional search path allows you to look for the test-cloud.exe utility file in a specific folder path in your nuget file, such as \"tools\". This may be needed in cases when the nuget file has multiple instances of the test-cloud.exe utility in different locations. This path override is a path relative to the root of the nuget file.", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2016-01-18T21:50:44.530+00:00", + "LastModifiedBy": "colin.dabritz@viewpoint.com", + "$Meta": { + "ExportedAt": "2016-01-19T23:14:49.354+00:00", + "OctopusVersion": "2.6.4.951", + "Type": "ActionTemplate" + }, + "Category": "xamarin" +} diff --git a/step-templates/xml-attribute-update.json.human b/step-templates/xml-attribute-update.json.human new file mode 100644 index 000000000..c0766899f --- /dev/null +++ b/step-templates/xml-attribute-update.json.human @@ -0,0 +1,87 @@ +{ + "Id": "170a9b93-96f2-470b-81ca-e0ff53fa7c3d", + "Name": "XML update", + "Description": null, + "ActionType": "Octopus.Script", + "Version": 0, + "Properties": { + "Octopus.Action.Script.ScriptBody": "[xml]$xml = Get-Content $path \r +$ns = new-object Xml.XmlNamespaceManager $xml.NameTable\r +$ns.AddNamespace($nsKey, $nsValue)\r +\r +$xml.SelectNodes($xmlPath, $ns) | % {\r +\tif ($_.key -eq $key)\r +\t{\r +\t\t$_.value = $value\r +\t}\r +}\r +\r +$xml.Save($path)", + "Octopus.Action.Script.Syntax": "PowerShell" + }, + "SensitiveProperties": {}, + "Parameters": [ + { + "Name": "path", + "Label": "XML file path", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "xmlPath", + "Label": "XPath", + "HelpText": "XPath element with namespace //b:setting", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "key", + "Label": "Key", + "HelpText": "Key to change", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "value", + "Label": "Value", + "HelpText": "New value", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "nsKey", + "Label": "NS key", + "HelpText": "Namespace key", + "DefaultValue": "B", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "nsValue", + "Label": "NS Value", + "HelpText": "Namespace value", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedOn": "2015-08-03T13:33:12.919+00:00", + "LastModifiedBy": "pitrew", + "$Meta": { + "ExportedAt": "2015-08-03T13:50:55.041+00:00", + "OctopusVersion": "2.6.5.1010", + "Type": "ActionTemplate" + }, + "Category": "xml" +} diff --git a/step-templates/xml-transform-using-xdt.json.human b/step-templates/xml-transform-using-xdt.json.human new file mode 100644 index 000000000..72d2c046f --- /dev/null +++ b/step-templates/xml-transform-using-xdt.json.human @@ -0,0 +1,135 @@ +{ + "Id": "569a5f1b-bb57-491a-9c4e-a6d6e44e6b26", + "Name": "XML Transform using XDT", + "Description": "You can use this script to easily transform any XML file using XDT. +The script will download its dependencies automatically.", + "ActionType": "Octopus.Script", + "Version": 8, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "function Get-Parameter($Name, $Default, [switch]$Required) {\r + $result = $null\r +\r + if ($OctopusParameters -ne $null) {\r + $result = $OctopusParameters[$Name]\r + }\r +\r + if ($result -eq $null) {\r + if ($Required) {\r + throw \"Missing parameter value $Name\"\r + } else {\r + $result = $Default\r + }\r + }\r +\r + return $result\r +}\r +\r +$toolsDir = Get-Parameter \"toolsDir\" -Required\r +$sourceFile = Get-Parameter \"sourceFile\" -Required\r +$transformFile = Get-Parameter \"transformFile\" -Required\r +$destFile = Get-Parameter \"destFile\" -Required\r +\r +if(!(Test-Path $toolsDir)){\r + New-Item -Path $toolsDir -ItemType Directory\r +}\r +$nugetDestPath = Join-Path -Path $toolsDir -ChildPath nuget.exe\r +if(!(Test-Path $nugetDestPath)){\r + Write-Output 'Downloading nuget.exe'\r +\t# download nuget\r + Invoke-WebRequest 'http://nuget.org/nuget.exe' -OutFile $nugetDestPath\r + # double check that it was written to disk\r + if(!(Test-Path $nugetDestPath)){\r + throw 'unable to download nuget'\r + }\r +}\r +\r +$xdtExe = (Get-ChildItem -Path $toolsDir -Include 'SlowCheetah.Xdt.exe' -Recurse) | Select-Object -First 1\r +\r +if(!$xdtExe){\r + Write-Output 'Downloading xdt since it was not found in the tools folder'\r + # nuget install SlowCheetah.Xdt -Prerelease -OutputDirectory toolsDir\\\r + $nugetInstallCmdArgs = @('install','SlowCheetah.Xdt','-Prerelease','-OutputDirectory',(Resolve-Path $toolsDir).ToString())\r +\r + Write-Output ('Calling nuget.exe to download SlowCheetah.Xdt with the following args: [{0} {1}]' -f $nugetDestPath, ($nugetInstallCmdArgs -join ' '))\r + &($nugetDestPath) $nugetInstallCmdArgs\r +\r + $xdtExe = (Get-ChildItem -Path $toolsDir -Include 'SlowCheetah.Xdt.exe' -Recurse) | Select-Object -First 1\r +\r + if(!$xdtExe){\r + throw ('SlowCheetah.Xdt not found')\r + }\r +\r + # copy the xdt assemlby if the xdt directory is missing it\r + $xdtDllExpectedPath = (Join-Path $xdtExe.Directory.FullName 'Microsoft.Web.XmlTransform.dll')\r +\r + if(!(Test-Path $xdtDllExpectedPath)){\r + # copy the xdt.dll next to the slowcheetah .exe\r + $xdtDll = (Get-ChildItem -Path $toolsDir -Include 'Microsoft.Web.XmlTransform.dll' -Recurse) | Select-Object -First 1\r +\r + if(!$xdtDll){\r +\t\t throw 'Microsoft.Web.XmlTransform.dll not found'\r +\t\t}\r +\r + Copy-Item -Path $xdtDll.Fullname -Destination $xdtDllExpectedPath\r + }\r +}\r +\r +$cmdArgs = @((Resolve-Path $sourceFile).ToString(),\r + (Resolve-Path $transformFile).ToString(),\r + (Resolve-Path $destFile).ToString())\r +\r +Write-Output ('Calling slowcheetah.xdt.exe with the args: [{0} {1}]' -f $xdtExe, ($cmdArgs -join ' '))\r +&($xdtExe) $cmdArgs\r +", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.NuGetFeedId": null, + "Octopus.Action.Package.NuGetPackageId": null + }, + "Parameters": [ + { + "Name": "sourceFile", + "Label": "Source file path", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "transformFile", + "Label": "Transform file path", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "destFile", + "Label": "Destination file path", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Name": "toolsDir", + "Label": "Directory to download tools programs", + "HelpText": null, + "DefaultValue": "C:\\tools", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "LastModifiedBy": "lijunkang", + "$Meta": { + "ExportedAt": "2016-08-24T07:11:46.726+00:00", + "OctopusVersion": "3.3.17", + "Type": "ActionTemplate" + }, + "Category": "xml" +} diff --git a/step-templates/yams-upload.json.human b/step-templates/yams-upload.json.human new file mode 100644 index 000000000..645314596 --- /dev/null +++ b/step-templates/yams-upload.json.human @@ -0,0 +1,104 @@ +{ + "Id": "a1d95c5f-42fb-43b3-8bee-74a255f2ae71", + "Name": "YAMS Uploader", + "Description": "Upload YAMS application. + +[YAMS](https://github.com/Microsoft/Yams) is a library that can be used to deploy and host microservices in the cloud or on premises. This step uses [YAMS Uploader](https://github.com/Applicita/YamsUploader) to publish applications to YAMS cluster.", + "ActionType": "Octopus.Script", + "Version": 1, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.RunOnServer": "false", + "Octopus.Action.Script.ScriptBody": "$yamsUploaderInstallationParameter = \"Octopus.Action[$YamsUploaderStep].Output.Package.InstallationDirectoryPath\" +Write-Host \"Yams Uploader installation path parameter: $yamsUploaderInstallationParameter\" +$applicationInstallationParameter = \"Octopus.Action[$PackageStep].Output.Package.InstallationDirectoryPath\" +Write-Host \"Application Package installation path parameter: $applicationInstallationParameter\" + +$yamsUploader = $OctopusParameters[$yamsUploaderInstallationParameter] + \"\\content\\YamsUploader.exe\" +Write-Host \"Running Yams Uploader: $yamsUploader\" + +$binaries = $OctopusParameters[$applicationInstallationParameter] +Write-Host \"Uploading application: $binaries\" + +& \"$yamsUploader\" -YamsStorage \"$Storage\" -ClusterId \"$ClusterId\" -BinariesPath \"$binaries\" -AppVersion \"$AppVersion\" -AppId \"$AppId\"", + "Octopus.Action.Script.ScriptFileName": null, + "Octopus.Action.Package.FeedId": null, + "Octopus.Action.Package.PackageId": null + }, + "Parameters": [ + { + "Id": "b3c01e47-0141-4756-bbf5-d43069a8a30b", + "Name": "YamsUploaderStep", + "Label": "Yams Uploader Step", + "HelpText": "The step that installed Applicita.YamsUploader package", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "StepName" + }, + "Links": {} + }, + { + "Id": "605e9675-50b0-4c84-86a0-897c9c2d9124", + "Name": "PackageStep", + "Label": "Package Step", + "HelpText": "The step that installed Application package", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "StepName" + }, + "Links": {} + }, + { + "Id": "d1b26565-cd68-45df-bf14-0b68724a0e29", + "Name": "AppId", + "Label": "Application Id", + "HelpText": "YAMS Application Id", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "cc6d856a-a8d8-4621-81f0-c8f2063e5dc0", + "Name": "AppVersion", + "Label": "Application Version", + "HelpText": "SemVer version of the application", + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "c582ccd6-4f00-4362-ace7-dffe5ed23bae", + "Name": "ClusterId", + "Label": "Cluster Id", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "1d8eb780-e240-4a65-bcdc-9df1c8bac7cb", + "Name": "Storage", + "Label": "Storage Connection String", + "HelpText": "Connection string for YAMS storage account", + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "jkonecki", + "$Meta": { + "ExportedAt": "2016-11-24T18:45:19.864Z", + "OctopusVersion": "3.5.1", + "Type": "ActionTemplate" + }, + "Category": "other" +} diff --git a/step-templates/zabbix-node-api-maintenance.json.human b/step-templates/zabbix-node-api-maintenance.json.human new file mode 100644 index 000000000..e01164b21 --- /dev/null +++ b/step-templates/zabbix-node-api-maintenance.json.human @@ -0,0 +1,250 @@ +{ + "Id": "2fdc47cd-d120-4919-b0d5-2ed22ca8ff62", + "Name": "Zabbix API maintenance", + "Description": "This step template adds single host on Zabbix maintenance.", + "ActionType": "Octopus.Script", + "Version": 12, + "Properties": { + "Octopus.Action.Script.Syntax": "PowerShell", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.ScriptBody": "$Zserver=\"#{zserver}\" +$Zuser=\"#{zuser}\" +$Zpassword=\"#{zpass}\" +$Zhost=\"#{zhost}\" +$setgmt=#{gmt} +$hours=#{hours} +$action=\"#{action}\" +$number=\"#{number}\" + +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; + +function Get-Auth{ + param( + $server, + $user, + $pass, + $url + ) + $body='{\"jsonrpc\": \"2.0\", \"method\": \"user.login\", \"params\": {\"user\": \"'+\"$user\"+'\", \"password\": \"'+\"$pass\"+'\"}, \"id\": 1, \"auth\": null}' + try { + $key=Invoke-WebRequest -Uri \"$url\" -ContentType application/json-rpc -Body $body -Method Put -UseBasicParsing + } catch [Exception] { + Write-Error \"Error: cannot connect to zabbix server ($($_.Exception.Message)), check hostname/url! Frequently zabbix is installed on a virtual folder like {hostname}/zabbix, please include the folder into the hostname variable.`r`n\" -ErrorAction Stop + } + $token=($key.Content | ConvertFrom-Json).result + return $token +} + +function Remove-Maintenance{ + param( + $srvr, + $usr, + $pswd, + $uri, + $mname + ) + $remove='{\"jsonrpc\": \"2.0\", \"method\": \"maintenance.get\", \"params\": {\"output\": \"extend\", \"selectHosts\": \"extend\", \"selectTimeperiods\": \"extend\"},\"auth\": \"'+\"$auth\"+'\",\"id\": 1}' + + $maintenace=Invoke-WebRequest -Uri \"$uri\" -ContentType application/json-rpc -Body $remove -Method Put -UseBasicParsing + $select= ($maintenace.Content | ConvertFrom-Json).result | where{$_.name -like \"$mname\"} + $id=$select.maintenanceid + if($id){ + Write-Output \"Remove maintenance ID: $id\" + $rmv='{\"jsonrpc\": \"2.0\", \"method\": \"maintenance.delete\", \"params\": [\"'+\"$id\"+'\"], \"auth\": \"'+\"$auth\"+'\",\"id\": 1}' + $actionremove=Invoke-WebRequest -Uri \"$uri\" -ContentType application/json-rpc -Body $rmv -Method Put -UseBasicParsing + $check=(($actionremove.Content | ConvertFrom-Json).result).maintenanceids + if($check -like $id){ + Write-Output \"Maintenance $id removed\" + } + else{ + Write-Error \"Something wrong. Please contact your system administrator\" + } + } + else{ + Write-Error \"NO Maintenance ID - contact your system administrator\" + } +} + +###GLOBAL VARIABLES### +if (!$Zserver.StartsWith(\"http\")) { $Zserver=\"http://$Zserver\" } +$Zurl=\"$Zserver/api_jsonrpc.php\" +$maintenancename=\"Octo-$number-$Zhost\" + +###GET AUTH FROM ZABBIX SERVER### +$auth=Get-Auth -server $Zserver -user $Zuser -pass $Zpassword -url $Zurl +if ($auth -eq $null) { + Write-Error \"Authentication failure for user $Zuser on server $Zserver!\" -ErrorAction Stop + exit +} + +###GET HOST ID### +$content='{\"jsonrpc\": \"2.0\", \"method\": \"host.get\", \"params\": {\"output\": \"extend\", \"filter\": {\"host\": \"'+\"$Zhost\"+'\"}},\"auth\": \"'+\"$auth\"+'\",\"id\": 1}' +$zabbixhost=Invoke-WebRequest -Uri \"$Zurl\" -ContentType application/json-rpc -Body $content -Method Put -UseBasicParsing +$nameserver=$zabbixhost.Content | ConvertFrom-Json +$hostid=$nameserver.result.hostid +if($hostid){ + Write-Output \"Host $Zhost found with ID: $hostid\" +} +else{ + Write-Error \"Host $Zhost not found, or user not authorized for this host - please contact your system administrator!\" + exit +} + +###ADD NEW MAINTENANCE### +if ($action -eq \"create\"){ + ###REMOVE MAINTENANCE IF ALREADY EXISTS WITH THE SAME NAME### + $remove='{\"jsonrpc\": \"2.0\", \"method\": \"maintenance.get\", \"params\": {\"output\": \"extend\", \"selectHosts\": \"extend\", \"selectTimeperiods\": \"extend\"},\"auth\": \"'+\"$auth\"+'\",\"id\": 1}' + $maintenace=Invoke-WebRequest -Uri \"$Zurl\" -ContentType application/json-rpc -Body $remove -Method Put -UseBasicParsing + + $select= ($maintenace.Content | ConvertFrom-Json).result | where{$_.name -like \"$maintenancename\"} + if(!$select){ + Write-Output \"No maintenance with the same name is already registered\" + } + else{ + Remove-Maintenance -srvr $Zserver -usr $Zuser -pswd $Zpassword -uri $Zurl -mname $maintenancename + } + + ###START TO CREATE NEW MAINTENANCE### + $since=[int][double]::Parse((get-date -UFormat %s)) + $till=0 + + ###ATTENTION ON GMT - THIS WORK FOR ITALIAN ZONE AND TAKES DAYLIGHT SAVINGSTIME FROM### + ###start check your ZABBIX configuration### + $workdate=(Get-Date) + if (![int32]::TryParse($setgmt, [ref] $gmt)) { $gmt=([TimeZoneInfo]::Local.BaseUtcOffset).Hours } + if ($workdate.IsDaylightSavingTime()) { $gmt+=1 } + + $min=$workdate.AddHours(-$gmt).Minute + $h=$workdate.AddHours(-$gmt).Hour + $minutetoseconds=$min*=60 + $hourstoseconds=$h*=3600 + $starttime=$minutetoseconds+=$hourstoseconds + $seconds=$hours*=3600 + + $sincesum=$since + $till=$sincesum+=$seconds + $since=$since-=(60*60*$gmt) + $till=$till-=(60*60*$gmt) + + ###stop check your ZABBIX configuration### + $add='{\"jsonrpc\": \"2.0\", \"method\": \"maintenance.create\", \"params\": {\"name\": \"'+\"$maintenancename\"+'\", \"active_since\": \"'+\"$since\"+'\", \"active_till\": '+\"$till\"+', \"hostids\": [\"'+$hostid+'\"], \"timeperiods\": [{\"timeperiod_type\": 0, \"start_time\": '+$starttime+', \"period\": '+$seconds+'}]}, \"auth\": \"'+$auth+'\", \"id\": 1}' + $maintenance=Invoke-WebRequest -Uri \"$Zurl\" -ContentType application/json-rpc -Body $add -Method Put -UseBasicParsing + $check=(($maintenance.Content | ConvertFrom-Json).result).maintenanceids + if($check){ + Write-Output \"Maintenance $check created\" + } + else{ + Write-Error \"Something wrong. Please contact your system administrator\" + } +} +else{ + if($action -eq \"remove\"){ + Remove-Maintenance -srvr $Zserver -usr $Zuser -pswd $Zpassword -uri $Zurl -mname $maintenancename + } + else{ + Write-Error \"Action $action not possible\" + } +}" + }, + "Parameters": [ + { + "Id": "2d679eba-f403-4548-a57d-dacacfb5b299", + "Name": "zserver", + "Label": "Zabbix Server", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "898bbce8-28d3-43d1-9d19-a659ffce3865", + "Name": "zuser", + "Label": "Zabbix Username", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "86e4c608-e37f-4a0a-a432-e1c07b8f2b6b", + "Name": "zpass", + "Label": "Zabbix Password", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + }, + "Links": {} + }, + { + "Id": "1f51ac2a-852a-4f88-be40-a1304eb11cb2", + "Name": "zhost", + "Label": "Host", + "HelpText": "host in maintenance - single host", + "DefaultValue": "#{Octopus.Machine.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "50e5fbdd-949e-4ce8-8a10-94a403c3fb2d", + "Name": "action", + "Label": "", + "HelpText": null, + "DefaultValue": null, + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "create|Create maintenance +remove|Remove maintenance" + }, + "Links": {} + }, + { + "Id": "b1ef0d32-ec8e-44f3-a7f4-a34af6cd252c", + "Name": "gmt", + "Label": "GMT", + "HelpText": "This option depends on Zabbix server configuration. +If it is not necessary, set GMT=0 (UTC). +If UTC+2, insert 2. If UTC-4, insert -4.", + "DefaultValue": "0", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "6fa1f56d-b9ee-4830-9e88-a434c6b82774", + "Name": "hours", + "Label": "Maintenance Period", + "HelpText": "in hours", + "DefaultValue": "4", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + }, + { + "Id": "ca825d16-f0ff-4bcc-b0e7-abcde1f2188e", + "Name": "number", + "Label": "ID for maintenance name", + "HelpText": "maintenance name: Octo-[number]-[host]", + "DefaultValue": "#{Octopus.Release.Number}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Links": {} + } + ], + "LastModifiedBy": "dpijl10", + "$Meta": { + "ExportedAt": "2018-04-11T11:33:40.330Z", + "OctopusVersion": "2018.2.7", + "Type": "ActionTemplate" + }, + "Category": "zabbix" +}