Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions TerraformRegistry.API/Interfaces/IDatabaseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,12 @@ public interface IDatabaseService
Task<IEnumerable<ApiKey>> GetApiKeysByPrefixAsync(string prefix);
Task UpdateApiKeyAsync(ApiKey apiKey);
Task DeleteApiKeyAsync(ApiKey apiKey);

// Provider Methods
Task<ProviderVersions?> GetProviderVersionsAsync(string @namespace, string type);
Task<ProviderPackage?> GetProviderPackageAsync(string @namespace, string type, string version, string os, string arch);
Task AddProviderPackageAsync(string @namespace, string type, string version, string os, string arch, string filename, string downloadUrl, string shasum, string protocolsJson, string signingKeyId);
Task<GpgKey?> GetGpgKeyAsync(string @namespace, string keyId);
Task<IEnumerable<GpgKey>> GetGpgKeysAsync(string @namespace);
Task AddGpgKeyAsync(GpgKey key);
}
20 changes: 20 additions & 0 deletions TerraformRegistry.API/Interfaces/IProviderService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using TerraformRegistry.Models;

namespace TerraformRegistry.API.Interfaces;

/// <summary>
/// Interface for Provider Service
/// </summary>
public interface IProviderService
{
Task<ProviderVersions?> GetProviderVersionsAsync(string @namespace, string type);
Task<ProviderPackage?> GetProviderPackageAsync(string @namespace, string type, string version, string os, string arch);
Task<ProviderPackage> UploadProviderAsync(string @namespace, string type, string version, string os, string arch, string filename, Stream stream, string shasum, string signingKeyId, List<string>? protocols = null);
Task UploadShasumsAsync(string @namespace, string type, string version, Stream stream);
Task UploadShasumsSigAsync(string @namespace, string type, string version, Stream stream);

// GPG Key Management
Task<IEnumerable<GpgKey>> GetGpgKeysAsync(string @namespace);
Task<GpgKey?> GetGpgKeyAsync(string @namespace, string keyId);
Task AddGpgKeyAsync(GpgKey key);
}
23 changes: 23 additions & 0 deletions TerraformRegistry.API/Interfaces/IProviderStorageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Security.Cryptography;
using TerraformRegistry.Models;

namespace TerraformRegistry.API.Interfaces;

/// <summary>
/// Interface for Provider Storage
/// </summary>
public interface IProviderStorageService
{
// Provider Binaries
Task<string> UploadProviderAsync(string @namespace, string type, string version, string os, string arch, Stream stream);
Task<string?> GetProviderDownloadUrlAsync(string @namespace, string type, string version, string os, string arch);

// Checksums and Signatures (Per Version)
Task UploadShasumsAsync(string @namespace, string type, string version, Stream stream);
Task UploadShasumsSigAsync(string @namespace, string type, string version, Stream stream);
Task<string?> GetShasumsDownloadUrlAsync(string @namespace, string type, string version);
Task<string?> GetShasumsSigDownloadUrlAsync(string @namespace, string type, string version);

// File Serving (For Local Storage)
Task<Stream?> GetFileStreamAsync(string relativePath);
}
174 changes: 174 additions & 0 deletions TerraformRegistry.AzureBlob/AzureBlobProviderStorageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
using Azure.Identity;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Azure.Storage.Sas;
using TerraformRegistry.API.Interfaces;

namespace TerraformRegistry.AzureBlob;

