diff --git a/eng/pipelines/kerberos/build-and-test-steps.yml b/eng/pipelines/kerberos/build-and-test-steps.yml new file mode 100644 index 0000000000..307c93f978 --- /dev/null +++ b/eng/pipelines/kerberos/build-and-test-steps.yml @@ -0,0 +1,144 @@ +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# + +# Shared build-and-test steps used by both the Windows and Linux Kerberos jobs. +# +# Parameters: +# buildTarget — The build.proj target that builds SqlClient for the current +# OS (e.g. BuildSqlClientWindows or BuildSqlClientUnix). +# testFramework — The TFM to test against (e.g. net9.0, net462). +# testRunTitle — Title for the published test results (displayed in the ADO +# Tests tab). +# artifactName — Name of the published pipeline artifact that carries the +# test results and coverage files. + +parameters: + + # build.proj target to build SqlClient for the current OS. + - name: buildTarget + type: string + + # TFM to pass to the test targets (-p:TestFramework). + - name: testFramework + type: string + + # Title shown in the ADO Tests tab for this run. + - name: testRunTitle + type: string + + # Pipeline artifact name for test results and coverage. + - name: artifactName + type: string + +steps: + + # --------------------------------------------------------------------------- + # Build + # --------------------------------------------------------------------------- + + # Build the given target. + # + # The test stages build as part of their targets, but this separate step isolates build failures + # so we can fail fast before running tests. Retries are enabled intentionally (1 attempt for + # build, 2 attempts for test steps) to reduce transient infrastructure-related failures. + # + - task: DotNetCoreCLI@2 + displayName: Build SqlClient + retryCountOnTaskFailure: 1 + inputs: + command: build + projects: build.proj + arguments: >- + -t:${{ parameters.buildTarget }} + -p:Configuration=Release + + # --------------------------------------------------------------------------- + # Run tests in separate steps to permit focused retries. + # --------------------------------------------------------------------------- + + # Run the Unit Test suite. + - task: DotNetCoreCLI@2 + displayName: Run Unit Tests (${{ parameters.testFramework }}) + retryCountOnTaskFailure: 2 + inputs: + command: build + projects: build.proj + arguments: >- + -t:TestSqlClientUnit + -p:TestFramework=${{ parameters.testFramework }} + -p:Configuration=Release + + # Run the Functional Test suite. + - task: DotNetCoreCLI@2 + displayName: Run Functional Tests (${{ parameters.testFramework }}) + retryCountOnTaskFailure: 2 + inputs: + command: build + projects: build.proj + arguments: >- + -t:TestSqlClientFunctional + -p:TestFramework=${{ parameters.testFramework }} + -p:Configuration=Release + + # Run the Manual Test suite. + - task: DotNetCoreCLI@2 + displayName: Run Manual Tests (${{ parameters.testFramework }}) + retryCountOnTaskFailure: 2 + inputs: + command: build + projects: build.proj + arguments: >- + -t:TestSqlClientManual + -p:TestFramework=${{ parameters.testFramework }} + -p:Configuration=Release + + # --------------------------------------------------------------------------- + # Publish results & coverage + # --------------------------------------------------------------------------- + + # Publish the TRX test results to the pipeline run. + - task: PublishTestResults@2 + displayName: Publish Test Results + condition: succeededOrFailed() + inputs: + testResultsFormat: VSTest + # build.proj defines TestResultsFolderPath which defaults to + # $(Build.SourcesDirectory)/test_results, so we look there for the results and coverage files. + testResultsFiles: $(Build.SourcesDirectory)/test_results/**/*.trx + mergeTestResults: true + testRunTitle: ${{ parameters.testRunTitle }} + buildConfiguration: Release + + # Azure Pipelines task conditions do not support path existence checks directly, + # so compute this once and gate later steps on the variable. + - pwsh: | + $resultsDir = "$(Build.SourcesDirectory)/test_results" + if (Test-Path -LiteralPath $resultsDir) { + Write-Host "##vso[task.setvariable variable=HasTestResultsDir]true" + } + else { + Write-Host "##vso[task.setvariable variable=HasTestResultsDir]false" + } + displayName: Detect test_results directory + condition: succeededOrFailed() + + # Give our coverage files a unique name to make it clear where they originated when we download + # the artifacts from all jobs in the merge stage. + - pwsh: | + cd $(Build.SourcesDirectory)/test_results + Get-ChildItem -Filter "*.coverage" -Recurse | + Rename-Item -NewName { "${{ parameters.testFramework }}" + $_.Name } + displayName: Rename coverage files + condition: and(succeededOrFailed(), eq(variables['HasTestResultsDir'], 'true')) + + # Publish TRX test results and coverage files as pipeline artifacts. The merge stage needs the + # coverage files from all of the jobs. + - task: PublishPipelineArtifact@1 + displayName: Publish Test Artifacts + condition: and(succeededOrFailed(), eq(variables['HasTestResultsDir'], 'true')) + inputs: + targetPath: $(Build.SourcesDirectory)/test_results + artifact: ${{ parameters.artifactName }} diff --git a/eng/pipelines/kerberos/linux-cleanup-step.yml b/eng/pipelines/kerberos/linux-cleanup-step.yml new file mode 100644 index 0000000000..d3491bb337 --- /dev/null +++ b/eng/pipelines/kerberos/linux-cleanup-step.yml @@ -0,0 +1,45 @@ +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# + +# This template leaves the Active Directory domain and destroys Kerberos +# credentials. It should be referenced at the end of any job that called +# linux-init-step.yml. +# +# All steps use condition: always() so that cleanup runs even when previous +# steps fail. + +parameters: + + # The Active Directory domain to leave (e.g. mydomain.contoso.com). + - name: kerberosDomain + type: string + + # The domain user account used during the join. + - name: kerberosDomainUser + type: string + + # The password for the domain user account. + - name: kerberosDomainPassword + type: string + +steps: + + - bash: | + set -uo pipefail + + DOMAIN="${{ parameters.kerberosDomain }}" + DOMAIN_USER="${{ parameters.kerberosDomainUser }}" + DOMAIN_PASSWORD="${{ parameters.kerberosDomainPassword }}" + DOMAIN_UPPER=$(echo "$DOMAIN" | tr '[:lower:]' '[:upper:]') + + # Leave the domain + echo "$DOMAIN_PASSWORD" | sudo realm leave "$DOMAIN_UPPER" --verbose \ + -U "$DOMAIN_USER@$DOMAIN_UPPER" || true + + # Destroy the TGT and credential cache + kdestroy || true + displayName: Clean up Kerberos (domain leave + kdestroy) + condition: always() diff --git a/eng/pipelines/kerberos/linux-init-step.yml b/eng/pipelines/kerberos/linux-init-step.yml new file mode 100644 index 0000000000..c0c4603232 --- /dev/null +++ b/eng/pipelines/kerberos/linux-init-step.yml @@ -0,0 +1,117 @@ +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# + +# This template joins a Linux agent to an Active Directory domain using Kerberos +# and acquires a TGT (Ticket-Granting Ticket) for the specified domain user. +# +# Prerequisites: +# - The agent must be running on Ubuntu/Debian (uses apt-get). +# - The domain controller must be reachable from the agent network. +# +# After this step completes successfully, the agent will have: +# - Kerberos packages installed (krb5-user, realmd, sssd, adcli, etc.) +# - Hostname set to FQDN within the domain +# - NTP synchronized with the domain controller +# - Machine joined to the AD domain +# - A valid Kerberos TGT for the specified user + +parameters: + + # The Active Directory domain to join (e.g. mydomain.contoso.com). + - name: kerberosDomain + type: string + + # The Organizational Unit in which to place the computer account. + - name: kerberosDomainOU + type: string + + # The domain user account to authenticate with (sAMAccountName, without @realm). + - name: kerberosDomainUser + type: string + + # The password for the domain user account. + - name: kerberosDomainPassword + type: string + +steps: + + - bash: | + set -euo pipefail + + DOMAIN="${{ parameters.kerberosDomain }}" + DOMAIN_OU="${{ parameters.kerberosDomainOU }}" + DOMAIN_USER="${{ parameters.kerberosDomainUser }}" + DOMAIN_PASSWORD="${{ parameters.kerberosDomainPassword }}" + DOMAIN_UPPER=$(echo "$DOMAIN" | tr '[:lower:]' '[:upper:]') + + echo "Domain: $DOMAIN" + echo "Realm: $DOMAIN_UPPER" + echo "User: $DOMAIN_USER" + echo "OU: $DOMAIN_OU" + + if [ -z "$DOMAIN_PASSWORD" ]; then + echo "##vso[task.logissue type=error]KerberosDomainPassword is empty" + exit 1 + fi + + # ----------------------------------------------------------------------- + # Install Kerberos and AD integration packages + # ----------------------------------------------------------------------- + echo 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections + + sudo apt-get -y update + sudo apt-get install -y dialog apt-utils + sudo apt-get install -y \ + krb5-user samba sssd sssd-tools libnss-sss libpam-sss \ + ntp ntpdate realmd adcli + + # ----------------------------------------------------------------------- + # Set the hostname to FQDN within the domain + # ----------------------------------------------------------------------- + CURRENT_HOSTNAME="$(hostname)" + if [ "$CURRENT_HOSTNAME" = "$DOMAIN" ] || [[ "$CURRENT_HOSTNAME" == *".$DOMAIN" ]]; then + echo "Hostname already uses domain suffix '.$DOMAIN': $CURRENT_HOSTNAME" + else + sudo hostnamectl set-hostname "$CURRENT_HOSTNAME.$DOMAIN" + fi + + # ----------------------------------------------------------------------- + # Synchronize time with the domain controller (required for Kerberos) + # ----------------------------------------------------------------------- + if ! sudo grep -Fqx "server $DOMAIN" /etc/ntp.conf; then + echo "server $DOMAIN" | sudo tee -a /etc/ntp.conf + fi + sudo systemctl stop ntp + sudo ntpdate "$DOMAIN" + sudo systemctl start ntp + + # ----------------------------------------------------------------------- + # Configure Kerberos realm + # ----------------------------------------------------------------------- + echo "[libdefaults] + default_realm = $DOMAIN_UPPER + rdns = false" | sudo tee /etc/krb5.conf + + # ----------------------------------------------------------------------- + # Discover and join the domain + # ----------------------------------------------------------------------- + sudo realm discover "$DOMAIN_UPPER" + + echo "$DOMAIN_PASSWORD" | sudo realm join --verbose "$DOMAIN_UPPER" \ + -U "$DOMAIN_USER@$DOMAIN_UPPER" \ + --computer-ou "OU=$DOMAIN_OU" + + realm list + + # ----------------------------------------------------------------------- + # Acquire a Kerberos TGT + # ----------------------------------------------------------------------- + echo "$DOMAIN_PASSWORD" | kinit "$DOMAIN_USER@$DOMAIN_UPPER" + + klist + sudo ip addr + sudo ip route + displayName: Initialize Kerberos (domain join + kinit) diff --git a/eng/pipelines/kerberos/sqlclient-kerberos.yml b/eng/pipelines/kerberos/sqlclient-kerberos.yml new file mode 100644 index 0000000000..1cd19961e7 --- /dev/null +++ b/eng/pipelines/kerberos/sqlclient-kerberos.yml @@ -0,0 +1,292 @@ +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# + +# ============================================================================= +# sqlclient-kerberos +# ============================================================================= +# Daily Kerberos authentication test pipeline for Microsoft.Data.SqlClient. +# Replaces the Classic "Test-SqlClient-Kerberos-Azure" pipeline. +# +# Schedule: daily at 07:00 UTC on main, release/6.1, and release/7.0. +# +# Job breakdown (10 total): +# Stage: windows → 7 jobs (net462 + net8/9/10 × NativeSNI/ManagedSNI) +# Stage: linux → 3 jobs (net8, net9, net10 — ManagedSNI only) +# Stage: Merge-Code-Coverage → 1 job +# +# Shared steps: +# Build and test steps are defined in build-and-test-steps.yml and reused +# by both stages. Each stage passes its OS-specific build target and the +# per-job testFramework matrix variable. +# +# Required ADO variable groups (link in pipeline UI): +# - kv-sqldrivers-shared (provides the agent-at-sqldrv-ad secret) +# +# Variables defined in this file (no pipeline UI configuration needed): +# - KerberosDomain sqldrv.ad +# - KerberosDomainOU agents +# - KerberosDomainUser agent +# - KerberosDomainPassword $(agent-at-sqldrv-ad) from kv-sqldrivers-shared +# - REMOTE_TCP_CONN_STRING TCP connection string to sqldrv-sql22 +# - REMOTE_NP_CONN_STRING Named Pipe connection string to sqldrv-sql22 +# ============================================================================= + +trigger: none # Scheduled runs only — no CI trigger +pr: none # Not triggered by PRs + +schedules: + - cron: '0 7 * * *' + displayName: Daily run (07:00 UTC) + branches: + include: + - main + - release/6.1 + - release/7.0 + always: true + +name: $(date:yyyyMMdd)$(rev:.r) + +variables: + # Our Kerberos environment doesn't change often, so we can define these variables here rather than + # in the Pipeline UI or in Libraries. + - name: KerberosDomain + value: sqldrv.ad + + - name: KerberosDomainOU + value: agents + + - name: KerberosDomainUser + value: agent + + - name: KerberosDomainPassword + value: $(agent-at-sqldrv-ad) # Secret from kv-sqldrivers-shared variable group + + - name: REMOTE_TCP_CONN_STRING + value: Data Source=tcp:sqldrv-sql22.sqldrv.ad\sql2022;Initial Catalog=Northwind;Integrated Security=true;Encrypt=false;TrustServerCertificate=true + + - name: REMOTE_NP_CONN_STRING + value: Data Source=np:sqldrv-sql22.sqldrv.ad\sql2022;Initial Catalog=Northwind;Integrated Security=true;Encrypt=false;TrustServerCertificate=true + +# ============================================================================= +# STAGES +# ============================================================================= +stages: + + # =========================================================================== + # Stage 1 - Windows (7 jobs = net462 + 3 TFs × 2 ManagedSNI) + # =========================================================================== + - stage: windows + displayName: Windows + dependsOn: [] + variables: + # KerberosDomainPassword expands at runtime in this stage from the agent-at-sqldrv-ad secret + # exposed by this variable group. + - group: kv-sqldrivers-shared + jobs: + - job: windows + displayName: Windows + timeoutInMinutes: 90 + workspace: + clean: all # Purge obj/artifacts from prior runs on self-hosted agents + strategy: + matrix: + # Azure Pipelines exposes matrix variables as environment variables for each step. + # Do not use the name targetFramework here: MSBuild imports environment variables as + # properties, and TargetFramework would leak into dotnet build build.proj, forcing + # transitive project references onto an invalid TFM (for example SqlServer.Server -> net9.0). + net462_NativeSNI: + testFramework: net462 + managedSNI: 'false' + net8_NativeSNI: + testFramework: net8.0 + managedSNI: 'false' + net8_ManagedSNI: + testFramework: net8.0 + managedSNI: 'true' + net9_NativeSNI: + testFramework: net9.0 + managedSNI: 'false' + net9_ManagedSNI: + testFramework: net9.0 + managedSNI: 'true' + net10_NativeSNI: + testFramework: net10.0 + managedSNI: 'false' + net10_ManagedSNI: + testFramework: net10.0 + managedSNI: 'true' + pool: + name: ADO-Trusted-Domain-Win-WestUS2 + demands: + - ImageOverride -equals ADO-MMS22-SQL19 + steps: + + - checkout: self + clean: true + fetchDepth: 1 + fetchTags: false + + - template: /eng/pipelines/steps/install-dotnet.yml@self + parameters: + runtimes: [8.x, 9.x] + + # --- Update test configuration --- + # Uses runtime variables from the matrix ($(managedSNI)) so we use + # inline PowerShell instead of the shared config template which + # requires compile-time parameters. + - pwsh: | + $managedSni = [System.Convert]::ToBoolean($env:MANAGED_SNI) + $jdata = Get-Content -Raw "config.default.json" | ConvertFrom-Json + foreach ($p in $jdata) { + $p.TCPConnectionString = $env:REMOTE_TCP_CONN_STRING + $p.NPConnectionString = $env:REMOTE_NP_CONN_STRING + $p.SupportsIntegratedSecurity = $true + $p.UseManagedSNIOnWindows = $managedSni + } + $jdata | ConvertTo-Json | Set-Content "config.json" + workingDirectory: src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities + displayName: Update test config.json + env: + REMOTE_TCP_CONN_STRING: $(REMOTE_TCP_CONN_STRING) + REMOTE_NP_CONN_STRING: $(REMOTE_NP_CONN_STRING) + MANAGED_SNI: $(managedSNI) + + # --- Prepare Windows services --- + - powershell: | + $svc = Get-Service -Name SQLBrowser -ErrorAction SilentlyContinue + if ($null -ne $svc) { + Set-Service -StartupType Automatic SQLBrowser + if ($svc.Status -ne 'Running') { Start-Service SQLBrowser } + Get-Service SQLBrowser | Select-Object Name, StartType, Status + } + displayName: Start SQL Server Browser + + - powershell: | + Set-DtcNetworkSetting -DtcName "Local" ` + -InboundTransactionsEnabled $true ` + -OutboundTransactionsEnabled $true ` + -RemoteClientAccessEnabled $true ` + -Confirm:$false + + Get-NetFirewallRule -DisplayName "Distributed Transaction Coordinator (RPC)" | Set-NetFirewallRule -Profile Domain -Action Allow -Enabled True + Get-NetFirewallRule -DisplayName "Distributed Transaction Coordinator (RPC-EPMAP)" | Set-NetFirewallRule -Profile Domain -Action Allow -Enabled True + Get-NetFirewallRule -DisplayName "Distributed Transaction Coordinator (TCP-Out)" | Set-NetFirewallRule -Profile Domain -Action Allow -Enabled True + Get-NetFirewallRule -DisplayName "Distributed Transaction Coordinator (TCP-In)" | Set-NetFirewallRule -Profile Domain -Action Allow -Enabled True + displayName: Enable Network DTC Access + + # --- Build and test --- + - template: /eng/pipelines/kerberos/build-and-test-steps.yml@self + parameters: + buildTarget: BuildSqlClientWindows + testFramework: $(testFramework) + testRunTitle: Windows-$(testFramework)-ManagedSNI_$(managedSNI) + artifactName: $(testFramework)-ManagedSNI_$(managedSNI)-$(System.JobId) + + # =========================================================================== + # Stage 2 - Linux - .NET Core + Kerberos (3 jobs = 3 TFs) + # =========================================================================== + - stage: linux + displayName: Linux + dependsOn: [] + variables: + # KerberosDomainPassword expands at runtime in this stage from the agent-at-sqldrv-ad secret + # exposed by this variable group. + - group: kv-sqldrivers-shared + jobs: + - job: linux + displayName: Linux + timeoutInMinutes: 90 + workspace: + clean: all # Purge leftovers on self-hosted agents to reduce cross-run flakiness + strategy: + matrix: + net8: + testFramework: net8.0 + net9: + testFramework: net9.0 + net10: + testFramework: net10.0 + pool: + name: ADO-Trusted-Linux-WestUS2 + demands: + - ImageOverride -equals ADO-UB20-SQL22 + + steps: + + - checkout: self + clean: true + fetchDepth: 1 + fetchTags: false + + # --- Install .NET SDK and runtimes --- + - template: /eng/pipelines/steps/install-dotnet.yml@self + parameters: + runtimes: [8.x, 9.x] + + # --- Update test configuration (with Kerberos credentials) --- + - pwsh: | + $jdata = Get-Content -Raw "config.default.json" | ConvertFrom-Json + foreach ($p in $jdata) { + $p.TCPConnectionString = $env:REMOTE_TCP_CONN_STRING + $p.NPConnectionString = $env:REMOTE_NP_CONN_STRING + $p.SupportsIntegratedSecurity = $true + } + $jdata | Add-Member -NotePropertyName "KerberosDomainUser" -NotePropertyValue $env:KERBEROS_DOMAIN_USER -Force + $jdata | Add-Member -NotePropertyName "KerberosDomainPassword" -NotePropertyValue $env:KERBEROS_DOMAIN_PASSWORD -Force + $jdata | ConvertTo-Json | Set-Content "config.json" + workingDirectory: src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities + displayName: Update test config.json (Kerberos) + env: + REMOTE_TCP_CONN_STRING: $(REMOTE_TCP_CONN_STRING) + REMOTE_NP_CONN_STRING: $(REMOTE_NP_CONN_STRING) + KERBEROS_DOMAIN_USER: $(KerberosDomainUser) + KERBEROS_DOMAIN_PASSWORD: $(KerberosDomainPassword) + + # --- Kerberos domain join --- + - template: /eng/pipelines/kerberos/linux-init-step.yml@self + parameters: + kerberosDomain: $(KerberosDomain) + kerberosDomainOU: $(KerberosDomainOU) + kerberosDomainUser: $(KerberosDomainUser) + kerberosDomainPassword: $(KerberosDomainPassword) + + # --- Verify SQL connectivity --- + - pwsh: | + Install-Module -Name SqlServer -Force -Confirm:$false + Import-Module SqlServer + Invoke-Sqlcmd -Query "SELECT @@VERSION, @@SERVERNAME" -ConnectionString $env:REMOTE_TCP_CONN_STRING + displayName: Verify SQL connectivity + env: + REMOTE_TCP_CONN_STRING: $(REMOTE_TCP_CONN_STRING) + + # --- Build and test --- + - template: /eng/pipelines/kerberos/build-and-test-steps.yml@self + parameters: + buildTarget: BuildSqlClientUnix + testFramework: $(testFramework) + testRunTitle: Linux-$(testFramework) + artifactName: $(testFramework)-linux-$(System.JobId) + + # --- Kerberos cleanup (always runs) --- + - template: /eng/pipelines/kerberos/linux-cleanup-step.yml@self + parameters: + kerberosDomain: $(KerberosDomain) + kerberosDomainUser: $(KerberosDomainUser) + kerberosDomainPassword: $(KerberosDomainPassword) + + # =========================================================================== + # Stage 3 — Merge Code Coverage (1 job) + # =========================================================================== + - stage: merge + displayName: Merge Code Coverage + dependsOn: + - windows + - linux + condition: succeededOrFailed() + jobs: + - template: /eng/pipelines/common/templates/jobs/ci-code-coverage-job.yml@self + parameters: + upload: false diff --git a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml index ba2ff262bf..be7e2e9e3b 100644 --- a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml @@ -51,6 +51,7 @@ pr: - global.json - NuGet.config exclude: + - eng/pipelines/kerberos/* - eng/pipelines/onebranch/* - eng/pipelines/stress/* diff --git a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml index a928175f17..30950ec21c 100644 --- a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml @@ -51,6 +51,7 @@ pr: - global.json - NuGet.config exclude: + - eng/pipelines/kerberos/* - eng/pipelines/onebranch/* - eng/pipelines/stress/*