Octopus.Script exported 2023-06-01 by twerthi belongs to ‘SQL Server’ category.
Calls SqlPackage commands such as:
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.
- SqlCmdVariable.Variable1
- my.sqlcmdvariable.variable2
NOTE:
- Requires version 2019.10 or above.
TrustServerCertificate=true
is set by default- Requires PowerShell or *PowerShell Core
Parameters
When steps based on the template are included in a project’s deployment process, the parameters below can be set.
DACPACPackageName
DACPACPackageName =
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
Publish profile file name
DACPACPublishProfile =
Searches the package for the specified file name. To use a specific folder, use the relative location ex: publish/publish-profile.xml
Report
DACPACReport = True
Whether a deployment report should be generated and loaded into OctopusDeploy as an artifact
Script
DACPACScript = True
Whether a deploy script should be generated and loaded into OctopusDeploy as an artifact
Deploy
DACPACDeploy = True
Whether a deployment of the dacpac should occur
Target Servername
DACPACTargetServer =
Name of the server to target this deployment against
Target Database
DACPACTargetDatabase =
Name of the database to target this deployment against
Authentication type
DACPACAuthenticationType =
Select the method to authenticate to the SQL Server.
Username
DACPACSQLUsername =
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}
Password
DACPACSQLPassword =
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}
DACPAC Package
DACPACPackage =
The package containing the .dacpac
file from the specified repository.
Command Timeout
DACPACCommandTimeout =
Override the default command timeout for longer-running scripts.
SqlPackage executable location
DACPACExeLocation =
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
Additional arguments
DACPACAdditionalArguments =
A comma-delimited list of additional arguments to add to the SqlPackage command.
Script body
Steps based on this template will execute the following PowerShell script.
# 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)
{
"Win32NT"
{
# Set variable
$IsWindows = $true
$IsLinux = $false
}
"Unix"
{
$IsWindows = $false
$IsLinux = $true
}
}
}
if ($IsWindows)
{
Write-Host "Detected OS is Windows"
$ProgressPreference = 'SilentlyContinue'
}
else
{
Write-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)
{
"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"
{
Write-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
}
"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)
}
$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
{
# Get all SqlCmdVariables
$sqlCmdVariables = $OctopusParameters.Keys -imatch "SqlCmdVariable.*"
$argumentList = @()
# Check to see if something is there
if ($null -ne $sqlCmdVariables)
{
Write-Host "Adding SqlCmdVariables ..."
# Loop through the variable collection
foreach ($sqlCmdVariable in $sqlCmdVariables)
{
# 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
{
# Define parameters
param (
$AdditionalArguments
)
# Define local variables
$argumentsToAdd = @()
# Check for emmpty or null
if (![string]::IsNullOrWhitespace($AdditionalArguments))
{
# Split the arguments
$argumentsToAdd += $AdditionalArguments.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries).Trim()
}
# Return list
return $argumentsToAdd
}
function Get-SqlPackage
{
# Define local variables
$workFolder = $OctopusParameters['Octopus.Action.Package[DACPACPackage].ExtractedPath']
$downloadUrl = ""
# 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)
{
# Set url
$downloadUrl = "https://aka.ms/sqlpackage-windows"
}
if ($IsLinux)
{
# Set url
$downloadUrl = "https://aka.ms/sqlpackage-linux"
}
# Download sql package
if ($PSVersionTable.PSVersion.Major -ge 6)
{
# Download
Invoke-WebRequest -Uri $downloadUrl -OutFile "$workFolder/sqlpackage/sqlpackage.zip"
}
else
{
Invoke-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)
{
& 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
{
# 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)
{
$token = Invoke-RestMethod -Method GET -Uri $tokenUrl -Headers @{"MetaData" = "true"}
}
else
{
$token = Invoke-RestMethod -Method GET -Uri $tokenUrl -Headers @{"MetaData" = "true"} -UseBasicParsing
}
# Return the token
return $token.access_token
}
function Invoke-SqlPackage
{
# Define parameters
param (
$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
# Check exit code
if ($lastExitCode -ne 0)
{
# Fail the step
Write-Error "Execution failed!"
}
}
function Validate-Folder
{
# Define parameters
param (
$TestPath
)
# Check for folder
if ((Test-Path -Path $TestPath) -eq $false)
{
# Create the folder
New-Item -Path "$TestPath" -ItemType "directory"
}
}
Function Remove-InvalidFileNameChars {
Param(
[string]$FileName
)
[IO.Path]::GetinvalidFileNameChars() | ForEach-Object { $FileName = $FileName.Replace($_, "_") }
Return $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
{
# Declare working variables
$sqlPackageArguments = @()
# Build arugment list
$sqlPackageArguments += "/SourceFile:`"$DACPACPackagePath`""
$sqlPackageArguments += "/TargetConnectionString:`"$(Get-ConnectionString -ServerName $DACPACTargetServer -Database $DACPACTargetDatabase -UserName $UserName -Password $Password -AuthenticationType $AuthenticationType)`""
# Check to see if a publish profile was designated
If ($PublishProfile){
$profileFolderName = [System.IO.Path]::GetDirectoryName($PublishProfile)
$profileFileName = [System.IO.Path]::GetFileName($PublishProfile)
$PublishProfilePath = Get-DacFilePath -BasePath ($ExtractPath + ([IO.Path]::DirectorySeparatorChar) + $profileFolderName) -FileName $profileFileName -FileType "PublishProfile"
# Add to arguments
$sqlPackageArguments += "/Profile:`"$PublishProfilePath`""
}
# Check to see if it's using managed identity
if ($authenticationType -eq "AzureADManaged")
{
# Add access token
$Password = Get-ManagedIdentityToken
$sqlPackageArguments += "/AccessToken:$Password"
}
# Add sqlcmd variables
$sqlPackageArguments += Add-SqlCmdVariables
# Add addtional arguments
$sqlPackageArguments += Add-AdditionalArguments -AdditionalArguments $DACPACAdditionalArguments
# Check to see if command timeout was specified
if (![string]::IsNullOrWhitespace($DACPACCommandTimeout))
{
# Add timeout parameter
$sqlPackageArguments += "/Properties:CommandTimeout=$DACPACCommandTimeout"
}
# Check to see if sqlpackage needs to be downloaded
if ([string]::IsNullOrWhitespace($DACPACExeLocation))
{
# Download and extract sqlpackage
Get-SqlPackage
}
else
{
# 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)
{
$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))
{
# Upload artifact
New-OctopusArtifact $item.FullName
}
}
if ($DACPACScript)
{
$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))
{
# Upload artifact
New-OctopusArtifact $item.FullName
}
}
if ($DACPACDeploy)
{
# Execute action
Invoke-SqlPackage -Action "Publish" -Arguments $sqlPackageArguments
}
}
catch
{
Write-Host $_.Exception.ToString()
throw;
}
Provided under the Apache License version 2.0.
To use this template in Octopus Deploy, copy the JSON below and paste it into the Library → Step templates → Import dialog.
{
"Id": "c323cbcd-aab8-4229-b07c-e6c26f7e9a8a",
"Name": "SQL - Deploy DACPAC using SqlPackage",
"Description": "Calls SqlPackage commands such as:\n * [Deploy](https://learn.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage-publish?view=sql-server-ver16)\n * [Script](https://learn.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage-script?view=sql-server-ver16)\n * [DeployReport](https://learn.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage-deploy-drift-report?view=sql-server-ver16)\n\nAs SqlPackage is cross-platform, this template is both Windows and Linux* compatible.\n\nResults 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.\n\nSqlCmd variables are now supported. To specify SqlCmd variables, create your Octopus variable with the following naming convention: SqlCmdVariable.<Variable name> (case insensitive) and then assign it a value. Examples:\n* SqlCmdVariable.Variable1\n* my.sqlcmdvariable.variable2\n\nNOTE: \n - Requires version 2019.10 or above. \n - `TrustServerCertificate=true` is set by default\n - Requires PowerShell or *PowerShell Core",
"Version": 6,
"ExportedAt": "2023-06-01T22:27:25.564Z",
"ActionType": "Octopus.Script",
"Author": "twerthi",
"Packages": [
{
"Id": "edff7d94-0feb-48a9-8185-48feb084a94f",
"Name": "DACPACPackage",
"PackageId": null,
"FeedId": null,
"AcquisitionLocation": "Server",
"Properties": {
"Extract": "True",
"SelectionMode": "deferred",
"PackageParameterName": "DACPACPackage"
}
}
],
"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\nWindowsIntegrated|Windows Integrated\nAzureADManaged|Azure Active Directory Managed Identity\nAzureADPassword|Azure Active Directory Username/Password\nAzureADIntegrated|Azure Active Directory Integrated\nAzureADServicePrincipal|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.\n\nIf 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.\n\nIf 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.<br />\nExamples:<br />\nEmbedded within the package:`#{Octopus.Action.Package[DACPACPackage].ExtractedPath}/MySubFolder`<br />\nOn 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"
}
}
],
"Properties": {
"Octopus.Action.Script.Syntax": "PowerShell",
"Octopus.Action.Script.ScriptBody": "# Set TLS\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls12\n\nWrite-Host \"Determining Operating System...\"\n# Check to see if $IsWindows is available\nif ($null -eq $IsWindows)\n{\n switch ([System.Environment]::OSVersion.Platform)\n {\n \t\"Win32NT\"\n {\n \t# Set variable\n $IsWindows = $true\n $IsLinux = $false\n }\n \"Unix\"\n {\n \t$IsWindows = $false\n $IsLinux = $true\n }\n }\n}\n\nif ($IsWindows)\n{\n\tWrite-Host \"Detected OS is Windows\"\n $ProgressPreference = 'SilentlyContinue'\n}\nelse\n{\n\tWrite-Host \"Detected OS is Linux\"\n}\n\n<#\n .SYNOPSIS\n Finds the DAC File that you specify\n\n .DESCRIPTION\n Looks through the supplied PathList array and searches for the file you specify. It will return the first one that it finds.\n\n .PARAMETER FileName\n Name of the file you are looking for\n\n .PARAMETER PathList\n Array of Paths to search through.\n\n .EXAMPLE\n Find-DacFile -FileName \"Microsoft.SqlServer.TransactSql.ScriptDom.dll\" -PathList @(\"${env:ProgramFiles}\\Microsoft SQL Server\", \"${env:ProgramFiles(x86)}\\Microsoft SQL Server\")\n#>\nFunction Find-DacFile {\n Param(\n [Parameter(Mandatory=$true)]\n [string]$FileName,\n [Parameter(Mandatory=$true)]\n [string[]]$PathList\n )\n\n $File = $null\n\n ForEach($Path in $PathList)\n {\n Write-Debug (\"Searching: {0}\" -f $Path)\n\n If (!($File))\n {\n $File = (\n Get-ChildItem $Path -ErrorAction SilentlyContinue -Filter $FileName -Recurse |\n Sort-Object FullName -Descending |\n Select-Object -First 1\n )\n\n If ($File)\n {\n Write-Debug (\"Found: {0}\" -f $File.FullName)\n }\n }\n }\n\n Return $File\n}\n\n\n<#\n .SYNOPSIS\n Generates a connection string\n\n .DESCRIPTION\n Derive a connection string from the supplied variables\n\n .PARAMETER ServerName\n Name of the server to connect to\n\n .PARAMETER Database\n Name of the database to connect to\n\n .PARAMETER UseIntegratedSecurity\n Boolean value to indicate if Integrated Security should be used or not\n\n .PARAMETER UserName\n User name to use if we are not using integrated security\n\n .PASSWORD Password\n Password to use if we are not using integrated security\n\n .PARAMETER EnableMultiSubnetFailover\n Flag as to whether we should enable multi subnet failover\n\n .EXAMPLE\n Get-ConnectionString -ServerName localhost -UseIntegratedSecurity -Database OctopusDeploy\n\n .EXAMPLE\n Get-ConnectionString -ServerName localhost -UserName sa -Password ProbablyNotSecure -Database OctopusDeploy\n#>\nFunction Get-ConnectionString {\n Param(\n [Parameter(Mandatory=$True)]\n [string]$ServerName,\n [string]$UserName,\n [string]$Password,\n [string]$Database,\n [string]$AuthenticationType\n )\n\n $ApplicationName = \"OctopusDeploy\"\n $connectionString = (\"Application Name={0};Server={1}\" -f $ApplicationName, $ServerName)\n\n switch ($AuthenticationType)\n {\n \t\"AzureADPassword\"\n {\n Write-Verbose \"Using Azure Active Directory username and password\"\n $connectionString += (\";Authentication='Active Directory Password';Uid={0};Pwd={1}\" -f $UserName, $Password) \n break\n }\n \"AzureADIntegrated\"\n {\n Write-Verbose \"Using Azure Active Directory integrated\"\n $connectionString += (\";Authentication='Active Directory Integrated'\") \n break\n }\n \"AzureADManaged\"\n {\n \tWrite-Verbose \"Using Azure Active Directory managed identity\"\n break\n }\n \"AzureADServicePrincipal\"\n {\n Write-Verbose \"Using Azure Active Directory username and password\"\n $connectionString += (\";Authentication='ActiveDirectoryServicePrincipal';Uid={0};Pwd={1}\" -f $UserName, $Password) \n break \t\n }\n \"SqlAuthentication\"\n {\n Write-Verbose \"Using SQL Authentication username and password\"\n $connectionString += (\";Uid={0};Pwd={1}\" -f $UserName, $Password) \n break \n }\n \"WindowsIntegrated\"\n {\n Write-Verbose \"Using integrated security\"\n $connectionString += \";Trusted_Connection=True\"\n break\n }\n }\n \n if ($EnableMultiSubnetFailover)\n {\n Write-Verbose \"Enabling multi subnet failover\"\n $connectionString += \";MultisubnetFailover=True\"\n }\n\n If ($Database)\n {\n $connectionString += (\";Initial Catalog={0}\" -f $Database)\n }\n\n\t$connectionString += \";TrustServerCertificate=true;\"\n\n Return $connectionString\n}\n\n<#\n .SYNOPSIS\n Will find the full path of a given filename (For dacpac or publish profile)\n .DESCRIPTION\n Will search through an extracted package folder provided as the BasePath and hunt for any matches for the given filename.\n .PARAMETER BasePath\n String value of the root folder to begine the recursive search.\n .PARAMETER FileName\n String value of the name of the file to search for.\n .PARAMETER FileType\n String value of \"DacPac\" or \"PublishProfile\" to identify the type of file to search for.\n .EXAMPLE\n Get-DacFilePath -BasePath $ExtractPath -FileName $DACPACPackageName -FileType \"DacPac\"\n#>\nfunction Get-DacFilePath {\n [cmdletbinding()]\n param(\n [parameter(Mandatory=$true)]\n [string]$BasePath,\n\n [parameter(Mandatory=$true)]\n [string]$FileName,\n\n [parameter(Mandatory=$true)]\n [ValidateSet(\"DacPac\",\"PublishProfile\")]\n [string]$FileType\n )\n\n # Add file extension for a dacpac if it's missing\n if($FileName.Split(\".\")[-1] -ne \"dacpac\" -and $FileType -eq \"DacPac\"){\n $FileName = \"$FileName.dacpac\"\n }\n\n Write-Verbose \"Looking for $FileType $FileName in $BasePath.\"\n\n $filePath = (Get-ChildItem -Path $BasePath -Recurse -Filter $FileName).FullName\n\n if(@($filePath).Length -gt 1){\n Write-Warning \"Found $(@($filePath).Length) instances of $FileName. Using $($filePath[0]).\"\n Write-Warning \"Multiple paths for $FileName`: $(@($filePath) -join \"; \")\"\n $filePath = $filePath[0]\n }\n elseif(@($filePath).Length -lt 1 -or $null -eq $filePath){\n Throw \"Could not find $FileName.\"\n }\n\n return $filePath\n}\n\nfunction Add-SqlCmdVariables\n{\n\t# Get all SqlCmdVariables\n $sqlCmdVariables = $OctopusParameters.Keys -imatch \"SqlCmdVariable.*\"\n $argumentList = @()\n \n\t# Check to see if something is there\n\tif ($null -ne $sqlCmdVariables)\n {\n \tWrite-Host \"Adding SqlCmdVariables ...\"\n \n\t\t# Loop through the variable collection\n foreach ($sqlCmdVariable in $sqlCmdVariables)\n {\n \t# Add variable to the deploy options\n $sqlCmdVariableKey = $sqlCmdVariable.Substring(($sqlCmdVariable.ToLower().IndexOf(\"sqlcmdvariable.\") + \"sqlcmdvariable.\".Length))\n \n Write-Host \"Adding variable: $sqlCmdVariableKey with value: $($OctopusParameters[$sqlCmdVariable])\"\n \n $argumentList += (\"/variables:{0}={1}\" -f $sqlCmdVariableKey, $OctopusParameters[$sqlCmdVariable])\n }\n }\n \n # return the list of variables\n return $argumentList\n}\n\nfunction Add-AdditionalArguments\n{\n\t# Define parameters\n param (\n \t$AdditionalArguments\n )\n \n # Define local variables\n $argumentsToAdd = @()\n \n # Check for emmpty or null\n if (![string]::IsNullOrWhitespace($AdditionalArguments))\n {\n \t# Split the arguments\n \t$argumentsToAdd += $AdditionalArguments.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries).Trim()\n }\n \n # Return list\n return $argumentsToAdd\n}\n\nfunction Get-SqlPackage\n{\n\t# Define local variables\n $workFolder = $OctopusParameters['Octopus.Action.Package[DACPACPackage].ExtractedPath']\n $downloadUrl = \"\"\n\n\t# Check to see if a folder needs to be created\n if((Test-Path -Path \"$workFolder/sqlpackage\") -eq $false)\n {\n # Create new folder\n New-Item -ItemType Directory -Path \"$workFolder/sqlpackage\"\n }\n \n Write-Host \"Downloading SqlPackage ...\"\n \n if ($IsWindows)\n {\n \t# Set url\n $downloadUrl = \"https://aka.ms/sqlpackage-windows\"\n }\n \n if ($IsLinux)\n {\n \t# Set url\n $downloadUrl = \"https://aka.ms/sqlpackage-linux\"\n }\n \n # Download sql package\n if ($PSVersionTable.PSVersion.Major -ge 6)\n {\n \t# Download\n Invoke-WebRequest -Uri $downloadUrl -OutFile \"$workFolder/sqlpackage/sqlpackage.zip\"\n }\n else\n {\n \tInvoke-WebRequest -Uri $downloadUrl -OutFile \"$workFolder/sqlpackage/sqlpackage.zip\" -UseBasicParsing\n }\n \n # Expand the archive\n Write-Host \"Extracting .zip ...\"\n Expand-Archive -Path \"$workFolder/sqlpackage/sqlpackage.zip\" -DestinationPath \"$workFolder/sqlpackage\"\n \n # Add to PATH\n $env:PATH = \"$workFolder/sqlpackage$([IO.Path]::PathSeparator)\" + $env:PATH\n \n # Make it executable\n if ($IsLinux)\n {\n \t& chmod a+x \"$workFolder/sqlpackage/sqlpackage\"\n }\n}\n\nFunction Format-OctopusArgument {\n\n Param(\n [string]$Value\n )\n\n $Value = $Value.Trim()\n\n # There must be a better way to do this\n Switch -Wildcard ($Value){\n\n \"True\" { Return $True }\n \"False\" { Return $False }\n \"#{*}\" { Return $null }\n Default { Return $Value }\n }\n}\n\nFunction Get-ManagedIdentityToken\n{\n\t# Get the identity token\n Write-Host \"Getting Azure Managed Identity token ...\"\n $token = $null\n $tokenUrl = \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fdatabase.windows.net%2F\"\n \n if ($PSVersionTable.PSVersion.Major -ge 6)\n {\n \t$token = Invoke-RestMethod -Method GET -Uri $tokenUrl -Headers @{\"MetaData\" = \"true\"}\n }\n else\n {\n \t$token = Invoke-RestMethod -Method GET -Uri $tokenUrl -Headers @{\"MetaData\" = \"true\"} -UseBasicParsing\n }\n \n # Return the token\n return $token.access_token\n}\n\nfunction Invoke-SqlPackage\n{\n\t# Define parameters\n param (\n \t$Action,\n $Arguments\n )\n \n # Add the action\n $Arguments += \"/Action:$Action\"\n\n # Display what's going to be run\n if (![string]::IsNullOrWhitespace($Password))\n {\n $displayArguments = $Arguments.PSObject.Copy()\n for ($i = 0; $i -lt $displayArguments.Count; $i++)\n {\n if ($null -ne $displayArguments[$i])\n {\n if ($displayArguments[$i].Contains($Password))\n {\n $DisplayArguments[$i] = $displayArguments[$i].Replace($Password, \"****\")\n }\n }\n }\n\n Write-Host \"Executing the following command: sqlpackage $displayArguments\"\n }\n else \n {\n Write-Host \"Executing the following command: sqlpackage $Arguments\"\n } \n \n & sqlpackage $Arguments\n\n\t# Check exit code\n\tif ($lastExitCode -ne 0)\n\t{\n\t\t# Fail the step\n \tWrite-Error \"Execution failed!\"\n\t}\n}\n\nfunction Validate-Folder\n{\n\t# Define parameters\n param (\n \t$TestPath\n )\n \n # Check for folder\n if ((Test-Path -Path $TestPath) -eq $false)\n {\n \t# Create the folder\n New-Item -Path \"$TestPath\" -ItemType \"directory\"\n }\n}\n\nFunction Remove-InvalidFileNameChars {\n\n\tParam(\n\t\t[string]$FileName\n\t)\n\n\t[IO.Path]::GetinvalidFileNameChars() | ForEach-Object { $FileName = $FileName.Replace($_, \"_\") }\n\tReturn $FileName\n}\n\n# Get the supplied parameters\n$PublishProfile = $OctopusParameters[\"DACPACPublishProfile\"]\n$DACPACReport = Format-OctopusArgument -Value $OctopusParameters[\"DACPACReport\"]\n$DACPACScript = Format-OctopusArgument -Value $OctopusParameters[\"DACPACScript\"]\n$DACPACDeploy = Format-OctopusArgument -Value $OctopusParameters[\"DACPACDeploy\"]\n$DACPACTargetServer = $OctopusParameters[\"DACPACTargetServer\"]\n$DACPACTargetDatabase = $OctopusParameters[\"DACPACTargetDatabase\"]\n$DACPACAdditionalArguments = $OctopusParameters[\"DACPACAdditionalArguments\"]\n$DACPACExeLocation = $OctopusParameters[\"DACPACExeLocation\"]\n$DACPACDateTime = ((Get-Date).ToUniversalTime().ToString(\"yyyyMMddHHmmss\"))\n\n$Username = $OctopusParameters[\"DACPACSQLUsername\"]\n$Password = $OctopusParameters[\"DACPACSQLPassword\"]\n$PackageReferenceName = \"DACPACPackage\"\n\n$authenticationType = $OctopusParameters[\"DACPACAuthenticationType\"]\n\n$ExtractPathKey = (\"Octopus.Action.Package[{0}].ExtractedPath\" -f $PackageReferenceName)\n$ExtractPath = $OctopusParameters[$ExtractPathKey]\n\nif(!(Test-Path $ExtractPath)) {\n Throw (\"The package extraction folder '{0}' does not exist or the Octopus Tentacle does not have permission to access it.\" -f $ExtractPath)\n}\n\n# Get the DACPAC location\n$dacpacFolderName = [System.IO.Path]::GetDirectoryName($DACPACPackageName)\n$dacpacFileName = [System.IO.Path]::GetFileName($DACPACPackageName)\n$DACPACPackagePath = Get-DacFilePath -BasePath ($ExtractPath + ([IO.Path]::DirectorySeparatorChar) + $dacpacFolderName) -FileName $dacpacFileName -FileType \"DacPac\"\n\n# Invoke the DacPac utility\ntry\n{\n\t# Declare working variables\n $sqlPackageArguments = @()\n \n # Build arugment list\n $sqlPackageArguments += \"/SourceFile:`\"$DACPACPackagePath`\"\"\n $sqlPackageArguments += \"/TargetConnectionString:`\"$(Get-ConnectionString -ServerName $DACPACTargetServer -Database $DACPACTargetDatabase -UserName $UserName -Password $Password -AuthenticationType $AuthenticationType)`\"\"\n \n\t# Check to see if a publish profile was designated\n\tIf ($PublishProfile){\n \t$profileFolderName = [System.IO.Path]::GetDirectoryName($PublishProfile)\n $profileFileName = [System.IO.Path]::GetFileName($PublishProfile)\n \t$PublishProfilePath = Get-DacFilePath -BasePath ($ExtractPath + ([IO.Path]::DirectorySeparatorChar) + $profileFolderName) -FileName $profileFileName -FileType \"PublishProfile\"\n \n \t# Add to arguments\n \t$sqlPackageArguments += \"/Profile:`\"$PublishProfilePath`\"\"\n\t} \n \n # Check to see if it's using managed identity\n if ($authenticationType -eq \"AzureADManaged\")\n {\n \t# Add access token\n $Password = Get-ManagedIdentityToken\n $sqlPackageArguments += \"/AccessToken:$Password\"\n }\n \n # Add sqlcmd variables\n $sqlPackageArguments += Add-SqlCmdVariables\n \n\t# Add addtional arguments\n $sqlPackageArguments += Add-AdditionalArguments -AdditionalArguments $DACPACAdditionalArguments\n \n # Check to see if command timeout was specified\n if (![string]::IsNullOrWhitespace($DACPACCommandTimeout))\n {\n \t# Add timeout parameter\n $sqlPackageArguments += \"/Properties:CommandTimeout=$DACPACCommandTimeout\"\n }\n \n # Check to see if sqlpackage needs to be downloaded\n if ([string]::IsNullOrWhitespace($DACPACExeLocation))\n {\n \t# Download and extract sqlpackage\n Get-SqlPackage\n }\n else\n {\n \t# Add folder location to path\n $env:PATH = \"$([IO.Path]::GetDirectoryName($DACPACExeLocation))$([IO.Path]::PathSeparator)\" + $env:PATH\n Write-Host \"It is $($env:PATH)\"\n }\n \n # Execute the actions\n if ($DACPACReport)\n {\n \t$workFolder = \"$($OctopusParameters['Octopus.Action.Package[DACPACPackage].ExtractedPath'])/reports\"\n $sqlReportArguments = @()\n $reportArtifact = Remove-InvalidFileNameChars -FileName (\"{0}.{1}.{2}.{3}\" -f $DACPACTargetServer, $DACPACTargetDatabase, $DACPACDateTime, \"DeployReport.xml\")\n $sqlReportArguments += \"/OutputPath:$workFolder/$reportArtifact\"\n \n # Validate the folder\n Validate-Folder -TestPath $workFolder\n \n # Execute the action\n Invoke-SqlPackage -Action \"DeployReport\" -Arguments ($sqlPackageArguments + $sqlReportArguments)\n \n # Attach artifacts\n foreach ($item in (Get-ChildItem -Path $workFolder))\n {\n \t# Upload artifact\n New-OctopusArtifact $item.FullName\n }\n }\n \n if ($DACPACScript)\n {\n \t$workFolder = \"$($OctopusParameters['Octopus.Action.Package[DACPACPackage].ExtractedPath'])/scripts\"\n $sqlScriptArguments = @()\n $scriptArtifact = Remove-InvalidFileNameChars -FileName (\"{0}.{1}.{2}.{3}\" -f $DACPACTargetServer, $DACPACTargetDatabase, $DACPACDateTime, \"DeployScript.sql\")\n $sqlScriptArguments += \"/OutputPath:$workFolder/$scriptArtifact\"\n \n # Validate folder\n Validate-Folder -TestPath $workFolder\n \n # Execute the action\n Invoke-SqlPackage -Action \"Script\" -Arguments ($sqlPackageArguments + $sqlScriptArguments)\n \n # Attach artifacts\n foreach ($item in (Get-ChildItem -Path $workFolder))\n {\n \t# Upload artifact\n New-OctopusArtifact $item.FullName\n } \n }\n \n if ($DACPACDeploy)\n {\n \t# Execute action\n Invoke-SqlPackage -Action \"Publish\" -Arguments $sqlPackageArguments\n }\n}\ncatch\n{\n Write-Host $_.Exception.ToString()\n throw;\n}\n",
"Octopus.Action.Script.ScriptSource": "Inline",
"Octopus.Action.RunOnServer": "false"
},
"Category": "SQL Server",
"HistoryUrl": "https://github.com/OctopusDeploy/Library/commits/master/step-templates//opt/buildagent/work/75443764cd38076d/step-templates/sql-deploy-dacpac-sqlpackage.json",
"Website": "/step-templates/c323cbcd-aab8-4229-b07c-e6c26f7e9a8a",
"Logo": "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",
"$Meta": {
"Type": "ActionTemplate"
}
}
Page updated on Thursday, June 1, 2023