diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 056926a..c636d47 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,139 @@ 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
+ inlineScript: deployment/create-container.ps1 -StorageAccountName $(storageAccountName)
+ workingDirectory: $(Build.SourcesDirectory)
- 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)
+ # FIXME: Health endpoints disabled till they can be audited: "https://$(stagingHost)/health", "https://$(stagingHost)/health/alive"
- 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)/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
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"
}
}
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
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
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 eaa62d9..6e653bd 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)
@@ -58,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/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);
diff --git a/src/SourceBrowser/src/SourceIndexServer/Startup.cs b/src/SourceBrowser/src/SourceIndexServer/Startup.cs
index 62469c5..782b8af 100644
--- a/src/SourceBrowser/src/SourceIndexServer/Startup.cs
+++ b/src/SourceBrowser/src/SourceIndexServer/Startup.cs
@@ -1,6 +1,11 @@
-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.Http;
+using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.FileProviders.Physical;
@@ -34,6 +39,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 +56,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 +92,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();
});
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 @@
-
+