Frontend gaps fill work. Testing fixes work. Auditing in progress.

This commit is contained in:
StellaOps Bot
2025-12-30 01:22:58 +02:00
parent 1dc4bcbf10
commit 7a5210e2aa
928 changed files with 183942 additions and 3941 deletions

View File

@@ -0,0 +1,192 @@
using System.Net.Http.Headers;
using System.Text.Json;
using StellaOps.Integrations.Contracts;
using StellaOps.Integrations.Core;
namespace StellaOps.Integrations.Plugin.GitHubApp;
/// <summary>
/// GitHub App connector plugin for SCM integration.
/// Supports GitHub.com and GitHub Enterprise Server.
/// </summary>
public sealed class GitHubAppConnectorPlugin : IIntegrationConnectorPlugin
{
public string Name => "github-app";
public IntegrationType Type => IntegrationType.Scm;
public IntegrationProvider Provider => IntegrationProvider.GitHubApp;
public bool IsAvailable(IServiceProvider services) => true;
public async Task<TestConnectionResult> TestConnectionAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
{
var startTime = DateTimeOffset.UtcNow;
using var client = CreateHttpClient(config);
try
{
// Call GitHub API to verify authentication
var response = await client.GetAsync("/app", cancellationToken);
var duration = DateTimeOffset.UtcNow - startTime;
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync(cancellationToken);
var app = JsonSerializer.Deserialize<GitHubAppResponse>(content, JsonOptions);
return new TestConnectionResult(
Success: true,
Message: $"Connected as GitHub App: {app?.Name}",
Details: new Dictionary<string, string>
{
["endpoint"] = config.Endpoint,
["appName"] = app?.Name ?? "unknown",
["appId"] = app?.Id.ToString() ?? "unknown",
["slug"] = app?.Slug ?? "unknown"
},
Duration: duration);
}
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
return new TestConnectionResult(
Success: false,
Message: $"GitHub returned {response.StatusCode}",
Details: new Dictionary<string, string>
{
["endpoint"] = config.Endpoint,
["statusCode"] = ((int)response.StatusCode).ToString(),
["error"] = errorContent.Length > 200 ? errorContent[..200] : errorContent
},
Duration: duration);
}
catch (Exception ex)
{
var duration = DateTimeOffset.UtcNow - startTime;
return new TestConnectionResult(
Success: false,
Message: $"Connection failed: {ex.Message}",
Details: new Dictionary<string, string>
{
["endpoint"] = config.Endpoint,
["error"] = ex.GetType().Name
},
Duration: duration);
}
}
public async Task<HealthCheckResult> CheckHealthAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
{
var startTime = DateTimeOffset.UtcNow;
using var client = CreateHttpClient(config);
try
{
// Check GitHub API status
var response = await client.GetAsync("/rate_limit", cancellationToken);
var duration = DateTimeOffset.UtcNow - startTime;
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync(cancellationToken);
var rateLimit = JsonSerializer.Deserialize<GitHubRateLimitResponse>(content, JsonOptions);
var remaining = rateLimit?.Resources?.Core?.Remaining ?? 0;
var limit = rateLimit?.Resources?.Core?.Limit ?? 1;
var percentUsed = (int)((1 - (double)remaining / limit) * 100);
var status = percentUsed switch
{
< 80 => HealthStatus.Healthy,
< 95 => HealthStatus.Degraded,
_ => HealthStatus.Unhealthy
};
return new HealthCheckResult(
Status: status,
Message: $"Rate limit: {remaining}/{limit} remaining ({percentUsed}% used)",
Details: new Dictionary<string, string>
{
["remaining"] = remaining.ToString(),
["limit"] = limit.ToString(),
["percentUsed"] = percentUsed.ToString()
},
CheckedAt: DateTimeOffset.UtcNow,
Duration: duration);
}
return new HealthCheckResult(
Status: HealthStatus.Unhealthy,
Message: $"GitHub returned {response.StatusCode}",
Details: new Dictionary<string, string> { ["statusCode"] = ((int)response.StatusCode).ToString() },
CheckedAt: DateTimeOffset.UtcNow,
Duration: duration);
}
catch (Exception ex)
{
var duration = DateTimeOffset.UtcNow - startTime;
return new HealthCheckResult(
Status: HealthStatus.Unhealthy,
Message: $"Health check failed: {ex.Message}",
Details: new Dictionary<string, string> { ["error"] = ex.GetType().Name },
CheckedAt: DateTimeOffset.UtcNow,
Duration: duration);
}
}
private static HttpClient CreateHttpClient(IntegrationConfig config)
{
var baseUrl = string.IsNullOrEmpty(config.Endpoint) || config.Endpoint == "https://github.com"
? "https://api.github.com"
: config.Endpoint.TrimEnd('/') + "/api/v3";
var client = new HttpClient
{
BaseAddress = new Uri(baseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28");
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOps", "1.0"));
// Add JWT token if provided (GitHub App authentication)
if (!string.IsNullOrEmpty(config.ResolvedSecret))
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", config.ResolvedSecret);
}
return client;
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private sealed class GitHubAppResponse
{
public long Id { get; set; }
public string? Name { get; set; }
public string? Slug { get; set; }
}
private sealed class GitHubRateLimitResponse
{
public GitHubResources? Resources { get; set; }
}
private sealed class GitHubResources
{
public GitHubRateLimit? Core { get; set; }
}
private sealed class GitHubRateLimit
{
public int Limit { get; set; }
public int Remaining { get; set; }
public int Reset { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<RootNamespace>StellaOps.Integrations.Plugin.GitHubApp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Integrations.Contracts\StellaOps.Integrations.Contracts.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,166 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using StellaOps.Integrations.Contracts;
using StellaOps.Integrations.Core;
namespace StellaOps.Integrations.Plugin.Harbor;
/// <summary>
/// Harbor container registry connector plugin.
/// Supports Harbor v2.x API.
/// </summary>
public sealed class HarborConnectorPlugin : IIntegrationConnectorPlugin
{
public string Name => "harbor";
public IntegrationType Type => IntegrationType.Registry;
public IntegrationProvider Provider => IntegrationProvider.Harbor;
public bool IsAvailable(IServiceProvider services) => true;
public async Task<TestConnectionResult> TestConnectionAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
{
var startTime = DateTimeOffset.UtcNow;
using var client = CreateHttpClient(config);
try
{
// Call Harbor health endpoint
var response = await client.GetAsync("/api/v2.0/health", cancellationToken);
var duration = DateTimeOffset.UtcNow - startTime;
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync(cancellationToken);
var health = JsonSerializer.Deserialize<HarborHealthResponse>(content, JsonOptions);
return new TestConnectionResult(
Success: health?.Status == "healthy",
Message: health?.Status == "healthy" ? "Harbor connection successful" : $"Harbor unhealthy: {health?.Status}",
Details: new Dictionary<string, string>
{
["endpoint"] = config.Endpoint,
["status"] = health?.Status ?? "unknown",
["version"] = response.Headers.TryGetValues("X-Harbor-Version", out var versions)
? versions.FirstOrDefault() ?? "unknown"
: "unknown"
},
Duration: duration);
}
return new TestConnectionResult(
Success: false,
Message: $"Harbor returned {response.StatusCode}",
Details: new Dictionary<string, string>
{
["endpoint"] = config.Endpoint,
["statusCode"] = ((int)response.StatusCode).ToString()
},
Duration: duration);
}
catch (Exception ex)
{
var duration = DateTimeOffset.UtcNow - startTime;
return new TestConnectionResult(
Success: false,
Message: $"Connection failed: {ex.Message}",
Details: new Dictionary<string, string>
{
["endpoint"] = config.Endpoint,
["error"] = ex.GetType().Name
},
Duration: duration);
}
}
public async Task<HealthCheckResult> CheckHealthAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
{
var startTime = DateTimeOffset.UtcNow;
using var client = CreateHttpClient(config);
try
{
var response = await client.GetAsync("/api/v2.0/health", cancellationToken);
var duration = DateTimeOffset.UtcNow - startTime;
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync(cancellationToken);
var health = JsonSerializer.Deserialize<HarborHealthResponse>(content, JsonOptions);
var status = health?.Status switch
{
"healthy" => HealthStatus.Healthy,
"degraded" => HealthStatus.Degraded,
_ => HealthStatus.Unhealthy
};
return new HealthCheckResult(
Status: status,
Message: $"Harbor status: {health?.Status}",
Details: health?.Components?.ToDictionary(c => c.Name, c => c.Status) ?? new Dictionary<string, string>(),
CheckedAt: DateTimeOffset.UtcNow,
Duration: duration);
}
return new HealthCheckResult(
Status: HealthStatus.Unhealthy,
Message: $"Harbor returned {response.StatusCode}",
Details: new Dictionary<string, string> { ["statusCode"] = ((int)response.StatusCode).ToString() },
CheckedAt: DateTimeOffset.UtcNow,
Duration: duration);
}
catch (Exception ex)
{
var duration = DateTimeOffset.UtcNow - startTime;
return new HealthCheckResult(
Status: HealthStatus.Unhealthy,
Message: $"Health check failed: {ex.Message}",
Details: new Dictionary<string, string> { ["error"] = ex.GetType().Name },
CheckedAt: DateTimeOffset.UtcNow,
Duration: duration);
}
}
private static HttpClient CreateHttpClient(IntegrationConfig config)
{
var client = new HttpClient
{
BaseAddress = new Uri(config.Endpoint.TrimEnd('/')),
Timeout = TimeSpan.FromSeconds(30)
};
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// Add basic auth if secret is provided
if (!string.IsNullOrEmpty(config.ResolvedSecret))
{
// Expect format: username:password
var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes(config.ResolvedSecret));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
}
return client;
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private sealed class HarborHealthResponse
{
public string? Status { get; set; }
public List<HarborHealthComponent>? Components { get; set; }
}
private sealed class HarborHealthComponent
{
public string Name { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<RootNamespace>StellaOps.Integrations.Plugin.Harbor</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Integrations.Contracts\StellaOps.Integrations.Contracts.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,61 @@
using StellaOps.Integrations.Contracts;
using StellaOps.Integrations.Core;
namespace StellaOps.Integrations.Plugin.InMemory;
/// <summary>
/// In-memory connector plugin for testing and development.
/// Always succeeds with simulated delays.
/// </summary>
public sealed class InMemoryConnectorPlugin : IIntegrationConnectorPlugin
{
public string Name => "inmemory";
public IntegrationType Type => IntegrationType.Registry;
public IntegrationProvider Provider => IntegrationProvider.InMemory;
public bool IsAvailable(IServiceProvider services) => true;
public async Task<TestConnectionResult> TestConnectionAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
{
var startTime = DateTimeOffset.UtcNow;
// Simulate network delay
await Task.Delay(100, cancellationToken);
var duration = DateTimeOffset.UtcNow - startTime;
return new TestConnectionResult(
Success: true,
Message: "In-memory connector test successful",
Details: new Dictionary<string, string>
{
["endpoint"] = config.Endpoint,
["provider"] = config.Provider.ToString(),
["simulated"] = "true"
},
Duration: duration);
}
public async Task<HealthCheckResult> CheckHealthAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
{
var startTime = DateTimeOffset.UtcNow;
// Simulate health check
await Task.Delay(50, cancellationToken);
var duration = DateTimeOffset.UtcNow - startTime;
return new HealthCheckResult(
Status: HealthStatus.Healthy,
Message: "In-memory connector is healthy",
Details: new Dictionary<string, string>
{
["endpoint"] = config.Endpoint,
["uptime"] = "simulated"
},
CheckedAt: DateTimeOffset.UtcNow,
Duration: duration);
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<RootNamespace>StellaOps.Integrations.Plugin.InMemory</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Integrations.Contracts\StellaOps.Integrations.Contracts.csproj" />
</ItemGroup>
</Project>