public class AzureBlobProviderStorageService : IProviderStorageService
{
private readonly BlobContainerClient _containerClient;
private readonly string _containerName;
private readonly ILogger<AzureBlobProviderStorageService> _logger;

Check failure on line 13 in TerraformRegistry.AzureBlob/AzureBlobProviderStorageService.cs

View workflow job for this annotation

GitHub Actions / test

The type or namespace name 'ILogger<>' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 13 in TerraformRegistry.AzureBlob/AzureBlobProviderStorageService.cs

View workflow job for this annotation

GitHub Actions / test

The type or namespace name 'ILogger<>' could not be found (are you missing a using directive or an assembly reference?)
private readonly int _sasTokenExpiryMinutes;

public AzureBlobProviderStorageService(
IConfiguration configuration,

Check failure on line 17 in TerraformRegistry.AzureBlob/AzureBlobProviderStorageService.cs

View workflow job for this annotation

GitHub Actions / test

The type or namespace name 'IConfiguration' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 17 in TerraformRegistry.AzureBlob/AzureBlobProviderStorageService.cs

View workflow job for this annotation

GitHub Actions / test

The type or namespace name 'IConfiguration' could not be found (are you missing a using directive or an assembly reference?)
ILogger<AzureBlobProviderStorageService> logger,

Check failure on line 18 in TerraformRegistry.AzureBlob/AzureBlobProviderStorageService.cs

View workflow job for this annotation

GitHub Actions / test

The type or namespace name 'ILogger<>' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 18 in TerraformRegistry.AzureBlob/AzureBlobProviderStorageService.cs

View workflow job for this annotation

GitHub Actions / test

The type or namespace name 'ILogger<>' could not be found (are you missing a using directive or an assembly reference?)
BlobServiceClient? blobServiceClient = null)
{
_logger = logger;

// Get Azure Storage configuration values
_containerName = configuration["AzureStorage:ContainerName"]
?? throw new ArgumentNullException("AzureStorage:ContainerName",
"Azure Storage container name is required.");

_sasTokenExpiryMinutes = int.Parse(configuration["AzureStorage:SasTokenExpiryMinutes"] ?? "5");
if (_sasTokenExpiryMinutes <= 0)
{
_logger.LogWarning(
"AzureStorage:SasTokenExpiryMinutes must be a positive integer, but was configured as {ConfiguredValue}. Defaulting to 5 minutes.",
_sasTokenExpiryMinutes);
_sasTokenExpiryMinutes = 5;
}

BlobServiceClient clientToUse;

if (blobServiceClient != null)
{
_logger.LogInformation("Using provided BlobServiceClient instance.");
clientToUse = blobServiceClient;
}
else
{
_logger.LogInformation("BlobServiceClient not provided; attempting to create one based on configuration.");
// Get Azure Storage connection settings from configuration
var connectionString = configuration["AzureStorage:ConnectionString"];
var accountName = configuration["AzureStorage:AccountName"];

// Initialize Azure Blob Storage clients
if (string.IsNullOrEmpty(connectionString))
{
if (string.IsNullOrEmpty(accountName))
{
const string errorMessage =
"Azure Storage AccountName ('AzureStorage:AccountName') is required when connection string is not provided (for Managed Identity).";
_logger.LogError(errorMessage);
throw new ArgumentNullException("AzureStorage:AccountName", errorMessage);
}

_logger.LogInformation(
"Azure Storage connection string not found. Attempting to use Managed Identity for account: {AccountName}.",
accountName);
// Use Managed Identity
var blobServiceUri = new Uri($"https://{accountName}.blob.core.windows.net");
clientToUse = new BlobServiceClient(blobServiceUri, new DefaultAzureCredential());
}
else
{
_logger.LogInformation("Using Azure Storage connection string to create BlobServiceClient.");
clientToUse = new BlobServiceClient(connectionString);
}
}

// Initialize Azure Blob Storage container client
_containerClient = clientToUse.GetBlobContainerClient(_containerName);

// Ensure container exists
_containerClient.CreateIfNotExists();
}

public async Task<string> UploadProviderAsync(string @namespace, string type, string version, string os, string arch, Stream stream)
{
var blobPath = $"providers/{@namespace}/{type}/{version}/{os}_{arch}.zip";
var blobClient = _containerClient.GetBlobClient(blobPath);

await blobClient.UploadAsync(stream, new BlobUploadOptions
{
Metadata = new Dictionary<string, string>
{
{ "namespace", @namespace },
{ "type", type },
{ "version", version },
{ "os", os },
{ "arch", arch }
}
});

// We return the blob path to be stored in DB (or valid URL if public).
// The DB expects 'download_url' in current implementation.
// For Azure Blob, usually we want a SAS URL generated on the fly (GetProviderDownloadUrlAsync).
// So here we return the blob path identifier.
// Wait, if I return blob path, the DB stores it as 'download_url'.
// My 'GetProviderPackage' calls 'GetProviderDownloadUrlAsync' or relies on DB?
// My 'ProviderHandlers' calls 'providerService.GetProviderPackageAsync'.
// 'ProviderService' calls '_db.GetProviderPackageAsync'.
// The DB returns whatever string is in 'download_url' column.

// ISSUE: If I store "providers/namespace/..." in DB, the 'download_url' field in JSON response will be that string, which is not a valid URL.
// SOLUTION: The 'ProviderService.GetProviderPackageAsync' should intercept the DB result and transform it if needed.
// Or I store a placeholder/flag in DB.
// Let's modify 'ProviderService.GetProviderPackageAsync' to use 'IProviderStorageService.GetProviderDownloadUrlAsync' if needed.

return blobPath;
}

public async Task<string?> GetProviderDownloadUrlAsync(string @namespace, string type, string version, string os, string arch)
{
var blobPath = $"providers/{@namespace}/{type}/{version}/{os}_{arch}.zip";
return await GenerateSasToken(blobPath);
}

public async Task UploadShasumsAsync(string @namespace, string type, string version, Stream stream)
{
var blobPath = $"providers/{@namespace}/{type}/{version}/SHA256SUMS";
var blobClient = _containerClient.GetBlobClient(blobPath);
await blobClient.UploadAsync(stream, true);
}

public async Task UploadShasumsSigAsync(string @namespace, string type, string version, Stream stream)
{
var blobPath = $"providers/{@namespace}/{type}/{version}/SHA256SUMS.sig";
var blobClient = _containerClient.GetBlobClient(blobPath);
await blobClient.UploadAsync(stream, true);
}

public async Task<string?> GetShasumsDownloadUrlAsync(string @namespace, string type, string version)
{
var blobPath = $"providers/{@namespace}/{type}/{version}/SHA256SUMS";
return await GenerateSasToken(blobPath);
}

public async Task<string?> GetShasumsSigDownloadUrlAsync(string @namespace, string type, string version)
{
var blobPath = $"providers/{@namespace}/{type}/{version}/SHA256SUMS.sig";
return await GenerateSasToken(blobPath);
}

public Task<Stream?> GetFileStreamAsync(string relativePath)
{
// Azure Blob storage does not support direct stream serving via this API logic.
// It relies on Redirect URLs.
return Task.FromResult<Stream?>(null);
}

private async Task<string?> GenerateSasToken(string blobPath)
{
var blobClient = _containerClient.GetBlobClient(blobPath);

if (!await blobClient.ExistsAsync()) return null;

var sasBuilder = new BlobSasBuilder
{
BlobContainerName = _containerName,
BlobName = blobPath,
Resource = "b",
ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(_sasTokenExpiryMinutes)
};
sasBuilder.SetPermissions(BlobSasPermissions.Read);

return blobClient.GenerateSasUri(sasBuilder).ToString();
}
}
13 changes: 13 additions & 0 deletions TerraformRegistry.Models/GpgKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace TerraformRegistry.Models;

