Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce config as code support #1284

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
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
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
Loading