internal override async Task <int> RunAsync(CancellationToken cancellationToken) { var context = await Context .WithAzureLogon() .WithDevOpsLogon() .BuildAsync(cancellationToken); context.ResourceGroupDeprecationCheck(this.ResourceGroup); var mappings = new AggregatorMappings(context.Devops, context.Azure, context.Logger, context.Naming); bool ok = DevOpsEvents.IsValidEvent(Event); if (!ok) { context.Logger.WriteError($"Invalid event type."); return(ExitCodes.InvalidArguments); } var filters = new EventFilters { AreaPath = FilterAreaPath, Type = FilterType, Tag = FilterTag, Fields = FilterFields, OnlyLinks = FilterOnlyLinks, }; var instance = context.Naming.Instance(Instance, ResourceGroup); var id = await mappings.AddAsync(Project, Event, filters, instance, Rule, ImpersonateExecution, cancellationToken); return(id.Equals(Guid.Empty) ? ExitCodes.Failure : ExitCodes.Success); }
public static IEnumerable <KeyValuePair <string, string> > ToInputs(this EventFilters filters) { if (!string.IsNullOrWhiteSpace(filters.AreaPath)) { var areaPath = filters.AreaPath.First() == '\\' ? filters.AreaPath : $@"\{filters.AreaPath}"; areaPath = filters.AreaPath.Last() == '\\' ? areaPath : $@"{areaPath}\"; // Filter events to include only work items under the specified area path. yield return(new KeyValuePair <string, string>("areaPath", areaPath)); } if (!string.IsNullOrWhiteSpace(filters.Type)) { // Filter events to include only work items of the specified type. yield return(new KeyValuePair <string, string>("workItemType", filters.Type)); } if (!string.IsNullOrWhiteSpace(filters.Tag)) { // Filter events to include only work items containing the specified tag. yield return(new KeyValuePair <string, string>("tag", filters.Tag)); } if (filters.Fields?.Any() ?? false) { // Filter events to include only work items with the specified field(s) changed yield return(new KeyValuePair <string, string>("changedFields", string.Join(',', filters.Fields))); } }
internal override async Task <int> RunAsync(CancellationToken cancellationToken) { var context = await Context .WithAzureLogon() .WithDevOpsLogon() .BuildAsync(cancellationToken); var mappings = new AggregatorMappings(context.Devops, context.Azure, context.Logger); bool ok = DevOpsEvents.IsValidEvent(Event); if (!ok) { context.Logger.WriteError($"Invalid event type."); return(2); } var filters = new EventFilters { AreaPath = FilterAreaPath, Type = FilterType, Tag = FilterTag, Fields = FilterFields }; var instance = new InstanceName(Instance, ResourceGroup); var id = await mappings.AddAsync(Project, Event, filters, instance, Rule, ImpersonateExecution, cancellationToken); return(id.Equals(Guid.Empty) ? 1 : 0); }
internal async Task <Guid> AddAsync(string projectName, string @event, EventFilters filters, InstanceName instance, string ruleName, bool impersonateExecution, CancellationToken cancellationToken) { async Task <(Uri, string)> RetrieveAzureFunctionUrl(string _ruleName, CancellationToken _cancellationToken) { var rules = new AggregatorRules(azure, logger); return(await rules.GetInvocationUrlAndKey(instance, _ruleName, _cancellationToken)); } return(await CoreAddAsync(projectName, @event, filters, ruleName, impersonateExecution, RetrieveAzureFunctionUrl, "x-functions-key", cancellationToken)); }
public static IEnumerable <InputFilterCondition> ToFilterConditions(this EventFilters filters) { return(filters.ToInputs() .Select(input => new InputFilterCondition { InputId = input.Key, InputValue = input.Value, Operator = InputFilterOperator.Equals, CaseSensitive = false })); }
internal async Task <Guid> AddFromUrlAsync(string projectName, string @event, EventFilters filters, Uri targetUrl, string ruleName, bool impersonateExecution, CancellationToken cancellationToken) { async Task <(Uri, string)> RetrieveHostedUrl(string _ruleName, CancellationToken _cancellationToken) { string apiKey = MagicConstants.InvalidApiKey; logger.WriteVerbose($"Validating target URL {targetUrl.AbsoluteUri}"); string userManagedPassword = Environment.GetEnvironmentVariable(MagicConstants.EnvironmentVariable_SharedSecret); if (string.IsNullOrEmpty(userManagedPassword)) { throw new ApplicationException($"{MagicConstants.EnvironmentVariable_SharedSecret} environment variable is required for this command"); } string proof = SharedSecret.DeriveFromPassword(userManagedPassword); var configUrl = new UriBuilder(targetUrl); configUrl.Path += $"config/key"; var handler = new HttpClientHandler() { SslProtocols = SslProtocols.Tls12,// | SslProtocols.Tls11 | SslProtocols.Tls, ServerCertificateCustomValidationCallback = delegate { return(true); } }; using (var client = new HttpClient(handler)) using (var request = new HttpRequestMessage(HttpMethod.Post, configUrl.Uri)) { using (request.Content = new StringContent($"\"{proof}\"", Encoding.UTF8, "application/json")) using (var response = await client.SendAsync(request, cancellationToken)) { switch (response.StatusCode) { case HttpStatusCode.OK: logger.WriteVerbose($"Connection to {targetUrl} succeded"); apiKey = await response.Content.ReadAsStringAsync(); logger.WriteInfo($"Configuration retrieved."); break; default: logger.WriteError($"{targetUrl} returned {response.ReasonPhrase}."); break; }//switch } } if (string.IsNullOrEmpty(apiKey) || apiKey == MagicConstants.InvalidApiKey) { throw new ApplicationException("Unable to retrieve API Key, please check Shared secret configuration"); } var b = new UriBuilder(targetUrl); b.Path += $"workitem/{_ruleName}"; return(b.Uri, apiKey); } return(await CoreAddAsync(projectName, @event, filters, ruleName, impersonateExecution, RetrieveHostedUrl, MagicConstants.ApiKeyAuthenticationHeaderName, cancellationToken)); }
#pragma warning disable S107 // Methods should not have too many parameters protected async Task <Guid> CoreAddAsync(string projectName, string @event, EventFilters filters, string ruleName, bool impersonateExecution, Func <string, CancellationToken, Task <(Uri, string)> > urlRetriever, string headerName, CancellationToken cancellationToken)
internal async Task <Guid> AddAsync(string projectName, string @event, EventFilters filters, InstanceName instance, string ruleName, bool impersonateExecution, CancellationToken cancellationToken) { logger.WriteVerbose($"Reading Azure DevOps project data..."); var projectClient = devops.GetClient <ProjectHttpClient>(); var project = await projectClient.GetProject(projectName); logger.WriteInfo($"Project {projectName} data read."); var rules = new AggregatorRules(azure, logger); logger.WriteVerbose($"Retrieving {ruleName} Function Key..."); (Uri ruleUrl, string ruleKey) = await rules.GetInvocationUrlAndKey(instance, ruleName, cancellationToken); logger.WriteInfo($"{ruleName} Function Key retrieved."); ruleUrl = ruleUrl.AddToUrl(impersonate: impersonateExecution); // check if the subscription already exists and bail out var query = new SubscriptionsQuery { PublisherId = DevOpsEvents.PublisherId, PublisherInputFilters = new InputFilter[] { new InputFilter { Conditions = new List <InputFilterCondition> (filters.ToFilterConditions()) { new InputFilterCondition { InputId = "projectId", InputValue = project.Id.ToString(), Operator = InputFilterOperator.Equals, CaseSensitive = false } } } }, EventType = @event, ConsumerInputFilters = new InputFilter[] { new InputFilter { Conditions = new List <InputFilterCondition> { new InputFilterCondition { InputId = "url", InputValue = ruleUrl.ToString(), Operator = InputFilterOperator.Equals, CaseSensitive = false } } } } }; cancellationToken.ThrowIfCancellationRequested(); var serviceHooksClient = devops.GetClient <ServiceHooksPublisherHttpClient>(); var queryResult = await serviceHooksClient.QuerySubscriptionsAsync(query); if (queryResult.Results.Any()) { logger.WriteWarning($"There is already such a mapping."); return(Guid.Empty); } // see https://docs.microsoft.com/en-us/azure/devops/service-hooks/consumers?toc=%2Fvsts%2Fintegrate%2Ftoc.json&bc=%2Fvsts%2Fintegrate%2Fbreadcrumb%2Ftoc.json&view=vsts#web-hooks var subscriptionParameters = new Subscription() { ConsumerId = "webHooks", ConsumerActionId = "httpRequest", ConsumerInputs = new Dictionary <string, string> { { "url", ruleUrl.ToString() }, { "httpHeaders", $"x-functions-key:{ruleKey}" }, // careful with casing! { "resourceDetailsToSend", "all" }, { "messagesToSend", "none" }, { "detailedMessagesToSend", "none" }, }, EventType = @event, PublisherId = DevOpsEvents.PublisherId, PublisherInputs = new Dictionary <string, string> (filters.ToInputs()) { { "projectId", project.Id.ToString() }, /* TODO consider offering additional filters using the following * { "tfsSubscriptionId", devops.ServerId }, * { "teamId", null }, * // The string that must be found in the comment. * { "commentPattern", null }, */ }, // Resource Version 1.0 currently needed for WorkItems, newer Version send EMPTY Relation Information. ResourceVersion = "1.0", }; logger.WriteVerbose($"Adding mapping for {@event}..."); var newSubscription = await serviceHooksClient.CreateSubscriptionAsync(subscriptionParameters); logger.WriteInfo($"Event subscription {newSubscription.Id} setup."); return(newSubscription.Id); }