jellyfin: remove retired oidc pipeline artifact
This commit is contained in:
parent
9ca75d3fb3
commit
3bc1a7eb40
683
services/jellyfin/oidc/Jenkinsfile
vendored
683
services/jellyfin/oidc/Jenkinsfile
vendored
@ -1,683 +0,0 @@
|
|||||||
pipeline {
|
|
||||||
agent {
|
|
||||||
kubernetes {
|
|
||||||
yaml """
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Pod
|
|
||||||
spec:
|
|
||||||
restartPolicy: Never
|
|
||||||
containers:
|
|
||||||
- name: dotnet
|
|
||||||
image: mcr.microsoft.com/dotnet/sdk:9.0
|
|
||||||
command:
|
|
||||||
- cat
|
|
||||||
tty: true
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parameters {
|
|
||||||
string(name: 'HARBOR_REPO', defaultValue: 'registry.bstein.dev/streaming/oidc-plugin', description: 'OCI repository for the plugin artifact')
|
|
||||||
string(name: 'JELLYFIN_VERSION', defaultValue: '10.11.5', description: 'Jellyfin version to tag the plugin with')
|
|
||||||
string(name: 'PLUGIN_VERSION', defaultValue: '1.0.2.0', description: 'Plugin version')
|
|
||||||
}
|
|
||||||
environment {
|
|
||||||
ORAS_VERSION = "1.2.0"
|
|
||||||
DOTNET_CLI_TELEMETRY_OPTOUT = "1"
|
|
||||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "1"
|
|
||||||
SUITE_NAME = "jellyfin-oidc-plugin"
|
|
||||||
PUSHGATEWAY_URL = "http://platform-quality-gateway.monitoring.svc.cluster.local:9091"
|
|
||||||
}
|
|
||||||
stages {
|
|
||||||
stage('Checkout') {
|
|
||||||
steps {
|
|
||||||
container('dotnet') {
|
|
||||||
checkout scm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Build plugin') {
|
|
||||||
steps {
|
|
||||||
container('dotnet') {
|
|
||||||
sh '''
|
|
||||||
set -eu
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y --no-install-recommends zip curl ca-certificates git
|
|
||||||
WORKDIR="$(pwd)/build"
|
|
||||||
SRC_DIR="${WORKDIR}/src"
|
|
||||||
DIST_DIR="${WORKDIR}/dist"
|
|
||||||
ART_DIR="${WORKDIR}/artifact"
|
|
||||||
rm -rf "${SRC_DIR}" "${DIST_DIR}" "${ART_DIR}"
|
|
||||||
mkdir -p "${SRC_DIR}" "${DIST_DIR}" "${ART_DIR}"
|
|
||||||
git clone https://github.com/lolerskatez/JellyfinOIDCPlugin.git "${SRC_DIR}"
|
|
||||||
cd "${SRC_DIR}"
|
|
||||||
# Override controllers to avoid DI version issues and add injection script
|
|
||||||
cat > Controllers/OidcController.cs <<'EOF'
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using IdentityModel.OidcClient;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace JellyfinOIDCPlugin.Controllers;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/oidc")]
|
|
||||||
public class OidcController : ControllerBase
|
|
||||||
{
|
|
||||||
private IUserManager UserManager => HttpContext.RequestServices.GetRequiredService<IUserManager>();
|
|
||||||
private static readonly Dictionary<string, object> StateManager = new(); // Store AuthorizeState objects
|
|
||||||
|
|
||||||
[HttpGet("start")]
|
|
||||||
public async Task<IActionResult> Start()
|
|
||||||
{
|
|
||||||
var config = Plugin.Instance?.Configuration;
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
return BadRequest("Plugin not initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = new OidcClientOptions
|
|
||||||
{
|
|
||||||
Authority = config.OidEndpoint?.Trim(),
|
|
||||||
ClientId = config.OidClientId?.Trim(),
|
|
||||||
ClientSecret = config.OidSecret?.Trim(),
|
|
||||||
RedirectUri = GetRedirectUri(),
|
|
||||||
Scope = string.Join(" ", config.OidScopes)
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var client = new OidcClient(options);
|
|
||||||
var result = await client.PrepareLoginAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Store the authorize state for the callback
|
|
||||||
var stateString = (string)result.GetType().GetProperty("State")?.GetValue(result);
|
|
||||||
if (!string.IsNullOrEmpty(stateString))
|
|
||||||
{
|
|
||||||
StateManager[stateString] = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var startUrl = (string)result.GetType().GetProperty("StartUrl")?.GetValue(result);
|
|
||||||
if (string.IsNullOrEmpty(startUrl))
|
|
||||||
{
|
|
||||||
Console.WriteLine("OIDC: Could not get StartUrl from OIDC result");
|
|
||||||
return BadRequest("OIDC initialization failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Redirect(startUrl);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC start error: {ex}");
|
|
||||||
return BadRequest("OIDC error: " + ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("callback")]
|
|
||||||
public async Task<IActionResult> Callback()
|
|
||||||
{
|
|
||||||
var config = Plugin.Instance?.Configuration;
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
return BadRequest("Plugin not initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var stateParam = Request.Query["state"].ToString();
|
|
||||||
if (string.IsNullOrEmpty(stateParam) || !StateManager.TryGetValue(stateParam, out var storedState))
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC: Invalid state {stateParam}");
|
|
||||||
return BadRequest("Invalid state");
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = new OidcClientOptions
|
|
||||||
{
|
|
||||||
Authority = config.OidEndpoint?.Trim(),
|
|
||||||
ClientId = config.OidClientId?.Trim(),
|
|
||||||
ClientSecret = config.OidSecret?.Trim(),
|
|
||||||
RedirectUri = GetRedirectUri(),
|
|
||||||
Scope = string.Join(" ", config.OidScopes)
|
|
||||||
};
|
|
||||||
|
|
||||||
var client = new OidcClient(options);
|
|
||||||
// Cast stored state to AuthorizeState - it's stored as object
|
|
||||||
var authorizeState = (AuthorizeState)storedState;
|
|
||||||
var result = await client.ProcessResponseAsync(Request.QueryString.Value, authorizeState).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (result.IsError)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC callback failed: {result.Error} - {result.ErrorDescription}");
|
|
||||||
return BadRequest("OIDC authentication failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get email from claims
|
|
||||||
var email = result.User?.FindFirst("email")?.Value ??
|
|
||||||
result.User?.FindFirst("preferred_username")?.Value ??
|
|
||||||
result.User?.FindFirst("sub")?.Value;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(email))
|
|
||||||
{
|
|
||||||
Console.WriteLine("OIDC: No email/username found in OIDC response");
|
|
||||||
return BadRequest("No email/username found in OIDC response");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get or create user
|
|
||||||
var user = UserManager.GetUserByName(email);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC: Creating new user {email}");
|
|
||||||
user = await UserManager.CreateUserAsync(email).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set authentication provider
|
|
||||||
user.AuthenticationProviderId = "OIDC";
|
|
||||||
|
|
||||||
// Get roles from claims
|
|
||||||
var rolesClaimValue = result.User?.FindFirst(config.RoleClaim)?.Value;
|
|
||||||
var roles = string.IsNullOrEmpty(rolesClaimValue)
|
|
||||||
? Array.Empty<string>()
|
|
||||||
: rolesClaimValue.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
// Set permissions based on groups
|
|
||||||
var isAdmin = roles.Any(r => r.Equals("admin", StringComparison.OrdinalIgnoreCase));
|
|
||||||
var isPowerUser = roles.Any(r => r.Equals("Power User", StringComparison.OrdinalIgnoreCase)) && !isAdmin;
|
|
||||||
|
|
||||||
Console.WriteLine($"OIDC: User {email} authenticated. Admin: {isAdmin}, PowerUser: {isPowerUser}");
|
|
||||||
|
|
||||||
// Update user in database
|
|
||||||
await UserManager.UpdateUserAsync(user).ConfigureAwait(false);
|
|
||||||
|
|
||||||
StateManager.Remove(stateParam);
|
|
||||||
|
|
||||||
// Redirect to Jellyfin main page
|
|
||||||
return Redirect("/");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC callback error: {ex}");
|
|
||||||
return BadRequest("OIDC error: " + ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("token")]
|
|
||||||
public async Task<IActionResult> ExchangeToken([FromBody] TokenExchangeRequest request)
|
|
||||||
{
|
|
||||||
var config = Plugin.Instance?.Configuration;
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("OIDC: Plugin not initialized");
|
|
||||||
return BadRequest("Plugin not initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request?.AccessToken))
|
|
||||||
{
|
|
||||||
Console.WriteLine("OIDC: No access token provided");
|
|
||||||
return BadRequest("Access token is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.WriteLine("OIDC: Processing token exchange request");
|
|
||||||
|
|
||||||
// Validate the token with the OIDC provider using UserInfo endpoint
|
|
||||||
var options = new OidcClientOptions
|
|
||||||
{
|
|
||||||
Authority = config.OidEndpoint?.Trim(),
|
|
||||||
ClientId = config.OidClientId?.Trim(),
|
|
||||||
ClientSecret = config.OidSecret?.Trim(),
|
|
||||||
Scope = string.Join(" ", config.OidScopes)
|
|
||||||
};
|
|
||||||
|
|
||||||
var client = new OidcClient(options);
|
|
||||||
|
|
||||||
// Use the access token to get user info
|
|
||||||
var userInfoResult = await client.GetUserInfoAsync(request.AccessToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (userInfoResult.IsError)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC: Failed to get user info: {userInfoResult.Error}");
|
|
||||||
return Unauthorized("Invalid access token");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract email/username from user info
|
|
||||||
var email = userInfoResult.Claims.FirstOrDefault(c => c.Type == "email")?.Value ??
|
|
||||||
userInfoResult.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value ??
|
|
||||||
userInfoResult.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(email))
|
|
||||||
{
|
|
||||||
Console.WriteLine("OIDC: No email/username found in token");
|
|
||||||
return BadRequest("No email/username found in token");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get or create user
|
|
||||||
var user = UserManager.GetUserByName(email);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
if (!config.AutoCreateUser)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC: User {email} not found and auto-create disabled");
|
|
||||||
return Unauthorized("User does not exist and auto-creation is disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($"OIDC: Creating new user from token {email}");
|
|
||||||
user = await UserManager.CreateUserAsync(email).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update user authentication provider
|
|
||||||
user.AuthenticationProviderId = "OIDC";
|
|
||||||
|
|
||||||
// Get roles from claims
|
|
||||||
var rolesClaimName = config.RoleClaim ?? "groups";
|
|
||||||
var rolesClaimValue = userInfoResult.Claims.FirstOrDefault(c => c.Type == rolesClaimName)?.Value;
|
|
||||||
var roles = string.IsNullOrEmpty(rolesClaimValue)
|
|
||||||
? Array.Empty<string>()
|
|
||||||
: rolesClaimValue.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
// Set permissions based on groups
|
|
||||||
var isAdmin = roles.Any(r => r.Equals("admin", StringComparison.OrdinalIgnoreCase));
|
|
||||||
var isPowerUser = roles.Any(r => r.Equals("Power User", StringComparison.OrdinalIgnoreCase)) && !isAdmin;
|
|
||||||
|
|
||||||
Console.WriteLine($"OIDC: Token exchange for {email} Admin:{isAdmin} Power:{isPowerUser}");
|
|
||||||
|
|
||||||
// Update user in database
|
|
||||||
await UserManager.UpdateUserAsync(user).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Return success with user info
|
|
||||||
return Ok(new TokenExchangeResponse
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
UserId = user.Id.ToString(),
|
|
||||||
Username = user.Username,
|
|
||||||
Email = email,
|
|
||||||
IsAdmin = isAdmin,
|
|
||||||
Message = "User authenticated successfully"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC token exchange error: {ex}");
|
|
||||||
return StatusCode(500, $"Token exchange failed: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetRedirectUri()
|
|
||||||
{
|
|
||||||
var configured = Plugin.Instance?.Configuration?.RedirectUri;
|
|
||||||
if (!string.IsNullOrWhiteSpace(configured))
|
|
||||||
{
|
|
||||||
return configured!;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{Request.Scheme}://{Request.Host}/api/oidc/callback";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TokenExchangeRequest
|
|
||||||
{
|
|
||||||
public string? AccessToken { get; set; }
|
|
||||||
public string? IdToken { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TokenExchangeResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string? UserId { get; set; }
|
|
||||||
public string? Username { get; set; }
|
|
||||||
public string? Email { get; set; }
|
|
||||||
public bool IsAdmin { get; set; }
|
|
||||||
public string? Message { get; set; }
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > Controllers/OidcStaticController.cs <<'EOF'
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
|
||||||
using MediaBrowser.Common.Plugins;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace JellyfinOIDCPlugin.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/oidc")]
|
|
||||||
public class OidcStaticController : ControllerBase
|
|
||||||
{
|
|
||||||
[HttpGet("login.js")]
|
|
||||||
public IActionResult GetLoginScript()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.oidc-login.js");
|
|
||||||
if (stream == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("OIDC: Login script resource not found");
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
using var reader = new StreamReader(stream);
|
|
||||||
var content = reader.ReadToEnd();
|
|
||||||
|
|
||||||
return Content(content, "application/javascript");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC: Error serving login script {ex}");
|
|
||||||
return StatusCode(500, "Error loading login script");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("loader.js")]
|
|
||||||
public IActionResult GetLoader()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.oidc-loader.js");
|
|
||||||
if (stream == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("OIDC: Loader script resource not found");
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
using var reader = new StreamReader(stream);
|
|
||||||
var content = reader.ReadToEnd();
|
|
||||||
|
|
||||||
return Content(content, "application/javascript");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC: Error serving loader script {ex}");
|
|
||||||
return StatusCode(500, "Error loading loader script");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("inject")]
|
|
||||||
public IActionResult GetInject()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var script = @"
|
|
||||||
(function() {
|
|
||||||
console.log('[OIDC Plugin] Bootstrap inject started');
|
|
||||||
|
|
||||||
// Load oidc-loader.js dynamically
|
|
||||||
const loaderScript = document.createElement('script');
|
|
||||||
loaderScript.src = '/api/oidc/loader.js';
|
|
||||||
loaderScript.type = 'application/javascript';
|
|
||||||
loaderScript.onerror = function() {
|
|
||||||
console.error('[OIDC Plugin] Failed to load loader.js');
|
|
||||||
};
|
|
||||||
loaderScript.onload = function() {
|
|
||||||
console.log('[OIDC Plugin] Loader.js loaded successfully');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Append to head or body
|
|
||||||
const target = document.head || document.documentElement;
|
|
||||||
target.appendChild(loaderScript);
|
|
||||||
|
|
||||||
console.log('[OIDC Plugin] Bootstrap script appended to page');
|
|
||||||
})();
|
|
||||||
";
|
|
||||||
return Content(script, "application/javascript");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC: Error serving inject script {ex}");
|
|
||||||
return StatusCode(500, "Error loading inject script");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("global.js")]
|
|
||||||
public IActionResult GetGlobalInjector()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.oidc-global-injector.js");
|
|
||||||
if (stream == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("OIDC: Global injector resource not found");
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
using var reader = new StreamReader(stream);
|
|
||||||
var content = reader.ReadToEnd();
|
|
||||||
|
|
||||||
return Content(content, "application/javascript");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC: Error serving global injector {ex}");
|
|
||||||
return StatusCode(500, "Error loading global injector");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("config")]
|
|
||||||
public IActionResult GetConfigurationPage()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.configurationpage.html");
|
|
||||||
if (stream == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("OIDC: Configuration page resource not found");
|
|
||||||
return NotFound("Configuration page resource not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
using var reader = new StreamReader(stream);
|
|
||||||
var content = reader.ReadToEnd();
|
|
||||||
|
|
||||||
return Content(content, "text/html");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"OIDC: Error serving configuration page {ex}");
|
|
||||||
return StatusCode(500, $"Error loading configuration page: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
cat > JellyfinOIDCPlugin.csproj <<'EOF'
|
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<AssemblyName>JellyfinOIDCPlugin.v2</AssemblyName>
|
|
||||||
<RootNamespace>JellyfinOIDCPlugin</RootNamespace>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<AssemblyVersion>1.0.2.0</AssemblyVersion>
|
|
||||||
<FileVersion>1.0.2.0</FileVersion>
|
|
||||||
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.11.5">
|
|
||||||
<ExcludeAssets>runtime</ExcludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.11.5">
|
|
||||||
<ExcludeAssets>runtime</ExcludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Jellyfin.Common" Version="10.11.5">
|
|
||||||
<ExcludeAssets>runtime</ExcludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Jellyfin.Data" Version="10.11.5">
|
|
||||||
<ExcludeAssets>runtime</ExcludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Jellyfin.Database.Implementations" Version="10.11.5">
|
|
||||||
<ExcludeAssets>runtime</ExcludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="IdentityModel.OidcClient" Version="5.2.1">
|
|
||||||
<PrivateAssets>none</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11">
|
|
||||||
<ExcludeAssets>runtime</ExcludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="web\\*.html" />
|
|
||||||
<EmbeddedResource Include="web\\*.js" />
|
|
||||||
<EmbeddedResource Include="web\\*.css" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
EOF
|
|
||||||
dotnet restore
|
|
||||||
dotnet publish -c Release --no-self-contained -o "${DIST_DIR}"
|
|
||||||
cd "${DIST_DIR}"
|
|
||||||
zip -r "${ART_DIR}/OIDC_Authentication_${PLUGIN_VERSION}-net9.zip" .
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Quality gate smoke tests') {
|
|
||||||
steps {
|
|
||||||
container('dotnet') {
|
|
||||||
sh '''
|
|
||||||
set -eu
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y --no-install-recommends unzip
|
|
||||||
WORKDIR="$(pwd)/build"
|
|
||||||
ARTIFACT="${WORKDIR}/artifact/OIDC_Authentication_${PLUGIN_VERSION}-net9.zip"
|
|
||||||
test -s "${ARTIFACT}"
|
|
||||||
unzip -l "${ARTIFACT}" > "${WORKDIR}/artifact-list.txt"
|
|
||||||
grep -q 'JellyfinOIDCPlugin.v2.dll' "${WORKDIR}/artifact-list.txt"
|
|
||||||
cat > "${WORKDIR}/quality-summary.env" <<'EOF'
|
|
||||||
tests=1
|
|
||||||
passed=1
|
|
||||||
failed=0
|
|
||||||
errors=0
|
|
||||||
skipped=0
|
|
||||||
EOF
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Push to Harbor') {
|
|
||||||
steps {
|
|
||||||
container('dotnet') {
|
|
||||||
withCredentials([usernamePassword(credentialsId: 'harbor-robot', usernameVariable: 'HARBOR_USERNAME', passwordVariable: 'HARBOR_PASSWORD')]) {
|
|
||||||
sh '''
|
|
||||||
set -eu
|
|
||||||
WORKDIR="$(pwd)/build"
|
|
||||||
ORAS_BIN="/usr/local/bin/oras"
|
|
||||||
curl -sSL "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_amd64.tar.gz" | tar -xz -C /usr/local/bin oras
|
|
||||||
ref_host="$(echo "${HARBOR_REPO}" | cut -d/ -f1)"
|
|
||||||
"${ORAS_BIN}" login "${ref_host}" -u "${HARBOR_USERNAME}" -p "${HARBOR_PASSWORD}"
|
|
||||||
artifact="${WORKDIR}/artifact/OIDC_Authentication_${PLUGIN_VERSION}-net9.zip"
|
|
||||||
"${ORAS_BIN}" push "${HARBOR_REPO}:${JELLYFIN_VERSION}" "${artifact}:application/zip" --artifact-type application/zip --disable-path-validation
|
|
||||||
"${ORAS_BIN}" push "${HARBOR_REPO}:latest" "${artifact}:application/zip" --artifact-type application/zip --disable-path-validation
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
post {
|
|
||||||
always {
|
|
||||||
script {
|
|
||||||
env.QUALITY_STATUS = currentBuild.currentResult == 'SUCCESS' ? 'ok' : 'failed'
|
|
||||||
}
|
|
||||||
container('dotnet') {
|
|
||||||
sh '''
|
|
||||||
set -eu
|
|
||||||
apt-get update >/dev/null 2>&1
|
|
||||||
apt-get install -y --no-install-recommends python3 >/dev/null 2>&1
|
|
||||||
python3 - <<'PY'
|
|
||||||
import os
|
|
||||||
import urllib.request
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
suite = os.getenv("SUITE_NAME", "jellyfin-oidc-plugin")
|
|
||||||
status = os.getenv("QUALITY_STATUS", "failed")
|
|
||||||
gateway = os.getenv("PUSHGATEWAY_URL", "http://platform-quality-gateway.monitoring.svc.cluster.local:9091").rstrip("/")
|
|
||||||
summary_path = Path("build/quality-summary.env")
|
|
||||||
|
|
||||||
totals = {"tests": 1, "passed": 0, "failed": 1, "errors": 0, "skipped": 0}
|
|
||||||
if summary_path.exists():
|
|
||||||
for raw in summary_path.read_text(encoding="utf-8").splitlines():
|
|
||||||
if "=" not in raw:
|
|
||||||
continue
|
|
||||||
key, value = raw.split("=", 1)
|
|
||||||
key = key.strip()
|
|
||||||
if key in totals:
|
|
||||||
try:
|
|
||||||
totals[key] = int(float(value.strip()))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if status != "ok":
|
|
||||||
totals["failed"] = max(totals["failed"], 1)
|
|
||||||
totals["passed"] = 0
|
|
||||||
|
|
||||||
def read_metrics() -> str:
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(f"{gateway}/metrics", timeout=10) as resp:
|
|
||||||
return resp.read().decode("utf-8", errors="replace")
|
|
||||||
except Exception:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def read_counter(text: str, counter_status: str) -> float:
|
|
||||||
for line in text.splitlines():
|
|
||||||
if not line.startswith("platform_quality_gate_runs_total{"):
|
|
||||||
continue
|
|
||||||
if 'job="platform-quality-ci"' not in line:
|
|
||||||
continue
|
|
||||||
if f'suite="{suite}"' not in line:
|
|
||||||
continue
|
|
||||||
if f'status="{counter_status}"' not in line:
|
|
||||||
continue
|
|
||||||
parts = line.split()
|
|
||||||
if len(parts) < 2:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
return float(parts[1])
|
|
||||||
except ValueError:
|
|
||||||
return 0.0
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
metrics = read_metrics()
|
|
||||||
ok_count = read_counter(metrics, "ok")
|
|
||||||
failed_count = read_counter(metrics, "failed")
|
|
||||||
if status == "ok":
|
|
||||||
ok_count += 1
|
|
||||||
else:
|
|
||||||
failed_count += 1
|
|
||||||
|
|
||||||
payload = "\n".join(
|
|
||||||
[
|
|
||||||
"# TYPE platform_quality_gate_runs_total counter",
|
|
||||||
f'platform_quality_gate_runs_total{{suite="{suite}",status="ok"}} {ok_count:.0f}',
|
|
||||||
f'platform_quality_gate_runs_total{{suite="{suite}",status="failed"}} {failed_count:.0f}',
|
|
||||||
"# TYPE jellyfin_oidc_quality_gate_tests_total gauge",
|
|
||||||
f'jellyfin_oidc_quality_gate_tests_total{{suite="{suite}",result="passed"}} {totals["passed"]}',
|
|
||||||
f'jellyfin_oidc_quality_gate_tests_total{{suite="{suite}",result="failed"}} {totals["failed"]}',
|
|
||||||
f'jellyfin_oidc_quality_gate_tests_total{{suite="{suite}",result="error"}} {totals["errors"]}',
|
|
||||||
f'jellyfin_oidc_quality_gate_tests_total{{suite="{suite}",result="skipped"}} {totals["skipped"]}',
|
|
||||||
]
|
|
||||||
) + "\n"
|
|
||||||
|
|
||||||
req = urllib.request.Request(
|
|
||||||
f"{gateway}/metrics/job/platform-quality-ci/suite/{suite}",
|
|
||||||
data=payload.encode("utf-8"),
|
|
||||||
method="POST",
|
|
||||||
headers={"Content-Type": "text/plain"},
|
|
||||||
)
|
|
||||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
||||||
if resp.status >= 400:
|
|
||||||
raise RuntimeError(f"push failed: {resp.status}")
|
|
||||||
PY
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
container('dotnet') {
|
|
||||||
archiveArtifacts artifacts: 'build/artifact/*.zip,build/artifact-list.txt,build/quality-summary.env', allowEmptyArchive: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user