tools/pipeline-generator/Azure.Sdk.Tools.PipelineGenerator/Program.cs (265 lines of code) (raw):

using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommandLine; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PipelineGenerator.Conventions; using PipelineGenerator.CommandParserOptions; using Microsoft.TeamFoundation.Build.WebApi; using System.Text.Json.Nodes; namespace PipelineGenerator { public class Program { public static async Task Main(string[] args) { var cancellationTokenSource = new CancellationTokenSource(); Console.CancelKeyPress += (sender, e) => { cancellationTokenSource.Cancel(); }; await Parser.Default .ParseArguments<DefaultOptions, GenerateOptions>(args) .WithNotParsed(_ => { Environment.Exit((int)ExitCondition.InvalidArguments); }) .WithParsedAsync(async o => { await Run(o, cancellationTokenSource); }); } public static async Task Run(object commandObj, CancellationTokenSource cancellationTokenSource) { ExitCondition code = ExitCondition.Exception; switch (commandObj) { case GenerateOptions g: var serviceProvider = GetServiceProvider(g.Debug); var program = serviceProvider.GetService<Program>(); code = await program.RunAsync( g.Organization, g.Project, g.Prefix, g.Path, g.Endpoint, g.Repository, g.Branch, g.Agentpool, g.Convention, g.VariableGroups.ToArray(), g.ServiceConnections, g.DevOpsPath, g.WhatIf, g.Open, g.Destroy, g.NoSchedule, g.SetManagedVariables, g.OverwriteTriggers, cancellationTokenSource.Token ); break; default: code = ExitCondition.InvalidArguments; break; } Environment.Exit((int)code); } private static IServiceProvider GetServiceProvider(bool debug) { var serviceCollection = new ServiceCollection(); serviceCollection.AddLogging(config => config.AddConsole().SetMinimumLevel(debug ? LogLevel.Debug : LogLevel.Information)) .AddTransient<Program>() .AddTransient<SdkComponentScanner>() .AddTransient<PullRequestValidationPipelineConvention>() .AddTransient<IntegrationTestingPipelineConvention>(); return serviceCollection.BuildServiceProvider(); } public Program(IServiceProvider serviceProvider, ILogger<Program> logger) { this.serviceProvider = serviceProvider; this.logger = logger; } private IServiceProvider serviceProvider; private ILogger<Program> logger; public ILoggerFactory LoggerFactory { get; } private PipelineConvention GetPipelineConvention(string convention, PipelineGenerationContext context) { var normalizedConvention = convention.ToLower(); switch (normalizedConvention) { case "ci": var ciLogger = serviceProvider.GetService<ILogger<PullRequestValidationPipelineConvention>>(); return new PullRequestValidationPipelineConvention(ciLogger, context); case "up": var upLogger = serviceProvider.GetService<ILogger<UnifiedPipelineConvention>>(); return new UnifiedPipelineConvention(upLogger, context); case "upweekly": var upWeeklyTestLogger = serviceProvider.GetService<ILogger<WeeklyUnifiedPipelineConvention>>(); return new WeeklyUnifiedPipelineConvention(upWeeklyTestLogger, context); case "tests": var testLogger = serviceProvider.GetService<ILogger<IntegrationTestingPipelineConvention>>(); return new IntegrationTestingPipelineConvention(testLogger, context); case "testsweekly": var weeklyTestLogger = serviceProvider.GetService<ILogger<WeeklyIntegrationTestingPipelineConvention>>(); return new WeeklyIntegrationTestingPipelineConvention(weeklyTestLogger, context); default: throw new ArgumentOutOfRangeException(nameof(convention), "Could not find matching convention."); } } public async Task<ExitCondition> RunAsync( string organization, string project, string prefix, string path, string endpoint, string repository, string branch, string agentPool, string convention, int[] variableGroups, IEnumerable<string> serviceConnections, string devOpsPath, bool whatIf, bool open, bool destroy, bool noSchedule, bool setManagedVariables, bool overwriteTriggers, CancellationToken cancellationToken) { try { logger.LogDebug("Creating context."); // Fall back to a form of prefix if DevOps path is not specified var devOpsPathValue = string.IsNullOrEmpty(devOpsPath) ? $"\\{prefix}" : devOpsPath; var context = new PipelineGenerationContext( this.logger, organization, project, endpoint, repository, branch, agentPool, variableGroups, devOpsPathValue, prefix, whatIf, noSchedule, setManagedVariables, overwriteTriggers ); var pipelineConvention = GetPipelineConvention(convention, context); var components = ScanForComponents(path, pipelineConvention.SearchPattern); if (components.Count() == 0) { logger.LogWarning("No components were found."); return ExitCondition.NoComponentsFound; } logger.LogInformation("Found {0} components", components.Count()); if (HasPipelineDefinitionNameDuplicates(pipelineConvention, components)) { return ExitCondition.DuplicateComponentsFound; } var definitions = new List<BuildDefinition>(); foreach (var component in components) { logger.LogInformation("Processing component '{0}' in '{1}'.", component.Name, component.Path); if (destroy) { var definition = await pipelineConvention.DeleteDefinitionAsync(component, cancellationToken); } else { var definition = await pipelineConvention.CreateOrUpdateDefinitionAsync(component, cancellationToken); if (open) { OpenBrowser(definition.GetWebUrl()); } definitions.Add(definition); } } var serviceConnectionObjects = await context.GetServiceConnectionsAsync(serviceConnections, cancellationToken); foreach (var serviceConnection in serviceConnectionObjects) { // Get set of permissions for the service connection JsonNode pipelinePermissions = await context.GetPipelinePermissionsAsync(serviceConnection.Id, cancellationToken); var pipelines = pipelinePermissions["pipelines"].AsArray(); var pipelineIdsWithPermissions = new HashSet<int>(pipelines.Select(p => p["id"].GetValue<int>())); int definitionsToAdd = 0; foreach (var definition in definitions) { // Check this pipeline has permissions if (!pipelineIdsWithPermissions.Contains(definition.Id)) { pipelines.Add( new JsonObject { ["id"] = definition.Id, ["authorized"] = true, ["authorizedBy"] = null, ["authorizedOn"] = null } ); definitionsToAdd++; } } logger.LogInformation("'{0}' pipelines already have permissions to service connection '{1}'. Need to grant permission to '{2}' more.", pipelineIdsWithPermissions.Count, serviceConnection.Id, definitionsToAdd); if (definitionsToAdd > 0) { logger.LogInformation("Granting permissions for '{0}' definitions to service connection '{1}'.", definitionsToAdd, serviceConnection.Id); // Update the permissions if we added anything await context.UpdatePipelinePermissionsAsync(serviceConnection.Id, pipelinePermissions, cancellationToken); } } return ExitCondition.Success; } catch (Exception ex) { logger.LogCritical(ex, "BOOM! Something went wrong, try running with --debug."); return ExitCondition.Exception; } } private void OpenBrowser(string url) { if (Environment.OSVersion.Platform != PlatformID.Win32NT) { return; } logger.LogDebug("Launching browser window for: {0}", url); var processStartInfo = new ProcessStartInfo() { FileName = url, UseShellExecute = true, }; // TODO: Need to test this on macOS and Linux. System.Diagnostics.Process.Start(processStartInfo); } private IEnumerable<SdkComponent> ScanForComponents(string path, string searchPattern) { var scanner = serviceProvider.GetService<SdkComponentScanner>(); var scanDirectory = new DirectoryInfo(path); var components = scanner.Scan(scanDirectory, searchPattern); return components; } private bool HasPipelineDefinitionNameDuplicates(PipelineConvention convention, IEnumerable<SdkComponent> components) { var pipelineNames = new Dictionary<string, SdkComponent>(); var duplicates = new HashSet<SdkComponent>(); foreach (var component in components) { var definitionName = convention.GetDefinitionName(component); if (pipelineNames.TryGetValue(definitionName, out var duplicate)) { duplicates.Add(duplicate); duplicates.Add(component); } else { pipelineNames.Add(definitionName, component); } } if (duplicates.Count > 0) { logger.LogError("Found multiple pipeline definitions that will result in name collisions. This can happen when nested directory names are the same."); logger.LogError("Suggested fix: add a 'variant' to the yaml filename, e.g. 'sdk/keyvault/internal/ci.yml' => 'sdk/keyvault/internal/ci.keyvault.yml'"); var paths = duplicates.Select(d => $"'{d.RelativeYamlPath}'"); logger.LogError($"Pipeline definitions affected: {String.Join(", ", paths)}"); return true; } return false; } } }