Skip to content

Commit

Permalink
Introduce config as code support
Browse files Browse the repository at this point in the history
  • Loading branch information
FernandoRojo committed Oct 26, 2024
1 parent 402a932 commit af69fc4
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
namespace Microsoft.ComponentDetection.Common;

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.Extensions.Logging;
using YamlDotNet.Serialization;

/// <inheritdoc />
public class ComponentDetectionConfigFileService : IComponentDetectionConfigFileService
{
private const string ComponentDetectionConfigFileEnvVar = "ComponentDetection.ComponentDetectionConfigFile";
private readonly IFileUtilityService fileUtilityService;
private readonly IEnvironmentVariableService environmentVariableService;
private readonly IPathUtilityService pathUtilityService;
private readonly ComponentDetectionConfigFile componentDetectionConfig;
private readonly ILogger<FastDirectoryWalkerFactory> logger;
private bool serviceInitComplete;

public ComponentDetectionConfigFileService(
IFileUtilityService fileUtilityService,
IEnvironmentVariableService environmentVariableService,
IPathUtilityService pathUtilityService,
ILogger<FastDirectoryWalkerFactory> logger)
{
this.fileUtilityService = fileUtilityService;
this.pathUtilityService = pathUtilityService;
this.environmentVariableService = environmentVariableService;
this.logger = logger;
this.componentDetectionConfig = new ComponentDetectionConfigFile();
this.serviceInitComplete = false;
}

public ComponentDetectionConfigFile GetComponentDetectionConfig()
{
this.EnsureInit();
return this.componentDetectionConfig;
}

public async Task InitAsync(string explicitConfigPath, string rootDirectoryPath = null)
{
await this.LoadFromEnvironmentVariableAsync();
if (!string.IsNullOrEmpty(explicitConfigPath))
{
await this.LoadComponentDetectionConfigAsync(explicitConfigPath);
}

if (!string.IsNullOrEmpty(rootDirectoryPath))
{
await this.LoadComponentDetectionConfigFilesFromRootDirectoryAsync(rootDirectoryPath);
}

this.serviceInitComplete = true;
}

private async Task LoadComponentDetectionConfigFilesFromRootDirectoryAsync(string rootDirectoryPath)
{
var workingDir = this.pathUtilityService.NormalizePath(rootDirectoryPath);

var reportFile = new FileInfo(Path.Combine(workingDir, "ComponentDetection.yml"));
if (this.fileUtilityService.Exists(reportFile.FullName))
{
await this.LoadComponentDetectionConfigAsync(reportFile.FullName);
}
}

private async Task LoadFromEnvironmentVariableAsync()
{
if (this.environmentVariableService.DoesEnvironmentVariableExist(ComponentDetectionConfigFileEnvVar))
{
var possibleConfigFilePath = this.environmentVariableService.GetEnvironmentVariable(ComponentDetectionConfigFileEnvVar);
if (this.fileUtilityService.Exists(possibleConfigFilePath))
{
await this.LoadComponentDetectionConfigAsync(possibleConfigFilePath);
}
}
}

private async Task LoadComponentDetectionConfigAsync(string configFile)
{
if (!this.fileUtilityService.Exists(configFile))
{
throw new InvalidOperationException($"Attempted to load non-existant ComponentDetectionConfig file: {configFile}");
}

var configFileInfo = new FileInfo(configFile);
var fileContents = await this.fileUtilityService.ReadAllTextAsync(configFileInfo);
var newConfig = this.ParseComponentDetectionConfig(fileContents);
this.MergeComponentDetectionConfig(newConfig);
this.logger.LogInformation("Loaded component detection config file from {ConfigFile}", configFile);
}

/// <summary>
/// Merges two component detection configs, giving precedence to values already set in the first file.
/// </summary>
/// <param name="newConfig">The new config file to be merged into the existing config set.</param>
private void MergeComponentDetectionConfig(ComponentDetectionConfigFile newConfig)
{
foreach ((var name, var value) in newConfig.Variables)
{
if (!this.componentDetectionConfig.Variables.ContainsKey(name))
{
this.componentDetectionConfig.Variables[name] = value;
}
}
}

/// <summary>
/// Reads the component detection config from a file path.
/// </summary>
/// <param name="configFileContent">The string contents of the config yaml file.</param>
/// <returns>The ComponentDetection config file as an object.</returns>
private ComponentDetectionConfigFile ParseComponentDetectionConfig(string configFileContent)
{
var deserializer = new DeserializerBuilder()
.IgnoreUnmatchedProperties()
.Build();
return deserializer.Deserialize<ComponentDetectionConfigFile>(new StringReader(configFileContent));
}

private void EnsureInit()
{
if (!this.serviceInitComplete)
{
throw new InvalidOperationException("ComponentDetection config files have not been loaded yet!");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Microsoft.ComponentDetection.Common;

using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;

/// <summary>
/// Provides methods for writing files.
/// </summary>
public interface IComponentDetectionConfigFileService
{
/// <summary>
/// Initializes the Component detection config service.
/// Checks the following for the presence of a config file:
/// 1. The environment variable "ComponentDetection.ConfigFilePath" and the path exists
/// 2. If there is a file present at the root directory named "ComponentDetection.yml".
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
Task InitAsync(string explicitConfigPath, string rootDirectoryPath = null);

/// <summary>
/// Retrieves the merged config files.
/// </summary>
/// <returns>The ComponentDetection config file as an object.</returns>
ComponentDetectionConfigFile GetComponentDetectionConfig();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Threading.Tasks.Dataflow" />
<PackageReference Include="yamldotnet" />
</ItemGroup>

<ItemGroup Label="Package References">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Microsoft.ComponentDetection.Contracts;

using System.Collections.Generic;
using YamlDotNet.Serialization;

/// <summary>
/// Represents the ComponentDetection.yml config file.
/// </summary>
public class ComponentDetectionConfigFile
{
/// <summary>
/// Gets or sets a value indicating whether the detection should be stopped.
/// </summary>
[YamlMember(Alias = "variables")]
public Dictionary<string, string> Variables { get; set; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="packageurl-dotnet" />
<PackageReference Include="yamldotnet" />
<PackageReference Include="System.Memory" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Text.Json" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public abstract class BaseSettings : CommandSettings
[CommandOption("--Output")]
public string Output { get; set; }

[Description("File path for a ComponentDetection.yml config file with more instructions for detection")]
[CommandOption("--ComponentDetectionConfigFile")]
public string ComponentDetectionConfigFile { get; set; }

/// <inheritdoc />
public override ValidationResult Validate()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,33 @@ public sealed class ScanCommand : AsyncCommand<ScanSettings>
private const string ManifestRelativePath = "ScanManifest_{timestamp}.json";
private readonly IFileWritingService fileWritingService;
private readonly IScanExecutionService scanExecutionService;
private readonly IComponentDetectionConfigFileService componentDetectionConfigFileService;
private readonly ILogger<ScanCommand> logger;

/// <summary>
/// Initializes a new instance of the <see cref="ScanCommand"/> class.
/// </summary>
/// <param name="fileWritingService">The file writing service.</param>
/// <param name="scanExecutionService">The scan execution service.</param>
/// <param name="componentDetectionConfigFileService">The component detection config file service.</param>
/// <param name="logger">The logger.</param>
public ScanCommand(
IFileWritingService fileWritingService,
IScanExecutionService scanExecutionService,
IComponentDetectionConfigFileService componentDetectionConfigFileService,
ILogger<ScanCommand> logger)
{
this.fileWritingService = fileWritingService;
this.scanExecutionService = scanExecutionService;
this.componentDetectionConfigFileService = componentDetectionConfigFileService;
this.logger = logger;
}

/// <inheritdoc />
public override async Task<int> ExecuteAsync(CommandContext context, ScanSettings settings)
{
this.fileWritingService.Init(settings.Output);
await this.componentDetectionConfigFileService.InitAsync(settings.ComponentDetectionConfigFile, settings.SourceDirectory.FullName);
var result = await this.scanExecutionService.ExecuteScanAsync(settings);
this.WriteComponentManifest(settings, result);
return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton<IEnvironmentVariableService, EnvironmentVariableService>();
services.AddSingleton<IObservableDirectoryWalkerFactory, FastDirectoryWalkerFactory>();
services.AddSingleton<IFileUtilityService, FileUtilityService>();
services.AddSingleton<IComponentDetectionConfigFileService, ComponentDetectionConfigFileService>();
services.AddSingleton<IDirectoryUtilityService, DirectoryUtilityService>();
services.AddSingleton<IFileWritingService, FileWritingService>();
services.AddSingleton<IGraphTranslationService, DefaultGraphTranslationService>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class ScanCommandTests
{
private Mock<IFileWritingService> fileWritingServiceMock;
private Mock<IScanExecutionService> scanExecutionServiceMock;
private Mock<IComponentDetectionConfigFileService> componentDetectionConfigFileServiceMock;
private Mock<ILogger<ScanCommand>> loggerMock;
private ScanCommand command;

Expand All @@ -27,11 +28,13 @@ public void TestInitialize()
{
this.fileWritingServiceMock = new Mock<IFileWritingService>();
this.scanExecutionServiceMock = new Mock<IScanExecutionService>();
this.componentDetectionConfigFileServiceMock = new Mock<IComponentDetectionConfigFileService>();
this.loggerMock = new Mock<ILogger<ScanCommand>>();

this.command = new ScanCommand(
this.fileWritingServiceMock.Object,
this.scanExecutionServiceMock.Object,
this.componentDetectionConfigFileServiceMock.Object,
this.loggerMock.Object);
}

Expand Down

0 comments on commit af69fc4

Please sign in to comment.