From 9cc6adbd89a0f095e637088af5ab89e360ff64cc Mon Sep 17 00:00:00 2001 From: Unai Zorrilla Castro Date: Thu, 4 May 2017 13:01:14 +0200 Subject: [PATCH 01/31] Remove reference to HttpClient from IHttpClient. Change methods to allow specify authorization token and requestid headers in each request. Added API to group uri definitions --- .../Resilience/Resilience.Http/IHttpClient.cs | 14 ++-- .../Resilience.Http/ResilientHttpClient.cs | 81 +++++++++++++++---- .../Resilience.Http/StandardHttpClient.cs | 73 ++++++++++++++--- src/Web/WebMVC/Infrastructure/API.cs | 68 ++++++++++++++++ src/Web/WebMVC/Services/BasketService.cs | 45 +++++------ src/Web/WebMVC/Services/CatalogService.cs | 58 ++++++------- src/Web/WebMVC/Services/OrderingService.cs | 67 +++++++-------- src/Web/WebMVC/Startup.cs | 4 +- 8 files changed, 286 insertions(+), 124 deletions(-) create mode 100644 src/Web/WebMVC/Infrastructure/API.cs diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs index 0e56a66da..fb41b86d2 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs @@ -1,16 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http { public interface IHttpClient { - HttpClient Inst { get; } - Task GetStringAsync(string uri); - Task PostAsync(string uri, T item); - Task DeleteAsync(string uri); + Task GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer"); + + Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer"); + + Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer"); } } diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs index 2ccc84aaa..4d04f948e 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs @@ -3,9 +3,9 @@ using Newtonsoft.Json; using Polly; using Polly.Wrap; using System; -using System.Collections.Generic; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http @@ -20,7 +20,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http private HttpClient _client; private PolicyWrap _policyWrapper; private ILogger _logger; - public HttpClient Inst => _client; + //public HttpClient Inst => _client; public ResilientHttpClient(Policy[] policies, ILogger logger) { @@ -29,36 +29,87 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http // Add Policies to be applied _policyWrapper = Policy.WrapAsync(policies); - } + } - public Task GetStringAsync(string uri) => - HttpInvoker(() => - _client.GetStringAsync(uri)); + public Task GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer") + { + return HttpInvoker(async () => + { + var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri); + + if (authorizationToken != null) + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); + } - public Task PostAsync(string uri, T item) => + var response = await _client.SendAsync(requestMessage); + + return await response.Content.ReadAsStringAsync(); + }); + } + + public Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + { // a new StringContent must be created for each retry // as it is disposed after each call - HttpInvoker(() => + return HttpInvoker(async () => { - var response = _client.PostAsync(uri, new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json")); + var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri); + + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"); + + if (authorizationToken != null) + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); + } + + if (requestId != null) + { + requestMessage.Headers.Add("x-requestid", requestId); + } + + var response = await _client.SendAsync(requestMessage); + // raise exception if HttpResponseCode 500 // needed for circuit breaker to track fails - if (response.Result.StatusCode == HttpStatusCode.InternalServerError) + + if (response.StatusCode == HttpStatusCode.InternalServerError) { throw new HttpRequestException(); } return response; }); + } + + + public Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + { + return HttpInvoker(async () => + { + var requestMessage = new HttpRequestMessage(HttpMethod.Delete, uri); + + if (authorizationToken != null) + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); + } + + if (requestId != null) + { + requestMessage.Headers.Add("x-requestid", requestId); + } + + return await _client.SendAsync(requestMessage); + }); + } - public Task DeleteAsync(string uri) => - HttpInvoker(() => _client.DeleteAsync(uri)); - private Task HttpInvoker(Func> action) => + private Task HttpInvoker(Func> action) + { // Executes the action applying all // the policies defined in the wrapper - _policyWrapper.ExecuteAsync(() => action()); + return _policyWrapper.ExecuteAsync(() => action()); + } } - } diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs index 4f400caf5..54a9d5858 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs @@ -1,7 +1,8 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using System; +using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http @@ -10,24 +11,76 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http { private HttpClient _client; private ILogger _logger; - public HttpClient Inst => _client; + public StandardHttpClient(ILogger logger) { _client = new HttpClient(); _logger = logger; } - - public Task GetStringAsync(string uri) => - _client.GetStringAsync(uri); - public Task PostAsync(string uri, T item) + public async Task GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer") { - var contentString = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"); - return _client.PostAsync(uri, contentString); + var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri); + + if (authorizationToken != null) + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); + } + + var response = await _client.SendAsync(requestMessage); + + return await response.Content.ReadAsStringAsync(); } - public Task DeleteAsync(string uri) => - _client.DeleteAsync(uri); + public async Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + { + // a new StringContent must be created for each retry + // as it is disposed after each call + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri); + + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"); + + if (authorizationToken != null) + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); + } + + if (requestId != null) + { + requestMessage.Headers.Add("x-requestid", requestId); + } + + var response = await _client.SendAsync(requestMessage); + + // raise exception if HttpResponseCode 500 + // needed for circuit breaker to track fails + + if (response.StatusCode == HttpStatusCode.InternalServerError) + { + throw new HttpRequestException(); + } + + return response; + } + + + public async Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + { + var requestMessage = new HttpRequestMessage(HttpMethod.Delete, uri); + + if (authorizationToken != null) + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); + } + + if (requestId != null) + { + requestMessage.Headers.Add("x-requestid", requestId); + } + + return await _client.SendAsync(requestMessage); + } } } diff --git a/src/Web/WebMVC/Infrastructure/API.cs b/src/Web/WebMVC/Infrastructure/API.cs new file mode 100644 index 000000000..c837b8067 --- /dev/null +++ b/src/Web/WebMVC/Infrastructure/API.cs @@ -0,0 +1,68 @@ +namespace WebMVC.Infrastructure +{ + public static class API + { + public static class Basket + { + public static string GetBasket(string baseUri, string basketId) + { + return $"{baseUri}/{basketId}"; + } + + public static string UpdateBasket(string baseUri) + { + return baseUri; + } + + public static string CleanBasket(string baseUri, string basketId) + { + return $"{baseUri}/{basketId}"; + } + } + + public static class Order + { + public static string GetOrder(string baseUri, string orderId) + { + return $"{baseUri}/{orderId}"; + } + + public static string GetAllMyOrders(string baseUri) + { + return baseUri; + } + + public static string AddNewOrder(string baseUri) + { + return $"{baseUri}/new"; + } + } + + public static class Catalog + { + public static string GetAllCatalogItems(string baseUri, int page, int take, int? brand, int? type) + { + var filterQs = ""; + + if (brand.HasValue || type.HasValue) + { + var brandQs = (brand.HasValue) ? brand.Value.ToString() : "null"; + var typeQs = (type.HasValue) ? type.Value.ToString() : "null"; + filterQs = $"/type/{typeQs}/brand/{brandQs}"; + } + + return $"{baseUri}items{filterQs}?pageIndex={page}&pageSize={take}"; + } + + public static string GetAllBrands(string baseUri) + { + return $"{baseUri}catalogBrands"; + } + + public static string GetAllTypes(string baseUri) + { + return $"{baseUri}catalogTypes"; + } + } + } +} diff --git a/src/Web/WebMVC/Services/BasketService.cs b/src/Web/WebMVC/Services/BasketService.cs index 7d82a0fc6..bd418ea26 100644 --- a/src/Web/WebMVC/Services/BasketService.cs +++ b/src/Web/WebMVC/Services/BasketService.cs @@ -5,9 +5,8 @@ using Microsoft.eShopOnContainers.WebMVC.ViewModels; using Microsoft.Extensions.Options; using Newtonsoft.Json; using System.Collections.Generic; -using System.Linq; -using System.Net.Http; using System.Threading.Tasks; +using WebMVC.Infrastructure; namespace Microsoft.eShopOnContainers.WebMVC.Services { @@ -28,15 +27,13 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services public async Task GetBasket(ApplicationUser user) { - var context = _httpContextAccesor.HttpContext; - var token = await context.Authentication.GetTokenAsync("access_token"); + var token = await GetUserTokenAsync(); + var getBasketUri = API.Basket.GetBasket(_remoteServiceBaseUrl, user.Id); - _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + var dataString = await _apiClient.GetStringAsync(getBasketUri, token); - var basketUrl = $"{_remoteServiceBaseUrl}/{user.Id}"; - var dataString = await _apiClient.GetStringAsync(basketUrl); // Use the ?? Null conditional operator to simplify the initialization of response - var response = JsonConvert.DeserializeObject(dataString) ?? + var response = JsonConvert.DeserializeObject(dataString) ?? new Basket() { BuyerId = user.Id @@ -47,14 +44,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services public async Task UpdateBasket(Basket basket) { - var context = _httpContextAccesor.HttpContext; - var token = await context.Authentication.GetTokenAsync("access_token"); - - _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + var token = await GetUserTokenAsync(); + var updateBasketUri = API.Basket.UpdateBasket(_remoteServiceBaseUrl); - var basketUrl = _remoteServiceBaseUrl; - - var response = await _apiClient.PostAsync(basketUrl, basket); + var response = await _apiClient.PostAsync(updateBasketUri, basket, token); response.EnsureSuccessStatusCode(); @@ -88,7 +81,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services order.OrderItems.Add(new OrderItem() { ProductId = int.Parse(x.ProductId), - + PictureUrl = x.PictureUrl, ProductName = x.ProductName, Units = x.Quantity, @@ -102,7 +95,8 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services public async Task AddItemToBasket(ApplicationUser user, BasketItem product) { - Basket basket = await GetBasket(user); + var basket = await GetBasket(user); + if (basket == null) { basket = new Basket() @@ -113,20 +107,25 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services } basket.Items.Add(product); + await UpdateBasket(basket); } public async Task CleanBasket(ApplicationUser user) { - var context = _httpContextAccesor.HttpContext; - var token = await context.Authentication.GetTokenAsync("access_token"); + var token = await GetUserTokenAsync(); + var cleanBasketUri = API.Basket.CleanBasket(_remoteServiceBaseUrl, user.Id); + + var response = await _apiClient.DeleteAsync(cleanBasketUri, token); - _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); - var basketUrl = $"{_remoteServiceBaseUrl}/{user.Id}"; - var response = await _apiClient.DeleteAsync(basketUrl); - //CCE: response status code... } + + async Task GetUserTokenAsync() + { + var context = _httpContextAccesor.HttpContext; + return await context.Authentication.GetTokenAsync("access_token"); + } } } diff --git a/src/Web/WebMVC/Services/CatalogService.cs b/src/Web/WebMVC/Services/CatalogService.cs index f7225ff0b..2af428e2e 100644 --- a/src/Web/WebMVC/Services/CatalogService.cs +++ b/src/Web/WebMVC/Services/CatalogService.cs @@ -7,43 +7,32 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Collections.Generic; using System.Threading.Tasks; +using WebMVC.Infrastructure; namespace Microsoft.eShopOnContainers.WebMVC.Services { public class CatalogService : ICatalogService { private readonly IOptionsSnapshot _settings; - private IHttpClient _apiClient; + private readonly IHttpClient _apiClient; + private readonly ILogger _logger; + private readonly string _remoteServiceBaseUrl; - - public CatalogService(IOptionsSnapshot settings, ILoggerFactory loggerFactory, IHttpClient httpClient) { + + public CatalogService(IOptionsSnapshot settings, IHttpClient httpClient, ILogger logger) + { _settings = settings; - _remoteServiceBaseUrl = $"{_settings.Value.CatalogUrl}/api/v1/catalog/"; _apiClient = httpClient; - var log = loggerFactory.CreateLogger("catalog service"); - log.LogDebug(settings.Value.CatalogUrl); - } - - public async Task GetCatalogItems(int page,int take, int? brand, int? type) - { - var itemsQs = $"items?pageIndex={page}&pageSize={take}"; - var filterQs = ""; + _logger = logger; - if (brand.HasValue || type.HasValue) - { - var brandQs = (brand.HasValue) ? brand.Value.ToString() : "null"; - var typeQs = (type.HasValue) ? type.Value.ToString() : "null"; - filterQs = $"/type/{typeQs}/brand/{brandQs}"; - } - - var catalogUrl = $"{_remoteServiceBaseUrl}items{filterQs}?pageIndex={page}&pageSize={take}"; + _remoteServiceBaseUrl = $"{_settings.Value.CatalogUrl}/api/v1/catalog/"; + } - var dataString = ""; + public async Task GetCatalogItems(int page, int take, int? brand, int? type) + { + var allcatalogItemsUri = API.Catalog.GetAllCatalogItems(_remoteServiceBaseUrl, page, take, brand, type); - // - // Using a HttpClient wrapper with Retry and Exponential Backoff - // - dataString = await _apiClient.GetStringAsync(catalogUrl); + var dataString = await _apiClient.GetStringAsync(allcatalogItemsUri); var response = JsonConvert.DeserializeObject(dataString); @@ -52,14 +41,16 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services public async Task> GetBrands() { - var url = $"{_remoteServiceBaseUrl}catalogBrands"; - var dataString = await _apiClient.GetStringAsync(url); + var getBrandsUri = API.Catalog.GetAllBrands(_remoteServiceBaseUrl); + + var dataString = await _apiClient.GetStringAsync(getBrandsUri); var items = new List(); items.Add(new SelectListItem() { Value = null, Text = "All", Selected = true }); - JArray brands = JArray.Parse(dataString); - foreach (JObject brand in brands.Children()) + var brands = JArray.Parse(dataString); + + foreach (var brand in brands.Children()) { items.Add(new SelectListItem() { @@ -73,14 +64,15 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services public async Task> GetTypes() { - var url = $"{_remoteServiceBaseUrl}catalogTypes"; - var dataString = await _apiClient.GetStringAsync(url); + var getTypesUri = API.Catalog.GetAllTypes(_remoteServiceBaseUrl); + + var dataString = await _apiClient.GetStringAsync(getTypesUri); var items = new List(); items.Add(new SelectListItem() { Value = null, Text = "All", Selected = true }); - JArray brands = JArray.Parse(dataString); - foreach (JObject brand in brands.Children()) + var brands = JArray.Parse(dataString); + foreach (var brand in brands.Children()) { items.Add(new SelectListItem() { diff --git a/src/Web/WebMVC/Services/OrderingService.cs b/src/Web/WebMVC/Services/OrderingService.cs index 570a48361..8f198fbed 100644 --- a/src/Web/WebMVC/Services/OrderingService.cs +++ b/src/Web/WebMVC/Services/OrderingService.cs @@ -1,14 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.eShopOnContainers.WebMVC.ViewModels; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http; +using Microsoft.eShopOnContainers.WebMVC.ViewModels; using Microsoft.Extensions.Options; -using System.Net.Http; using Newtonsoft.Json; -using Microsoft.AspNetCore.Authentication; -using Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WebMVC.Infrastructure; namespace Microsoft.eShopOnContainers.WebMVC.Services { @@ -27,15 +26,13 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services _apiClient = httpClient; } - async public Task GetOrder(ApplicationUser user, string Id) + async public Task GetOrder(ApplicationUser user, string id) { - var context = _httpContextAccesor.HttpContext; - var token = await context.Authentication.GetTokenAsync("access_token"); - _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + var token = await GetUserTokenAsync(); + var getOrderUri = API.Order.GetOrder(_remoteServiceBaseUrl, id); + + var dataString = await _apiClient.GetStringAsync(getOrderUri, token); - var ordersUrl = $"{_remoteServiceBaseUrl}/{Id}"; - var dataString = await _apiClient.GetStringAsync(ordersUrl); - var response = JsonConvert.DeserializeObject(dataString); return response; @@ -43,16 +40,13 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services async public Task> GetMyOrders(ApplicationUser user) { - var context = _httpContextAccesor.HttpContext; - var token = await context.Authentication.GetTokenAsync("access_token"); + var token = await GetUserTokenAsync(); + var allMyOrdersUri = API.Order.GetAllMyOrders(_remoteServiceBaseUrl); - _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); - - var ordersUrl = _remoteServiceBaseUrl; - var dataString = await _apiClient.GetStringAsync(ordersUrl); + var dataString = await _apiClient.GetStringAsync(allMyOrdersUri, token); var response = JsonConvert.DeserializeObject>(dataString); - return response; + return response; } public Order MapUserInfoIntoOrder(ApplicationUser user, Order order) @@ -62,10 +56,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services order.State = user.State; order.Country = user.Country; order.ZipCode = user.ZipCode; - + order.CardNumber = user.CardNumber; order.CardHolderName = user.CardHolderName; - order.CardExpiration = new DateTime(int.Parse("20" + user.Expiration.Split('/')[1]),int.Parse(user.Expiration.Split('/')[0]), 1); + order.CardExpiration = new DateTime(int.Parse("20" + user.Expiration.Split('/')[1]), int.Parse(user.Expiration.Split('/')[0]), 1); order.CardSecurityNumber = user.SecurityNumber; return order; @@ -73,21 +67,21 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services async public Task CreateOrder(Order order) { - var context = _httpContextAccesor.HttpContext; - var token = await context.Authentication.GetTokenAsync("access_token"); - - _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); - _apiClient.Inst.DefaultRequestHeaders.Add("x-requestid", order.RequestId.ToString()); + var token = await GetUserTokenAsync(); + var requestId = order.RequestId.ToString(); + var addNewOrderUri = API.Order.AddNewOrder(_remoteServiceBaseUrl); - var ordersUrl = $"{_remoteServiceBaseUrl}/new"; order.CardTypeId = 1; order.CardExpirationApiFormat(); + SetFakeIdToProducts(order); - var response = await _apiClient.PostAsync(ordersUrl, order); + var response = await _apiClient.PostAsync(addNewOrderUri, order, token, requestId); if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) - throw new Exception("Error creating order, try later"); + { + throw new Exception("Error creating order, try later."); + } response.EnsureSuccessStatusCode(); } @@ -106,10 +100,17 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services destination.CardSecurityNumber = original.CardSecurityNumber; } - private void SetFakeIdToProducts(Order order) + void SetFakeIdToProducts(Order order) { var id = 1; order.OrderItems.ForEach(x => { x.ProductId = id; id++; }); } + + async Task GetUserTokenAsync() + { + var context = _httpContextAccesor.HttpContext; + + return await context.Authentication.GetTokenAsync("access_token"); + } } } diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index e86c88c04..afc266051 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -70,11 +70,11 @@ namespace Microsoft.eShopOnContainers.WebMVC if (Configuration.GetValue("UseResilientHttp") == bool.TrueString) { services.AddTransient(); - services.AddTransient(sp => sp.GetService().CreateResilientHttpClient()); + services.AddSingleton(sp => sp.GetService().CreateResilientHttpClient()); } else { - services.AddTransient(); + services.AddSingleton(); } } From d97ae6d6aeeba5cdc764288f0477d0115bbc7385 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 27 Apr 2017 17:45:34 -0400 Subject: [PATCH 02/31] Fix API signatures for collections The methods that return collections should return Task> not Task --- .../Ordering.API/Application/Queries/IOrderQueries.cs | 5 +++-- .../Ordering.API/Application/Queries/OrderQueries.cs | 8 ++++---- .../UnitTest/Ordering/Application/OrdersWebApiTest.cs | 5 +++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs index 8d78524ea..253b01e9c 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs @@ -1,13 +1,14 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries { + using System.Collections.Generic; using System.Threading.Tasks; public interface IOrderQueries { Task GetOrderAsync(int id); - Task GetOrdersAsync(); + Task> GetOrdersAsync(); - Task GetCardTypesAsync(); + Task> GetCardTypesAsync(); } } diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs index 9d909e254..afcfc71b5 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs @@ -44,13 +44,13 @@ } } - public async Task GetOrdersAsync() + public Task> GetOrdersAsync() { using (var connection = new SqlConnection(_connectionString)) { connection.Open(); - return await connection.QueryAsync(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status],SUM(oi.units*oi.unitprice) as total + return connection.QueryAsync(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status],SUM(oi.units*oi.unitprice) as total FROM [ordering].[Orders] o LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id @@ -58,13 +58,13 @@ } } - public async Task GetCardTypesAsync() + public Task> GetCardTypesAsync() { using (var connection = new SqlConnection(_connectionString)) { connection.Open(); - return await connection.QueryAsync("SELECT * FROM ordering.cardtypes"); + return connection.QueryAsync("SELECT * FROM ordering.cardtypes"); } } diff --git a/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs b/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs index 8c7659862..c0656f050 100644 --- a/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs +++ b/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs @@ -6,6 +6,7 @@ using Microsoft.eShopOnContainers.Services.Ordering.API.Controllers; using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services; using Moq; using System; +using System.Linq; using System.Threading.Tasks; using Xunit; @@ -59,7 +60,7 @@ namespace UnitTest.Ordering.Application public async Task Get_orders_success() { //Arrange - var fakeDynamicResult = new Object(); + var fakeDynamicResult = Enumerable.Empty(); _orderQueriesMock.Setup(x => x.GetOrdersAsync()) .Returns(Task.FromResult(fakeDynamicResult)); @@ -92,7 +93,7 @@ namespace UnitTest.Ordering.Application public async Task Get_cardTypes_success() { //Arrange - var fakeDynamicResult = new Object(); + var fakeDynamicResult = Enumerable.Empty(); _orderQueriesMock.Setup(x => x.GetCardTypesAsync()) .Returns(Task.FromResult(fakeDynamicResult)); From 223dcda0cbb3c2fa1e46adf04953afd2162f88b1 Mon Sep 17 00:00:00 2001 From: BillWagner Date: Tue, 2 May 2017 16:21:21 -0400 Subject: [PATCH 03/31] add back the async state machine Because of the using blocks, these one line methods need the async modifier so that the that async state machiner is created. Otherwise, if the method does not complete synchronously, the connection is closed before the database has returned its results. --- .../Ordering.API/Application/Queries/OrderQueries.cs | 8 ++++---- .../Ordering/Ordering.API/Controllers/OrdersController.cs | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs index afcfc71b5..e51cf04ce 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs @@ -44,13 +44,13 @@ } } - public Task> GetOrdersAsync() + public async Task> GetOrdersAsync() { using (var connection = new SqlConnection(_connectionString)) { connection.Open(); - return connection.QueryAsync(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status],SUM(oi.units*oi.unitprice) as total + return await connection.QueryAsync(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status],SUM(oi.units*oi.unitprice) as total FROM [ordering].[Orders] o LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id @@ -58,13 +58,13 @@ } } - public Task> GetCardTypesAsync() + public async Task> GetCardTypesAsync() { using (var connection = new SqlConnection(_connectionString)) { connection.Open(); - return connection.QueryAsync("SELECT * FROM ordering.cardtypes"); + return await connection.QueryAsync("SELECT * FROM ordering.cardtypes"); } } diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs index 05a9cd193..20c1e023f 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -68,8 +68,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers [HttpGet] public async Task GetOrders() { - var orders = await _orderQueries - .GetOrdersAsync(); + var orderTask = _orderQueries.GetOrdersAsync(); + + var orders = await orderTask; return Ok(orders); } From 47978017aaa176550fa41184f6b1d695e2847c04 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Sat, 6 May 2017 17:04:24 -0400 Subject: [PATCH 04/31] Update the CI Build docker file to build the SPA alient bits While at it, update the sh script too. --- cli-mac/build-bits.sh | 6 ++++++ docker-compose.ci.build.yml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) mode change 100644 => 100755 cli-mac/build-bits.sh diff --git a/cli-mac/build-bits.sh b/cli-mac/build-bits.sh old mode 100644 new mode 100755 index f8ad2e2f9..681c3605d --- a/cli-mac/build-bits.sh +++ b/cli-mac/build-bits.sh @@ -10,6 +10,12 @@ projectList=( "../src/Web/WebStatus" ) + +pushd $(pwd)/../src/Web/WebSPA +npm install +npm rebuild node-sass +popd + for project in "${projectList[@]}" do echo -e "\e[33mWorking on $(pwd)/$project" diff --git a/docker-compose.ci.build.yml b/docker-compose.ci.build.yml index 546b7690f..26122e3f8 100644 --- a/docker-compose.ci.build.yml +++ b/docker-compose.ci.build.yml @@ -6,5 +6,5 @@ services: volumes: - .:/src working_dir: /src - command: /bin/bash -c "dotnet restore ./eShopOnContainers-ServicesAndWebApps.sln && dotnet publish ./eShopOnContainers-ServicesAndWebApps.sln -c Release -o ./obj/Docker/publish" + command: /bin/bash -c "pushd ./src/Web/WebSPA && npm install && npm rebuild node-sass && popd && dotnet restore ./eShopOnContainers-ServicesAndWebApps.sln && dotnet publish ./eShopOnContainers-ServicesAndWebApps.sln -c Release -o ./obj/Docker/publish" From 5156ec81f7a1c195a50d77c7db68640cc17165ce Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Sat, 6 May 2017 15:37:31 -0700 Subject: [PATCH 05/31] Minor refactoring and deleted a ResilientPolicy class not being used. --- .../Resilience.Http/ResiliencePolicy.cs | 10 ---------- .../Resilience.Http/ResilientHttpClient.cs | 15 +++++++-------- 2 files changed, 7 insertions(+), 18 deletions(-) delete mode 100644 src/BuildingBlocks/Resilience/Resilience.Http/ResiliencePolicy.cs diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResiliencePolicy.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResiliencePolicy.cs deleted file mode 100644 index 63eadc857..000000000 --- a/src/BuildingBlocks/Resilience/Resilience.Http/ResiliencePolicy.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http -{ - public class ResiliencePolicy - { - } -} diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs index 4d04f948e..fb96b1608 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs @@ -31,6 +31,13 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http _policyWrapper = Policy.WrapAsync(policies); } + private Task HttpInvoker(Func> action) + { + // Executes the action applying all + // the policies defined in the wrapper + return _policyWrapper.ExecuteAsync(() => action()); + } + public Task GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer") { return HttpInvoker(async () => @@ -103,13 +110,5 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http }); } - - - private Task HttpInvoker(Func> action) - { - // Executes the action applying all - // the policies defined in the wrapper - return _policyWrapper.ExecuteAsync(() => action()); - } } } From 18c879a1a3085b7808dbd9cc8a31471a15685d33 Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Sat, 6 May 2017 15:38:04 -0700 Subject: [PATCH 06/31] Deleted policy class not being used --- .../Resilience/Resilience.Http/ResiliencePolicy.cs | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/BuildingBlocks/Resilience/Resilience.Http/ResiliencePolicy.cs diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResiliencePolicy.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResiliencePolicy.cs deleted file mode 100644 index 63eadc857..000000000 --- a/src/BuildingBlocks/Resilience/Resilience.Http/ResiliencePolicy.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http -{ - public class ResiliencePolicy - { - } -} From 99cbbe484e45168493b75717d35d9ab3844ef1d6 Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Sat, 6 May 2017 21:41:32 -0700 Subject: [PATCH 07/31] Minor description refactoring --- src/Web/WebMVC/Controllers/OrderController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/WebMVC/Controllers/OrderController.cs b/src/Web/WebMVC/Controllers/OrderController.cs index 90be194dc..83152d697 100644 --- a/src/Web/WebMVC/Controllers/OrderController.cs +++ b/src/Web/WebMVC/Controllers/OrderController.cs @@ -51,7 +51,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Controllers } catch(BrokenCircuitException) { - ModelState.AddModelError("Error", "It was not possible to create a new order, please try later on"); + ModelState.AddModelError("Error", "It was not possible to create a new order, please try later on. (Business Msg Due to Circuit-Breaker)"); } return View(model); } From 2aba3acd9fbe4c41a1f044b74de87d52ed11006c Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Sat, 6 May 2017 22:04:22 -0700 Subject: [PATCH 08/31] Revert "Fix mac ci build" --- cli-mac/build-bits.sh | 6 ------ docker-compose.ci.build.yml | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) mode change 100755 => 100644 cli-mac/build-bits.sh diff --git a/cli-mac/build-bits.sh b/cli-mac/build-bits.sh old mode 100755 new mode 100644 index 681c3605d..f8ad2e2f9 --- a/cli-mac/build-bits.sh +++ b/cli-mac/build-bits.sh @@ -10,12 +10,6 @@ projectList=( "../src/Web/WebStatus" ) - -pushd $(pwd)/../src/Web/WebSPA -npm install -npm rebuild node-sass -popd - for project in "${projectList[@]}" do echo -e "\e[33mWorking on $(pwd)/$project" diff --git a/docker-compose.ci.build.yml b/docker-compose.ci.build.yml index 26122e3f8..546b7690f 100644 --- a/docker-compose.ci.build.yml +++ b/docker-compose.ci.build.yml @@ -6,5 +6,5 @@ services: volumes: - .:/src working_dir: /src - command: /bin/bash -c "pushd ./src/Web/WebSPA && npm install && npm rebuild node-sass && popd && dotnet restore ./eShopOnContainers-ServicesAndWebApps.sln && dotnet publish ./eShopOnContainers-ServicesAndWebApps.sln -c Release -o ./obj/Docker/publish" + command: /bin/bash -c "dotnet restore ./eShopOnContainers-ServicesAndWebApps.sln && dotnet publish ./eShopOnContainers-ServicesAndWebApps.sln -c Release -o ./obj/Docker/publish" From e0c96de6853e025f39dd3be6a07f77172091c39c Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Sat, 6 May 2017 22:59:24 -0700 Subject: [PATCH 09/31] Revert "Revert "Fix mac ci build"" --- cli-mac/build-bits.sh | 6 ++++++ docker-compose.ci.build.yml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) mode change 100644 => 100755 cli-mac/build-bits.sh diff --git a/cli-mac/build-bits.sh b/cli-mac/build-bits.sh old mode 100644 new mode 100755 index f8ad2e2f9..681c3605d --- a/cli-mac/build-bits.sh +++ b/cli-mac/build-bits.sh @@ -10,6 +10,12 @@ projectList=( "../src/Web/WebStatus" ) + +pushd $(pwd)/../src/Web/WebSPA +npm install +npm rebuild node-sass +popd + for project in "${projectList[@]}" do echo -e "\e[33mWorking on $(pwd)/$project" diff --git a/docker-compose.ci.build.yml b/docker-compose.ci.build.yml index 546b7690f..26122e3f8 100644 --- a/docker-compose.ci.build.yml +++ b/docker-compose.ci.build.yml @@ -6,5 +6,5 @@ services: volumes: - .:/src working_dir: /src - command: /bin/bash -c "dotnet restore ./eShopOnContainers-ServicesAndWebApps.sln && dotnet publish ./eShopOnContainers-ServicesAndWebApps.sln -c Release -o ./obj/Docker/publish" + command: /bin/bash -c "pushd ./src/Web/WebSPA && npm install && npm rebuild node-sass && popd && dotnet restore ./eShopOnContainers-ServicesAndWebApps.sln && dotnet publish ./eShopOnContainers-ServicesAndWebApps.sln -c Release -o ./obj/Docker/publish" From 4c0f0b01ad9dfd99a94f81bdceecd09ce5c74659 Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Sat, 6 May 2017 23:28:38 -0700 Subject: [PATCH 10/31] Removed NMP INSTALL from docker-compose.ci.build.yml. Shouldn't need to do that every time. --- docker-compose.ci.build.yml | 4 ++-- src/Web/WebMVC/wwwroot/css/site.min.css | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.ci.build.yml b/docker-compose.ci.build.yml index 26122e3f8..1b3f3bea3 100644 --- a/docker-compose.ci.build.yml +++ b/docker-compose.ci.build.yml @@ -6,5 +6,5 @@ services: volumes: - .:/src working_dir: /src - command: /bin/bash -c "pushd ./src/Web/WebSPA && npm install && npm rebuild node-sass && popd && dotnet restore ./eShopOnContainers-ServicesAndWebApps.sln && dotnet publish ./eShopOnContainers-ServicesAndWebApps.sln -c Release -o ./obj/Docker/publish" - + command: /bin/bash -c "pushd ./src/Web/WebSPA && npm rebuild node-sass && popd && dotnet restore ./eShopOnContainers-ServicesAndWebApps.sln && dotnet publish ./eShopOnContainers-ServicesAndWebApps.sln -c Release -o ./obj/Docker/publish" + \ No newline at end of file diff --git a/src/Web/WebMVC/wwwroot/css/site.min.css b/src/Web/WebMVC/wwwroot/css/site.min.css index f5fc90999..2a4a53b47 100644 --- a/src/Web/WebMVC/wwwroot/css/site.min.css +++ b/src/Web/WebMVC/wwwroot/css/site.min.css @@ -1 +1 @@ -.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%}.esh-app-footer-brand{height:50px;width:230px}.esh-app-footer-text{color:#83d01b;line-height:50px;text-align:right;width:100%}@font-face{font-family:Montserrat;font-weight:400;src:url("../fonts/Montserrat-Regular.eot?") format("eot"),url("../fonts/Montserrat-Regular.woff") format("woff"),url("../fonts/Montserrat-Regular.ttf") format("truetype"),url("../fonts/Montserrat-Regular.svg#Montserrat") format("svg")}@font-face{font-family:Montserrat;font-weight:700;src:url("../fonts/Montserrat-Bold.eot?") format("eot"),url("../fonts/Montserrat-Bold.woff") format("woff"),url("../fonts/Montserrat-Bold.ttf") format("truetype"),url("../fonts/Montserrat-Bold.svg#Montserrat") format("svg")}html,body{font-family:Montserrat,sans-serif;font-size:16px;font-weight:400;z-index:10}*,*::after,*::before{box-sizing:border-box}.preloading{color:#00a69c;display:block;font-size:1.5rem;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}select::-ms-expand{display:none}@media screen and (min-width:992px){.form-input{max-width:360px;width:360px}}.form-input{border-radius:0;height:45px;padding:10px}.form-input-small{max-width:100px !important}.form-input-medium{width:150px !important}.alert{padding-left:0}.alert-danger{background-color:transparent;border:0;color:#fb0d0d;font-size:12px}a,a:active,a:hover,a:visited{color:#000;text-decoration:none;transition:color .35s}a:hover,a:active{color:#75b918;transition:color .35s}.esh-basket{min-height:80vh}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem}.esh-basket-titles--clean{padding-bottom:0;padding-top:0}.esh-basket-title{text-transform:uppercase}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-basket-items--border:last-of-type{border-color:transparent}.esh-basket-item{font-size:1rem;font-weight:300}.esh-basket-item--middle{line-height:8rem}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem}}.esh-basket-item--mark{color:#00a69c}.esh-basket-image{height:8rem}.esh-basket-input{line-height:1rem;width:100%}.esh-basket-checkout{border:none;border-radius:0;background-color:#83d01b;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s}.esh-basket-margin12{margin-left:12px}.esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none}.esh-basketstatus-image{height:36px;margin-top:.5rem}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s}.esh-catalog-hero{background-image:url("../images/main_banner.png");background-size:cover;height:260px;width:100%}.esh-catalog-title{position:relative;top:74.28571px}.esh-catalog-filters{background-color:#00a69c;height:65px}.esh-catalog-filter{background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem;min-width:140px;-webkit-appearance:none}.esh-catalog-filter option{background-color:#00a69c}.esh-catalog-label{display:inline-block;position:relative;z-index:0}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-top:.65rem;margin-left:.5rem;position:absolute;text-transform:uppercase;z-index:1}.esh-catalog-label::after{background-image:url("../images/arrow-down.png");height:7px;content:'';position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1}.esh-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;transform:translateY(.5rem);padding:.5rem;transition:all .35s}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-items{margin-top:1rem}.esh-catalog-item{text-align:center;margin-bottom:1.5rem;width:33%;display:inline-block;float:none !important}@media screen and (max-width:1024px){.esh-catalog-item{width:50%}}@media screen and (max-width:768px){.esh-catalog-item{width:100%}}.esh-catalog-thumbnail{max-width:370px;width:100%}.esh-catalog-button{background-color:#83d01b;border:none;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:80%}.esh-catalog-button.is-disabled{opacity:.5;pointer-events:none}.esh-catalog-button:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-name{font-size:1rem;font-weight:300;margin-top:.5rem;text-align:center;text-transform:uppercase}.esh-catalog-price{text-align:center;font-weight:900;font-size:28px}.esh-catalog-price::before{content:'$'}.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s}.esh-orders_new{min-height:80vh}.esh-orders_new-header{background-color:#00a69c;height:4rem}.esh-orders_new-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders_new-back:hover{color:#fff;transition:color .35s}.esh-orders_new-section{padding:1rem 0}.esh-orders_new-section--right{text-align:right}.esh-orders_new-placeOrder{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-orders_new-placeOrder:hover{background-color:#4a760f;transition:all .35s}.esh-orders_new-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_new-title{font-size:1.25rem;text-transform:uppercase}.esh-orders_new-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_new-items--border:last-of-type{border-color:transparent}.esh-orders_new-item{font-size:1rem;font-weight:300}.esh-orders_new-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_new-item--middle{line-height:1rem}}.esh-orders_new-item--mark{color:#83d01b}.esh-orders_new-image{height:8rem}.esh-orders_detail{min-height:80vh}.esh-orders_detail-section{padding:1rem 0}.esh-orders_detail-section--right{text-align:right}.esh-orders_detail-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_detail-title{text-transform:uppercase}.esh-orders_detail-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_detail-items--border:last-of-type{border-color:transparent}.esh-orders_detail-item{font-size:1rem;font-weight:300}.esh-orders_detail-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_detail-item--middle{line-height:1rem}}.esh-orders_detail-item--mark{color:#83d01b}.esh-orders_detail-image{height:8rem}.esh-identity{line-height:3rem;position:relative;text-align:right}.esh-identity-section{display:inline-block;width:100%}.esh-identity-name{display:inline-block}.esh-identity-name--upper{text-transform:uppercase}@media screen and (max-width:768px){.esh-identity-name{font-size:.85rem}}.esh-identity-image{display:inline-block}.esh-identity-drop{background:#fff;height:0;min-width:14rem;right:0;overflow:hidden;padding:.5rem;position:absolute;top:2.5rem;transition:height .35s}.esh-identity:hover .esh-identity-drop{border:1px solid #eee;height:7rem;transition:height .35s}.esh-identity-item{cursor:pointer;display:block;transition:color .35s}.esh-identity-item:hover{color:#75b918;transition:color .35s}.esh-header{background-color:#00a69c;height:4rem}.esh-header-back{color:rgba(255,255,255,.5) !important;line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-header-back:hover{color:#fff !important;transition:color .35s}.esh-pager-wrapper{padding-top:1rem;text-align:center}.esh-pager-item{margin:0 5vw}.esh-pager-item--navigable{display:inline-block;cursor:pointer}.esh-pager-item--navigable.is-disabled{opacity:0;pointer-events:none}.esh-pager-item--navigable:hover{color:#83d01b}@media screen and (max-width:1280px){.esh-pager-item{font-size:.85rem}}@media screen and (max-width:1024px){.esh-pager-item{margin:0 4vw}} \ No newline at end of file +.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%}.esh-app-footer-brand{height:50px;width:230px}.esh-app-footer-text{color:#83d01b;line-height:50px;text-align:right;width:100%}@font-face{font-family:Montserrat;font-weight:400;src:url("../fonts/Montserrat-Regular.eot?") format("eot"),url("../fonts/Montserrat-Regular.woff") format("woff"),url("../fonts/Montserrat-Regular.ttf") format("truetype"),url("../fonts/Montserrat-Regular.svg#Montserrat") format("svg")}@font-face{font-family:Montserrat;font-weight:700;src:url("../fonts/Montserrat-Bold.eot?") format("eot"),url("../fonts/Montserrat-Bold.woff") format("woff"),url("../fonts/Montserrat-Bold.ttf") format("truetype"),url("../fonts/Montserrat-Bold.svg#Montserrat") format("svg")}html,body{font-family:Montserrat,sans-serif;font-size:16px;font-weight:400;z-index:10}*,*::after,*::before{box-sizing:border-box}.preloading{color:#00a69c;display:block;font-size:1.5rem;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}select::-ms-expand{display:none}@media screen and (min-width:992px){.form-input{max-width:360px;width:360px}}.form-input{border-radius:0;height:45px;padding:10px}.form-input-small{max-width:100px !important}.form-input-medium{width:150px !important}.alert{padding-left:0}.alert-danger{background-color:transparent;border:0;color:#fb0d0d;font-size:12px}a,a:active,a:hover,a:visited{color:#000;text-decoration:none;transition:color .35s}a:hover,a:active{color:#75b918;transition:color .35s}.esh-pager-wrapper{padding-top:1rem;text-align:center}.esh-pager-item{margin:0 5vw}.esh-pager-item--navigable{display:inline-block;cursor:pointer}.esh-pager-item--navigable.is-disabled{opacity:0;pointer-events:none}.esh-pager-item--navigable:hover{color:#83d01b}@media screen and (max-width:1280px){.esh-pager-item{font-size:.85rem}}@media screen and (max-width:1024px){.esh-pager-item{margin:0 4vw}}.esh-identity{line-height:3rem;position:relative;text-align:right}.esh-identity-section{display:inline-block;width:100%}.esh-identity-name{display:inline-block}.esh-identity-name--upper{text-transform:uppercase}@media screen and (max-width:768px){.esh-identity-name{font-size:.85rem}}.esh-identity-image{display:inline-block}.esh-identity-drop{background:#fff;height:0;min-width:14rem;right:0;overflow:hidden;padding:.5rem;position:absolute;top:2.5rem;transition:height .35s}.esh-identity:hover .esh-identity-drop{border:1px solid #eee;height:7rem;transition:height .35s}.esh-identity-item{cursor:pointer;display:block;transition:color .35s}.esh-identity-item:hover{color:#75b918;transition:color .35s}.esh-header{background-color:#00a69c;height:4rem}.esh-header-back{color:rgba(255,255,255,.5) !important;line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-header-back:hover{color:#fff !important;transition:color .35s}.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s}.esh-orders_new{min-height:80vh}.esh-orders_new-header{background-color:#00a69c;height:4rem}.esh-orders_new-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders_new-back:hover{color:#fff;transition:color .35s}.esh-orders_new-section{padding:1rem 0}.esh-orders_new-section--right{text-align:right}.esh-orders_new-placeOrder{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-orders_new-placeOrder:hover{background-color:#4a760f;transition:all .35s}.esh-orders_new-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_new-title{font-size:1.25rem;text-transform:uppercase}.esh-orders_new-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_new-items--border:last-of-type{border-color:transparent}.esh-orders_new-item{font-size:1rem;font-weight:300}.esh-orders_new-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_new-item--middle{line-height:1rem}}.esh-orders_new-item--mark{color:#83d01b}.esh-orders_new-image{height:8rem}.esh-orders_detail{min-height:80vh}.esh-orders_detail-section{padding:1rem 0}.esh-orders_detail-section--right{text-align:right}.esh-orders_detail-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_detail-title{text-transform:uppercase}.esh-orders_detail-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_detail-items--border:last-of-type{border-color:transparent}.esh-orders_detail-item{font-size:1rem;font-weight:300}.esh-orders_detail-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_detail-item--middle{line-height:1rem}}.esh-orders_detail-item--mark{color:#83d01b}.esh-orders_detail-image{height:8rem}.esh-catalog-hero{background-image:url("../images/main_banner.png");background-size:cover;height:260px;width:100%}.esh-catalog-title{position:relative;top:74.28571px}.esh-catalog-filters{background-color:#00a69c;height:65px}.esh-catalog-filter{background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem;min-width:140px;-webkit-appearance:none}.esh-catalog-filter option{background-color:#00a69c}.esh-catalog-label{display:inline-block;position:relative;z-index:0}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-top:.65rem;margin-left:.5rem;position:absolute;text-transform:uppercase;z-index:1}.esh-catalog-label::after{background-image:url("../images/arrow-down.png");height:7px;content:'';position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1}.esh-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;transform:translateY(.5rem);padding:.5rem;transition:all .35s}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-items{margin-top:1rem}.esh-catalog-item{text-align:center;margin-bottom:1.5rem;width:33%;display:inline-block;float:none !important}@media screen and (max-width:1024px){.esh-catalog-item{width:50%}}@media screen and (max-width:768px){.esh-catalog-item{width:100%}}.esh-catalog-thumbnail{max-width:370px;width:100%}.esh-catalog-button{background-color:#83d01b;border:none;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:80%}.esh-catalog-button.is-disabled{opacity:.5;pointer-events:none}.esh-catalog-button:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-name{font-size:1rem;font-weight:300;margin-top:.5rem;text-align:center;text-transform:uppercase}.esh-catalog-price{text-align:center;font-weight:900;font-size:28px}.esh-catalog-price::before{content:'$'}.esh-basket{min-height:80vh}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem}.esh-basket-titles--clean{padding-bottom:0;padding-top:0}.esh-basket-title{text-transform:uppercase}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-basket-items--border:last-of-type{border-color:transparent}.esh-basket-item{font-size:1rem;font-weight:300}.esh-basket-item--middle{line-height:8rem}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem}}.esh-basket-item--mark{color:#00a69c}.esh-basket-image{height:8rem}.esh-basket-input{line-height:1rem;width:100%}.esh-basket-checkout{border:none;border-radius:0;background-color:#83d01b;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s}.esh-basket-margin12{margin-left:12px}.esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none}.esh-basketstatus-image{height:36px;margin-top:.5rem}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s} \ No newline at end of file From ad6ed8a688f78317476a389d0f16def58e4107db Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Mon, 8 May 2017 14:40:55 +0200 Subject: [PATCH 11/31] Added support for PUT HTTP method --- .../Resilience/Resilience.Http/IHttpClient.cs | 2 ++ .../Resilience.Http/ResilientHttpClient.cs | 19 ++++++++++++++++--- .../Resilience.Http/StandardHttpClient.cs | 17 ++++++++++++++++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs index fb41b86d2..5ea3003ed 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs @@ -10,5 +10,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer"); Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer"); + + Task PutAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer"); } } diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs index fb96b1608..88aef22aa 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs @@ -55,13 +55,19 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http }); } - public Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + private Task DoPostPutAsync(HttpMethod method, string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") { + + if (method != HttpMethod.Post && method != HttpMethod.Put) + { + throw new ArgumentException("Value must be either post or put.", nameof(method)); + } + // a new StringContent must be created for each retry // as it is disposed after each call return HttpInvoker(async () => { - var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri); + var requestMessage = new HttpRequestMessage(method, uri); requestMessage.Content = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"); @@ -89,7 +95,14 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http }); } - + public Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + { + return DoPostPutAsync(HttpMethod.Post, uri, item, authorizationToken, requestId, authorizationMethod); + } + public Task PutAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + { + return DoPostPutAsync(HttpMethod.Put, uri, item, authorizationToken, requestId, authorizationMethod); + } public Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") { return HttpInvoker(async () => diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs index 54a9d5858..3d5217064 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -32,8 +33,13 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http return await response.Content.ReadAsStringAsync(); } - public async Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + private async Task DoPostPutAsync(HttpMethod method, string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") { + if (method != HttpMethod.Post && method != HttpMethod.Put) + { + throw new ArgumentException("Value must be either post or put.", nameof(method)); + } + // a new StringContent must be created for each retry // as it is disposed after each call @@ -65,6 +71,15 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http } + public async Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + { + return await DoPostPutAsync(HttpMethod.Post, uri, item, authorizationToken, requestId, authorizationToken); + } + + public async Task PutAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + { + return await DoPostPutAsync(HttpMethod.Put, uri, item, authorizationToken, requestId, authorizationToken); + } public async Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") { var requestMessage = new HttpRequestMessage(HttpMethod.Delete, uri); From aecb81aefa1e5b37e5fd0d3e5bf97ec96f6dc74b Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Mon, 8 May 2017 16:07:51 +0200 Subject: [PATCH 12/31] Fixed error on sharing policies between origins --- .../Resilience.Http/ResilientHttpClient.cs | 48 ++++++++++++++----- .../ResilientHttpClientFactory.cs | 2 +- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs index 88aef22aa..a76e60f0b 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs @@ -3,6 +3,8 @@ using Newtonsoft.Json; using Polly; using Polly.Wrap; using System; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -18,29 +20,45 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http public class ResilientHttpClient : IHttpClient { private HttpClient _client; - private PolicyWrap _policyWrapper; + private readonly Dictionary _policiesPerOrigin; private ILogger _logger; + private readonly Func> _policyCreator; //public HttpClient Inst => _client; - public ResilientHttpClient(Policy[] policies, ILogger logger) + public ResilientHttpClient(Func> policyCreator, ILogger logger) { _client = new HttpClient(); _logger = logger; - - // Add Policies to be applied - _policyWrapper = Policy.WrapAsync(policies); + _policiesPerOrigin = new Dictionary(); + _policyCreator = policyCreator; } - private Task HttpInvoker(Func> action) + private Task HttpInvoker(string origin, Func> action) { + var normalizedOrigin = NormalizeOrigin(origin); + + if (!_policiesPerOrigin.ContainsKey(normalizedOrigin)) + { + var newWrapper = Policy.WrapAsync(_policyCreator(normalizedOrigin).ToArray()); + _policiesPerOrigin.Add(normalizedOrigin, newWrapper); + } + + var policyWrapper = _policiesPerOrigin[normalizedOrigin]; + // Executes the action applying all // the policies defined in the wrapper - return _policyWrapper.ExecuteAsync(() => action()); + return policyWrapper.ExecuteAsync(() => action()); + } + + private static string NormalizeOrigin(string origin) + { + return origin?.Trim()?.ToLower(); } public Task GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer") { - return HttpInvoker(async () => + var origin = GetOriginFromUri(uri); + return HttpInvoker(origin, async () => { var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri); @@ -55,9 +73,15 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http }); } - private Task DoPostPutAsync(HttpMethod method, string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + private static string GetOriginFromUri(string uri) { + var url = new Uri(uri); + var origin = $"{url.Scheme}://{url.DnsSafeHost}:{url.Port}"; + return origin; + } + private Task DoPostPutAsync(HttpMethod method, string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + { if (method != HttpMethod.Post && method != HttpMethod.Put) { throw new ArgumentException("Value must be either post or put.", nameof(method)); @@ -65,7 +89,8 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http // a new StringContent must be created for each retry // as it is disposed after each call - return HttpInvoker(async () => + var origin = GetOriginFromUri(uri); + return HttpInvoker(origin, async () => { var requestMessage = new HttpRequestMessage(method, uri); @@ -105,7 +130,8 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http } public Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") { - return HttpInvoker(async () => + var origin = GetOriginFromUri(uri); + return HttpInvoker(origin, async () => { var requestMessage = new HttpRequestMessage(HttpMethod.Delete, uri); diff --git a/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs b/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs index 8efadf366..8eb43179d 100644 --- a/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs +++ b/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs @@ -17,7 +17,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Infrastructure =>_logger = logger; public ResilientHttpClient CreateResilientHttpClient() - => new ResilientHttpClient(CreatePolicies(), _logger); + => new ResilientHttpClient((origin) => CreatePolicies(), _logger); private Policy[] CreatePolicies() From dbbcc95c5e31036a8b6c9f0a299cbf0a5f77705b Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Mon, 8 May 2017 08:50:02 -0700 Subject: [PATCH 13/31] Removing -all rights reserved- as this is Open Source. --- src/Web/WebMVC/Views/Shared/_Layout.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/WebMVC/Views/Shared/_Layout.cshtml b/src/Web/WebMVC/Views/Shared/_Layout.cshtml index 456248b94..990018b09 100644 --- a/src/Web/WebMVC/Views/Shared/_Layout.cshtml +++ b/src/Web/WebMVC/Views/Shared/_Layout.cshtml @@ -55,7 +55,7 @@
- +
From 8daebced2ffaf00b8ec857789ed4aa5644b0a596 Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Mon, 8 May 2017 18:54:11 +0200 Subject: [PATCH 14/31] Healthchecks updated --- .../HealthCheckBuilderSqlServerExtensions.cs | 11 +++- .../Checks/NumericChecks.cs | 22 ++++++-- .../Checks/SystemChecks.cs | 10 ++++ .../Checks/UrlChecks.cs | 53 ++++++++++++++++--- .../HealthCheckBuilder.cs | 2 +- 5 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/HealthCheckBuilderSqlServerExtensions.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/HealthCheckBuilderSqlServerExtensions.cs index 4998c91ed..1837d9638 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/HealthCheckBuilderSqlServerExtensions.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/HealthCheckBuilderSqlServerExtensions.cs @@ -7,9 +7,18 @@ using System.Data.SqlClient; namespace Microsoft.Extensions.HealthChecks { + // REVIEW: What are the appropriate guards for these functions? + public static class HealthCheckBuilderSqlServerExtensions { public static HealthCheckBuilder AddSqlCheck(this HealthCheckBuilder builder, string name, string connectionString) + { + Guard.ArgumentNotNull(nameof(builder), builder); + + return AddSqlCheck(builder, name, connectionString, builder.DefaultCacheDuration); + } + + public static HealthCheckBuilder AddSqlCheck(this HealthCheckBuilder builder, string name, string connectionString, TimeSpan cacheDuration) { builder.AddCheck($"SqlCheck({name})", async () => { @@ -37,7 +46,7 @@ namespace Microsoft.Extensions.HealthChecks { return HealthCheckResult.Unhealthy($"SqlCheck({name}): Exception during check: {ex.GetType().FullName}"); } - }); + }, cacheDuration); return builder; } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs index f3c795629..4c958234e 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs @@ -10,7 +10,14 @@ namespace Microsoft.Extensions.HealthChecks { // Numeric checks - public static HealthCheckBuilder AddMinValueCheck(this HealthCheckBuilder builder, string name, T minValue, Func currentValueFunc) + public static HealthCheckBuilder AddMinValueCheck(this HealthCheckBuilder builder, string name, T minValue, Func currentValueFunc) where T : IComparable + { + Guard.ArgumentNotNull(nameof(builder), builder); + + return AddMinValueCheck(builder, name, minValue, currentValueFunc, builder.DefaultCacheDuration); + } + + public static HealthCheckBuilder AddMinValueCheck(this HealthCheckBuilder builder, string name, T minValue, Func currentValueFunc, TimeSpan cacheDuration) where T : IComparable { Guard.ArgumentNotNull(nameof(builder), builder); @@ -26,12 +33,19 @@ namespace Microsoft.Extensions.HealthChecks $"min={minValue}, current={currentValue}", new Dictionary { { "min", minValue }, { "current", currentValue } } ); - }); + }, cacheDuration); return builder; } - public static HealthCheckBuilder AddMaxValueCheck(this HealthCheckBuilder builder, string name, T maxValue, Func currentValueFunc) + public static HealthCheckBuilder AddMaxValueCheck(this HealthCheckBuilder builder, string name, T maxValue, Func currentValueFunc) where T : IComparable + { + Guard.ArgumentNotNull(nameof(builder), builder); + + return AddMaxValueCheck(builder, name, maxValue, currentValueFunc, builder.DefaultCacheDuration); + } + + public static HealthCheckBuilder AddMaxValueCheck(this HealthCheckBuilder builder, string name, T maxValue, Func currentValueFunc, TimeSpan cacheDuration) where T : IComparable { Guard.ArgumentNotNull(nameof(builder), builder); @@ -47,7 +61,7 @@ namespace Microsoft.Extensions.HealthChecks $"max={maxValue}, current={currentValue}", new Dictionary { { "max", maxValue }, { "current", currentValue } } ); - }); + }, cacheDuration); return builder; } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/SystemChecks.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/SystemChecks.cs index d4491fda4..dbd9feff2 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/SystemChecks.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/SystemChecks.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Diagnostics; namespace Microsoft.Extensions.HealthChecks @@ -12,10 +13,19 @@ namespace Microsoft.Extensions.HealthChecks public static HealthCheckBuilder AddPrivateMemorySizeCheck(this HealthCheckBuilder builder, long maxSize) => AddMaxValueCheck(builder, $"PrivateMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().PrivateMemorySize64); + public static HealthCheckBuilder AddPrivateMemorySizeCheck(this HealthCheckBuilder builder, long maxSize, TimeSpan cacheDuration) + => AddMaxValueCheck(builder, $"PrivateMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().PrivateMemorySize64, cacheDuration); + public static HealthCheckBuilder AddVirtualMemorySizeCheck(this HealthCheckBuilder builder, long maxSize) => AddMaxValueCheck(builder, $"VirtualMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().VirtualMemorySize64); + public static HealthCheckBuilder AddVirtualMemorySizeCheck(this HealthCheckBuilder builder, long maxSize, TimeSpan cacheDuration) + => AddMaxValueCheck(builder, $"VirtualMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().VirtualMemorySize64, cacheDuration); + public static HealthCheckBuilder AddWorkingSetCheck(this HealthCheckBuilder builder, long maxSize) => AddMaxValueCheck(builder, $"WorkingSet({maxSize})", maxSize, () => Process.GetCurrentProcess().WorkingSet64); + + public static HealthCheckBuilder AddWorkingSetCheck(this HealthCheckBuilder builder, long maxSize, TimeSpan cacheDuration) + => AddMaxValueCheck(builder, $"WorkingSet({maxSize})", maxSize, () => Process.GetCurrentProcess().WorkingSet64, cacheDuration); } } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs index d7df58def..2a6cfe908 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs @@ -10,36 +10,73 @@ namespace Microsoft.Extensions.HealthChecks { public static partial class HealthCheckBuilderExtensions { - // URL checks + // Default URL check public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url) - => AddUrlCheck(builder, url, response => UrlChecker.DefaultUrlCheck(response)); + { + Guard.ArgumentNotNull(nameof(builder), builder); + + return AddUrlCheck(builder, url, builder.DefaultCacheDuration); + } + + public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, TimeSpan cacheDuration) + => AddUrlCheck(builder, url, response => UrlChecker.DefaultUrlCheck(response), cacheDuration); + + // Func returning IHealthCheckResult + + public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, Func checkFunc) + { + Guard.ArgumentNotNull(nameof(builder), builder); + + return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration); + } public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, - Func checkFunc) + Func checkFunc, + TimeSpan cacheDuration) { Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); - return AddUrlCheck(builder, url, response => new ValueTask(checkFunc(response))); + return AddUrlCheck(builder, url, response => new ValueTask(checkFunc(response)), cacheDuration); + } + + // Func returning Task + + public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, Func> checkFunc) + { + Guard.ArgumentNotNull(nameof(builder), builder); + + return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration); } public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, - Func> checkFunc) + Func> checkFunc, + TimeSpan cacheDuration) { Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); - return AddUrlCheck(builder, url, response => new ValueTask(checkFunc(response))); + return AddUrlCheck(builder, url, response => new ValueTask(checkFunc(response)), cacheDuration); + } + + // Func returning ValueTask + + public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, Func> checkFunc) + { + Guard.ArgumentNotNull(nameof(builder), builder); + + return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration); } public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, - Func> checkFunc) + Func> checkFunc, + TimeSpan cacheDuration) { Guard.ArgumentNotNull(nameof(builder), builder); Guard.ArgumentNotNullOrEmpty(nameof(url), url); Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); var urlCheck = new UrlChecker(checkFunc, url); - builder.AddCheck($"UrlCheck({url})", () => urlCheck.CheckAsync()); + builder.AddCheck($"UrlCheck({url})", () => urlCheck.CheckAsync(), cacheDuration); return builder; } } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs index 4e1c6e4c9..006e4a6ef 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs @@ -21,7 +21,7 @@ namespace Microsoft.Extensions.HealthChecks [string.Empty] = _currentGroup }; - DefaultCacheDuration = TimeSpan.FromMinutes(1); + DefaultCacheDuration = TimeSpan.FromMinutes(5); } /// From 4d485da9db66d0c0636c08f2cf90f87585077af0 Mon Sep 17 00:00:00 2001 From: Unai Zorrilla Castro Date: Mon, 8 May 2017 18:57:58 +0200 Subject: [PATCH 15/31] First iteration over ResilientHttpClient. In this case ConcurrentDictionary is used and control concurrency to add new origin and policies --- .../Resilience.Http/ResilientHttpClient.cs | 110 +++++++++++------- 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs index a76e60f0b..88f6cb1d6 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Polly; using Polly.Wrap; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; @@ -20,44 +21,56 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http public class ResilientHttpClient : IHttpClient { private HttpClient _client; - private readonly Dictionary _policiesPerOrigin; + private readonly ConcurrentDictionary _policiesPerOrigin; private ILogger _logger; private readonly Func> _policyCreator; - //public HttpClient Inst => _client; + public ResilientHttpClient(Func> policyCreator, ILogger logger) { _client = new HttpClient(); _logger = logger; - _policiesPerOrigin = new Dictionary(); + _policiesPerOrigin = new ConcurrentDictionary(); _policyCreator = policyCreator; } - private Task HttpInvoker(string origin, Func> action) + + public Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") { - var normalizedOrigin = NormalizeOrigin(origin); + return DoPostPutAsync(HttpMethod.Post, uri, item, authorizationToken, requestId, authorizationMethod); + } - if (!_policiesPerOrigin.ContainsKey(normalizedOrigin)) + public Task PutAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + { + return DoPostPutAsync(HttpMethod.Put, uri, item, authorizationToken, requestId, authorizationMethod); + } + + public Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + { + var origin = GetOriginFromUri(uri); + + return HttpInvoker(origin, async () => { - var newWrapper = Policy.WrapAsync(_policyCreator(normalizedOrigin).ToArray()); - _policiesPerOrigin.Add(normalizedOrigin, newWrapper); - } + var requestMessage = new HttpRequestMessage(HttpMethod.Delete, uri); - var policyWrapper = _policiesPerOrigin[normalizedOrigin]; + if (authorizationToken != null) + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); + } - // Executes the action applying all - // the policies defined in the wrapper - return policyWrapper.ExecuteAsync(() => action()); - } + if (requestId != null) + { + requestMessage.Headers.Add("x-requestid", requestId); + } - private static string NormalizeOrigin(string origin) - { - return origin?.Trim()?.ToLower(); + return await _client.SendAsync(requestMessage); + }); } public Task GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer") { var origin = GetOriginFromUri(uri); + return HttpInvoker(origin, async () => { var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri); @@ -73,13 +86,6 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http }); } - private static string GetOriginFromUri(string uri) - { - var url = new Uri(uri); - var origin = $"{url.Scheme}://{url.DnsSafeHost}:{url.Port}"; - return origin; - } - private Task DoPostPutAsync(HttpMethod method, string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") { if (method != HttpMethod.Post && method != HttpMethod.Put) @@ -90,6 +96,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http // a new StringContent must be created for each retry // as it is disposed after each call var origin = GetOriginFromUri(uri); + return HttpInvoker(origin, async () => { var requestMessage = new HttpRequestMessage(method, uri); @@ -120,34 +127,49 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http }); } - public Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") - { - return DoPostPutAsync(HttpMethod.Post, uri, item, authorizationToken, requestId, authorizationMethod); - } - public Task PutAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + private Task HttpInvoker(string origin, Func> action) { - return DoPostPutAsync(HttpMethod.Put, uri, item, authorizationToken, requestId, authorizationMethod); + var policyWrapper = GetPolicyForOrigin(origin); + + if (policyWrapper != null) + { + // Executes the action applying all + // the policies defined in the wrapper + return policyWrapper.ExecuteAsync(() => action()); + } + else + { + throw new InvalidOperationException($"PolicyWrapper can't be created for origin {origin}"); + } } - public Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") + + private PolicyWrap GetPolicyForOrigin(string origin) { - var origin = GetOriginFromUri(uri); - return HttpInvoker(origin, async () => + var normalizedOrigin = NormalizeOrigin(origin); + + if (!_policiesPerOrigin.TryGetValue(normalizedOrigin, out PolicyWrap policyWrapper)) { - var requestMessage = new HttpRequestMessage(HttpMethod.Delete, uri); + policyWrapper = Policy.WrapAsync(_policyCreator(normalizedOrigin) + .ToArray()); - if (authorizationToken != null) - { - requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); - } + _policiesPerOrigin.TryAdd(normalizedOrigin, policyWrapper); + } - if (requestId != null) - { - requestMessage.Headers.Add("x-requestid", requestId); - } + return policyWrapper; + } - return await _client.SendAsync(requestMessage); - }); + private static string NormalizeOrigin(string origin) + { + return origin?.Trim()?.ToLower(); } + private static string GetOriginFromUri(string uri) + { + var url = new Uri(uri); + + var origin = $"{url.Scheme}://{url.DnsSafeHost}:{url.Port}"; + + return origin; + } } } From aa8e943ba9b7d39c8bfee988fe6db27f22f53aed Mon Sep 17 00:00:00 2001 From: Unai Zorrilla Castro Date: Tue, 9 May 2017 09:19:27 +0200 Subject: [PATCH 16/31] Added ExecutionKey into PolicyWrap and remove COncurrentDictioanry --- .../Resilience.Http/ResilientHttpClient.cs | 43 ++++--------------- .../ResilientHttpClientFactory.cs | 2 +- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs index 88f6cb1d6..d1b34f49a 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using Polly; using Polly.Wrap; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; @@ -20,18 +19,15 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http /// public class ResilientHttpClient : IHttpClient { - private HttpClient _client; - private readonly ConcurrentDictionary _policiesPerOrigin; - private ILogger _logger; - private readonly Func> _policyCreator; + private readonly HttpClient _client; + private readonly ILogger _logger; + private PolicyWrap _policyWrap; - - public ResilientHttpClient(Func> policyCreator, ILogger logger) + public ResilientHttpClient(IEnumerable policies, ILogger logger) { _client = new HttpClient(); _logger = logger; - _policiesPerOrigin = new ConcurrentDictionary(); - _policyCreator = policyCreator; + _policyWrap = Policy.Wrap(policies.ToArray()); } @@ -128,36 +124,15 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http } private Task HttpInvoker(string origin, Func> action) - { - var policyWrapper = GetPolicyForOrigin(origin); - - if (policyWrapper != null) - { - // Executes the action applying all - // the policies defined in the wrapper - return policyWrapper.ExecuteAsync(() => action()); - } - else - { - throw new InvalidOperationException($"PolicyWrapper can't be created for origin {origin}"); - } - } - - private PolicyWrap GetPolicyForOrigin(string origin) { var normalizedOrigin = NormalizeOrigin(origin); - if (!_policiesPerOrigin.TryGetValue(normalizedOrigin, out PolicyWrap policyWrapper)) - { - policyWrapper = Policy.WrapAsync(_policyCreator(normalizedOrigin) - .ToArray()); - - _policiesPerOrigin.TryAdd(normalizedOrigin, policyWrapper); - } - - return policyWrapper; + // Executes the action applying all + // the policies defined in the wrapper + return _policyWrap.ExecuteAsync(() => action(), new Context(normalizedOrigin)); } + private static string NormalizeOrigin(string origin) { return origin?.Trim()?.ToLower(); diff --git a/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs b/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs index 8eb43179d..8efadf366 100644 --- a/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs +++ b/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs @@ -17,7 +17,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Infrastructure =>_logger = logger; public ResilientHttpClient CreateResilientHttpClient() - => new ResilientHttpClient((origin) => CreatePolicies(), _logger); + => new ResilientHttpClient(CreatePolicies(), _logger); private Policy[] CreatePolicies() From 95af2d9a02525799a557b639f23054fa789d2832 Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Tue, 9 May 2017 13:54:45 +0200 Subject: [PATCH 17/31] Autorefresh on healthchecks with config timeout --- src/Services/Catalog/Catalog.API/Startup.cs | 7 +++++- src/Services/Identity/Identity.API/Startup.cs | 7 +++++- src/Services/Ordering/Ordering.API/Startup.cs | 7 +++++- src/Web/WebMVC/Startup.cs | 25 +++++++++++-------- src/Web/WebSPA/Startup.cs | 14 ++++++++--- .../WebStatus/Controllers/HomeController.cs | 1 + .../HealthCheckBuilderExtensions.cs | 4 +-- src/Web/WebStatus/Startup.cs | 18 ++++++++----- src/Web/WebStatus/Views/Shared/_Layout.cshtml | 6 +++++ 9 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index 9eb195674..65fb26515 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -50,7 +50,12 @@ services.AddHealthChecks(checks => { - checks.AddSqlCheck("CatalogDb", Configuration["ConnectionString"]); + var minutes = 1; + if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) + { + minutes = minutesParsed; + } + checks.AddSqlCheck("CatalogDb", Configuration["ConnectionString"], TimeSpan.FromMinutes(minutes)); }); services.AddMvc(options => diff --git a/src/Services/Identity/Identity.API/Startup.cs b/src/Services/Identity/Identity.API/Startup.cs index b47f0535d..981e305c8 100644 --- a/src/Services/Identity/Identity.API/Startup.cs +++ b/src/Services/Identity/Identity.API/Startup.cs @@ -66,7 +66,12 @@ namespace eShopOnContainers.Identity services.AddHealthChecks(checks => { - checks.AddSqlCheck("Identity_Db", Configuration.GetConnectionString("DefaultConnection")); + var minutes = 1; + if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) + { + minutes = minutesParsed; + } + checks.AddSqlCheck("Identity_Db", Configuration.GetConnectionString("DefaultConnection"), TimeSpan.FromMinutes(minutes)); }); services.AddTransient(); diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index 58d8f1cbe..f5b1261d5 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -61,7 +61,12 @@ services.AddHealthChecks(checks => { - checks.AddSqlCheck("OrderingDb", Configuration["ConnectionString"]); + var minutes = 1; + if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) + { + minutes = minutesParsed; + } + checks.AddSqlCheck("OrderingDb", Configuration["ConnectionString"], TimeSpan.FromMinutes(minutes)); }); services.AddEntityFrameworkSqlServer() diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index afc266051..c299165b4 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -54,16 +54,21 @@ namespace Microsoft.eShopOnContainers.WebMVC services.AddHealthChecks(checks => { - checks.AddUrlCheck(Configuration["CatalogUrl"]); - checks.AddUrlCheck(Configuration["OrderingUrl"]); - checks.AddUrlCheck(Configuration["BasketUrl"]); - checks.AddUrlCheck(Configuration["IdentityUrl"]); + var minutes = 1; + if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) + { + minutes = minutesParsed; + } + checks.AddUrlCheck(Configuration["CatalogUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["OrderingUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["BasketUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["IdentityUrl"], TimeSpan.FromMinutes(minutes)); }); // Add application services. - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient, IdentityParser>(); @@ -76,7 +81,7 @@ namespace Microsoft.eShopOnContainers.WebMVC { services.AddSingleton(); } - } + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) @@ -113,10 +118,10 @@ namespace Microsoft.eShopOnContainers.WebMVC AuthenticationScheme = "oidc", SignInScheme = "Cookies", Authority = identityUrl.ToString(), - PostLogoutRedirectUri = callBackUrl.ToString(), + PostLogoutRedirectUri = callBackUrl.ToString(), ClientId = "mvc", ClientSecret = "secret", - ResponseType = "code id_token", + ResponseType = "code id_token", SaveTokens = true, GetClaimsFromUserInfoEndpoint = true, RequireHttpsMetadata = false, diff --git a/src/Web/WebSPA/Startup.cs b/src/Web/WebSPA/Startup.cs index ee932f476..e834c1977 100644 --- a/src/Web/WebSPA/Startup.cs +++ b/src/Web/WebSPA/Startup.cs @@ -45,10 +45,16 @@ namespace eShopConContainers.WebSPA { services.AddHealthChecks(checks => { - checks.AddUrlCheck(Configuration["CatalogUrl"]); - checks.AddUrlCheck(Configuration["OrderingUrl"]); - checks.AddUrlCheck(Configuration["BasketUrl"]); - checks.AddUrlCheck(Configuration["IdentityUrl"]); + var minutes = 1; + if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) + { + minutes = minutesParsed; + } + + checks.AddUrlCheck(Configuration["CatalogUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["OrderingUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["BasketUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["IdentityUrl"], TimeSpan.FromMinutes(minutes)); }); services.Configure(Configuration); diff --git a/src/Web/WebStatus/Controllers/HomeController.cs b/src/Web/WebStatus/Controllers/HomeController.cs index 34a31f1f5..cd59c44d2 100644 --- a/src/Web/WebStatus/Controllers/HomeController.cs +++ b/src/Web/WebStatus/Controllers/HomeController.cs @@ -28,6 +28,7 @@ namespace WebStatus.Controllers data.AddResult(checkResult.Key, checkResult.Value); } + ViewBag.RefreshSeconds = 60; return View(data); } diff --git a/src/Web/WebStatus/Extensions/HealthCheckBuilderExtensions.cs b/src/Web/WebStatus/Extensions/HealthCheckBuilderExtensions.cs index 369f59722..c0655b753 100644 --- a/src/Web/WebStatus/Extensions/HealthCheckBuilderExtensions.cs +++ b/src/Web/WebStatus/Extensions/HealthCheckBuilderExtensions.cs @@ -8,11 +8,11 @@ namespace WebStatus.Extensions { public static class HealthCheckBuilderExtensions { - public static HealthCheckBuilder AddUrlCheckIfNotNull(this HealthCheckBuilder builder, string url) + public static HealthCheckBuilder AddUrlCheckIfNotNull(this HealthCheckBuilder builder, string url, TimeSpan cacheDuration) { if (!string.IsNullOrEmpty(url)) { - builder.AddUrlCheck(url); + builder.AddUrlCheck(url, cacheDuration); } return builder; diff --git a/src/Web/WebStatus/Startup.cs b/src/Web/WebStatus/Startup.cs index 06416940c..0b9ecb937 100644 --- a/src/Web/WebStatus/Startup.cs +++ b/src/Web/WebStatus/Startup.cs @@ -32,12 +32,18 @@ namespace WebStatus // Add framework services. services.AddHealthChecks(checks => { - checks.AddUrlCheckIfNotNull(Configuration["OrderingUrl"]); - checks.AddUrlCheckIfNotNull(Configuration["BasketUrl"]); - checks.AddUrlCheckIfNotNull(Configuration["CatalogUrl"]); - checks.AddUrlCheckIfNotNull(Configuration["IdentityUrl"]); - checks.AddUrlCheckIfNotNull(Configuration["mvc"]); - checks.AddUrlCheckIfNotNull(Configuration["spa"]); + var minutes = 1; + if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) + { + minutes = minutesParsed; + } + + checks.AddUrlCheckIfNotNull(Configuration["OrderingUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheckIfNotNull(Configuration["BasketUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheckIfNotNull(Configuration["CatalogUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheckIfNotNull(Configuration["IdentityUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheckIfNotNull(Configuration["mvc"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheckIfNotNull(Configuration["spa"], TimeSpan.FromMinutes(minutes)); }); services.AddMvc(); } diff --git a/src/Web/WebStatus/Views/Shared/_Layout.cshtml b/src/Web/WebStatus/Views/Shared/_Layout.cshtml index a7eb2e3b6..b7a4d7698 100644 --- a/src/Web/WebStatus/Views/Shared/_Layout.cshtml +++ b/src/Web/WebStatus/Views/Shared/_Layout.cshtml @@ -16,6 +16,12 @@ asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" /> + + @if (ViewBag.RefreshSeconds != null && ViewBag.RefreshSeconds > 0) + { + + } + @Html.Raw(JavaScriptSnippet.FullScript) From 37cef95fecd30726d0f9307bc5db101e91bab393 Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Tue, 9 May 2017 16:59:23 +0200 Subject: [PATCH 18/31] Use good endpoints for healthcheck --- src/Web/WebMVC/Startup.cs | 8 ++++---- src/Web/WebSPA/Startup.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index c299165b4..cd18e1c3a 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -59,10 +59,10 @@ namespace Microsoft.eShopOnContainers.WebMVC { minutes = minutesParsed; } - checks.AddUrlCheck(Configuration["CatalogUrl"], TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(Configuration["OrderingUrl"], TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(Configuration["BasketUrl"], TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(Configuration["IdentityUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["CatalogUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["OrderingUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["BasketUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["IdentityUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); }); // Add application services. diff --git a/src/Web/WebSPA/Startup.cs b/src/Web/WebSPA/Startup.cs index e834c1977..d9c91e5ee 100644 --- a/src/Web/WebSPA/Startup.cs +++ b/src/Web/WebSPA/Startup.cs @@ -51,10 +51,10 @@ namespace eShopConContainers.WebSPA minutes = minutesParsed; } - checks.AddUrlCheck(Configuration["CatalogUrl"], TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(Configuration["OrderingUrl"], TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(Configuration["BasketUrl"], TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(Configuration["IdentityUrl"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["CatalogUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["OrderingUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["BasketUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["IdentityUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); }); services.Configure(Configuration); From d23576b8ca6316075ce5aa612d96c885526ca216 Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Tue, 9 May 2017 18:33:58 +0200 Subject: [PATCH 19/31] FailingMiddleware is now IStartupFilter --- .../Middlewares/FailingStartupFilter.cs | 25 +++++++++++++++++++ .../FailingWebHostBuilderExtensions.cs | 23 +++++++++++++++++ src/Services/Ordering/Ordering.API/Program.cs | 1 + src/Services/Ordering/Ordering.API/Startup.cs | 2 -- 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/Services/Ordering/Ordering.API/Infrastructure/Middlewares/FailingStartupFilter.cs create mode 100644 src/Services/Ordering/Ordering.API/Infrastructure/Middlewares/FailingWebHostBuilderExtensions.cs diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Middlewares/FailingStartupFilter.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Middlewares/FailingStartupFilter.cs new file mode 100644 index 000000000..028239f2d --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Middlewares/FailingStartupFilter.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ordering.API.Infrastructure.Middlewares +{ + public class FailingStartupFilter : IStartupFilter + { + public FailingStartupFilter() + { + } + + public Action Configure(Action next) + { + return app => + { + app.UseFailingMiddleware(); + next(app); + }; + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Middlewares/FailingWebHostBuilderExtensions.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Middlewares/FailingWebHostBuilderExtensions.cs new file mode 100644 index 000000000..1c4979fae --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Middlewares/FailingWebHostBuilderExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Ordering.API.Infrastructure.Middlewares; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class WebHostBuildertExtensions + { + public static IWebHostBuilder UseFailing(this IWebHostBuilder builder, string path) + { + builder.ConfigureServices(services => + { + services.AddSingleton(new FailingStartupFilter()); + }); + return builder; + } + + } +} diff --git a/src/Services/Ordering/Ordering.API/Program.cs b/src/Services/Ordering/Ordering.API/Program.cs index ba92a2da9..752c15e80 100644 --- a/src/Services/Ordering/Ordering.API/Program.cs +++ b/src/Services/Ordering/Ordering.API/Program.cs @@ -10,6 +10,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API { var host = new WebHostBuilder() .UseKestrel() + .UseFailing("/Failing") .UseHealthChecks("/hc") .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index f5b1261d5..0cc8dbc0a 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -148,8 +148,6 @@ app.UseCors("CorsPolicy"); - app.UseFailingMiddleware(); - ConfigureAuth(app); app.UseMvcWithDefaultRoute(); From efe496e156d7ab670c288d79085c9329344c8d10 Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Tue, 9 May 2017 20:27:00 +0200 Subject: [PATCH 20/31] ResilientHttp client policies fixed --- .../Resilience.Http/ResilientHttpClient.cs | 99 ++++++++++++++----- .../ResilientHttpClientFactory.cs | 39 +------- src/Web/WebMVC/Startup.cs | 2 +- 3 files changed, 75 insertions(+), 65 deletions(-) diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs index d1b34f49a..d8ee11d77 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Polly; using Polly.Wrap; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; @@ -21,13 +22,13 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http { private readonly HttpClient _client; private readonly ILogger _logger; - private PolicyWrap _policyWrap; + private ConcurrentDictionary _policyWrappers; - public ResilientHttpClient(IEnumerable policies, ILogger logger) + public ResilientHttpClient(ILogger logger) { _client = new HttpClient(); _logger = logger; - _policyWrap = Policy.Wrap(policies.ToArray()); + _policyWrappers = new ConcurrentDictionary(); } @@ -93,43 +94,49 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http // as it is disposed after each call var origin = GetOriginFromUri(uri); - return HttpInvoker(origin, async () => - { - var requestMessage = new HttpRequestMessage(method, uri); + return HttpInvoker(origin, () => + { + var requestMessage = new HttpRequestMessage(method, uri); - requestMessage.Content = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"); + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"); - if (authorizationToken != null) - { - requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); - } + if (authorizationToken != null) + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); + } - if (requestId != null) - { - requestMessage.Headers.Add("x-requestid", requestId); - } + if (requestId != null) + { + requestMessage.Headers.Add("x-requestid", requestId); + } - var response = await _client.SendAsync(requestMessage); + var response = _client.SendAsync(requestMessage).Result; - // raise exception if HttpResponseCode 500 - // needed for circuit breaker to track fails + // raise exception if HttpResponseCode 500 + // needed for circuit breaker to track fails - if (response.StatusCode == HttpStatusCode.InternalServerError) - { - throw new HttpRequestException(); - } + if (response.StatusCode == HttpStatusCode.InternalServerError) + { + throw new HttpRequestException(); + } - return response; - }); + return Task.FromResult(response); + }); } - private Task HttpInvoker(string origin, Func> action) + private async Task HttpInvoker(string origin, Func> action) { var normalizedOrigin = NormalizeOrigin(origin); + if (!_policyWrappers.TryGetValue(normalizedOrigin, out PolicyWrap policyWrap)) + { + policyWrap = Policy.Wrap(CreatePolicies()); + _policyWrappers.TryAdd(normalizedOrigin, policyWrap); + } + // Executes the action applying all // the policies defined in the wrapper - return _policyWrap.ExecuteAsync(() => action(), new Context(normalizedOrigin)); + return await policyWrap.Execute(action, new Context(normalizedOrigin)); } @@ -146,5 +153,45 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http return origin; } + + + private Policy[] CreatePolicies() + { + return new Policy[] + { + Policy.Handle() + .WaitAndRetry( + // number of retries + 6, + // exponential backofff + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + // on retry + (exception, timeSpan, retryCount, context) => + { + var msg = $"Retry {retryCount} implemented with Polly's RetryPolicy " + + $"of {context.PolicyKey} " + + $"at {context.ExecutionKey}, " + + $"due to: {exception}."; + _logger.LogWarning(msg); + _logger.LogDebug(msg); + }), + Policy.Handle() + .CircuitBreaker( + // number of exceptions before breaking circuit + 5, + // time circuit opened before retry + TimeSpan.FromMinutes(1), + (exception, duration) => + { + // on circuit opened + _logger.LogTrace("Circuit breaker opened"); + }, + () => + { + // on circuit closed + _logger.LogTrace("Circuit breaker reset"); + }) + }; + } } } diff --git a/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs b/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs index 8efadf366..6aa108add 100644 --- a/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs +++ b/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs @@ -17,43 +17,6 @@ namespace Microsoft.eShopOnContainers.WebMVC.Infrastructure =>_logger = logger; public ResilientHttpClient CreateResilientHttpClient() - => new ResilientHttpClient(CreatePolicies(), _logger); - - - private Policy[] CreatePolicies() - => new Policy[] - { - Policy.Handle() - .WaitAndRetryAsync( - // number of retries - 6, - // exponential backofff - retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), - // on retry - (exception, timeSpan, retryCount, context) => - { - var msg = $"Retry {retryCount} implemented with Polly's RetryPolicy " + - $"of {context.PolicyKey} " + - $"at {context.ExecutionKey}, " + - $"due to: {exception}."; - _logger.LogWarning(msg); - _logger.LogDebug(msg); - }), - Policy.Handle() - .CircuitBreakerAsync( - // number of exceptions before breaking circuit - 5, - // time circuit opened before retry - TimeSpan.FromMinutes(1), - (exception, duration) => - { - // on circuit opened - _logger.LogTrace("Circuit breaker opened"); - }, - () => - { - // on circuit closed - _logger.LogTrace("Circuit breaker reset"); - })}; + => new ResilientHttpClient(_logger); } } diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index cd18e1c3a..ba0b1244f 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -74,7 +74,7 @@ namespace Microsoft.eShopOnContainers.WebMVC if (Configuration.GetValue("UseResilientHttp") == bool.TrueString) { - services.AddTransient(); + services.AddSingleton(); services.AddSingleton(sp => sp.GetService().CreateResilientHttpClient()); } else From c33a6ecdf0755d9676e38e9cb279b082f1d32f31 Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Tue, 9 May 2017 21:04:04 +0200 Subject: [PATCH 21/31] Commenting non-wanted task in csproj --- src/Web/WebSPA/WebSPA.csproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Web/WebSPA/WebSPA.csproj b/src/Web/WebSPA/WebSPA.csproj index 03c3cd442..bf126f271 100644 --- a/src/Web/WebSPA/WebSPA.csproj +++ b/src/Web/WebSPA/WebSPA.csproj @@ -73,12 +73,14 @@ + + From b8f01a91996fe49e1cb0c4b35e95cd6c6537a3eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Wed, 10 May 2017 11:26:45 +0200 Subject: [PATCH 22/31] Fix failing tests --- .../FunctionalTests/Services/Catalog/CatalogScenariosBase.cs | 4 ++-- .../FunctionalTests/Services/IntegrationEventsScenarios.cs | 2 +- .../IntegrationTests/Services/Ordering/OrderingScenarios.cs | 2 +- .../Ordering/Application/NewOrderCommandHandlerTest.cs | 4 +++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/test/Services/FunctionalTests/Services/Catalog/CatalogScenariosBase.cs b/test/Services/FunctionalTests/Services/Catalog/CatalogScenariosBase.cs index 2c8c1280b..afc43c2eb 100644 --- a/test/Services/FunctionalTests/Services/Catalog/CatalogScenariosBase.cs +++ b/test/Services/FunctionalTests/Services/Catalog/CatalogScenariosBase.cs @@ -32,9 +32,9 @@ namespace FunctionalTests.Services.Catalog } } - public static class Post + public static class Put { - public static string UpdateCatalogProduct = "api/v1/catalog/update"; + public static string UpdateCatalogProduct = "api/v1/catalog/items"; } } } diff --git a/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs b/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs index 8ed11be3a..fd270d159 100644 --- a/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs +++ b/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs @@ -43,7 +43,7 @@ namespace FunctionalTests.Services var itemToModify = basket.Items[2]; var oldPrice = itemToModify.UnitPrice; var newPrice = oldPrice + priceModification; - var pRes = await catalogClient.PostAsync(CatalogScenariosBase.Post.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice, originalCatalogProducts), UTF8Encoding.UTF8, "application/json")); + var pRes = await catalogClient.PutAsync(CatalogScenariosBase.Put.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice, originalCatalogProducts), UTF8Encoding.UTF8, "application/json")); var modifiedCatalogProducts = await GetCatalogAsync(catalogClient); diff --git a/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs b/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs index 49f04fa3b..52119997d 100644 --- a/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs +++ b/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs @@ -93,7 +93,7 @@ string BuildOrderWithInvalidExperationTime() { var order = new CreateOrderCommand( - null, + new List(), cardExpiration: DateTime.UtcNow.AddYears(-1), cardNumber: "5145-555-5555", cardHolderName: "Jhon Senna", diff --git a/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs b/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs index 9a4a70bf8..1615bcd0b 100644 --- a/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs +++ b/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs @@ -14,6 +14,8 @@ namespace UnitTest.Ordering.Application using System.Collections; using System.Collections.Generic; using Xunit; + using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand; + public class NewOrderRequestHandlerTest { private readonly Mock _orderRepositoryMock; @@ -72,7 +74,7 @@ namespace UnitTest.Ordering.Application private CreateOrderCommand FakeOrderRequestWithBuyer(Dictionary args = null) { return new CreateOrderCommand( - null, + new List(), city: args != null && args.ContainsKey("city") ? (string)args["city"] : null, street: args != null && args.ContainsKey("street") ? (string)args["street"] : null, state: args != null && args.ContainsKey("state") ? (string)args["state"] : null, From d2c950d94eb769168d0d12d7e9ccf9bb0d9761a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Wed, 10 May 2017 11:37:29 +0200 Subject: [PATCH 23/31] Fix issue WebMVC bootstrap lib not deployed to container in windows cli --- src/Web/WebMVC/WebMVC.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Web/WebMVC/WebMVC.csproj b/src/Web/WebMVC/WebMVC.csproj index 3f0f6b84b..f109548e6 100644 --- a/src/Web/WebMVC/WebMVC.csproj +++ b/src/Web/WebMVC/WebMVC.csproj @@ -13,12 +13,12 @@ ..\..\..\docker-compose.dcproj - + From d18b3581a627e0462b9bb3b124d88e5ab797ab50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Wed, 10 May 2017 12:39:11 +0200 Subject: [PATCH 24/31] Remove hard-coded values from Identity srv configuration --- .../Identity/Identity.API/Configuration/Config.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Services/Identity/Identity.API/Configuration/Config.cs b/src/Services/Identity/Identity.API/Configuration/Config.cs index c26674591..744d0a0ce 100644 --- a/src/Services/Identity/Identity.API/Configuration/Config.cs +++ b/src/Services/Identity/Identity.API/Configuration/Config.cs @@ -60,7 +60,7 @@ namespace Identity.API.Configuration AllowAccessTokensViaBrowser = true, RedirectUris = { clientsUrl["Xamarin"] }, RequireConsent = false, - PostLogoutRedirectUris = { "http://13.88.8.119:5105/Account/Redirecting", "http://10.6.1.234:5105/Account/Redirecting" }, + PostLogoutRedirectUris = { $"{clientsUrl["Xamarin"]}/Account/Redirecting" }, AllowedCorsOrigins = { "http://eshopxamarin" }, AllowedScopes = { @@ -84,15 +84,11 @@ namespace Identity.API.Configuration AllowOfflineAccess = true, RedirectUris = new List { - $"{clientsUrl["Mvc"]}/signin-oidc", - "http://104.40.62.65:5100/signin-oidc", - "http://localhost:5100/signin-oidc", - "http://13.88.8.119:5100/signin-oidc" + $"{clientsUrl["Mvc"]}/signin-oidc" }, PostLogoutRedirectUris = new List { - $"{clientsUrl["Mvc"]}/signout-callback-oidc", - "http://localhost:5100/signout-callback-oidc" + $"{clientsUrl["Mvc"]}/signout-callback-oidc" }, AllowedScopes = new List { From 564e2df93a21ea9b98334ae07f85fa7021fc682f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Wed, 10 May 2017 13:25:43 +0200 Subject: [PATCH 25/31] Extract Policies to ResilientHttpClientFactory --- .../Resilience.Http/ResilientHttpClient.cs | 48 ++----------------- .../ResilientHttpClientFactory.cs | 44 +++++++++++++++-- 2 files changed, 44 insertions(+), 48 deletions(-) diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs index d8ee11d77..d80352862 100644 --- a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs +++ b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs @@ -22,12 +22,14 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http { private readonly HttpClient _client; private readonly ILogger _logger; + private readonly Func> _policyCreator; private ConcurrentDictionary _policyWrappers; - public ResilientHttpClient(ILogger logger) + public ResilientHttpClient(Func> policyCreator, ILogger logger) { _client = new HttpClient(); _logger = logger; + _policyCreator = policyCreator; _policyWrappers = new ConcurrentDictionary(); } @@ -130,7 +132,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http if (!_policyWrappers.TryGetValue(normalizedOrigin, out PolicyWrap policyWrap)) { - policyWrap = Policy.Wrap(CreatePolicies()); + policyWrap = Policy.Wrap(_policyCreator(normalizedOrigin).ToArray()); _policyWrappers.TryAdd(normalizedOrigin, policyWrap); } @@ -152,46 +154,6 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http var origin = $"{url.Scheme}://{url.DnsSafeHost}:{url.Port}"; return origin; - } - - - private Policy[] CreatePolicies() - { - return new Policy[] - { - Policy.Handle() - .WaitAndRetry( - // number of retries - 6, - // exponential backofff - retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), - // on retry - (exception, timeSpan, retryCount, context) => - { - var msg = $"Retry {retryCount} implemented with Polly's RetryPolicy " + - $"of {context.PolicyKey} " + - $"at {context.ExecutionKey}, " + - $"due to: {exception}."; - _logger.LogWarning(msg); - _logger.LogDebug(msg); - }), - Policy.Handle() - .CircuitBreaker( - // number of exceptions before breaking circuit - 5, - // time circuit opened before retry - TimeSpan.FromMinutes(1), - (exception, duration) => - { - // on circuit opened - _logger.LogTrace("Circuit breaker opened"); - }, - () => - { - // on circuit closed - _logger.LogTrace("Circuit breaker reset"); - }) - }; - } + } } } diff --git a/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs b/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs index 6aa108add..6322869ff 100644 --- a/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs +++ b/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs @@ -1,10 +1,7 @@ using Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Polly; +using System; using System.Net.Http; namespace Microsoft.eShopOnContainers.WebMVC.Infrastructure @@ -17,6 +14,43 @@ namespace Microsoft.eShopOnContainers.WebMVC.Infrastructure =>_logger = logger; public ResilientHttpClient CreateResilientHttpClient() - => new ResilientHttpClient(_logger); + => new ResilientHttpClient((origin) => CreatePolicies(), _logger); + + private Policy[] CreatePolicies() + => new Policy[] + { + Policy.Handle() + .WaitAndRetry( + // number of retries + 6, + // exponential backofff + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + // on retry + (exception, timeSpan, retryCount, context) => + { + var msg = $"Retry {retryCount} implemented with Polly's RetryPolicy " + + $"of {context.PolicyKey} " + + $"at {context.ExecutionKey}, " + + $"due to: {exception}."; + _logger.LogWarning(msg); + _logger.LogDebug(msg); + }), + Policy.Handle() + .CircuitBreaker( + // number of exceptions before breaking circuit + 5, + // time circuit opened before retry + TimeSpan.FromMinutes(1), + (exception, duration) => + { + // on circuit opened + _logger.LogTrace("Circuit breaker opened"); + }, + () => + { + // on circuit closed + _logger.LogTrace("Circuit breaker reset"); + }) + }; } } From afd3fe2fe5cf3176fbf497fb4e4b04bf7f26e1c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Wed, 10 May 2017 15:01:02 +0200 Subject: [PATCH 26/31] Fix issue SPA HealthCheck Urls --- docker-compose.override.yml | 6 +++++- docker-compose.prod.yml | 6 +++++- src/Web/WebSPA/Startup.cs | 8 ++++---- src/Web/WebStatus/Views/Shared/_Layout.cshtml | 7 ++++--- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index f96a8d177..a8ee546f4 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -56,7 +56,11 @@ services: - CatalogUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101 - OrderingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102 - IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - - BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103 + - BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103 + - CatalogUrlHC=http://catalog.api:5101/hc + - OrderingUrlHC=http://ordering.api:5102/hc + - IdentityUrlHC=http://identity.api:5105/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. + - BasketUrlHC=http://basket.api:5103/hc ports: - "5104:5104" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 58bfe5f82..31b256ff5 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -61,7 +61,11 @@ services: - CatalogUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5101 - OrderingUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5102 - IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. - - BasketUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5103 + - BasketUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5103 + - CatalogUrlHC=http://catalog.api:5101/hc + - OrderingUrlHC=http://ordering.api:5102/hc + - IdentityUrlHC=http://identity.api:5105/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. + - BasketUrlHC=http://basket.api:5103/hc ports: - "5104:5104" diff --git a/src/Web/WebSPA/Startup.cs b/src/Web/WebSPA/Startup.cs index d9c91e5ee..53476266a 100644 --- a/src/Web/WebSPA/Startup.cs +++ b/src/Web/WebSPA/Startup.cs @@ -51,10 +51,10 @@ namespace eShopConContainers.WebSPA minutes = minutesParsed; } - checks.AddUrlCheck(Configuration["CatalogUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(Configuration["OrderingUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(Configuration["BasketUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(Configuration["IdentityUrl"] + "/hc", TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["CatalogUrlHC"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["OrderingUrlHC"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["BasketUrlHC"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(Configuration["IdentityUrlHC"], TimeSpan.FromMinutes(minutes)); }); services.Configure(Configuration); diff --git a/src/Web/WebStatus/Views/Shared/_Layout.cshtml b/src/Web/WebStatus/Views/Shared/_Layout.cshtml index b7a4d7698..7429afcaf 100644 --- a/src/Web/WebStatus/Views/Shared/_Layout.cshtml +++ b/src/Web/WebStatus/Views/Shared/_Layout.cshtml @@ -48,9 +48,10 @@
@RenderBody()
-
-

© 2017 - WebStatus

-
+
+ From 2741fde27157c4213f46a24faebf8fd60018d0a9 Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Thu, 11 May 2017 08:06:40 +0200 Subject: [PATCH 27/31] URLs on 80 - WIP --- docker-compose-windows.override.yml | 28 +++++++------- docker-compose.override.yml | 58 ++++++++++++++-------------- docker-compose.prod.yml | 59 +++++++++++++++-------------- 3 files changed, 73 insertions(+), 72 deletions(-) diff --git a/docker-compose-windows.override.yml b/docker-compose-windows.override.yml index c85864d72..98d2c69d9 100644 --- a/docker-compose-windows.override.yml +++ b/docker-compose-windows.override.yml @@ -13,7 +13,7 @@ services: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://0.0.0.0:5103 - ConnectionString=basket.data - - identityUrl=http://identity.api:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. + - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: - "5103:5103" @@ -21,7 +21,7 @@ services: catalog.api: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5101 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word - ExternalCatalogBaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq @@ -31,45 +31,45 @@ services: identity.api: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5105 + - ASPNETCORE_URLS=http://0.0.0.0:80 - SpaClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5104 - ConnectionStrings__DefaultConnection=Server=sql.data;Database=Microsoft.eShopOnContainers.Service.IdentityDb;User Id=sa;Password=Pass@word - MvcClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5100 #Local: You need to open your local dev-machine firewall at range 5100-5105. ports: - - "5105:5105" + - "5105:80" ordering.api: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5102 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word - - identityUrl=http://identity.api:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. + - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: - - "5102:5102" + - "5102:80" webspa: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5104 + - ASPNETCORE_URLS=http://0.0.0.0:80 - CatalogUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101 - OrderingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102 - IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103 ports: - - "5104:5104" + - "5104:80" webmvc: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5100 - - CatalogUrl=http://catalog.api:5101 - - OrderingUrl=http://ordering.api:5102 - - BasketUrl=http://basket.api:5103 + - ASPNETCORE_URLS=http://0.0.0.0:80 + - CatalogUrl=http://catalog.api + - OrderingUrl=http://ordering.api + - BasketUrl=http://basket.api - IdentityUrl=http://10.0.75.1:5105 #Local: Use 10.0.75.1 in a "Docker for Windows" environment, if using "localhost" from browser. #Remote: Use ${ESHOP_EXTERNAL_DNS_NAME_OR_IP} if using external IP or DNS name from browser. ports: - - "5100:5100" + - "5100:80" sql.data: environment: diff --git a/docker-compose.override.yml b/docker-compose.override.yml index a8ee546f4..04d1c4d9c 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -11,70 +11,70 @@ services: basket.api: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5103 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=basket.data - - identityUrl=http://identity.api:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. + - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: - - "5103:5103" + - "5103:80" catalog.api: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5101 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word - ExternalCatalogBaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: - - "5101:5101" + - "5101:80" identity.api: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5105 + - ASPNETCORE_URLS=http://0.0.0.0:80 - SpaClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5104 - XamarinCallback=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105/xamarincallback #localhost do not work for UWP login, so we have to use "external" IP always - ConnectionStrings__DefaultConnection=Server=sql.data;Database=Microsoft.eShopOnContainers.Service.IdentityDb;User Id=sa;Password=Pass@word - MvcClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5100 #Local: You need to open your local dev-machine firewall at range 5100-5105. ports: - - "5105:5105" + - "5105:80" ordering.api: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5102 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word - - identityUrl=http://identity.api:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. + - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: - - "5102:5102" + - "5102:80" webspa: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5104 + - ASPNETCORE_URLS=http://0.0.0.0:80 - CatalogUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101 - OrderingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102 - IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103 - - CatalogUrlHC=http://catalog.api:5101/hc - - OrderingUrlHC=http://ordering.api:5102/hc - - IdentityUrlHC=http://identity.api:5105/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. - - BasketUrlHC=http://basket.api:5103/hc + - CatalogUrlHC=http://catalog.api/hc + - OrderingUrlHC=http://ordering.api/hc + - IdentityUrlHC=http://identity.api/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. + - BasketUrlHC=http://basket.api/hc ports: - - "5104:5104" + - "5104:80" webmvc: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5100 - - CatalogUrl=http://catalog.api:5101 - - OrderingUrl=http://ordering.api:5102 - - BasketUrl=http://basket.api:5103 + - ASPNETCORE_URLS=http://0.0.0.0:80 + - CatalogUrl=http://catalog.api + - OrderingUrl=http://ordering.api + - BasketUrl=http://basket.api - IdentityUrl=http://10.0.75.1:5105 #Local: Use 10.0.75.1 in a "Docker for Windows" environment, if using "localhost" from browser. #Remote: Use ${ESHOP_EXTERNAL_DNS_NAME_OR_IP} if using external IP or DNS name from browser. ports: - - "5100:5100" + - "5100:80" sql.data: environment: @@ -86,12 +86,12 @@ services: webstatus: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5107 - - CatalogUrl=http://catalog.api:5101/hc - - OrderingUrl=http://ordering.api:5102/hc - - BasketUrl=http://basket.api:5103/hc - - IdentityUrl=http://identity.api:5105/hc - - mvc=http://webmvc:5100/hc - - spa=http://webspa:5104/hc + - ASPNETCORE_URLS=http://0.0.0.0:80 + - CatalogUrl=http://catalog.api/hc + - OrderingUrl=http://ordering.api/hc + - BasketUrl=http://basket.api/hc + - IdentityUrl=http://identity.api/hc + - mvc=http://webmvc/hc + - spa=http://webspa/hc ports: - - "5107:5107" + - "5107:80" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 31b256ff5..c5d8839ea 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -16,69 +16,69 @@ services: basket.api: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5103 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=basket.data - - identityUrl=http://identity.api:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. + - identityUrl=http://identity.api #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: - - "5103:5103" + - "5103:80" catalog.api: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5101 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word - ExternalCatalogBaseUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5101 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: - - "5101:5101" + - "5101:80" identity.api: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5105 + - ASPNETCORE_URLS=http://0.0.0.0:80 - SpaClient=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5104 - ConnectionStrings__DefaultConnection=Server=sql.data;Database=Microsoft.eShopOnContainers.Service.IdentityDb;User Id=sa;Password=Pass@word - MvcClient=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5100 #Local: You need to open your host's firewall at range 5100-5105. - XamarinCallback=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105/xamarincallback ports: - - "5105:5105" + - "5105:80" ordering.api: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5102 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word - - identityUrl=http://identity.api:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. + - identityUrl=http://identity.api #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: - - "5102:5102" + - "5102:80" webspa: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5104 + - ASPNETCORE_URLS=http://0.0.0.0:80 - CatalogUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5101 - OrderingUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5102 - IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. - BasketUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5103 - - CatalogUrlHC=http://catalog.api:5101/hc - - OrderingUrlHC=http://ordering.api:5102/hc - - IdentityUrlHC=http://identity.api:5105/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. - - BasketUrlHC=http://basket.api:5103/hc + - CatalogUrlHC=http://catalog.api/hc + - OrderingUrlHC=http://ordering.api/hc + - IdentityUrlHC=http://identity.api/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. + - BasketUrlHC=http://basket.api/hc ports: - - "5104:5104" + - "5104:80" webmvc: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5100 - - CatalogUrl=http://catalog.api:5101 - - OrderingUrl=http://ordering.api:5102 + - ASPNETCORE_URLS=http://0.0.0.0:80 + - CatalogUrl=http://catalog.api + - OrderingUrl=http://ordering.api - IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. - - BasketUrl=http://basket.api:5103 + - BasketUrl=http://basket.api ports: - - "5100:5100" + - "5100:80" sql.data: environment: @@ -90,12 +90,13 @@ services: webstatus: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5107 - - CatalogUrl=http://catalog.api:5101/hc - - OrderingUrl=http://ordering.api:5102/hc - - BasketUrl=http://basket.api:5103/hc - - mvc=http://webmvc:5100/hc - - spa=http://webspa:5104/hc - - IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. + - ASPNETCORE_URLS=http://0.0.0.0:80 + - CatalogUrl=http://catalog.api/hc + - OrderingUrl=http://ordering.api/hc + - BasketUrl=http://basket.api/hc + - IdentityUrl=http://identity.api/hc + - mvc=http://webmvc/hc + - spa=http://webspa/hc + ports: - - "5107:5107" \ No newline at end of file + - "5107:80" \ No newline at end of file From 20a7427d3889428384fe0fc0423b6bf615e0ac22 Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Thu, 11 May 2017 19:00:44 +0200 Subject: [PATCH 28/31] more config setups --- docker-compose-windows.override.yml | 10 ++++++--- docker-compose-windows.prod.yml | 34 ++++++++++++++++------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/docker-compose-windows.override.yml b/docker-compose-windows.override.yml index 98d2c69d9..c3733c164 100644 --- a/docker-compose-windows.override.yml +++ b/docker-compose-windows.override.yml @@ -11,12 +11,12 @@ services: basket.api: environment: - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:5103 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=basket.data - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: - - "5103:5103" + - "5103:80" catalog.api: environment: @@ -26,7 +26,7 @@ services: - ExternalCatalogBaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: - - "5101:5101" + - "5101:80" identity.api: environment: @@ -56,6 +56,10 @@ services: - OrderingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102 - IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103 + - CatalogUrlHC=http://catalog.api/hc + - OrderingUrlHC=http://ordering.api/hc + - IdentityUrlHC=http://identity.api/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. + - BasketUrlHC=http://basket.api/hc ports: - "5104:80" diff --git a/docker-compose-windows.prod.yml b/docker-compose-windows.prod.yml index 8d00df4ca..7b9c9ab22 100644 --- a/docker-compose-windows.prod.yml +++ b/docker-compose-windows.prod.yml @@ -16,61 +16,65 @@ services: basket.api: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5103 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=basket.data - - identityUrl=http://identity.api:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. + - identityUrl=http://identity.api #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. ports: - "5103:5103" catalog.api: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5101 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word - ExternalCatalogBaseUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5101 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. ports: - - "5101:5101" + - "5101:80" identity.api: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5105 + - ASPNETCORE_URLS=http://0.0.0.0:80 - SpaClient=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5104 - ConnectionStrings__DefaultConnection=Server=sql.data;Database=Microsoft.eShopOnContainers.Service.IdentityDb;User Id=sa;Password=Pass@word - MvcClient=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5100 #Local: You need to open your host's firewall at range 5100-5105. ports: - - "5105:5105" + - "5105:80" ordering.api: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5102 + - ASPNETCORE_URLS=http://0.0.0.0:80 - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word - identityUrl=http://identity.api:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. ports: - - "5102:5102" + - "5102:80" webspa: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5104 + - ASPNETCORE_URLS=http://0.0.0.0 - CatalogUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5101 - OrderingUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5102 - IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. - BasketUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5103 + - CatalogUrlHC=http://catalog.api/hc + - OrderingUrlHC=http://ordering.api/hc + - IdentityUrlHC=http://identity.api/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. + - BasketUrlHC=http://basket.api/hc ports: - - "5104:5104" + - "5104:80" webmvc: environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_URLS=http://0.0.0.0:5100 - - CatalogUrl=http://catalog.api:5101 - - OrderingUrl=http://ordering.api:5102 + - ASPNETCORE_URLS=http://0.0.0.0:80 + - CatalogUrl=http://catalog.api + - OrderingUrl=http://ordering.api - IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. - - BasketUrl=http://basket.api:5103 + - BasketUrl=http://basket.api ports: - - "5100:5100" + - "5100:80" sql.data: environment: From f6bb2acd05e2adb75b3e0500baf675a4c9df43e8 Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Fri, 12 May 2017 11:06:39 +0200 Subject: [PATCH 29/31] Removed custom docker images for redis and rabbitmq --- _docker/rabbitmq/Dockerfile.nanowin | 29 ---------------------------- _docker/rabbitmq/enabled_plugins | 1 - _docker/rabbitmq/rabbitmq.config | 1 - _docker/redis/Dockerfile.nanowin | 30 ----------------------------- docker-compose-windows.yml | 4 ++-- 5 files changed, 2 insertions(+), 63 deletions(-) delete mode 100644 _docker/rabbitmq/Dockerfile.nanowin delete mode 100644 _docker/rabbitmq/enabled_plugins delete mode 100644 _docker/rabbitmq/rabbitmq.config delete mode 100644 _docker/redis/Dockerfile.nanowin diff --git a/_docker/rabbitmq/Dockerfile.nanowin b/_docker/rabbitmq/Dockerfile.nanowin deleted file mode 100644 index 26474c235..000000000 --- a/_docker/rabbitmq/Dockerfile.nanowin +++ /dev/null @@ -1,29 +0,0 @@ -#https://github.com/spring2/dockerfiles/tree/master/rabbitmq - -FROM microsoft/windowsservercore - -SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] - -ENV chocolateyUseWindowsCompression false - -RUN iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1')); \ - choco install -y curl; - -RUN choco install -y erlang -ENV ERLANG_SERVICE_MANAGER_PATH="C:\Program Files\erl8.2\erts-8.2\bin" -RUN choco install -y rabbitmq -ENV RABBITMQ_SERVER="C:\Program Files\RabbitMQ Server\rabbitmq_server-3.6.5" - -ENV RABBITMQ_CONFIG_FILE="c:\rabbitmq" -COPY rabbitmq.config C:/ -COPY rabbitmq.config C:/Users/ContainerAdministrator/AppData/Roaming/RabbitMQ/ -COPY enabled_plugins C:/Users/ContainerAdministrator/AppData/Roaming/RabbitMQ/ - - -EXPOSE 4369 -EXPOSE 5672 -EXPOSE 5671 -EXPOSE 15672 - -WORKDIR C:/Program\ Files/RabbitMQ\ Server/rabbitmq_server-3.6.5/sbin -CMD .\rabbitmq-server.bat \ No newline at end of file diff --git a/_docker/rabbitmq/enabled_plugins b/_docker/rabbitmq/enabled_plugins deleted file mode 100644 index 9eafc419b..000000000 --- a/_docker/rabbitmq/enabled_plugins +++ /dev/null @@ -1 +0,0 @@ -[rabbitmq_amqp1_0,rabbitmq_management]. diff --git a/_docker/rabbitmq/rabbitmq.config b/_docker/rabbitmq/rabbitmq.config deleted file mode 100644 index f7837f213..000000000 --- a/_docker/rabbitmq/rabbitmq.config +++ /dev/null @@ -1 +0,0 @@ -[{rabbit, [{loopback_users, []}]}]. \ No newline at end of file diff --git a/_docker/redis/Dockerfile.nanowin b/_docker/redis/Dockerfile.nanowin deleted file mode 100644 index a08bcb0b6..000000000 --- a/_docker/redis/Dockerfile.nanowin +++ /dev/null @@ -1,30 +0,0 @@ -# The MSI installs a service which is hard to override, so let's use a zip file. - -FROM microsoft/windowsservercore -MAINTAINER alexellis2@gmail.com - -SHELL ["powershell"] -RUN $ErrorActionPreference = 'Stop'; \ - wget https://github.com/MSOpenTech/redis/releases/download/win-3.2.100/Redis-x64-3.2.100.zip -OutFile Redis-x64-3.2.100.zip ; \ - Expand-Archive Redis-x64-3.2.100.zip -dest 'C:\\Program Files\\Redis\\' ; \ - Remove-Item Redis-x64-3.2.100.zip -Force - -RUN setx PATH '%PATH%;C:\\Program Files\\Redis\\' -WORKDIR 'C:\\Program Files\\Redis\\' - - - -RUN Get-Content redis.windows.conf | Where { $_ -notmatch 'bind 127.0.0.1' } | Set-Content redis.openport.conf ; \ - Get-Content redis.openport.conf | Where { $_ -notmatch 'protected-mode yes' } | Set-Content redis.unprotected.conf ; \ - Add-Content redis.unprotected.conf 'protected-mode no' ; \ - Add-Content redis.unprotected.conf 'bind 0.0.0.0' ; \ - Get-Content redis.unprotected.conf - -EXPOSE 6379 - -RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord - -# Define our command to be run when launching the container -CMD .\\redis-server.exe .\\redis.unprotected.conf --port 6379 ; \ - Write-Host Redis Started... ; \ - while ($true) { Start-Sleep -Seconds 3600 } diff --git a/docker-compose-windows.yml b/docker-compose-windows.yml index 5a08a9302..48f7ef7b4 100644 --- a/docker-compose-windows.yml +++ b/docker-compose-windows.yml @@ -58,7 +58,7 @@ services: image: microsoft/mssql-server-windows basket.data: - image: eshop/redis-win + image: redis:nanoserver # build: # context: ./_docker/redis # dockerfile: Dockerfile.nanowin @@ -66,7 +66,7 @@ services: - "6379:6379" rabbitmq: - image: eshop/rabbitmq-win + image: spring2/rabbitmq # build: # context: ./_docker/rabbitmq # dockerfile: Dockerfile.nanowin From 845b65eb82535e385db88df747d6a5bc7a7a790a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Tue, 16 May 2017 12:13:49 +0200 Subject: [PATCH 30/31] Modify k8s deployment script to support CD/CI deployment Create documentation --- README.CICD.k8s.md | 48 +++++++++++ img/k8s/blob_creation.png | Bin 0 -> 26340 bytes img/k8s/deploy_script_task.png | Bin 0 -> 37144 bytes img/k8s/get_kubectlbin_task.png | Bin 0 -> 32167 bytes img/k8s/get_kubectlconfig_task.png | Bin 0 -> 31190 bytes k8s/deploy.ps1 | 129 +++++++++++++++++++---------- 6 files changed, 131 insertions(+), 46 deletions(-) create mode 100644 README.CICD.k8s.md create mode 100644 img/k8s/blob_creation.png create mode 100644 img/k8s/deploy_script_task.png create mode 100644 img/k8s/get_kubectlbin_task.png create mode 100644 img/k8s/get_kubectlconfig_task.png diff --git a/README.CICD.k8s.md b/README.CICD.k8s.md new file mode 100644 index 000000000..182880fc5 --- /dev/null +++ b/README.CICD.k8s.md @@ -0,0 +1,48 @@ +# Kubernetes CI/CD VSTS +For k8s CI/CD pipeline delivery a series of tasks must be created in VSTS to deploy k8s in Azure + +## Prerequisites +* A Kubernetes cluster. Follow Azure Container Service's [walkthrough](https://docs.microsoft.com/en-us/azure/container-service/container-service-kubernetes-walkthrough) to create one. +* A private Docker registry. Follow Azure Container Registry's [guide](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-get-started-portal) to create one. +* Optionally, previous steps can be skipped if you run gen-k8s-env.ps1 script to automatically create the azure environment needed for kubernetes deployment. Azure cli 2.0 must be previously installed [installation guide](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli). For example: +>``` +>./gen-k8s-env -resourceGroupName k8sGroup -location westeurope -registryName k8sregistry -orchestratorName k8s-cluster -dnsName k8s-dns +>``` +* An `Azure Blob storage`. It is needed for storing the kubernetes config file used by the hosted agent to access to Kubernetes cluster. Example: + + + +* Upload the `kubernetes config file` to the blob storage previously created. Execute the following command which will download the config file into the directory `c:\Users\\.kube\` and then, upload it to your blob storage: +>``` +>https://eshopk8s.blob.core.windows.net/k8s-config/config +>``` +## Create the VSTS tasks +1. Create a `Download File` task to download the kubernetes binary `kubectl` to the hosted agent. For example: +>``` +>https://storage.googleapis.com/kubernetes-release/release/v0.0.1.7.0-alpha.0/bin/windows/386/kubectl.exe +>``` + + +2. Create a Download File task to download the kubernetes config file to the hosted agent. For example: +>``` +>https://eshopk8s.blob.core.windows.net/k8s-config/config +>``` + + +3. Create a powershell task to execute the k8s deployment script. For example: + +* Deployment script path +>``` +>$(System.DefaultWorkingDirectory)/All Microservices/docker-compose/deploy.ps1 +>``` + +* Deployment script path arguments. Where: + - userDockerHub: indicates if Docker Hub is used instead of ACR + - deployCI: indicates that it is a CI/CD deployment + - execPath: path where the k8s binary is stored + - kubeconfigPath: path where the k8s config file is stored +>``` +>-deployCI $true -useDockerHub $true -execPath '$(System.DefaultWorkingDirectory)/' -kubeconfigPath '$(System.DefaultWorkingDirectory)/' +>``` + + diff --git a/img/k8s/blob_creation.png b/img/k8s/blob_creation.png new file mode 100644 index 0000000000000000000000000000000000000000..a9e386eadca24830e9ec9d985fc7820108a4f5a4 GIT binary patch literal 26340 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 literal 0 HcmV?d00001 diff --git a/img/k8s/get_kubectlconfig_task.png b/img/k8s/get_kubectlconfig_task.png new file mode 100644 index 0000000000000000000000000000000000000000..594e68ba7bd42e82051233847ac468c7b8681a27 GIT binary patch literal 31190 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 literal 0 HcmV?d00001 diff --git a/k8s/deploy.ps1 b/k8s/deploy.ps1 index 61c7bd909..255243f34 100644 --- a/k8s/deploy.ps1 +++ b/k8s/deploy.ps1 @@ -1,80 +1,117 @@ Param( - [parameter(Mandatory=$true)][string]$registry, - [parameter(Mandatory=$true)][string]$dockerUser, - [parameter(Mandatory=$true)][string]$dockerPassword + [parameter(Mandatory=$false)][string]$registry, + [parameter(Mandatory=$false)][string]$dockerUser, + [parameter(Mandatory=$false)][string]$dockerPassword, + [parameter(Mandatory=$false)][bool]$deployCI, + [parameter(Mandatory=$false)][bool]$useDockerHub, + [parameter(Mandatory=$false)][string]$execPath, + [parameter(Mandatory=$false)][string]$kubeconfigPath ) -$requiredCommands = ("docker", "docker-compose", "kubectl") -foreach ($command in $requiredCommands) { - if ((Get-Command $command -ErrorAction SilentlyContinue) -eq $null) { - Write-Host "$command must be on path" -ForegroundColor Red - exit +function ExecKube($cmd) { + if($deployCI) { + $kubeconfig = $kubeconfigPath + 'config'; + $exp = $execPath + 'kubectl ' + $cmd + ' --kubeconfig=' + $kubeconfig + Invoke-Expression $exp + } + else{ + $exp = $execPath + 'kubectl ' + $cmd + Invoke-Expression $exp } } -Write-Host "Logging in to $registry" -ForegroundColor Yellow -docker login -u $dockerUser -p $dockerPassword $registry -if (-not $LastExitCode -eq 0) { - Write-Host "Login failed" -ForegroundColor Red - exit +# Not used when deploying through CI VSTS +if(-not $deployCI) { + $requiredCommands = ("docker", "docker-compose", "kubectl") + foreach ($command in $requiredCommands) { + if ((Get-Command $command -ErrorAction SilentlyContinue) -eq $null) { + Write-Host "$command must be on path" -ForegroundColor Red + exit + } + } } -# create registry key secret -kubectl create secret docker-registry registry-key ` +# Use ACR instead of DockerHub as image repository +if(-not $useDockerHub) { + Write-Host "Logging in to $registry" -ForegroundColor Yellow + docker login -u $dockerUser -p $dockerPassword $registry + if (-not $LastExitCode -eq 0) { + Write-Host "Login failed" -ForegroundColor Red + exit + } + + # create registry key secret + ExecKube -cmd 'create secret docker-registry registry-key ` --docker-server=$registry ` --docker-username=$dockerUser ` --docker-password=$dockerPassword ` - --docker-email=not@used.com + --docker-email=not@used.com' +} -# start sql, rabbitmq, frontend deployments -kubectl create configmap config-files --from-file=nginx-conf=nginx.conf -kubectl label configmap config-files app=eshop -kubectl create -f sql-data.yaml -f rabbitmq.yaml -f services.yaml -f frontend.yaml +# Removing previous services & deployments +Write-Host "Removing existing services & deployments.." -ForegroundColor Yellow +ExecKube -cmd 'delete -f sql-data.yaml -f rabbitmq.yaml' +ExecKube -cmd 'delete -f services.yaml -f frontend.yaml -f deployments.yaml' +ExecKube -cmd 'delete configmap config-files' +ExecKube -cmd 'delete configmap urls' -Write-Host "Building and publishing eShopOnContainers..." -ForegroundColor Yellow -dotnet restore ../eShopOnContainers-ServicesAndWebApps.sln -dotnet publish -c Release -o obj/Docker/publish ../eShopOnContainers-ServicesAndWebApps.sln +# start sql, rabbitmq, frontend deploymentsExecKube -cmd 'delete configmap config-files' +ExecKube -cmd 'create configmap config-files --from-file=nginx-conf=nginx.conf' +ExecKube -cmd 'label configmap config-files app=eshop' +ExecKube -cmd 'create -f sql-data.yaml -f rabbitmq.yaml -f services.yaml -f frontend.yaml' -Write-Host "Building Docker images..." -ForegroundColor Yellow -docker-compose -p .. -f ../docker-compose.yml build +# building and publishing docker images not necessary when deploying through CI VSTS +if(-not $deployCI) { + Write-Host "Building and publishing eShopOnContainers..." -ForegroundColor Yellow + dotnet restore ../eShopOnContainers-ServicesAndWebApps.sln + dotnet publish -c Release -o obj/Docker/publish ../eShopOnContainers-ServicesAndWebApps.sln -Write-Host "Pushing images to $registry..." -ForegroundColor Yellow -$services = ("basket.api", "catalog.api", "identity.api", "ordering.api", "webmvc", "webspa") -foreach ($service in $services) { - docker tag eshop/$service $registry/eshop/$service - docker push $registry/eshop/$service + Write-Host "Building Docker images..." -ForegroundColor Yellow + docker-compose -p .. -f ../docker-compose.yml build + + Write-Host "Pushing images to $registry..." -ForegroundColor Yellow + $services = ("basket.api", "catalog.api", "identity.api", "ordering.api", "webmvc", "webspa") + foreach ($service in $services) { + docker tag eshop/$service $registry/eshop/$service + docker push $registry/eshop/$service + } } Write-Host "Waiting for frontend's external ip..." -ForegroundColor Yellow while ($true) { - $frontendUrl = kubectl get svc frontend -o=jsonpath="{.status.loadBalancer.ingress[0].ip}" + $frontendUrl = & ExecKube -cmd 'get svc frontend -o=jsonpath="{.status.loadBalancer.ingress[0].ip}"' if ([bool]($frontendUrl -as [ipaddress])) { break } Start-Sleep -s 15 } -kubectl create configmap urls ` +ExecKube -cmd 'create configmap urls ` --from-literal=BasketUrl=http://$($frontendUrl)/basket-api ` --from-literal=CatalogUrl=http://$($frontendUrl)/catalog-api ` --from-literal=IdentityUrl=http://$($frontendUrl)/identity ` --from-literal=OrderingUrl=http://$($frontendUrl)/ordering-api ` --from-literal=MvcClient=http://$($frontendUrl)/webmvc ` - --from-literal=SpaClient=http://$($frontendUrl) -kubectl label configmap urls app=eshop + --from-literal=SpaClient=http://$($frontendUrl)' + +ExecKube -cmd 'label configmap urls app=eshop' Write-Host "Creating deployments..." -kubectl apply -f deployments.yaml - -# update deployments with the private registry before k8s tries to pull images -# (deployment templating, or Helm, would obviate this) -kubectl set image -f deployments.yaml ` - basket=$registry/eshop/basket.api ` - catalog=$registry/eshop/catalog.api ` - identity=$registry/eshop/identity.api ` - ordering=$registry/eshop/ordering.api ` - webmvc=$registry/eshop/webmvc ` - webspa=$registry/eshop/webspa -kubectl rollout resume -f deployments.yaml +ExecKube -cmd 'create -f deployments.yaml' + +# not using ACR for pulling images when deploying through CI VSTS +if(-not $deployCI) { + # update deployments with the private registry before k8s tries to pull images + # (deployment templating, or Helm, would obviate this) + ExecKube -cmd 'set image -f deployments.yaml ` + basket=$registry/eshop/basket.api ` + catalog=$registry/eshop/catalog.api ` + identity=$registry/eshop/identity.api ` + ordering=$registry/eshop/ordering.api ` + webmvc=$registry/eshop/webmvc ` + webspa=$registry/eshop/webspa' +} + +ExecKube -cmd 'rollout resume -f deployments.yaml' Write-Host "WebSPA is exposed at http://$frontendUrl, WebMVC at http://$frontendUrl/webmvc" -ForegroundColor Yellow From 427b1d507dfd7e0f2af368f64ec09da40590fad1 Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Thu, 18 May 2017 12:54:22 +0200 Subject: [PATCH 31/31] iOS solution for VSTS build --- eShopOnContainers-iOS.sln | 237 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 eShopOnContainers-iOS.sln diff --git a/eShopOnContainers-iOS.sln b/eShopOnContainers-iOS.sln new file mode 100644 index 000000000..a6e889786 --- /dev/null +++ b/eShopOnContainers-iOS.sln @@ -0,0 +1,237 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26430.6 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{932D8224-11F6-4D07-B109-DA28AD288A63}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3AF739CD-81D8-428D-A08A-0A58372DEBF6}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + global.json = global.json + NuGet.config = NuGet.config + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mobile Apps", "Mobile Apps", "{F61357CE-1CC2-410E-8776-B16EEBC98EB8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A857AD10-40FF-4303-BEC2-FF1C58D5735E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eShopOnContainers.Core", "src\Mobile\eShopOnContainers\eShopOnContainers.Core\eShopOnContainers.Core.csproj", "{67F9D3A8-F71E-4428-913F-C37AE82CDB24}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eShopOnContainers.iOS", "src\Mobile\eShopOnContainers\eShopOnContainers.iOS\eShopOnContainers.iOS.csproj", "{6EEB23DC-7063-4444-9AF8-90DF24F549C0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared Code", "Shared Code", "{778289CA-31F7-4464-8C2A-612EE846F8A7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{9CC7814B-72A6-465B-A61C-57B512DEE303}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mobile Apps", "Mobile Apps", "{B7B1D395-4E06-4036-BE86-C216756B9367}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eShopOnContainers.UnitTests", "src\Mobile\eShopOnContainers\eShopOnContainers.UnitTests\eShopOnContainers.UnitTests.csproj", "{F7B6A162-BC4D-4924-B16A-713F9B0344E7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eShopOnContainers.TestRunner.iOS", "src\Mobile\eShopOnContainers\eShopOnContainers.TestRunner.iOS\eShopOnContainers.TestRunner.iOS.csproj", "{B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Ad-Hoc|Any CPU = Ad-Hoc|Any CPU + Ad-Hoc|ARM = Ad-Hoc|ARM + Ad-Hoc|iPhone = Ad-Hoc|iPhone + Ad-Hoc|iPhoneSimulator = Ad-Hoc|iPhoneSimulator + Ad-Hoc|x64 = Ad-Hoc|x64 + Ad-Hoc|x86 = Ad-Hoc|x86 + AppStore|Any CPU = AppStore|Any CPU + AppStore|ARM = AppStore|ARM + AppStore|iPhone = AppStore|iPhone + AppStore|iPhoneSimulator = AppStore|iPhoneSimulator + AppStore|x64 = AppStore|x64 + AppStore|x86 = AppStore|x86 + Debug|Any CPU = Debug|Any CPU + Debug|ARM = Debug|ARM + Debug|iPhone = Debug|iPhone + Debug|iPhoneSimulator = Debug|iPhoneSimulator + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|ARM = Release|ARM + Release|iPhone = Release|iPhone + Release|iPhoneSimulator = Release|iPhoneSimulator + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|ARM.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|x64.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Ad-Hoc|x86.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|Any CPU.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|ARM.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|ARM.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|iPhone.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|x64.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|x64.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|x86.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.AppStore|x86.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|ARM.ActiveCfg = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|ARM.Build.0 = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|iPhone.Build.0 = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|x64.ActiveCfg = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|x64.Build.0 = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|x86.ActiveCfg = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Debug|x86.Build.0 = Debug|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|Any CPU.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|ARM.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|ARM.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|iPhone.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|iPhone.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|x64.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|x64.Build.0 = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|x86.ActiveCfg = Release|Any CPU + {67F9D3A8-F71E-4428-913F-C37AE82CDB24}.Release|x86.Build.0 = Release|Any CPU + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Ad-Hoc|ARM.ActiveCfg = Ad-Hoc|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Ad-Hoc|x64.ActiveCfg = Ad-Hoc|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Ad-Hoc|x86.ActiveCfg = Ad-Hoc|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.AppStore|ARM.ActiveCfg = AppStore|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.AppStore|iPhone.ActiveCfg = AppStore|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.AppStore|iPhone.Build.0 = AppStore|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.AppStore|x64.ActiveCfg = AppStore|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.AppStore|x86.ActiveCfg = AppStore|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Debug|Any CPU.ActiveCfg = Debug|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Debug|Any CPU.Build.0 = Debug|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Debug|ARM.ActiveCfg = Debug|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Debug|iPhone.ActiveCfg = Debug|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Debug|iPhone.Build.0 = Debug|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Debug|x64.ActiveCfg = Debug|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Debug|x86.ActiveCfg = Debug|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Release|Any CPU.ActiveCfg = Release|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Release|ARM.ActiveCfg = Release|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Release|iPhone.ActiveCfg = Release|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Release|iPhone.Build.0 = Release|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Release|x64.ActiveCfg = Release|iPhone + {6EEB23DC-7063-4444-9AF8-90DF24F549C0}.Release|x86.ActiveCfg = Release|iPhone + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|ARM.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|x64.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Ad-Hoc|x86.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|Any CPU.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|ARM.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|ARM.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|iPhone.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|x64.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|x64.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|x86.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.AppStore|x86.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|ARM.ActiveCfg = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|ARM.Build.0 = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|iPhone.Build.0 = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|x64.ActiveCfg = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|x64.Build.0 = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|x86.ActiveCfg = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Debug|x86.Build.0 = Debug|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|Any CPU.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|ARM.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|ARM.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|iPhone.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|iPhone.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|x64.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|x64.Build.0 = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|x86.ActiveCfg = Release|Any CPU + {F7B6A162-BC4D-4924-B16A-713F9B0344E7}.Release|x86.Build.0 = Release|Any CPU + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Ad-Hoc|ARM.ActiveCfg = Ad-Hoc|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Ad-Hoc|x64.ActiveCfg = Ad-Hoc|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Ad-Hoc|x86.ActiveCfg = Ad-Hoc|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.AppStore|ARM.ActiveCfg = AppStore|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.AppStore|iPhone.ActiveCfg = AppStore|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.AppStore|iPhone.Build.0 = AppStore|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.AppStore|x64.ActiveCfg = AppStore|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.AppStore|x86.ActiveCfg = AppStore|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Debug|Any CPU.ActiveCfg = Debug|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Debug|Any CPU.Build.0 = Debug|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Debug|ARM.ActiveCfg = Debug|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Debug|iPhone.ActiveCfg = Debug|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Debug|iPhone.Build.0 = Debug|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Debug|x64.ActiveCfg = Debug|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Debug|x86.ActiveCfg = Debug|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Release|Any CPU.ActiveCfg = Release|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Release|ARM.ActiveCfg = Release|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Release|iPhone.ActiveCfg = Release|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Release|iPhone.Build.0 = Release|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Release|x64.ActiveCfg = Release|iPhone + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3}.Release|x86.ActiveCfg = Release|iPhone + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {F61357CE-1CC2-410E-8776-B16EEBC98EB8} = {932D8224-11F6-4D07-B109-DA28AD288A63} + {67F9D3A8-F71E-4428-913F-C37AE82CDB24} = {778289CA-31F7-4464-8C2A-612EE846F8A7} + {6EEB23DC-7063-4444-9AF8-90DF24F549C0} = {9CC7814B-72A6-465B-A61C-57B512DEE303} + {778289CA-31F7-4464-8C2A-612EE846F8A7} = {F61357CE-1CC2-410E-8776-B16EEBC98EB8} + {9CC7814B-72A6-465B-A61C-57B512DEE303} = {F61357CE-1CC2-410E-8776-B16EEBC98EB8} + {B7B1D395-4E06-4036-BE86-C216756B9367} = {A857AD10-40FF-4303-BEC2-FF1C58D5735E} + {F7B6A162-BC4D-4924-B16A-713F9B0344E7} = {B7B1D395-4E06-4036-BE86-C216756B9367} + {B68C2B56-7581-46AE-B55D-D25DDFD3BFE3} = {B7B1D395-4E06-4036-BE86-C216756B9367} + EndGlobalSection +EndGlobal