/// <summary>
/// Represents a stored GPG Key in the database
/// </summary>
public class GpgKey
{
public string KeyId { get; set; } = string.Empty;
public string Namespace { get; set; } = string.Empty;
public string AsciiArmor { get; set; } = string.Empty;
public string TrustSignature { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
97 changes: 97 additions & 0 deletions TerraformRegistry.Models/ProviderModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Text.Json.Serialization;

namespace TerraformRegistry.Models;

/// <summary>
/// Represents a Terraform Provider
/// </summary>
public class Provider
{
public string Namespace { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Source { get; set; } = string.Empty;
public string PublishedAt { get; set; } = string.Empty;
public List<string> Versions { get; set; } = new();
}

/// <summary>
/// Represents the list of available versions for a provider
/// </summary>
public class ProviderVersions
{
[JsonPropertyName("versions")]
public List<ProviderVersionInfo> Versions { get; set; } = new();
}

public class ProviderVersionInfo
{
[JsonPropertyName("version")]
public string Version { get; set; } = string.Empty;

[JsonPropertyName("protocols")]
public List<string> Protocols { get; set; } = new();

[JsonPropertyName("platforms")]
public List<PlatformInfo> Platforms { get; set; } = new();
}

public class PlatformInfo
{
[JsonPropertyName("os")]
public string Os { get; set; } = string.Empty;

[JsonPropertyName("arch")]
public string Arch { get; set; } = string.Empty;
}

/// <summary>
/// Represents the download information for a specific provider platform
/// </summary>
public class ProviderPackage
{
[JsonPropertyName("protocols")]
public List<string> Protocols { get; set; } = new();

[JsonPropertyName("os")]
public string Os { get; set; } = string.Empty;

[JsonPropertyName("arch")]
public string Arch { get; set; } = string.Empty;

[JsonPropertyName("filename")]
public string Filename { get; set; } = string.Empty;

[JsonPropertyName("download_url")]
public string DownloadUrl { get; set; } = string.Empty;

[JsonPropertyName("shasum")]
public string Shasum { get; set; } = string.Empty;

[JsonPropertyName("signing_keys")]
public SigningKeys SigningKeys { get; set; } = new();
}

public class SigningKeys
{
[JsonPropertyName("gpg_public_keys")]
public List<GpgPublicKey> GpgPublicKeys { get; set; } = new();
}

public class GpgPublicKey
{
[JsonPropertyName("key_id")]
public string KeyId { get; set; } = string.Empty;

[JsonPropertyName("ascii_armor")]
public string AsciiArmor { get; set; } = string.Empty;

[JsonPropertyName("trust_signature")]
public string TrustSignature { get; set; } = string.Empty;

[JsonPropertyName("source")]
public string Source { get; set; } = string.Empty;

[JsonPropertyName("source_url")]
public string SourceUrl { get; set; } = string.Empty;
}
4 changes: 2 additions & 2 deletions TerraformRegistry.Models/ServiceDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public class ServiceDiscovery
[JsonPropertyName("modules.v1")]
public string ModulesV1 { get; set; } = "/v1/modules/";

// [JsonPropertyName("providers.v1")]
// public string ProvidersV1 { get; set; } = "/v1/providers/";
[JsonPropertyName("providers.v1")]
public string ProvidersV1 { get; set; } = "/v1/providers/";
}
Loading
Loading