From c4243771745492e565da64d15c123647ac953f91 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 19 Nov 2025 16:55:29 -0500 Subject: [PATCH 1/9] deploy-storage-proxy.ps1: misc improvements --- deployment/deploy-storage-proxy.ps1 | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/deployment/deploy-storage-proxy.ps1 b/deployment/deploy-storage-proxy.ps1 index 75e612b..5d4634a 100644 --- a/deployment/deploy-storage-proxy.ps1 +++ b/deployment/deploy-storage-proxy.ps1 @@ -1,6 +1,7 @@ param( - [string]$ProxyUrlFile, + [string]$NewContainerName, [string]$ResourceGroup, + [string]$StorageAccountName, [string]$WebappName, [string]$Slot ) @@ -10,8 +11,27 @@ param( $ErrorActionPreference = "Stop" Import-Module $PSScriptRoot/util.ps1 -$proxyUrl = Get-Content -Raw $ProxyUrlFile +# validate arguments +if ([string]::IsNullOrEmpty($NewContainerName)) { + throw "NewContainerName is null or empty" +} +if ([string]::IsNullOrEmpty($ResourceGroup)) { + throw "ResourceGroup is null or empty" +} +if ([string]::IsNullOrEmpty($StorageAccountName)) { + throw "StorageAccountName is null or empty" +} +if ([string]::IsNullOrEmpty($WebappName)) { + throw "WebappName is null or empty" +} +if ([string]::IsNullOrEmpty($Slot)) { + throw "Slot is null or empty" +} + +$proxyUrl = "https://$StorageAccountName.blob.core.windows.net/$NewContainerName" + +Write-Host "Setting SOURCE_BROWSER_INDEX_PROXY_URL to: '$proxyUrl'" { az webapp config appsettings set --resource-group $ResourceGroup --name $WebappName --slot $Slot --settings "SOURCE_BROWSER_INDEX_PROXY_URL=$proxyUrl" -} | Check-Failure +} | Check-Failure \ No newline at end of file From bcb3d69f26789abad3dea442050a9905e01ae180 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 19 Nov 2025 16:56:16 -0500 Subject: [PATCH 2/9] remove un-needed script --- deployment/upload-index-to-container.ps1 | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 deployment/upload-index-to-container.ps1 diff --git a/deployment/upload-index-to-container.ps1 b/deployment/upload-index-to-container.ps1 deleted file mode 100644 index 395d89a..0000000 --- a/deployment/upload-index-to-container.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -param( - [string]$StorageAccountName, - [string]$OutFile -) - -$ErrorActionPreference = "Stop" -Import-Module $PSScriptRoot/util.ps1 - -$newContainerName = "index-$((New-Guid).ToString("N"))" - -Write-Host "Creating new container '$newContainerName'..." -{ - az storage container create --name "$newContainerName" --auth-mode login --public-access off --fail-on-exist --account-name $StorageAccountName -} | Check-Failure - -Write-Output "##vso[task.setvariable variable=NEW_CONTAINER_NAME]$newContainerName" - -"https://$StorageAccountName.blob.core.windows.net/$newContainerName" | Out-File $OutFile From d337281712e2350185f86a6e338ba4a50f549b2d Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 19 Nov 2025 16:57:05 -0500 Subject: [PATCH 3/9] Remove hardcoded values in cleanup-old-containers script. And don't fail .. if an old container cannot be removed - it does not need to block a new deployment. --- deployment/cleanup-old-containers.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/cleanup-old-containers.ps1 b/deployment/cleanup-old-containers.ps1 index 2caa692..fdf10e4 100644 --- a/deployment/cleanup-old-containers.ps1 +++ b/deployment/cleanup-old-containers.ps1 @@ -15,7 +15,7 @@ $allContainers = New-Object System.Collections.Generic.HashSet[string] Write-Host "Finding containers..." { - az storage container list --account-name netsourceindex --auth-mode login --query '[*].name' | ConvertFrom-Json | Write-Output | %{ + az storage container list --account-name $StorageAccountName --auth-mode login --query '[*].name' | ConvertFrom-Json | Write-Output | %{ $allContainers.Add($_) } | Out-Null } | Check-Failure @@ -60,7 +60,7 @@ $toDelete = New-Object System.Collections.Generic.HashSet[string] -ArgumentList $usedContainers | %{ if (-not $toDelete.Remove($_)) { - throw "Used container $_ not found, aborting." + Write-Warning "Used container $_ not found, ignoring" } } From 50c62e5fac5c999a5bd21ac20fbfaf1122bf5d02 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Fri, 21 Nov 2025 16:00:25 -0500 Subject: [PATCH 4/9] Add error handling when accessing blob storage --- .../src/SourceIndexServer/Helpers.cs | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/SourceBrowser/src/SourceIndexServer/Helpers.cs b/src/SourceBrowser/src/SourceIndexServer/Helpers.cs index eaa62d9..874e541 100644 --- a/src/SourceBrowser/src/SourceIndexServer/Helpers.cs +++ b/src/SourceBrowser/src/SourceIndexServer/Helpers.cs @@ -12,22 +12,39 @@ public static class Helpers { private static async Task ProxyRequestAsync(this HttpContext context, string targetPath, Action configureRequest = null) { - var fs = new AzureBlobFileSystem(IndexProxyUrl); - var props = fs.FileProperties(targetPath); - context.Response.Headers.Append("Content-Md5", Convert.ToBase64String(props.ContentHash)); - context.Response.Headers.Append("Content-Type", props.ContentType); - context.Response.Headers.Append("Etag", props.ETag.ToString()); - context.Response.Headers.Append("Last-Modified", props.LastModified.ToString("R")); - using (var data = fs.OpenSequentialReadStream(targetPath)) + try { - await data.CopyToAsync(context.Response.Body).ConfigureAwait(false); + var fs = new AzureBlobFileSystem(IndexProxyUrl); + var props = fs.FileProperties(targetPath); + + context.Response.Headers.Append("Content-Md5", Convert.ToBase64String(props.ContentHash)); + context.Response.Headers.Append("Content-Type", props.ContentType); + context.Response.Headers.Append("Etag", props.ETag.ToString()); + context.Response.Headers.Append("Last-Modified", props.LastModified.ToString("R")); + using (var data = fs.OpenSequentialReadStream(targetPath)) + { + await data.CopyToAsync(context.Response.Body).ConfigureAwait(false); + } + } + catch (Exception ex) + { + Program.Logger?.LogError(ex, $"ProxyRequestAsync: Failed to serve '{targetPath}' from '{IndexProxyUrl}'"); + throw new InvalidOperationException($"Failed to proxy file '{targetPath}' from storage. IndexProxyUrl: '{IndexProxyUrl}'", ex); } } private static bool FileExists(string proxyRequestPath) { - var fs = new AzureBlobFileSystem(IndexProxyUrl); - return fs.FileExists(proxyRequestPath); + try + { + var fs = new AzureBlobFileSystem(IndexProxyUrl); + return fs.FileExists(proxyRequestPath); + } + catch (Exception ex) + { + Program.Logger?.LogError(ex, $"FileExists: Error checking '{proxyRequestPath}' in '{IndexProxyUrl}'"); + return false; + } } public static async Task ServeProxiedIndex(HttpContext context, Func next) From e1f46f85e057832818c39b2f68adb79be0fc1e4f Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Fri, 21 Nov 2025 16:08:58 -0500 Subject: [PATCH 5/9] AzureBlobFileSystem - use AZURE_CLIENT_ID instead of ARM_CLIENT_ID --- .../Models/AzureBlobFileSystem.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs b/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs index 915d7e4..5b380a7 100644 --- a/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs +++ b/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs @@ -12,18 +12,14 @@ namespace Microsoft.SourceBrowser.SourceIndexServer.Models public class AzureBlobFileSystem : IFileSystem { private readonly BlobContainerClient container; - private TokenCredential credential; - private string clientId; + private readonly TokenCredential credential; public AzureBlobFileSystem(string uri) { - if (string.IsNullOrEmpty(clientId) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ARM_CLIENT_ID"))) - clientId = Environment.GetEnvironmentVariable("ARM_CLIENT_ID"); - - if (string.IsNullOrEmpty(clientId)) - credential = new AzureCliCredential(); - else - credential = new ManagedIdentityCredential(clientId); + var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"); + credential = string.IsNullOrEmpty(clientId) + ? new AzureCliCredential() + : new ManagedIdentityCredential(clientId); container = new BlobContainerClient(new Uri(uri), credential); From 626d508404dc1e2f02db26e6b067c14e5f3f08fd Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Fri, 21 Nov 2025 16:09:34 -0500 Subject: [PATCH 6/9] Add /health, /health/alive, /health/detailed endpoints --- src/SourceBrowser/src/Directory.Build.props | 1 + .../HealthChecks/HealthCheckResponseWriter.cs | 100 ++++++++++++++++++ .../HealthChecks/StorageHealthCheck.cs | 88 +++++++++++++++ .../src/SourceIndexServer/Helpers.cs | 8 +- .../src/SourceIndexServer/Startup.cs | 71 ++++++++++++- 5 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 src/SourceBrowser/src/SourceIndexServer/HealthChecks/HealthCheckResponseWriter.cs create mode 100644 src/SourceBrowser/src/SourceIndexServer/HealthChecks/StorageHealthCheck.cs diff --git a/src/SourceBrowser/src/Directory.Build.props b/src/SourceBrowser/src/Directory.Build.props index a393de4..5a9a0c2 100644 --- a/src/SourceBrowser/src/Directory.Build.props +++ b/src/SourceBrowser/src/Directory.Build.props @@ -5,6 +5,7 @@ 5 latest + $(DefineConstants);DEBUG_LOGGING diff --git a/src/SourceBrowser/src/SourceIndexServer/HealthChecks/HealthCheckResponseWriter.cs b/src/SourceBrowser/src/SourceIndexServer/HealthChecks/HealthCheckResponseWriter.cs new file mode 100644 index 0000000..c66bcab --- /dev/null +++ b/src/SourceBrowser/src/SourceIndexServer/HealthChecks/HealthCheckResponseWriter.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.SourceBrowser.SourceIndexServer.HealthChecks +{ + /// + /// Custom response writer for health check endpoints. + /// Provides detailed JSON output for diagnostics. + /// + public static class HealthCheckResponseWriter + { + public static Task WriteResponse(HttpContext context, HealthReport healthReport) + { + context.Response.ContentType = "application/json; charset=utf-8"; + + var options = new JsonWriterOptions { Indented = true }; + + using var memoryStream = new MemoryStream(); + using (var jsonWriter = new Utf8JsonWriter(memoryStream, options)) + { + jsonWriter.WriteStartObject(); + jsonWriter.WriteString("status", healthReport.Status.ToString()); + jsonWriter.WriteString("timestamp", DateTime.UtcNow); + jsonWriter.WriteNumber("total_duration_ms", healthReport.TotalDuration.TotalMilliseconds); + + jsonWriter.WriteStartObject("checks"); + + foreach (var healthReportEntry in healthReport.Entries) + { + jsonWriter.WriteStartObject(healthReportEntry.Key); + jsonWriter.WriteString("status", healthReportEntry.Value.Status.ToString()); + jsonWriter.WriteString("description", healthReportEntry.Value.Description); + jsonWriter.WriteNumber("duration_ms", healthReportEntry.Value.Duration.TotalMilliseconds); + + if (healthReportEntry.Value.Exception != null) + { + var ex = healthReportEntry.Value.Exception; + jsonWriter.WriteString("exception", ex.Message); + jsonWriter.WriteString("exception_type", ex.GetType().FullName); + jsonWriter.WriteString("stack_trace", ex.StackTrace); + + // Include inner exception details if present + if (ex.InnerException != null) + { + jsonWriter.WriteStartObject("inner_exception"); + jsonWriter.WriteString("message", ex.InnerException.Message); + jsonWriter.WriteString("type", ex.InnerException.GetType().FullName); + jsonWriter.WriteString("stack_trace", ex.InnerException.StackTrace); + jsonWriter.WriteEndObject(); + } + } + + jsonWriter.WriteStartObject("data"); + + foreach (var item in healthReportEntry.Value.Data) + { + jsonWriter.WritePropertyName(item.Key); + + JsonSerializer.Serialize(jsonWriter, item.Value, + item.Value?.GetType() ?? typeof(object)); + } + + jsonWriter.WriteEndObject(); + jsonWriter.WriteEndObject(); + } + + jsonWriter.WriteEndObject(); + jsonWriter.WriteEndObject(); + } + + return context.Response.WriteAsync( + Encoding.UTF8.GetString(memoryStream.ToArray())); + } + + public static Task WriteMinimalResponse(HttpContext context, HealthReport healthReport) + { + context.Response.ContentType = "application/json; charset=utf-8"; + + var options = new JsonWriterOptions { Indented = false }; + + using var memoryStream = new MemoryStream(); + using (var jsonWriter = new Utf8JsonWriter(memoryStream, options)) + { + jsonWriter.WriteStartObject(); + jsonWriter.WriteString("status", healthReport.Status.ToString()); + jsonWriter.WriteString("timestamp", DateTime.UtcNow); + jsonWriter.WriteEndObject(); + } + + return context.Response.WriteAsync( + Encoding.UTF8.GetString(memoryStream.ToArray())); + } + } +} diff --git a/src/SourceBrowser/src/SourceIndexServer/HealthChecks/StorageHealthCheck.cs b/src/SourceBrowser/src/SourceIndexServer/HealthChecks/StorageHealthCheck.cs new file mode 100644 index 0000000..0099e79 --- /dev/null +++ b/src/SourceBrowser/src/SourceIndexServer/HealthChecks/StorageHealthCheck.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.SourceBrowser.SourceIndexServer.Models; + +namespace Microsoft.SourceBrowser.SourceIndexServer.HealthChecks +{ + /// + /// Health check for Azure Blob Storage connectivity. + /// Verifies that the storage URL is configured and accessible. + /// + public class StorageHealthCheck : IHealthCheck + { + private readonly ILogger _logger; + + public StorageHealthCheck(ILogger logger) + { + _logger = logger; + } + + public Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + try + { + var storageUrl = Helpers.IndexProxyUrl; + + if (string.IsNullOrEmpty(storageUrl)) + { + _logger.LogWarning("Storage health check failed: SOURCE_BROWSER_INDEX_PROXY_URL not configured"); + return Task.FromResult( + HealthCheckResult.Unhealthy( + "Storage URL not configured", + data: new Dictionary + { + ["config_key"] = "SOURCE_BROWSER_INDEX_PROXY_URL" + })); + } + + // Check storage access by verifying a marker file exists + var fs = new AzureBlobFileSystem(storageUrl); + var testFile = "/.health"; + var exists = fs.FileExists(testFile); + + _logger.LogInformation( + "Storage health check passed: Storage accessible, test_file={TestFile}, exists={Exists}", + testFile, exists); + + if (!exists) + { + return Task.FromResult( + HealthCheckResult.Unhealthy( + "Storage could not be verified", + data: new Dictionary + { + ["error_type"] = "HealthMarkerMissing" + })); + } + + return Task.FromResult( + HealthCheckResult.Healthy( + "Storage accessible", + data: new Dictionary + { + ["storage_url"] = storageUrl, + ["test_file"] = testFile, + ["file_exists"] = exists + })); + } + catch (Exception ex) + { + _logger.LogError(ex, "Storage health check failed: Storage access error"); + return Task.FromResult( + HealthCheckResult.Unhealthy( + "Storage access failed", + exception: ex, + data: new Dictionary + { + ["error_type"] = ex.GetType().Name + })); + } + } + } +} diff --git a/src/SourceBrowser/src/SourceIndexServer/Helpers.cs b/src/SourceBrowser/src/SourceIndexServer/Helpers.cs index 874e541..6e653bd 100644 --- a/src/SourceBrowser/src/SourceIndexServer/Helpers.cs +++ b/src/SourceBrowser/src/SourceIndexServer/Helpers.cs @@ -75,6 +75,12 @@ public static async Task ServeProxiedIndex(HttpContext context, Func next) await context.ProxyRequestAsync(proxyRequestPathSuffix).ConfigureAwait(false); } +#if DEBUG_LOGGING + public readonly static bool DebugLoggingEnabled = true; +#else + public readonly static bool DebugLoggingEnabled; +#endif + public static string IndexProxyUrl => Environment.GetEnvironmentVariable("SOURCE_BROWSER_INDEX_PROXY_URL"); } -} \ No newline at end of file +} diff --git a/src/SourceBrowser/src/SourceIndexServer/Startup.cs b/src/SourceBrowser/src/SourceIndexServer/Startup.cs index 62469c5..0e5c1a3 100644 --- a/src/SourceBrowser/src/SourceIndexServer/Startup.cs +++ b/src/SourceBrowser/src/SourceIndexServer/Startup.cs @@ -1,7 +1,14 @@ -using System.IO; +using System.Collections; +using System.IO; +using System.Linq; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders.Physical; using Microsoft.Extensions.Hosting; @@ -34,6 +41,16 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(new Index(RootPath)); services.AddControllersWithViews(); services.AddRazorPages(); + + // Add health checks + services.AddHealthChecks() + .AddCheck( + name: "storage", + tags: ["ready"]) + .AddCheck( + name: "startup", + check: () => HealthCheckResult.Healthy("Application is running"), + tags: ["alive"]); } public string RootPath { get; set; } @@ -41,6 +58,16 @@ public void ConfigureServices(IServiceCollection services) // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + // Configure forwarded headers for Azure Front Door + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost, + KnownNetworks = { }, + KnownProxies = { } + }); + app.Use(async (context, next) => { context.Response.Headers["X-UA-Compatible"] = "IE=edge"; @@ -67,6 +94,48 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endPoints => { + const int healthCacheSeconds = 30; + + static Task CacheableMinimalResponse(HttpContext context, HealthReport report) + { + context.Response.Headers.CacheControl = $"public,max-age={healthCacheSeconds}"; + context.Response.Headers.Pragma = "public"; + context.Response.Headers.Expires = "0"; + return HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse(context, report); + } + + // Health check endpoints + // Basic health check with minimal information (cached by default) + endPoints.MapHealthChecks("/health", new HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = CacheableMinimalResponse + }); + + // Liveness probe (always healthy if app is running) + endPoints.MapHealthChecks("/health/alive", new HealthCheckOptions + { + Predicate = check => check.Tags.Contains("alive"), + ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse + }); + + if (env.IsDevelopment() || Helpers.DebugLoggingEnabled) + { + // Detailed health check with full diagnostics + endPoints.MapHealthChecks("/health/detailed", new HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteResponse + }); + + // Readiness probe (checks storage) + endPoints.MapHealthChecks("/health/ready", new HealthCheckOptions + { + Predicate = check => check.Tags.Contains("ready"), + ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse + }); + } + endPoints.MapRazorPages(); endPoints.MapControllers(); }); From 17924984b08e8c78168a7e37c908c43816cd838e Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Fri, 21 Nov 2025 15:50:44 -0500 Subject: [PATCH 7/9] fixup msbuild targets to work without any V1 repos --- src/index/index.proj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index/index.proj b/src/index/index.proj index 82124c3..9d7445f 100644 --- a/src/index/index.proj +++ b/src/index/index.proj @@ -120,7 +120,7 @@ - + $([System.String]::Copy('%(ClonedRepository.PrepareCommand)').Trim()) @@ -134,7 +134,7 @@ - + From 341f57e7e05ed7be35d83683550c27565dd57928 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Fri, 21 Nov 2025 15:57:53 -0500 Subject: [PATCH 8/9] azure-pipelines.yml: Enable publishing to the new prod setup --- azure-pipelines.yml | 179 ++++++++++++++++++++++++++------------------ 1 file changed, 106 insertions(+), 73 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 056926a..7550339 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,6 +1,6 @@ schedules: - cron: 0 10 * * * - displayName: Every day at 10:00 UTC + displayName: 🟣Every day at 10:00 UTC branches: include: - main @@ -13,12 +13,52 @@ resources: name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release +variables: +- name: system.debug + value: true +- name: azureSubscriptionForStage1Download + value: 'SourceDotNet Stage1 Publish' +- name: webAppName + value: 'netsourceindexprod' +- name: resourceGroupName + value: 'source.dot.net' + +# Note: the subscription name variables need to be at the pipeline level to be used in the AzureCLI tasks. +# that's because the tasks get looked at very early in the pipeline processing. +# Also, make sure to use the template expression syntax ${{ }} +# Issue: https://github.com/microsoft/azure-pipelines-tasks/issues/14365#issuecomment-2286398867 + +- ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranch'], 'refs/heads/main')) }}: + - name: poolName + value: 'NetSourceIndexProd-Pool' + - name: azureSubscriptionForStorageAndWebAppSlot + value: 'NetSourceIndex-Prod' + - name: isOfficialBuild + value: True + - name: temporaryDeploymentSlot + value: 'staging' + - name: storageAccountName + value: 'netsourceindexprod' + - name: stagingHost + value: 'staging.source.dot.net' +- ${{ else }}: + - name: poolName + value: 'NetSourceIndexValid-Pool' + - name: azureSubscriptionForStorageAndWebAppSlot + value: 'NetSourceIndex-Validation-Prod' + - name: isOfficialBuild + value: False + - name: temporaryDeploymentSlot + value: 'validation' + - name: storageAccountName + value: 'netsourceindexvalidprod' + extends: template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates parameters: pool: - name: NetCore1ESPool-Internal-XL - image: 1es-windows-2022 + name: ${{ variables.poolName }} + image: 1es-pt-agent-image os: windows customBuildTags: - ES365AIMigrationTooling @@ -29,25 +69,6 @@ extends: displayName: Build Source Index timeoutInMinutes: 360 - variables: - - name: system.debug - value: true - - ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranch'], 'refs/heads/main')) }}: - - name: isOfficialBuild - value: True - - name: deploymentSlot - value: staging - - name: storageAccount - value: netsourceindex - - ${{ else }}: - - name: isOfficialBuild - value: False - - name: deploymentSlot - value: validation - - name: storageAccount - value: netsourceindexvalidation - - group: source-dot-net stage1 variables - templateContext: outputs: - output: nuget @@ -67,18 +88,18 @@ extends: submodules: true - task: DeleteFiles@1 - displayName: Delete files from bin + displayName: 🟣Delete files from bin inputs: SourceFolder: bin Contents: '**/*' - task: UseDotNet@2 - displayName: Install .NET Sdk + displayName: 🟣Install .NET Sdk inputs: useGlobalJson: true - task: DotNetCoreCLI@2 - displayName: dotnet restore + displayName: 🟣dotnet restore inputs: command: custom custom: restore @@ -86,18 +107,18 @@ extends: **\*.sln - task: DotNetCoreCLI@2 - displayName: dotnet build + displayName: 🟣dotnet build inputs: command: 'build' projects: | src\source-indexer.sln src\SourceBrowser\SourceBrowser.sln - arguments: '/p:PackageOutputPath=$(Build.ArtifactStagingDirectory)/packages' + arguments: '/p:PackageOutputPath=$(Build.ArtifactStagingDirectory)/packages /p:EnableDebugLogging=true' - task: AzureCLI@2 - displayName: Log in to Azure and clone data + displayName: 🟣Clone Stage1 data inputs: - azureSubscription: 'SourceDotNet Stage1 Publish' + azureSubscription: ${{ variables.azureSubscriptionForStage1Download }} addSpnToEnvironment: true scriptType: 'ps' scriptLocation: 'inlineScript' @@ -105,129 +126,141 @@ extends: dotnet build build.proj /t:Clone /v:n /bl:$(Build.ArtifactStagingDirectory)/logs/clone.binlog /p:Stage1StorageAccount=netsourceindexstage1 /p:Stage1StorageContainer=stage1 - task: DotNetCoreCLI@2 - displayName: Prepare All Repositories + displayName: 🟣Prepare All Repositories inputs: command: 'build' projects: 'build.proj' arguments: '/t:Prepare /v:n /bl:$(Build.ArtifactStagingDirectory)/logs/prepare.binlog' - task: DotNetCoreCLI@2 - displayName: Build source index + displayName: 🟣Build source index inputs: command: 'build' projects: 'build.proj' arguments: '/t:BuildIndex /v:n /bl:$(Build.ArtifactStagingDirectory)/logs/build.binlog' - task: CopyFiles@2 + displayName: 🟣Copy webapp files inputs: sourceFolder: bin/index/ contents: | ** !index/** - targetFolder: bin/index-stage/ + targetFolder: bin/webapp-stage/ + cleanTargetFolder: true + - pwsh: New-Item -ItemType File -Force -Path bin/index/index/.health + displayName: 🟣Create .health file + - powershell: deployment/normalize-case.ps1 -Root bin/index/index/ - displayName: Normalize Case Of Index Files + displayName: 🟣Normalize Case Of Index Files - task: AzureCLI@2 - displayName: Create new storage container + displayName: 🟣Create new storage container inputs: - azureSubscription: SourceDotNet-Deployment-ARM + azureSubscription: ${{ variables.azureSubscriptionForStorageAndWebAppSlot }} scriptLocation: inlineScript scriptType: ps inlineScript: > - deployment/upload-index-to-container.ps1 - -StorageAccountName $(storageAccount) - -OutFile bin/index.url + deployment/create-container.ps1 + -StorageAccountName $(storageAccountName) - task: AzureFileCopy@6 - displayName: Upload index to Azure Storage + displayName: 🟣Upload index to Azure Storage inputs: - azureSubscription: SourceDotNet-Deployment-ARM + azureSubscription: ${{ variables.azureSubscriptionForStorageAndWebAppSlot }} SourcePath: "bin/index/index/*" Destination: AzureBlob - storage: $(storageAccount) + storage: $(storageAccountName) ContainerName: $(NEW_CONTAINER_NAME) - task: AzureRmWebAppDeployment@4 - displayName: 'Azure App Service Deploy: netsourceindex' + displayName: '🟣Azure App Service Deploy: $(temporaryDeploymentSlot) slot' inputs: ConnectionType: AzureRM - azureSubscription: SourceDotNet-Deployment-ARM + azureSubscription: ${{ variables.azureSubscriptionForStorageAndWebAppSlot }} appType: webApp - WebAppName: netsourceindex - ResourceGroupName: source.dot.net + WebAppName: ${{ variables.webAppName }} + ResourceGroupName: $(resourceGroupName) deployToSlotOrASE: true - SlotName: $(deploymentSlot) - packageForLinux: bin/index-stage/ + SlotName: $(temporaryDeploymentSlot) + packageForLinux: bin/webapp-stage/ enableCustomDeployment: true DeploymentType: zipDeploy RemoveAdditionalFilesFlag: true - task: AzureCLI@2 - displayName: Deploy Storage Proxy Url to WebApp + displayName: 🟣Deploy Storage Proxy Url to WebApp inputs: - azureSubscription: SourceDotNet-Deployment-ARM + azureSubscription: ${{ variables.azureSubscriptionForStorageAndWebAppSlot }} scriptLocation: inlineScript scriptType: ps inlineScript: > deployment/deploy-storage-proxy.ps1 - -ProxyUrlFile bin/index.url - -ResourceGroup source.dot.net - -WebappName netsourceindex - -Slot $(deploymentSlot) + -NewContainerName "$(NEW_CONTAINER_NAME)" + -ResourceGroup "$(resourceGroupName)" + -StorageAccountName "$(storageAccountName)" + -WebappName "${{ variables.webAppName }}" + -Slot "$(temporaryDeploymentSlot)" - task: AzureCLI@2 - displayName: Restart WebApp + displayName: 🟣Restart WebApp inputs: - azureSubscription: SourceDotNet-Deployment-ARM + azureSubscription: ${{ variables.azureSubscriptionForStorageAndWebAppSlot }} scriptLocation: inlineScript scriptType: ps inlineScript: | - az webapp restart --name netsourceindex --slot $(deploymentSlot) --resource-group source.dot.net + az webapp restart --name $(webAppName) --slot $(temporaryDeploymentSlot) --resource-group $(resourceGroupName) - pwsh: | Start-Sleep 60 $urls = @( - "https://netsourceindex-$(deploymentSlot).azurewebsites.net", - "https://netsourceindex-$(deploymentSlot).azurewebsites.net/System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/String.cs.html" + "https://$(stagingHost)" + "https://$(stagingHost)/health" + "https://$(stagingHost)/health/alive" + "https://$(stagingHost)/System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/String.cs.html" ) foreach ($url in $urls) { - $statusCode = Invoke-WebRequest $url -UseBasicParsing -SkipHttpErrorCheck | select -ExpandProperty StatusCode - if ($statusCode -ne 200) { - Write-Host "##vso[task.logissue type=error;]Deployed website returned undexpected status code $statusCode from url $url" - Write-Host "##vso[task.complete result=Failed;]Deployed website returned undexpected status code $statusCode from url $url" + Write-Host "Testing URL: $url" + try { + $statusCode = Invoke-WebRequest $url -UseBasicParsing -SkipHttpErrorCheck | select -ExpandProperty StatusCode + if ($statusCode -ne 200) { + Write-Error "##vso[task.logissue type=warning;]Deployed staging website returned unexpected status code $statusCode from url $url" + } + } catch { + Write-Error "##vso[task.logissue type=warning;]Failed to test URL $url : $_" } } - displayName: Test Deployed WebApp + displayName: 🟣Test Deployed WebApp + condition: and(succeeded(), eq(variables['isOfficialBuild'], 'True')) - task: AzureCLI@2 - displayName: Swap Staging Slot into Production + displayName: 🟣Swap Staging Slot into Production condition: and(succeeded(), eq(variables['isOfficialBuild'], 'True')) inputs: - azureSubscription: SourceDotNet-Deployment-ARM + azureSubscription: ${{ variables.azureSubscriptionForStorageAndWebAppSlot }} scriptLocation: inlineScript scriptType: ps inlineScript: > az webapp deployment slot swap - --resource-group source.dot.net - --name netsourceindex - --slot staging + --resource-group $(resourceGroupName) + --name $(webAppName) + --slot $(temporaryDeploymentSlot) --target-slot production - task: AzureCLI@2 - displayName: Cleanup Old Storage Containers + displayName: 🟣Cleanup Old Storage Containers condition: and(succeeded(), eq(variables['isOfficialBuild'], 'True')) inputs: - azureSubscription: SourceDotNet-Deployment-ARM + azureSubscription: ${{ variables.azureSubscriptionForStorageAndWebAppSlot }} scriptLocation: inlineScript scriptType: ps inlineScript: > deployment/cleanup-old-containers.ps1 - -ResourceGroup source.dot.net - -WebappName netsourceindex - -StorageAccountName $(storageAccount) + -ResourceGroup $(resourceGroupName) + -WebappName $(webAppName) + -StorageAccountName $(storageAccountName) - task: CopyFiles@2 displayName: Copy binlogs for upload From 49719d3f0c048c661b8155e26778887c0d09d3a1 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 26 Nov 2025 15:43:03 -0500 Subject: [PATCH 9/9] disable health endpoints --- azure-pipelines.yml | 8 +- .../src/SourceIndexServer/Startup.cs | 100 +++++++++--------- 2 files changed, 52 insertions(+), 56 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7550339..c636d47 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -162,9 +162,8 @@ extends: azureSubscription: ${{ variables.azureSubscriptionForStorageAndWebAppSlot }} scriptLocation: inlineScript scriptType: ps - inlineScript: > - deployment/create-container.ps1 - -StorageAccountName $(storageAccountName) + inlineScript: deployment/create-container.ps1 -StorageAccountName $(storageAccountName) + workingDirectory: $(Build.SourcesDirectory) - task: AzureFileCopy@6 displayName: 🟣Upload index to Azure Storage @@ -213,12 +212,11 @@ extends: inlineScript: | az webapp restart --name $(webAppName) --slot $(temporaryDeploymentSlot) --resource-group $(resourceGroupName) + # FIXME: Health endpoints disabled till they can be audited: "https://$(stagingHost)/health", "https://$(stagingHost)/health/alive" - pwsh: | Start-Sleep 60 $urls = @( "https://$(stagingHost)" - "https://$(stagingHost)/health" - "https://$(stagingHost)/health/alive" "https://$(stagingHost)/System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/String.cs.html" ) foreach ($url in $urls) { diff --git a/src/SourceBrowser/src/SourceIndexServer/Startup.cs b/src/SourceBrowser/src/SourceIndexServer/Startup.cs index 0e5c1a3..782b8af 100644 --- a/src/SourceBrowser/src/SourceIndexServer/Startup.cs +++ b/src/SourceBrowser/src/SourceIndexServer/Startup.cs @@ -4,11 +4,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders.Physical; using Microsoft.Extensions.Hosting; @@ -43,14 +41,14 @@ public void ConfigureServices(IServiceCollection services) services.AddRazorPages(); // Add health checks - services.AddHealthChecks() - .AddCheck( - name: "storage", - tags: ["ready"]) - .AddCheck( - name: "startup", - check: () => HealthCheckResult.Healthy("Application is running"), - tags: ["alive"]); + //services.AddHealthChecks() + //.AddCheck( + //name: "storage", + //tags: ["ready"]) + //.AddCheck( + //name: "startup", + //check: () => HealthCheckResult.Healthy("Application is running"), + //tags: ["alive"]); } public string RootPath { get; set; } @@ -94,47 +92,47 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endPoints => { - const int healthCacheSeconds = 30; - - static Task CacheableMinimalResponse(HttpContext context, HealthReport report) - { - context.Response.Headers.CacheControl = $"public,max-age={healthCacheSeconds}"; - context.Response.Headers.Pragma = "public"; - context.Response.Headers.Expires = "0"; - return HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse(context, report); - } - - // Health check endpoints - // Basic health check with minimal information (cached by default) - endPoints.MapHealthChecks("/health", new HealthCheckOptions - { - Predicate = _ => true, - ResponseWriter = CacheableMinimalResponse - }); - - // Liveness probe (always healthy if app is running) - endPoints.MapHealthChecks("/health/alive", new HealthCheckOptions - { - Predicate = check => check.Tags.Contains("alive"), - ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse - }); - - if (env.IsDevelopment() || Helpers.DebugLoggingEnabled) - { - // Detailed health check with full diagnostics - endPoints.MapHealthChecks("/health/detailed", new HealthCheckOptions - { - Predicate = _ => true, - ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteResponse - }); - - // Readiness probe (checks storage) - endPoints.MapHealthChecks("/health/ready", new HealthCheckOptions - { - Predicate = check => check.Tags.Contains("ready"), - ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse - }); - } + //const int healthCacheSeconds = 30; + + //static Task CacheableMinimalResponse(HttpContext context, HealthReport report) + //{ + //context.Response.Headers.CacheControl = $"public,max-age={healthCacheSeconds}"; + //context.Response.Headers.Pragma = "public"; + //context.Response.Headers.Expires = "0"; + //return HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse(context, report); + //} + + //// Health check endpoints + //// Basic health check with minimal information (cached by default) + //endPoints.MapHealthChecks("/health", new HealthCheckOptions + //{ + //Predicate = _ => true, + //ResponseWriter = CacheableMinimalResponse + //}); + + //// Liveness probe (always healthy if app is running) + //endPoints.MapHealthChecks("/health/alive", new HealthCheckOptions + //{ + //Predicate = check => check.Tags.Contains("alive"), + //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse + //}); + + //if (env.IsDevelopment() || Helpers.DebugLoggingEnabled) + //{ + //// Detailed health check with full diagnostics + //endPoints.MapHealthChecks("/health/detailed", new HealthCheckOptions + //{ + //Predicate = _ => true, + //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteResponse + //}); + + //// Readiness probe (checks storage) + //endPoints.MapHealthChecks("/health/ready", new HealthCheckOptions + //{ + //Predicate = check => check.Tags.Contains("ready"), + //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse + //}); + //} endPoints.MapRazorPages(); endPoints.MapControllers();