internal async Task <Guid> Add(string projectName, string @event, InstanceName instance, string ruleName) { logger.WriteVerbose($"Connecting to VSTS..."); var projectClient = vsts.GetClient <ProjectHttpClient>(); var project = await projectClient.GetProject(projectName); logger.WriteInfo($"{projectName} data read."); var rules = new AggregatorRules(azure, logger); logger.WriteVerbose($"Retrieving {ruleName} Function Key..."); (string ruleUrl, string ruleKey) = await rules.GetInvocationUrlAndKey(instance, ruleName); logger.WriteInfo($"{ruleName} Function Key retrieved."); var serviceHooksClient = vsts.GetClient <ServiceHooksPublisherHttpClient>(); // see https://docs.microsoft.com/en-us/vsts/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 }, { "httpHeaders", $"x-functions-key:{ruleKey}" }, // careful with casing! { "resourceDetailsToSend", "all" }, { "messagesToSend", "none" }, { "detailedMessagesToSend", "none" }, }, EventType = @event, PublisherId = VstsEvents.PublisherId, PublisherInputs = new Dictionary <string, string> { { "projectId", project.Id.ToString() }, /* TODO consider offering these filters * { "tfsSubscriptionId", vsts.ServerId }, * { "teamId", null }, * // Filter events to include only work items under the specified area path. * { "areaPath", null }, * // Filter events to include only work items of the specified type. * { "workItemType", null }, * // Filter events to include only work items with the specified field(s) changed * { "changedFields", null }, * // The string that must be found in the comment. * { "commentPattern", null }, */ }, }; logger.WriteVerbose($"Adding mapping for {@event}..."); var newSubscription = await serviceHooksClient.CreateSubscriptionAsync(subscriptionParameters); logger.WriteInfo($"Event subscription {newSubscription.Id} setup."); return(newSubscription.Id); }
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)); }
internal async Task <Guid> Add(string projectName, string @event, InstanceName instance, string ruleName) { var projectClient = vsts.GetClient <ProjectHttpClient>(); var project = await projectClient.GetProject(projectName); var rules = new AggregatorRules(azure, logger); (string ruleUrl, string ruleKey)invocation = await rules.GetInvocationUrlAndKey(instance, ruleName); var serviceHooksClient = vsts.GetClient <ServiceHooksPublisherHttpClient>(); // TODO see https://docs.microsoft.com/en-us/vsts/service-hooks/events?toc=%2Fvsts%2Fintegrate%2Ftoc.json&bc=%2Fvsts%2Fintegrate%2Fbreadcrumb%2Ftoc.json&view=vsts#work-item-created var subscriptionParameters = new Subscription() { ConsumerId = "webHooks", ConsumerActionId = "httpRequest", ConsumerInputs = new Dictionary <string, string> { { "url", invocation.ruleUrl }, { "httpHeaders", $"x-functions-key:{invocation.ruleKey}" }, { "resourceDetailsToSend", "All" }, // TODO these are not respected!!! { "messagesToSend", "None" }, { "detailedMessagesToSend", "None" }, }, EventType = @event, PublisherId = "tfs", PublisherInputs = new Dictionary <string, string> { { "projectId", project.Id.ToString() }, /* TODO consider offering these filters * { "tfsSubscriptionId", vsts.ServerId }, * { "teamId", null }, * // Filter events to include only work items under the specified area path. * { "areaPath", null }, * // Filter events to include only work items of the specified type. * { "workItemType", null }, * // Filter events to include only work items with the specified field(s) changed * { "changedFields", null }, * // The string that must be found in the comment. * { "commentPattern", null }, */ }, }; var newSubscription = await serviceHooksClient.CreateSubscriptionAsync(subscriptionParameters); return(newSubscription.Id); }
internal async Task <Guid> AddAsync(string projectName, string @event, EventFilters filters, InstanceName instance, string ruleName, 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..."); (string ruleUrl, string ruleKey) = await rules.GetInvocationUrlAndKey(instance, ruleName, cancellationToken); logger.WriteInfo($"{ruleName} Function Key retrieved."); var serviceHooksClient = devops.GetClient <ServiceHooksPublisherHttpClient>(); // 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> { 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, Operator = InputFilterOperator.Equals, CaseSensitive = false } } } } }; cancellationToken.ThrowIfCancellationRequested(); 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 }, { "httpHeaders", $"x-functions-key:{ruleKey}" }, // careful with casing! { "resourceDetailsToSend", "all" }, { "messagesToSend", "none" }, { "detailedMessagesToSend", "none" }, }, EventType = @event, PublisherId = DevOpsEvents.PublisherId, PublisherInputs = new Dictionary <string, string> { { "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 }, */ }, }; if (!string.IsNullOrWhiteSpace(filters.AreaPath)) { // Filter events to include only work items under the specified area path. subscriptionParameters.PublisherInputs.Add("areaPath", filters.AreaPath); } if (!string.IsNullOrWhiteSpace(filters.Type)) { // Filter events to include only work items of the specified type. subscriptionParameters.PublisherInputs.Add("workItemType", filters.Type); } if (!string.IsNullOrWhiteSpace(filters.Tag)) { // Filter events to include only work items containing the specified tag. subscriptionParameters.PublisherInputs.Add("tag", filters.Tag); } if (filters.Fields.Any()) { // Filter events to include only work items with the specified field(s) changed subscriptionParameters.PublisherInputs.Add("changedFields", string.Join(',', filters.Fields)); } logger.WriteVerbose($"Adding mapping for {@event}..."); var newSubscription = await serviceHooksClient.CreateSubscriptionAsync(subscriptionParameters); logger.WriteInfo($"Event subscription {newSubscription.Id} setup."); return(newSubscription.Id); }
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); }
internal async Task <UpdateOutcome> RemapAsync(InstanceName sourceInstance, InstanceName destInstance, string projectName, CancellationToken cancellationToken) { logger.WriteVerbose($"Searching aggregator mappings in Azure DevOps..."); var serviceHooksClient = devops.GetClient <ServiceHooksPublisherHttpClient>(); var subscriptions = await serviceHooksClient.QuerySubscriptionsAsync(); var filteredSubs = subscriptions.Where(s => s.PublisherId == DevOpsEvents.PublisherId && s.ConsumerInputs.GetValue("url", "").StartsWith( sourceInstance.FunctionAppUrl, StringComparison.OrdinalIgnoreCase)); var projectClient = devops.GetClient <ProjectHttpClient>(); var projects = await projectClient.GetProjects(); var projectsDict = projects.ToDictionary(p => p.Id); int processedCount = 0; int succeededCount = 0; foreach (var subscription in filteredSubs) { var foundProject = projectsDict[ new Guid(subscription.PublisherInputs["projectId"]) ]; if (!string.IsNullOrEmpty(projectName) && foundProject.Name != projectName) { logger.WriteInfo($"Skipping mapping {subscription.Id} in project {projectName}"); continue; } if (subscription.Status != SubscriptionStatus.Enabled && subscription.Status != SubscriptionStatus.OnProbation) { logger.WriteInfo($"Skipping mapping {subscription.Id} because has status {subscription.Status}"); continue; } processedCount++; Uri ruleUrl = new Uri(subscription.ConsumerInputs.GetValue("url", MagicConstants.MissingUrl)); string ruleName = ruleUrl.Segments.LastOrDefault() ?? string.Empty; var rules = new AggregatorRules(azure, logger); try { var(destRuleUrl, destRuleKey) = await rules.GetInvocationUrlAndKey(destInstance, ruleName, cancellationToken); // PATCH the object subscription.ConsumerInputs["url"] = destRuleUrl.AbsoluteUri; subscription.ConsumerInputs["httpHeaders"] = $"{MagicConstants.AzureFunctionKeyHeaderName}:{destRuleKey}"; logger.WriteVerbose($"Replacing {subscription.EventType} mapping from {ruleUrl.AbsoluteUri} to {subscription.Url}..."); try { var newSubscription = await serviceHooksClient.UpdateSubscriptionAsync(subscription); logger.WriteInfo($"Event subscription {newSubscription.Id} updated."); succeededCount++; } catch (Exception ex) { logger.WriteError($"Failed updating subscription {subscription.Id}: {ex.Message}."); } } catch (Exception ex) { logger.WriteError($"Destination rule {destInstance.PlainName}/{ruleName} does not exists or cannot retrieve key: {ex.Message}."); } } #pragma warning disable S3358 // Extract this nested ternary operation into an independent statement return(processedCount == 0 ? UpdateOutcome.NotFound : (processedCount > succeededCount) ? UpdateOutcome.Failed : UpdateOutcome.Succeeded); #pragma warning restore S3358 // Extract this nested ternary operation into an independent statement }