diff --git a/README.md b/README.md index 5eaac4c..f3b6622 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This repo contains a few [PowerShell](https://github.com/PowerShell/PowerShell) - Configure Terraform [azuread](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs#authenticating-to-azure-active-directory)/[azurerm](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure) provider `ARM_*` environment variables to use the [AzureCLI](https://learn.microsoft.com/azure/devops/pipelines/tasks/reference/azure-cli-v2?view=azure-pipelines) task [Service Connection](https://learn.microsoft.com/azure/devops/pipelines/library/connect-to-azure?view=azure-devops): [set_terraform_azurerm_vars.ps1](scripts/azure-devops/set_terraform_azurerm_vars.ps1) -- Create Managed Identity for Service Connection with Workload identity federation: [create_azurerm_msi_oidc_service_connection.ps1](scripts/azure-devops/create_azurerm_msi_oidc_service_connection.ps1) +- Create Managed Identity and (Azure or Docker Registry) Service Connection with Workload identity federation: [create_azure_msi_oidc_service_connection.ps1](scripts/azure-devops/create_azure_msi_oidc_service_connection.ps1) - List identities for Azure DevOps Service Connections in Entra ID pertaining to Azure DevOps organization and (optionally) project: [list_service_connection_identities.ps1](scripts/azure-devops/list_service_connection_identities.ps1) - List Azure DevOps Service Connections in an Azure DevOps organization and project: [list_service_connections.ps1](scripts/azure-devops/list_service_connections.ps1) - 'Pretty-name' Entra ID applications created for Service Connections, so the Service Connection name is included in the application display name: [rename_service_connection_applications.ps1](scripts/azure-devops/rename_service_connection_applications.ps1) diff --git a/scripts/azure-devops/azure-pipelines.yml b/scripts/azure-devops/azure-pipelines.yml index c03b12a..117d887 100644 --- a/scripts/azure-devops/azure-pipelines.yml +++ b/scripts/azure-devops/azure-pipelines.yml @@ -48,13 +48,16 @@ variables: value: true - name: AZURE_EXTENSION_USE_DYNAMIC_INSTALL value: yes_without_prompt +- name: acrName + value: ciacr$(Build.BuildId) +- name: acrServiceConnectionToCreate + value: oidc-msi-test-acr-$(Build.BuildId) +- name: azureServiceConnectionToCreate + value: oidc-msi-test-$(Build.BuildId) - name: scriptDirectory value: $(Build.SourcesDirectory)/scripts/azure-devops - name: organizationName value: ${{ split(variables['System.CollectionUri'],'/')[3] }} -- name: serviceConnectionToCreate - value: oidc-msi-test-$(Build.BuildId) - # TODO: Convert multiple service connections - name: serviceConnectionToConvert value: oidc-convert-test-$(Build.BuildId) @@ -178,7 +181,7 @@ jobs: Write-Host "##vso[task.setvariable variable=scopeResourceGroupName;isOutput=true]${scopeResourceGroupName}" - task: AzureCLI@2 - displayName: 'Create Managed Identity and Service Connection' + displayName: 'Create Managed Identity and Azure Service Connection' name: identity inputs: azureSubscription: '$(azureConnectionWIF)' @@ -195,11 +198,55 @@ jobs: Get-ChildItem -Path Env: -Force -Recurse -Include * | Sort-Object -Property Name | Format-Table -AutoSize | Out-String } - ./create_azurerm_msi_oidc_service_connection.ps1 -IdentityName ${{ variables['organizationName'] }}-service-connection-test-$(Build.BuildId) ` - -IdentityResourceGroupName $(resourceGroup.managedIdentityResourceGroupName) ` - -IdentitySubscriptionId $(az account show --query id -o tsv) ` - -ServiceConnectionName $(serviceConnectionToCreate) ` - -ServiceConnectionScope $(resourceGroup.scopeResourceGroupId) + ./create_azure_msi_oidc_service_connection.ps1 -IdentityName ${{ variables['organizationName'] }}-service-connection-test-$(Build.BuildId) ` + -IdentityResourceGroupName $(resourceGroup.managedIdentityResourceGroupName) ` + -IdentitySubscriptionId $(az account show --query id -o tsv) ` + -ServiceConnectionName $(azureServiceConnectionToCreate) ` + -ServiceConnectionRole Reader ` + -ServiceConnectionScope $(resourceGroup.scopeResourceGroupId) ` + -ServiceConnectionType AzureRM + + az identity list -g $(resourceGroup.managedIdentityResourceGroupName) ` + --query [0].clientId ` + -o tsv ` + | Set-Variable -Name clientId + Write-Host "##vso[task.setvariable variable=clientId;isOutput=true]${clientId}" + + workingDirectory: '$(scriptDirectory)' + + - task: AzureCLI@2 + displayName: 'Create ACR, Managed Identity and Docker Registry Service Connection' + name: acrIdentity + inputs: + azureSubscription: '$(azureConnectionWIF)' + failOnStandardError: true + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + if ($env:SYSTEM_DEBUG -eq "true") { + $InformationPreference = "Continue" + $VerbosePreference = "Continue" + $DebugPreference = "Continue" + + Set-PSDebug -Trace 1 + + Get-ChildItem -Path Env: -Force -Recurse -Include * | Sort-Object -Property Name | Format-Table -AutoSize | Out-String + } + Write-Host "Creating Azure Container Registry.." + az acr create --resource-group $(resourceGroup.scopeResourceGroupName) ` + --name $(acrName) ` + --sku Basic ` + -o json ` + | ConvertFrom-Json ` + | Tee-Object -Variable acr + + ./create_azure_msi_oidc_service_connection.ps1 -IdentityName ${{ variables['organizationName'] }}-service-connection-test-$(Build.BuildId) ` + -IdentityResourceGroupName $(resourceGroup.managedIdentityResourceGroupName) ` + -IdentitySubscriptionId $(az account show --query id -o tsv) ` + -ServiceConnectionName $(acrServiceConnectionToCreate) ` + -ServiceConnectionRole AcrPull ` + -ServiceConnectionScope $acr.id ` + -ServiceConnectionType dockerregistry az identity list -g $(resourceGroup.managedIdentityResourceGroupName) ` --query [0].clientId ` @@ -220,7 +267,7 @@ jobs: steps: - task: AzureCLI@2 - displayName: 'Test Service Connection $(serviceConnectionToCreate)' + displayName: 'Test Azure Service Connection $(azureServiceConnectionToCreate)' timeoutInMinutes: 5 inputs: azureSubscription: '$(azureConnectionWIF)' @@ -229,7 +276,7 @@ jobs: scriptLocation: inlineScript workingDirectory: '$(scriptDirectory)' inlineScript: | - ./test_service_connection.ps1 -ServiceConnectionName $(serviceConnectionToCreate) ` + ./test_service_connection.ps1 -ServiceConnectionName $(azureServiceConnectionToCreate) ` -ServiceConnectionTestPipelineId $(serviceConnectionTestPipelineId) - ${{ if or(eq(parameters.jobsToRun, 'Non-modifying tests'),eq(parameters.jobsToRun, 'Both')) }}: @@ -338,7 +385,8 @@ jobs: # convertedServiceConnectionId1: $[ dependencies.convertServiceConnection.outputs['job1.convert.serviceConnectionId'] ] # convertedServiceConnectionId2: $[ dependencies.convertServiceConnection.outputs['job2.convert.serviceConnectionId'] ] convertedServiceConnectionId: $[ dependencies.convertServiceConnection.outputs['convert.serviceConnectionId'] ] - createdClientId: $[ dependencies.createServiceConnection.outputs['identity.clientId'] ] + createdAzureClientId: $[ dependencies.createServiceConnection.outputs['identity.clientId'] ] + createdACRClientId: $[ dependencies.createServiceConnection.outputs['acrIdentity.clientId'] ] steps: - task: AzureCLI@2 name: teardownAzure @@ -390,14 +438,11 @@ jobs: $ErrorActionPreference = "Continue" # Continue to remove resources if remove by resource group fails az devops configure --defaults organization="$(System.CollectionUri)" project="$(System.TeamProject)" - az devops service-endpoint list --query "[?authorization.parameters.serviceprincipalid=='$(createdClientId)'].id" ` + az devops service-endpoint list --query "[?authorization.parameters.serviceprincipalid=='$(createdAzureClientId)' || authorization.parameters.serviceprincipalid=='$(createdACRClientId)'].id" ` -o tsv ` - | Set-Variable -Name serviceConnectionId - if (!$serviceConnectionId) { - Write-Host "No created service connections to remove" - exit 0 - } else { - Write-Host "Removing created service connection ${serviceConnectionId}..." + | Set-Variable -Name serviceConnectionIds + foreach ($serviceConnectionId in $serviceConnectionIds) { + Write-Host "Removing service connection ${serviceConnectionId}..." &{ # az writes information to stderr $ErrorActionPreference = 'SilentlyContinue' az devops service-endpoint delete --id $serviceConnectionId --yes 2>&1 diff --git a/scripts/azure-devops/create_azurerm_msi_oidc_service_connection.ps1 b/scripts/azure-devops/create_azure_msi_oidc_service_connection.ps1 similarity index 72% rename from scripts/azure-devops/create_azurerm_msi_oidc_service_connection.ps1 rename to scripts/azure-devops/create_azure_msi_oidc_service_connection.ps1 index f17034d..bce1e94 100644 --- a/scripts/azure-devops/create_azurerm_msi_oidc_service_connection.ps1 +++ b/scripts/azure-devops/create_azure_msi_oidc_service_connection.ps1 @@ -45,6 +45,11 @@ param ( [ValidatePattern("^$|(?i)/subscriptions/[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}(/resourcegroups/(.+?))?")] $ServiceConnectionScope, + [parameter(Mandatory=$false)] + [string] + [ValidateSet("AzureRM","dockerregistry")] + $ServiceConnectionType="AzureRM", + [parameter(Mandatory=$false,HelpMessage="Name of the Azure DevOps Project")] [string] [ValidateNotNullOrEmpty()] @@ -57,7 +62,27 @@ param ( ) Write-Verbose $MyInvocation.line . (Join-Path $PSScriptRoot .. functions.ps1) -$apiVersion = "7.1-preview.4" +$apiVersion = "7.2-preview" + +#----------------------------------------------------------- +# Validate parameters +switch ($ServiceConnectionType) +{ + AzureRM { + # Nothing to do + } + dockerregistry { + if (!$ServiceConnectionScope) { + Write-Error "Parameter ServiceConnectionScope is required for ServiceConnectionType 'dockerregistry'" + exit 1 + } + if ($ServiceConnectionScope -notmatch "/Microsoft.ContainerRegistry/registries/") { + Write-Error "Parameter ServiceConnectionScope must be a container registry scope for ServiceConnectionType 'dockerregistry'" + exit 1 + } + "{0}.azurecr.io" -f $ServiceConnectionScope.Split('/')[8] | Set-Variable acrLoginServer + } +} #----------------------------------------------------------- # Log in to Azure @@ -141,7 +166,7 @@ if ($resourceGroup) { $IdentityLocation = (az config get defaults.location --query value -o tsv) } if (!$IdentityLocation) { - # Azure location doesn't really matter for MI; the object is in AAD which is a global service + # Azure location doesn't really matter for MI; the actual object is in Entra ID which is a global service $IdentityLocation = "southcentralus" } az group create -g $IdentityResourceGroupName -l $IdentityLocation -o json | ConvertFrom-Json | Set-Variable resourceGroup @@ -184,11 +209,11 @@ do { $ServiceConnectionName = $ServiceConnectionNameBefore } if ($ServiceConnectionName -ieq $ServiceConnectionNameBefore) { - Write-Verbose "Service connection '${ServiceConnectionName}' (${serviceEndpointId}) wil be updated" + Write-Verbose "Service connection '${ServiceConnectionName}' (${serviceEndpointId}) will be updated" break } } else { - Write-Verbose "Service connection '${ServiceConnectionName}' (${serviceEndpointId}) wil be created" + Write-Verbose "Service connection '${ServiceConnectionName}' (${serviceEndpointId}) will be created" } } while ($serviceEndpointId) @@ -197,84 +222,104 @@ do { if (!$IdentityName) { $IdentityName = "${organizationName}-${Project}-${ServiceConnectionName}" } -Write-Verbose "Creating Managed Identity '${IdentityName}' in resource group '${IdentityResourceGroupName}'..." +Write-Host "Creating Managed Identity '${IdentityName}' in resource group '${IdentityResourceGroupName}'..." Write-Debug "az identity create -n $IdentityName -g $IdentityResourceGroupName -l $IdentityLocation --subscription $IdentitySubscriptionId" az identity create -n $IdentityName ` -g $IdentityResourceGroupName ` -l $IdentityLocation ` --subscription $IdentitySubscriptionId ` -o json ` + | Tee-Object -Variable identityJson ` | ConvertFrom-Json ` | Set-Variable identity +$identityJson | Write-Debug Write-Verbose "Created Managed Identity $($identity.id)" $identity | Format-List | Out-String | Write-Debug -Write-Verbose "Creating role assignment for Managed Identity '${IdentityName}' on subscription '$($subscription.name)'..." +Write-Host "Creating role assignment for Managed Identity '${IdentityName}' on scope '${ServiceConnectionScope}'..." az role assignment create --assignee-object-id $identity.principalId ` --assignee-principal-type ServicePrincipal ` --role $ServiceConnectionRole ` --scope $ServiceConnectionScope ` --subscription $serviceConnectionSubscriptionId ` -o json ` + | Tee-Object -Variable roleAssignmentJson ` | ConvertFrom-Json ` | Set-Variable roleAssignment +$roleAssignmentJson | Write-Debug Write-Verbose "Created role assignment $($roleAssignment.id)" - -Write-Host "`nManaged Identity '$($identity.name)':" -$identity | Format-List -Property id, clientId, federatedSubject, role, scope, subscriptionId, tenantId - -#----------------------------------------------------------- -# TODO: Create the service connection (Azure CLI) -# az devops service-endpoint azurerm create --azure-rm-service-principal-id $identity.clientId ` -# --azure-rm-subscription-id $serviceConnectionSubscriptionId ` -# --azure-rm-subscription-name $ServiceConnectionName ` -# --azure-rm-tenant-id $identity.tenantId ` -# --name $IdentityName ` -# --organization $OrganizationUrl ` -# --project $Project ` # Prepare service connection REST API request body -Write-Verbose "Creating / updating service connection '${ServiceConnectionName}'..." -Get-Content -Path (Join-Path $PSScriptRoot manualServiceEndpointRequest.json) ` - | ConvertFrom-Json ` - | Set-Variable serviceEndpointRequest - -$serviceEndpointDescription = "Created by $($MyInvocation.MyCommand.Name). Configured Managed Identity ${IdentityName} (clientId $($identity.clientId)) federated on ${federatedSubject} as ${ServiceConnectionRole} on scope ${ServiceConnectionScope}." -$serviceEndpointRequest.authorization.parameters.servicePrincipalId = $identity.clientId -$serviceEndpointRequest.authorization.parameters.tenantId = $identity.tenantId -$serviceEndpointRequest.data.subscriptionId = $serviceConnectionSubscriptionId -$serviceEndpointRequest.data.subscriptionName = $serviceConnectionSubscriptionName -$serviceEndpointRequest.description = $serviceEndpointDescription -$serviceEndpointRequest.name = $ServiceConnectionName -$serviceEndpointRequest.serviceEndpointProjectReferences[0].description = $serviceEndpointDescription -$serviceEndpointRequest.serviceEndpointProjectReferences[0].name = $ServiceConnectionName -$serviceEndpointRequest.serviceEndpointProjectReferences[0].projectReference.id = $projectId -$serviceEndpointRequest.serviceEndpointProjectReferences[0].projectReference.name = $Project -$serviceEndpointRequest | ConvertTo-Json -Depth 4 | Set-Variable serviceEndpointRequestBody -Write-Debug "Service connection request body: `n${serviceEndpointRequestBody}" +Write-Host "Creating / updating service connection '${ServiceConnectionName}'..." +$serviceEndpointDescription = "Created by $($MyInvocation.MyCommand.Name). Configured Managed Identity ${IdentityName} (clientId $($identity.clientId)) as ${ServiceConnectionRole} on scope ${ServiceConnectionScope}." +$serviceEndpointRequest = @{ + data = @{ + subscriptionId = $serviceConnectionSubscriptionId + subscriptionName = $serviceConnectionSubscriptionName + creationMode = 'Manual' + } + description = $serviceEndpointDescription + name = $ServiceConnectionName + type = $ServiceConnectionType + url = "https://management.azure.com/" + authorization = @{ + parameters = @{ + tenantid = $identity.tenantId + role = $roleAssignment.roleDefinitionId.Split('/')[-1] + scope = $ServiceConnectionScope + serviceprincipalid = $identity.clientId + } + scheme = "WorkloadIdentityFederation" + } + isShared = $false + isReady = $false + serviceEndpointProjectReferences = @( + @{ + projectReference = @{ + id = $projectId + name = $Project + } + name = $ServiceConnectionName + description = $serviceEndpointDescription + } + ) +} +if ($ServiceConnectionType -ieq "dockerregistry") { + Add-Member -InputObject $serviceEndpointRequest.authorization.parameters -NotePropertyName loginServer -NotePropertyValue $acrLoginServer + Add-Member -InputObject $serviceEndpointRequest.data -NotePropertyName registryId -NotePropertyValue $ServiceConnectionScope + Add-Member -InputObject $serviceEndpointRequest.data -NotePropertyName registryType -NotePropertyValue "ACR" +} +if ($ServiceConnectionType -ieq "AzureRM") { + Add-Member -InputObject $serviceEndpointRequest.data -NotePropertyName environment -NotePropertyValue "AzureCloud" + Add-Member -InputObject $serviceEndpointRequest.data -NotePropertyName scopeLevel -NotePropertyValue "Subscription" +} $apiUri = "${OrganizationUrl}/_apis/serviceendpoint/endpoints" if ($serviceEndpointId) { $apiUri += "/${serviceEndpointId}" } $apiUri += "?api-version=${apiVersion}" -Invoke-RestMethod -Uri $apiUri ` - -Method ($serviceEndpointId ? 'PUT' : 'POST') ` - -Body $serviceEndpointRequestBody ` - -ContentType 'application/json' ` - -Authentication Bearer ` - -Token (ConvertTo-SecureString $accessToken -AsPlainText) ` - | Set-Variable serviceEndpoint - -$serviceEndpoint | ConvertTo-Json -Depth 4 | Write-Debug +$serviceEndpointRequestBodyFile = (New-TemporaryFile).FullName +$serviceEndpointRequest | ConvertTo-Json -Depth 4 | Out-File $serviceEndpointRequestBodyFile +Write-Debug "Service connection request body file: ${serviceEndpointRequestBodyFile}" +Get-Content $serviceEndpointRequestBodyFile | Write-Debug +az rest --method ($serviceEndpointId ? 'PUT' : 'POST') ` + --uri $apiUri ` + --body "@$serviceEndpointRequestBodyFile" ` + --resource 499b84ac-1321-427f-aa17-267ca6975798 ` + --output json ` + | Tee-Object -Variable serviceEndpointResponseJson ` + | ConvertFrom-Json -Depth 4 ` + | Set-Variable serviceEndpoint +$serviceEndpointResponseJson | Write-Debug if (!$serviceEndpoint) { Write-Error "Failed to create / update service connection '${ServiceConnectionName}'" exit 1 } if ($serviceEndpointId) { - Write-Host "Service connection '${ServiceConnectionName}' updated:" + Write-Host "Service connection '${ServiceConnectionName}' updated." } else { - Write-Host "Service connection '${ServiceConnectionName}' created:" + Write-Host "Service connection '${ServiceConnectionName}' created." } Write-Debug "Service connection data:" $serviceEndpoint.data | Format-List | Out-String | Write-Debug @@ -282,7 +327,7 @@ Write-Debug "Service connection authorization parameters:" $serviceEndpoint.authorization.parameters | Format-List | Out-String | Write-Debug # Create Federated Credential -Write-Verbose "Configuring Managed Identity '${IdentityName}' with federated subject '$($serviceEndpoint.authorization.parameters.workloadIdentityFederationSubject)'..." +Write-Host "Configuring Managed Identity '${IdentityName}' with federated subject '$($serviceEndpoint.authorization.parameters.workloadIdentityFederationSubject)'..." az identity federated-credential create --name $IdentityName ` --identity-name $IdentityName ` --resource-group $IdentityResourceGroupName ` @@ -290,8 +335,10 @@ az identity federated-credential create --name $IdentityName ` --subject $serviceEndpoint.authorization.parameters.workloadIdentityFederationSubject ` --subscription $IdentitySubscriptionId ` -o json ` + | Tee-Object -Variable federatedCredentialJson ` | ConvertFrom-Json ` | Set-Variable federatedCredential +$federatedCredentialJson | Write-Debug Write-Verbose "Created federated credential $($federatedCredential.id)" $identity | Add-Member -NotePropertyName federatedSubject -NotePropertyValue $serviceEndpoint.authorization.parameters.workloadIdentityFederationSubject $identity | Add-Member -NotePropertyName role -NotePropertyValue $ServiceConnectionRole @@ -299,13 +346,17 @@ $identity | Add-Member -NotePropertyName scope -NotePropertyValue $ServiceConnec $identity | Add-Member -NotePropertyName subscriptionId -NotePropertyValue $IdentitySubscriptionId $identity | Format-List | Out-String | Write-Debug +Write-Host "`nService Connection '$($serviceEndpoint.name)':" + $serviceEndpoint | Select-Object -Property authorization, data, id, name, description, type, createdBy ` | ForEach-Object { $_.createdBy = $_.createdBy.uniqueName $_ | Add-Member -NotePropertyName clientId -NotePropertyValue $_.authorization.parameters.serviceprincipalid $_ | Add-Member -NotePropertyName creationMode -NotePropertyValue $_.data.creationMode + $_ | Add-Member -NotePropertyName managedIdentityPortalLink -NotePropertyValue ("https://portal.azure.com/#@{0}/resource{1}" -f $identity.tenantId, $identity.id) $_ | Add-Member -NotePropertyName scheme -NotePropertyValue $_.authorization.scheme - $_ | Add-Member -NotePropertyName scopeLevel -NotePropertyValue $_.data.scopeLevel + $_ | Add-Member -NotePropertyName scopePortalLink -NotePropertyValue ("https://portal.azure.com/#@{0}/resource{1}" -f $identity.tenantId, $ServiceConnectionScope) + $_ | Add-Member -NotePropertyName serviceConnectionPortalLink -NotePropertyValue ("{0}/{1}/_settings/adminservices?resourceId={2}" -f $OrganizationUrl, $Project, $serviceEndpoint.id) $_ | Add-Member -NotePropertyName subscriptionName -NotePropertyValue $_.data.subscriptionName $_ | Add-Member -NotePropertyName subscriptionId -NotePropertyValue $_.data.subscriptionId $_ | Add-Member -NotePropertyName tenantid -NotePropertyValue $_.authorization.parameters.tenantid diff --git a/scripts/azure-devops/manualServiceEndpointRequest.json b/scripts/azure-devops/manualServiceEndpointRequest.json index 3158e75..22a0ec5 100644 --- a/scripts/azure-devops/manualServiceEndpointRequest.json +++ b/scripts/azure-devops/manualServiceEndpointRequest.json @@ -2,8 +2,6 @@ "data": { "subscriptionId": "", "subscriptionName": "", - "environment": "AzureCloud", - "scopeLevel": "Subscription", "creationMode": "Manual" }, "description": "", @@ -13,6 +11,8 @@ "authorization": { "parameters": { "tenantid": "", + "role": "", + "scope": "", "serviceprincipalid": "" }, "scheme": "WorkloadIdentityFederation" diff --git a/scripts/azure-pipelines.yml b/scripts/azure-pipelines.yml index 05f1ba8..270aa76 100644 --- a/scripts/azure-pipelines.yml +++ b/scripts/azure-pipelines.yml @@ -43,7 +43,7 @@ jobs: - task: AzureCLI@2 displayName: 'find_workload_identity.ps1' inputs: - azureSubscription: '$(azureConnection)' + azureSubscription: '$(azureConnectionWIF)' scriptType: pscore scriptLocation: inlineScript inlineScript: | @@ -83,7 +83,7 @@ jobs: - task: AzureCLI@2 displayName: 'list_managed_identities.ps1' inputs: - azureSubscription: '$(azureConnection)' + azureSubscription: '$(azureConnectionWIF)' scriptType: pscore scriptLocation: inlineScript inlineScript: |