From 68c3ebaf4623b5af677708758b3ec3010eca7a1d Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 15 Mar 2017 08:57:01 -0700 Subject: [PATCH 01/32] Set data protection application discriminators This prevents cookie confusion when applications are hosted at the same domain and path. For example, under default settings, WebMVC may attempt to decrypt Identity's antiforgery cookie rather than its own. --- src/Services/Identity/Identity.API/Startup.cs | 5 +++++ src/Web/WebMVC/Startup.cs | 5 +++++ src/Web/WebSPA/Startup.cs | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/src/Services/Identity/Identity.API/Startup.cs b/src/Services/Identity/Identity.API/Startup.cs index d29459395..81c26eb16 100644 --- a/src/Services/Identity/Identity.API/Startup.cs +++ b/src/Services/Identity/Identity.API/Startup.cs @@ -54,6 +54,11 @@ namespace eShopOnContainers.Identity services.Configure(Configuration); + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.identity"; + }); + services.AddMvc(); services.AddTransient(); diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index 8bd7708d3..defcf31dd 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -41,6 +41,11 @@ namespace Microsoft.eShopOnContainers.WebMVC // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.webmvc"; + }); + services.AddMvc(); services.Configure(Configuration); diff --git a/src/Web/WebSPA/Startup.cs b/src/Web/WebSPA/Startup.cs index a0f33d8b3..1386849f7 100644 --- a/src/Web/WebSPA/Startup.cs +++ b/src/Web/WebSPA/Startup.cs @@ -41,6 +41,11 @@ namespace eShopConContainers.WebSPA { services.Configure(Configuration); + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.webspa"; + }); + services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN"); services.AddMvc() From 1efa55582555ba3b5b76f898bc7a954198743dab Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 15 Mar 2017 15:50:58 -0700 Subject: [PATCH 02/32] Replace string concatenation with Path.Combine --- cli-windows/build-bits-simple.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli-windows/build-bits-simple.ps1 b/cli-windows/build-bits-simple.ps1 index 7c5156da3..461384cc2 100644 --- a/cli-windows/build-bits-simple.ps1 +++ b/cli-windows/build-bits-simple.ps1 @@ -11,8 +11,7 @@ if ([string]::IsNullOrEmpty($rootPath)) { } Write-Host "Root path used is $rootPath" -ForegroundColor Yellow - -$SolutionFilePath = $rootPath + "eShopOnContainers-ServicesAndWebApps.sln" +$SolutionFilePath = [IO.Path]::Combine($rootPath, "eShopOnContainers-ServicesAndWebApps.sln") dotnet restore $SolutionFilePath From ed87b6b845968d18d3a66614294839d7f0cd711f Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 17 Mar 2017 15:38:18 -0700 Subject: [PATCH 03/32] Use to enable relative paths in WebSPA --- src/Web/WebSPA/AppSettings.cs | 1 + .../Client/modules/shared/services/configuration.service.ts | 3 ++- src/Web/WebSPA/Server/Controllers/HomeController.cs | 1 + src/Web/WebSPA/Startup.cs | 3 +++ src/Web/WebSPA/Views/Shared/_Layout.cshtml | 2 +- 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Web/WebSPA/AppSettings.cs b/src/Web/WebSPA/AppSettings.cs index 19e2b5cfe..9fab0d981 100644 --- a/src/Web/WebSPA/AppSettings.cs +++ b/src/Web/WebSPA/AppSettings.cs @@ -7,6 +7,7 @@ namespace eShopOnContainers.WebSPA { public class AppSettings { + public string BaseUrl { get; set; } public string CatalogUrl { get; set; } public string OrderingUrl { get; set; } public string IdentityUrl { get; set; } diff --git a/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts b/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts index 6921fd167..12fa85815 100644 --- a/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts +++ b/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts @@ -22,7 +22,8 @@ export class ConfigurationService { constructor(private http: Http, private storageService: StorageService) { } load() { - let url = "/Home/Configuration"; + const baseURI = document.baseURI.endsWith('/') ? document.baseURI : `${document.baseURI}/`; + let url = `${baseURI}Home/Configuration`; this.http.get(url).subscribe((response: Response) => { console.log('server settings loaded'); this.serverSettings = response.json(); diff --git a/src/Web/WebSPA/Server/Controllers/HomeController.cs b/src/Web/WebSPA/Server/Controllers/HomeController.cs index 7e78cd41a..c31012f83 100644 --- a/src/Web/WebSPA/Server/Controllers/HomeController.cs +++ b/src/Web/WebSPA/Server/Controllers/HomeController.cs @@ -22,6 +22,7 @@ namespace eShopConContainers.WebSPA.Server.Controllers public IActionResult Index() { ViewBag.HashedMain = GetHashedMainDotJs(); + ViewBag.BaseUrl = _settings.Value.BaseUrl; return View(); } diff --git a/src/Web/WebSPA/Startup.cs b/src/Web/WebSPA/Startup.cs index 1386849f7..6315cf14f 100644 --- a/src/Web/WebSPA/Startup.cs +++ b/src/Web/WebSPA/Startup.cs @@ -32,6 +32,9 @@ namespace eShopConContainers.WebSPA } Configuration = builder.Build(); + + var localPath = new Uri(Configuration["ASPNETCORE_URLS"])?.LocalPath ?? "/"; + Configuration["BaseUrl"] = localPath; } public static IConfigurationRoot Configuration { get; set;} diff --git a/src/Web/WebSPA/Views/Shared/_Layout.cshtml b/src/Web/WebSPA/Views/Shared/_Layout.cshtml index c7d974e27..582064ebb 100644 --- a/src/Web/WebSPA/Views/Shared/_Layout.cshtml +++ b/src/Web/WebSPA/Views/Shared/_Layout.cshtml @@ -5,7 +5,7 @@ eShopConContainers.WebSPA - + From e5cc7cd9441975f23fa2e1324282811185b85b7f Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 17 Mar 2017 17:56:23 -0700 Subject: [PATCH 04/32] Workaround for WebSPA publish issue Due to aspnet/websdk #114, dotnet publish only copies to the output directory files which existed before the command was run. --- src/Web/WebSPA/WebSPA.csproj | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Web/WebSPA/WebSPA.csproj b/src/Web/WebSPA/WebSPA.csproj index 0d30295a7..b483cab2a 100644 --- a/src/Web/WebSPA/WebSPA.csproj +++ b/src/Web/WebSPA/WebSPA.csproj @@ -78,6 +78,13 @@ + + + + + + + Always From 4dc56c4bf763bb8c4e3edea457ab68af86fcc1d0 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Mar 2017 10:19:11 -0700 Subject: [PATCH 05/32] Enable IP address connection string in Basket.API Dns.GetHostAddressesAsync can return problematic results when passed an IP address, and if the connection string is already an IP address, we needn't call it anyway. --- .../Basket.API/Model/RedisBasketRepository.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs b/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs index 973d1669b..8fe53167c 100644 --- a/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs +++ b/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs @@ -63,10 +63,18 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model { if (_redis == null) { - //TODO: Need to make this more robust. Also want to understand why the static connection method cannot accept dns names. - var ips = await Dns.GetHostAddressesAsync(_settings.ConnectionString); - _logger.LogInformation($"Connecting to database {_settings.ConnectionString} at IP {ips.First().ToString()}"); - _redis = await ConnectionMultiplexer.ConnectAsync(ips.First().ToString()); + if (IPAddress.TryParse(_settings.ConnectionString, out var ip)) + { + _redis = await ConnectionMultiplexer.ConnectAsync(ip.ToString()); + _logger.LogInformation($"Connecting to database at {_settings.ConnectionString}"); + } + else + { + // workaround for https://github.com/StackExchange/StackExchange.Redis/issues/410 + var ips = await Dns.GetHostAddressesAsync(_settings.ConnectionString); + _logger.LogInformation($"Connecting to database {_settings.ConnectionString} at IP {ips.First().ToString()}"); + _redis = await ConnectionMultiplexer.ConnectAsync(ips.First().ToString()); + } } return _redis.GetDatabase(); From a1f3a60ef919fa12a5957d0baef80f3cc6845ae3 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Thu, 16 Feb 2017 10:49:58 -0800 Subject: [PATCH 06/32] Configuration and deploy script for k8s on ACS --- k8s/README.md | 26 +++++ k8s/deploy.ps1 | 80 ++++++++++++++ k8s/deployments.yaml | 242 +++++++++++++++++++++++++++++++++++++++++++ k8s/frontend.yaml | 47 +++++++++ k8s/nginx.conf | 74 +++++++++++++ k8s/services.yaml | 83 +++++++++++++++ k8s/sql-data.yaml | 33 ++++++ 7 files changed, 585 insertions(+) create mode 100644 k8s/README.md create mode 100644 k8s/deploy.ps1 create mode 100644 k8s/deployments.yaml create mode 100644 k8s/frontend.yaml create mode 100644 k8s/nginx.conf create mode 100644 k8s/services.yaml create mode 100644 k8s/sql-data.yaml diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 000000000..057542983 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,26 @@ +# eShopOnContainers on Kubernetes +This directory contains Kubernetes configuration for the eShopOnContainers app and a PowerShell script to deploy it to a cluster. Each microservice has a deployment configuration in `deployments.yaml`, and is exposed to the cluster by a service in `services.yaml`. The microservices are exposed externally on individual routes (`/basket-api`, `/webmvc`, etc.) by an nginx reverse proxy as specified in `frontend.yaml` and `nginx.conf`. + +## Deploying the application +### Prerequisites +* A Docker build host. +* 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. +* 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. + +### Run the deployment script +1. Open a PowerShell command line at `eShopOnContainers/k8s`. +1. Ensure `docker`, `docker-compose`, and `kubectl` are on the path, and configured for your Docker machine and Kubernetes cluster. +1. Run `deploy.ps1` with your registry information. For example: + ``` + ./deploy.ps1 -registry myregistry.azurecr.io -dockerUser User -dockerPassword SecretPassword + ``` + The Docker username and password are provided by Azure Container Registry, and can be retrieved from the Azure portal. + +The script will build the code and corresponding Docker images, push the latter to your registry, and deploy the application to your Kubernetes cluster. + +TODOs +===== +* Host WebSPA at `/webspa` + * This is blocked on correct relative URLs for images. Presently these are set at build by webpack, which isn't aware of where the app will be sited. An Angular solution might exist. Another option is to encode the images in base64. +* Debug microservice resiliency issues--some microservices can enter failure states requiring their pod to be recreated. +* Respond to `kubectl` failures in `deploy.ps1`. \ No newline at end of file diff --git a/k8s/deploy.ps1 b/k8s/deploy.ps1 new file mode 100644 index 000000000..0c5f7a579 --- /dev/null +++ b/k8s/deploy.ps1 @@ -0,0 +1,80 @@ +Param( + [parameter(Mandatory=$true)][string]$registry, + [parameter(Mandatory=$true)][string]$dockerUser, + [parameter(Mandatory=$true)][string]$dockerPassword +) + +$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 + } +} + +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 +kubectl create secret docker-registry registry-key ` + --docker-server=$registry ` + --docker-username=$dockerUser ` + --docker-password=$dockerPassword ` + --docker-email=not@used.com + +# start sql and 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 services.yaml -f frontend.yaml + +Write-Host "Building solution..." -ForegroundColor Yellow +../cli-windows/build-bits-simple.ps1 + +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/$service + docker push $registry/$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}" 2> $_ + if ([bool]($frontendUrl -as [ipaddress])) { + break + } + Start-Sleep -s 15 +} + +kubectl 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 + +# TODO verify database readiness? +Write-Host "Creating deployments..." +kubectl apply -f deployments.yaml + +# update deployments with the private registry +# (deployment templating, or Helm, would obviate this) +kubectl set image -f deployments.yaml ` + basket=$registry/basket.api ` + catalog=$registry/catalog.api ` + identity=$registry/identity.api ` + ordering=$registry/ordering.api ` + webmvc=$registry/webmvc ` + webspa=$registry/webspa +kubectl rollout resume -f deployments.yaml + +Write-Host "WebSPA is exposed at http://$frontendUrl, WebMVC at http://$frontendUrl/webmvc" -ForegroundColor Yellow diff --git a/k8s/deployments.yaml b/k8s/deployments.yaml new file mode 100644 index 000000000..05c860dec --- /dev/null +++ b/k8s/deployments.yaml @@ -0,0 +1,242 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: basket +spec: + paused: true + template: + metadata: + labels: + app: eshop + component: basket + spec: + containers: + - name: basket + image: eshop/basket.api + imagePullPolicy: Always + env: + - name: ASPNETCORE_ENVIRONMENT + value: Development + - name: ASPNETCORE_URLS + value: http://0.0.0.0:80/basket-api + - name: ConnectionString + value: 127.0.0.1 + - name: IdentityUrl + valueFrom: + configMapKeyRef: + name: urls + key: IdentityUrl + ports: + - containerPort: 80 + - name: basket-data + image: redis + ports: + - containerPort: 6379 + imagePullSecrets: + - name: registry-key +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: catalog +spec: + paused: true + template: + metadata: + labels: + app: eshop + component: catalog + spec: + containers: + - name: catalog + image: eshop/catalog.api + imagePullPolicy: Always + env: + - name: ASPNETCORE_ENVIRONMENT + value: Development + - name: ASPNETCORE_URLS + value: http://0.0.0.0:80/catalog-api + - name: ConnectionString + value: "Server=sql-data;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word" + - name: ExternalCatalogBaseUrl + valueFrom: + configMapKeyRef: + name: urls + key: CatalogUrl + ports: + - containerPort: 80 + imagePullSecrets: + - name: registry-key +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: identity +spec: + paused: true + template: + metadata: + labels: + app: eshop + component: identity + spec: + containers: + - name: identity + image: eshop/identity.api + imagePullPolicy: Always + env: + - name: ASPNETCORE_ENVIRONMENT + value: Development + - name: ASPNETCORE_URLS + value: http://0.0.0.0:80/identity + - name: ConnectionStrings__DefaultConnection + value: "Server=sql-data;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word" + - name: MvcClient + valueFrom: + configMapKeyRef: + name: urls + key: MvcClient + - name: SpaClient + valueFrom: + configMapKeyRef: + name: urls + key: SpaClient + ports: + - containerPort: 80 + imagePullSecrets: + - name: registry-key +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: ordering +spec: + paused: true + template: + metadata: + labels: + app: eshop + component: ordering + spec: + containers: + - name: ordering + image: eshop/ordering.api + imagePullPolicy: Always + env: + - name: ASPNETCORE_ENVIRONMENT + value: Development + - name: ASPNETCORE_URLS + value: http://0.0.0.0:80/ordering-api + - name: ConnectionString + value: "Server=sql-data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;" + - name: IdentityUrl + valueFrom: + configMapKeyRef: + name: urls + key: IdentityUrl + ports: + - containerPort: 80 + imagePullSecrets: + - name: registry-key +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: webmvc +spec: + paused: true + template: + metadata: + labels: + app: eshop + component: webmvc + spec: + containers: + - name: webmvc + image: eshop/webmvc + imagePullPolicy: Always + env: + - name: ASPNETCORE_ENVIRONMENT + value: Development + - name: ASPNETCORE_URLS + value: http://0.0.0.0:80/webmvc + - name: BasketUrl + valueFrom: + configMapKeyRef: + name: urls + key: BasketUrl + - name: CallBackUrl + valueFrom: + configMapKeyRef: + name: urls + key: MvcClient + - name: CatalogUrl + valueFrom: + configMapKeyRef: + name: urls + key: CatalogUrl + - name: IdentityUrl + valueFrom: + configMapKeyRef: + name: urls + key: IdentityUrl + - name: OrderingUrl + valueFrom: + configMapKeyRef: + name: urls + key: OrderingUrl + ports: + - containerPort: 80 + imagePullSecrets: + - name: registry-key +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: webspa +spec: + paused: true + template: + metadata: + labels: + app: eshop + component: webspa + spec: + containers: + - name: webspa + image: eshop/webspa + imagePullPolicy: Always + env: + - name: ASPNETCORE_ENVIRONMENT + value: Development + - name: ASPNETCORE_URLS + value: http://0.0.0.0:80 + - name: BasketUrl + valueFrom: + configMapKeyRef: + name: urls + key: BasketUrl + - name: CallBackUrl + valueFrom: + configMapKeyRef: + name: urls + key: SpaClient + - name: CatalogUrl + valueFrom: + configMapKeyRef: + name: urls + key: CatalogUrl + - name: IdentityUrl + valueFrom: + configMapKeyRef: + name: urls + key: IdentityUrl + - name: OrderingUrl + valueFrom: + configMapKeyRef: + name: urls + key: OrderingUrl + ports: + - containerPort: 80 + imagePullSecrets: + - name: registry-key diff --git a/k8s/frontend.yaml b/k8s/frontend.yaml new file mode 100644 index 000000000..291542404 --- /dev/null +++ b/k8s/frontend.yaml @@ -0,0 +1,47 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: eshop + component: frontend + name: frontend +spec: + ports: + - port: 80 + targetPort: 8080 + selector: + app: eshop + component: frontend + type: LoadBalancer +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: frontend +spec: + template: + metadata: + labels: + app: eshop + component: frontend + spec: + containers: + - name: nginx + image: nginx:1.11.10-alpine + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 + lifecycle: + preStop: + exec: + command: ["/usr/sbin/nginx","-s","quit"] + volumeMounts: + - name: config + mountPath: /etc/nginx + volumes: + - name: config + configMap: + name: config-files + items: + - key: nginx-conf + path: nginx.conf diff --git a/k8s/nginx.conf b/k8s/nginx.conf new file mode 100644 index 000000000..33526d486 --- /dev/null +++ b/k8s/nginx.conf @@ -0,0 +1,74 @@ +pid /tmp/nginx.pid; + +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + server_tokens off; + + add_header X-Frame-Options SAMEORIGIN; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + + client_body_temp_path /tmp/client_body; + fastcgi_temp_path /tmp/fastcgi_temp; + proxy_temp_path /tmp/proxy_temp; + scgi_temp_path /tmp/scgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + + gzip on; + gzip_comp_level 6; + gzip_min_length 1024; + gzip_buffers 4 32k; + gzip_types text/plain application/javascript text/css; + gzip_vary on; + + keepalive_timeout 65; + + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + + server { + listen 8080; + + location /basket-api { + proxy_pass http://basket; + proxy_redirect off; + proxy_set_header Host $host; + } + + location /catalog-api { + proxy_pass http://catalog; + proxy_redirect off; + proxy_set_header Host $host; + } + + location /identity { + proxy_pass http://identity; + proxy_redirect off; + proxy_set_header Host $host; + } + + location /ordering-api { + proxy_pass http://ordering; + proxy_redirect off; + proxy_set_header Host $host; + } + + location /webmvc { + proxy_pass http://webmvc; + proxy_redirect off; + proxy_set_header Host $host; + } + + location / { + proxy_pass http://webspa; + proxy_redirect off; + proxy_set_header Host $host; + } + } +} \ No newline at end of file diff --git a/k8s/services.yaml b/k8s/services.yaml new file mode 100644 index 000000000..fda3e5ea3 --- /dev/null +++ b/k8s/services.yaml @@ -0,0 +1,83 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: eshop + component: basket + name: basket +spec: + ports: + - port: 80 + selector: + app: eshop + component: basket +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: eshop + component: catalog + name: catalog +spec: + ports: + - port: 80 + selector: + app: eshop + component: catalog +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: eshop + component: identity + name: identity +spec: + ports: + - port: 80 + selector: + app: eshop + component: identity +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: eshop + component: ordering + name: ordering +spec: + ports: + - port: 80 + selector: + app: eshop + component: ordering +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: eshop + component: webmvc + name: webmvc +spec: + ports: + - port: 80 + selector: + app: eshop + component: webmvc +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: eshop + component: webspa + name: webspa +spec: + ports: + - port: 80 + selector: + app: eshop + component: webspa \ No newline at end of file diff --git a/k8s/sql-data.yaml b/k8s/sql-data.yaml new file mode 100644 index 000000000..6edcd21bc --- /dev/null +++ b/k8s/sql-data.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: eshop + component: sql-data + name: sql-data +spec: + ports: + - port: 1433 + selector: + app: eshop + component: sql-data +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: sql-data +spec: + template: + metadata: + labels: + app: eshop + component: sql-data + spec: + containers: + - name: sql-data + image: microsoft/mssql-server-linux:ctp1-3 + env: + - name: ACCEPT_EULA + value: "Y" + - name: SA_PASSWORD + value: Pass@word From f0b5513190db0d6c53931880232e0293e788d5a7 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Mar 2017 15:36:50 -0700 Subject: [PATCH 07/32] specify redis image tag --- k8s/deployments.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/deployments.yaml b/k8s/deployments.yaml index 05c860dec..b5ebd8c2f 100644 --- a/k8s/deployments.yaml +++ b/k8s/deployments.yaml @@ -29,7 +29,7 @@ spec: ports: - containerPort: 80 - name: basket-data - image: redis + image: redis:3.2-alpine ports: - containerPort: 6379 imagePullSecrets: From 59571104deeb04b5743a7685223655f42d4a6c7d Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Thu, 23 Mar 2017 13:15:16 -0700 Subject: [PATCH 08/32] Add KUBERNETES.md --- KUBERNETES.md | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 KUBERNETES.md diff --git a/KUBERNETES.md b/KUBERNETES.md new file mode 100644 index 000000000..6e107b3a1 --- /dev/null +++ b/KUBERNETES.md @@ -0,0 +1,134 @@ +# Kubernetes 101 +## Docker vs. Kubernetes +Docker helps you package applications into images, and execute them in containers. Kubernetes is a robust platform for containerized applications. It abstracts away the underlying network infrastructure and hardware required to run them, simplifying their deployment, scaling, and management. + +## Kubernetes from the container up +### Pods +The basic unit of a Kubernetes deployment is the **Pod**. A Pod encapsulates one or more containers. For example, the `basket` Pod specifies two containers: +>`deployments.yaml` +> +>The first container runs the `eshop/basket.api` image: +>```yaml +>spec: +> containers: +> - name: basket +> image: eshop/basket.api +> env: +> - name: ConnectionString +> value: 127.0.0.1 +>``` +>Note the `ConnectionString` environment variable: containers within a Pod are networked via `localhost`. The second container runs the `redis` image: +>```yaml +>- name: basket-data +> image: redis:3.2-alpine +> ports: +> - containerPort: 6379 +>``` +Placing `basket` and `basket-data` in the same Pod is reasonable here because the former requires the latter, and owns all its data. If we wanted to scale the service, however, it would be better to place the containers in separate Pods because the basket API and redis scale at different rates. + +If the containers were in separate Pods, they would no longer be able to communicate via `localhost`; a **Service** would be required. + +### Services +Services expose Pods to external networks. For example, the `basket` Service exposes Pods with labels `app=eshop` and `component=basket` to the cluster at large: +>`services.yaml` +>```yaml +>kind: Service +>metadata: +> ... +> name: basket +>spec: +> ports: +> - port: 80 +> selector: +> app: eshop +> component: basket +>``` +Kubernetes's built-in DNS service resolves Service names to cluster-internal IP addresses. This allows the nginx frontend to proxy connections to the app's microservices by name: +>`nginx.conf` +>``` +>location /basket-api { +> proxy_pass http://basket; +>``` +The frontend Pod is different in that it needs to be exposed outside the cluster. This is accomplished with another Service: +>`frontend.yaml` +>```yaml +>spec: +> ports: +> - port: 80 +> targetPort: 8080 +> selector: +> app: eshop +> component: frontend +> type: LoadBalancer +>``` +`type: LoadBalancer` tells Kubernetes to expose the Service behind a load balancer appropriate for the cluster's platform. For Azure Container Service, this creates an Azure load balancer rule with a public IP. + +### Deployments +Kubernetes uses Pods to organize containers, and Services to network them. It uses **Deployments** to organize creating, and modifying, Pods. A Deployment describes a state of one or more Pods. When a Deployment is created or modified, Kubernetes attempts to realize that state. + +The Deployments in this project are basic. Still, `deploy.ps1` shows some more advanced Deployment capabilities. For example, Deployments can be paused. Each Deployment of this app is paused at creation: +>`deployments.yaml` +>```yaml +>kind: Deployment +>spec: +> paused: true +>``` +This allows the deployment script to change images before Kubernetes creates the Pods: +>`deploy.ps1` +>```powershell +>kubectl set image -f deployments.yaml basket=$registry/basket.api ... +>kubectl rollout resume -f deployments.yaml +>``` + +### ConfigMaps +A **ConfigMap** is a collection of key/value pairs commonly used to provide configuration information to Pods. The deployment script uses one to store the frontend's configuration: +>`deploy.ps1` +>``` +>kubectl create configmap config-files from-file=nginx-conf=nginx.conf +>``` +This creates a ConfigMap named `config-files` with key `nginx-conf` whose value is the content of nginx.conf. The frontend Pod mounts that value as `/etc/nginx/nginx.conf`: +>`frontend.yaml` +>```yaml +>spec: +> containers: +> - name: nginx +> ... +> volumeMounts: +> - name: config +> mountPath: /etc/nginx +> volumes: +> - name: config +> configMap: +> name: config-files +> items: +> - key: nginx-conf +> path: nginx.conf +>``` +This facilitates rapid iteration better than other techniques, e.g. building an image to bake in configuration. + +The script also stores public URLs for the app's components in a ConfigMap: +>`deploy.ps1` +>```powershell +>kubectl create configmap urls --from-literal=BasketUrl=http://$($frontendUrl)/basket-api ... +>``` +>Here's how the `webspa` Deployment uses it: +> +>`deployments.yaml` +>```yaml +>spec: +> containers: +> - name: webspa +> ... +> env: +> ... +> - name: BasketUrl +> valueFrom: +> configMapKeyRef: +> name: urls +> key: BasketUrl +>``` + +### Further reading +* [Kubernetes Concepts](https://kubernetes.io/docs/concepts/) +* [kubectl for Docker Users](https://kubernetes.io/docs/user-guide/docker-cli-to-kubectl/) +* [Kubernetes API reference](https://kubernetes.io/docs/api-reference/v1.5/) \ No newline at end of file From 0e55af7338120a26c7dafd40f7a5715bb7e0617b Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 11 Apr 2017 11:20:42 -0700 Subject: [PATCH 09/32] update READMEs --- README.k8s.md | 19 +++++++++++++++++++ README.md | 20 ++++++++++++++++++++ k8s/README.md | 26 -------------------------- 3 files changed, 39 insertions(+), 26 deletions(-) create mode 100644 README.k8s.md delete mode 100644 k8s/README.md diff --git a/README.k8s.md b/README.k8s.md new file mode 100644 index 000000000..bb64e3018 --- /dev/null +++ b/README.k8s.md @@ -0,0 +1,19 @@ +# eShopOnContainers on Kubernetes +The k8s directory contains Kubernetes configuration for the eShopOnContainers app and a PowerShell script to deploy it to a cluster. Each eShopOnContainers microservice has a deployment configuration in `deployments.yaml`, and is exposed to the cluster by a service in `services.yaml`. The microservices are exposed externally on individual routes (`/basket-api`, `/webmvc`, etc.) by an nginx reverse proxy specified in `frontend.yaml` and `nginx.conf`. + +## 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. +* A Docker development environment with `docker` and `docker-compose`. + * Visit [docker.com](https://docker.com) to download the tools and set up the environment. Docker's [installation guide](https://docs.docker.com/engine/getstarted/step_one/#step-3-verify-your-installation) covers verifying your Docker installation. +* The Kubernetes command line client, `kubectl`. + * This can be installed with the `az` tool as described in the Azure Container Service [walkthrough](https://docs.microsoft.com/en-us/azure/container-service/container-service-kubernetes-walkthrough). `az` is also helpful for getting the credentials `kubectl` needs to access your cluster. For other installation options, and information about configuring `kubectl` yourself, see the [Kubernetes documentation](https://kubernetes.io/docs/tasks/kubectl/install/). + +## Deploy the application with the deployment script +1. Open a PowerShell command line at the `k8s` directory of your local eShopOnContainers repository. +1. Ensure `docker`, `docker-compose`, and `kubectl` are on the path, and configured for your Docker machine and Kubernetes cluster. +1. Run `deploy.ps1` with your registry information. The Docker username and password are provided by Azure Container Registry, and can be retrieved from the Azure portal. For example: +>``` +>./deploy.ps1 -registry myregistry.azurecr.io -dockerUser User -dockerPassword SecretPassword +>``` +The script will build the code and corresponding Docker images, push the latter to your registry, and deploy the application to your cluster. You can watch the deployment unfold from the Kubernetes web interface: run `kubectl proxy` and open a browser to [http://localhost:8001/ui](http://localhost:8001/ui) diff --git a/README.md b/README.md index 770dfb0bf..ae3b7f54b 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,26 @@ https://github.com/dotnet/eShopOnContainers/wiki/04.-Setting-eShopOnContainer-so The Windows Containers scenario is currently being implemented/tested yet. The application should be able to run on Windows Nano Containers based on different Docker base images, as well, as the .NET Core services have also been tested running on plain Windows (with no Docker). The app was also partially tested on "Docker for Mac" using a development MacOS machine with .NET Core and VS Code installed, which is still a scenario using Linux containers running on the VM setup in the Mac by the "Docker for Windows" setup. But further testing and feedback on Mac environments and Windows Containers, from the community, will be appreciated. +## Kubernetes +The k8s directory contains Kubernetes configuration for the eShopOnContainers app and a PowerShell script to deploy it to a cluster. Each eShopOnContainers microservice has a deployment configuration in `deployments.yaml`, and is exposed to the cluster by a service in `services.yaml`. The microservices are exposed externally on individual routes (`/basket-api`, `/webmvc`, etc.) by an nginx reverse proxy specified in `frontend.yaml` and `nginx.conf`. + +### 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. +* A Docker development environment with `docker` and `docker-compose`. + * Visit [docker.com](https://docker.com) to download the tools and set up the environment. Docker's [installation guide](https://docs.docker.com/engine/getstarted/step_one/#step-3-verify-your-installation) covers verifying your Docker installation. +* The Kubernetes command line client, `kubectl`. + * This can be installed with the `az` tool as described in the Azure Container Service [walkthrough](https://docs.microsoft.com/en-us/azure/container-service/container-service-kubernetes-walkthrough). `az` is also helpful for getting the credentials `kubectl` needs to access your cluster. For other installation options, and information about configuring `kubectl` yourself, see the [Kubernetes documentation](https://kubernetes.io/docs/tasks/kubectl/install/). + +### Deploy the application with the deployment script +1. Open a PowerShell command line at the `k8s` directory of your local eShopOnContainers repository. +1. Ensure `docker`, `docker-compose`, and `kubectl` are on the path, and configured for your Docker machine and Kubernetes cluster. +1. Run `deploy.ps1` with your registry information. The Docker username and password are provided by Azure Container Registry, and can be retrieved from the Azure portal. For example: +>``` +>./deploy.ps1 -registry myregistry.azurecr.io -dockerUser User -dockerPassword SecretPassword +>``` +The script will build the code and corresponding Docker images, push the latter to your registry, and deploy the application to your cluster. You can watch the deployment unfold from the Kubernetes web interface: run `kubectl proxy` and open a browser to [http://localhost:8001/ui](http://localhost:8001/ui) + ## Sending feedback and pull requests As mentioned, we'd appreciate to your feedback, improvements and ideas. You can create new issues at the issues section, do pull requests and/or send emails to eshop_feedback@service.microsoft.com diff --git a/k8s/README.md b/k8s/README.md deleted file mode 100644 index 057542983..000000000 --- a/k8s/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# eShopOnContainers on Kubernetes -This directory contains Kubernetes configuration for the eShopOnContainers app and a PowerShell script to deploy it to a cluster. Each microservice has a deployment configuration in `deployments.yaml`, and is exposed to the cluster by a service in `services.yaml`. The microservices are exposed externally on individual routes (`/basket-api`, `/webmvc`, etc.) by an nginx reverse proxy as specified in `frontend.yaml` and `nginx.conf`. - -## Deploying the application -### Prerequisites -* A Docker build host. -* 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. -* 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. - -### Run the deployment script -1. Open a PowerShell command line at `eShopOnContainers/k8s`. -1. Ensure `docker`, `docker-compose`, and `kubectl` are on the path, and configured for your Docker machine and Kubernetes cluster. -1. Run `deploy.ps1` with your registry information. For example: - ``` - ./deploy.ps1 -registry myregistry.azurecr.io -dockerUser User -dockerPassword SecretPassword - ``` - The Docker username and password are provided by Azure Container Registry, and can be retrieved from the Azure portal. - -The script will build the code and corresponding Docker images, push the latter to your registry, and deploy the application to your Kubernetes cluster. - -TODOs -===== -* Host WebSPA at `/webspa` - * This is blocked on correct relative URLs for images. Presently these are set at build by webpack, which isn't aware of where the app will be sited. An Angular solution might exist. Another option is to encode the images in base64. -* Debug microservice resiliency issues--some microservices can enter failure states requiring their pod to be recreated. -* Respond to `kubectl` failures in `deploy.ps1`. \ No newline at end of file From 6cabf6691d4e07861e13787ecf2bb271c36c89d7 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 11 Apr 2017 13:38:19 -0700 Subject: [PATCH 10/32] update k8s configuration - add event bus - remove ASPNETCORE_ENVIRONMENT = Development (apps will default to Production) --- k8s/deployments.yaml | 18 ++++++------------ k8s/rabbitmq.yaml | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 k8s/rabbitmq.yaml diff --git a/k8s/deployments.yaml b/k8s/deployments.yaml index b5ebd8c2f..c89e4b7e3 100644 --- a/k8s/deployments.yaml +++ b/k8s/deployments.yaml @@ -15,12 +15,12 @@ spec: image: eshop/basket.api imagePullPolicy: Always env: - - name: ASPNETCORE_ENVIRONMENT - value: Development - name: ASPNETCORE_URLS value: http://0.0.0.0:80/basket-api - name: ConnectionString value: 127.0.0.1 + - name: EventBusConnection + value: rabbitmq - name: IdentityUrl valueFrom: configMapKeyRef: @@ -52,12 +52,12 @@ spec: image: eshop/catalog.api imagePullPolicy: Always env: - - name: ASPNETCORE_ENVIRONMENT - value: Development - name: ASPNETCORE_URLS value: http://0.0.0.0:80/catalog-api - name: ConnectionString value: "Server=sql-data;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word" + - name: EventBusConnection + value: rabbitmq - name: ExternalCatalogBaseUrl valueFrom: configMapKeyRef: @@ -85,8 +85,6 @@ spec: image: eshop/identity.api imagePullPolicy: Always env: - - name: ASPNETCORE_ENVIRONMENT - value: Development - name: ASPNETCORE_URLS value: http://0.0.0.0:80/identity - name: ConnectionStrings__DefaultConnection @@ -123,12 +121,12 @@ spec: image: eshop/ordering.api imagePullPolicy: Always env: - - name: ASPNETCORE_ENVIRONMENT - value: Development - name: ASPNETCORE_URLS value: http://0.0.0.0:80/ordering-api - name: ConnectionString value: "Server=sql-data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;" + - name: EventBusConnection + value: rabbitmq - name: IdentityUrl valueFrom: configMapKeyRef: @@ -156,8 +154,6 @@ spec: image: eshop/webmvc imagePullPolicy: Always env: - - name: ASPNETCORE_ENVIRONMENT - value: Development - name: ASPNETCORE_URLS value: http://0.0.0.0:80/webmvc - name: BasketUrl @@ -207,8 +203,6 @@ spec: image: eshop/webspa imagePullPolicy: Always env: - - name: ASPNETCORE_ENVIRONMENT - value: Development - name: ASPNETCORE_URLS value: http://0.0.0.0:80 - name: BasketUrl diff --git a/k8s/rabbitmq.yaml b/k8s/rabbitmq.yaml new file mode 100644 index 000000000..a0d87549c --- /dev/null +++ b/k8s/rabbitmq.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: eshop + component: rabbitmq + name: rabbitmq +spec: + ports: + - port: 5672 + selector: + app: eshop + component: rabbitmq +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: rabbitmq +spec: + template: + metadata: + labels: + app: eshop + component: rabbitmq + spec: + containers: + - name: rabbitmq + image: rabbitmq:3.6.9-alpine + ports: + - containerPort: 5672 From fb3c85ef564f0e29ffdaf4d706ba5b948055fb76 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 11 Apr 2017 15:04:20 -0700 Subject: [PATCH 11/32] update k8s deploy script --- k8s/deploy.ps1 | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/k8s/deploy.ps1 b/k8s/deploy.ps1 index 0c5f7a579..61c7bd909 100644 --- a/k8s/deploy.ps1 +++ b/k8s/deploy.ps1 @@ -26,13 +26,14 @@ kubectl create secret docker-registry registry-key ` --docker-password=$dockerPassword ` --docker-email=not@used.com -# start sql and frontend deployments +# 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 services.yaml -f frontend.yaml +kubectl create -f sql-data.yaml -f rabbitmq.yaml -f services.yaml -f frontend.yaml -Write-Host "Building solution..." -ForegroundColor Yellow -../cli-windows/build-bits-simple.ps1 +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 "Building Docker images..." -ForegroundColor Yellow docker-compose -p .. -f ../docker-compose.yml build @@ -40,13 +41,13 @@ 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/$service - docker push $registry/$service + 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}" 2> $_ + $frontendUrl = kubectl get svc frontend -o=jsonpath="{.status.loadBalancer.ingress[0].ip}" if ([bool]($frontendUrl -as [ipaddress])) { break } @@ -62,19 +63,18 @@ kubectl create configmap urls ` --from-literal=SpaClient=http://$($frontendUrl) kubectl label configmap urls app=eshop -# TODO verify database readiness? Write-Host "Creating deployments..." kubectl apply -f deployments.yaml -# update deployments with the private registry +# 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/basket.api ` - catalog=$registry/catalog.api ` - identity=$registry/identity.api ` - ordering=$registry/ordering.api ` - webmvc=$registry/webmvc ` - webspa=$registry/webspa + 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 Write-Host "WebSPA is exposed at http://$frontendUrl, WebMVC at http://$frontendUrl/webmvc" -ForegroundColor Yellow From 23079a516d2b5097b75c7c8d4f14aa2195a72ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Mon, 24 Apr 2017 13:02:23 +0200 Subject: [PATCH 12/32] Fix Connection string in identity deployment script --- k8s/deployments.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/deployments.yaml b/k8s/deployments.yaml index c89e4b7e3..cce221db6 100644 --- a/k8s/deployments.yaml +++ b/k8s/deployments.yaml @@ -88,7 +88,7 @@ spec: - name: ASPNETCORE_URLS value: http://0.0.0.0:80/identity - name: ConnectionStrings__DefaultConnection - value: "Server=sql-data;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word" + value: "Server=sql-data;Initial Catalog=Microsoft.eShopOnContainers.Services.IdentityDb;User Id=sa;Password=Pass@word" - name: MvcClient valueFrom: configMapKeyRef: From e74acc62e5bdbca896f073c692342d9d017ba8ea Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Wed, 3 May 2017 10:50:35 -0700 Subject: [PATCH 13/32] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index faf758f96..47a0a4da1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # eShopOnContainers - Microservices Architecture and Containers based Reference Application (**BETA state** - Visual Studio 2017 and CLI environments compatible) Sample .NET Core reference application, powered by Microsoft, based on a simplified microservices architecture and Docker containers.

+**Note for Pull Requests**: We accept pull request from the community. When doing it, please do it onto the DEV branch which is the consolidated work-in-progress branch. Do not request it onto Master, if possible. > ### DISCLAIMER > **IMPORTANT:** The current state of this sample application is **BETA**, consider it version a 0.1 foundational version, therefore, many areas could be improved and change significantly while refactoring current code and implementing new features. **Feedback with improvements and pull requests from the community will be highly appreciated and accepted.** From 17b3e0f14df07ec20987dc04a7be53175b58e6f2 Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Wed, 3 May 2017 13:42:06 -0700 Subject: [PATCH 14/32] Temporal change of DefaultCacheDuration for the Health Checks so it is shorter for a demo. Need to add overloads at the Health Check library so this is more flexible. --- .../src/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs index 006e4a6ef..4e1c6e4c9 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(5); + DefaultCacheDuration = TimeSpan.FromMinutes(1); } ///

From b1c0c72ec7661b0c3de73b4e16cf5b44ef5f0bc6 Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Wed, 3 May 2017 18:36:52 -0700 Subject: [PATCH 15/32] CreateOrder refactored so return code lines are more compact --- eShopOnContainers-ServicesAndWebApps.sln | 2 +- .../Ordering.API/Controllers/OrdersController.cs | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/eShopOnContainers-ServicesAndWebApps.sln b/eShopOnContainers-ServicesAndWebApps.sln index cf2be1c26..eb0e83e02 100644 --- a/eShopOnContainers-ServicesAndWebApps.sln +++ b/eShopOnContainers-ServicesAndWebApps.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26403.7 +VisualStudioVersion = 15.0.26403.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{932D8224-11F6-4D07-B109-DA28AD288A63}" EndProject diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs index d9a3752ed..05a9cd193 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -30,25 +30,21 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers [HttpPost] public async Task CreateOrder([FromBody]CreateOrderCommand command, [FromHeader(Name = "x-requestid")] string requestId) { - bool result = false; + bool commandResult = false; if (Guid.TryParse(requestId, out Guid guid) && guid != Guid.Empty) { var requestCreateOrder = new IdentifiedCommand(command, guid); - result = await _mediator.SendAsync(requestCreateOrder); + commandResult = await _mediator.SendAsync(requestCreateOrder); } else { // If no x-requestid header is found we process the order anyway. This is just temporary to not break existing clients // that aren't still updated. When all clients were updated this could be removed. - result = await _mediator.SendAsync(command); + commandResult = await _mediator.SendAsync(command); } - if (result) - { - return Ok(); - } + return commandResult ? (StatusCodeResult)Ok() : (StatusCodeResult)BadRequest(); - return BadRequest(); } [Route("{orderId:int}")] From 9cc6adbd89a0f095e637088af5ab89e360ff64cc Mon Sep 17 00:00:00 2001 From: Unai Zorrilla Castro Date: Thu, 4 May 2017 13:01:14 +0200 Subject: [PATCH 16/32] 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 17/32] 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 18/32] 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 c247261929a3eba2e2fc24d4107ab6f412e4d3af Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Thu, 4 May 2017 12:29:00 -0700 Subject: [PATCH 19/32] Minor refactoring for the CreateOrder Web API method and the Entity base class --- .../Ordering/Ordering.API/Controllers/OrdersController.cs | 2 +- src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs index 05a9cd193..c5b21aeef 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -43,7 +43,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers commandResult = await _mediator.SendAsync(command); } - return commandResult ? (StatusCodeResult)Ok() : (StatusCodeResult)BadRequest(); + return commandResult ? (IActionResult)Ok() : (IActionResult)BadRequest(); } diff --git a/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs b/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs index 7616a3230..086fbc904 100644 --- a/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs +++ b/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs @@ -50,6 +50,9 @@ if (Object.ReferenceEquals(this, obj)) return true; + if (this.GetType() != obj.GetType()) + return false; + Entity item = (Entity)obj; if (item.IsTransient() || this.IsTransient()) From 7a964ca5b8e42d378f6f29c4b5655cb5aeffb6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Fri, 5 May 2017 09:41:22 +0200 Subject: [PATCH 20/32] Added script to set up azure environment before deploying kubernetes Updated deployment documentation --- README.k8s.md | 13 +++++++++++-- k8s/gen-k8s-env.ps1 | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 k8s/gen-k8s-env.ps1 diff --git a/README.k8s.md b/README.k8s.md index bb64e3018..28ebd49e8 100644 --- a/README.k8s.md +++ b/README.k8s.md @@ -2,8 +2,12 @@ The k8s directory contains Kubernetes configuration for the eShopOnContainers app and a PowerShell script to deploy it to a cluster. Each eShopOnContainers microservice has a deployment configuration in `deployments.yaml`, and is exposed to the cluster by a service in `services.yaml`. The microservices are exposed externally on individual routes (`/basket-api`, `/webmvc`, etc.) by an nginx reverse proxy specified in `frontend.yaml` and `nginx.conf`. ## 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 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 +>``` * A Docker development environment with `docker` and `docker-compose`. * Visit [docker.com](https://docker.com) to download the tools and set up the environment. Docker's [installation guide](https://docs.docker.com/engine/getstarted/step_one/#step-3-verify-your-installation) covers verifying your Docker installation. * The Kubernetes command line client, `kubectl`. @@ -12,7 +16,12 @@ The k8s directory contains Kubernetes configuration for the eShopOnContainers ap ## Deploy the application with the deployment script 1. Open a PowerShell command line at the `k8s` directory of your local eShopOnContainers repository. 1. Ensure `docker`, `docker-compose`, and `kubectl` are on the path, and configured for your Docker machine and Kubernetes cluster. -1. Run `deploy.ps1` with your registry information. The Docker username and password are provided by Azure Container Registry, and can be retrieved from the Azure portal. For example: +1. Run `deploy.ps1` with your registry information. The Docker username and password are provided by Azure Container Registry, and can be retrieved from the Azure portal. Optionally, ACR credentials can be obtained by running the following command: +>``` +>az acr credential show -n eshopregistry +>``` + +Once the user and password are retrieved, run the following script for deployment. For example: >``` >./deploy.ps1 -registry myregistry.azurecr.io -dockerUser User -dockerPassword SecretPassword >``` diff --git a/k8s/gen-k8s-env.ps1 b/k8s/gen-k8s-env.ps1 new file mode 100644 index 000000000..39dacba24 --- /dev/null +++ b/k8s/gen-k8s-env.ps1 @@ -0,0 +1,25 @@ +Param( + [parameter(Mandatory=$true)][string]$resourceGroupName, + [parameter(Mandatory=$true)][string]$location, + [parameter(Mandatory=$true)][string]$registryName, + [parameter(Mandatory=$true)][string]$orchestratorName, + [parameter(Mandatory=$true)][string]$dnsName +) + +# Create resource group +Write-Host "Creating resource group..." -ForegroundColor Yellow +az group create --name=$resourceGroupName --location=$location + +# Create Azure Container Registry +Write-Host "Creating Azure Container Registry..." -ForegroundColor Yellow +az acr create -n $registryName -g $resourceGroupName -l $location --admin-enabled true --sku Basic + +# Create kubernetes orchestrator +Write-Host "Creating kubernetes orchestrator..." -ForegroundColor Yellow +az acs create --orchestrator-type=kubernetes --resource-group $resourceGroupName --name=$orchestratorName --dns-prefix=$dnsName --generate-ssh-keys + +# Retrieve kubernetes cluster configuration and save it under ~/.kube/config +az acs kubernetes get-credentials --resource-group=$resourceGroupName --name=$orchestratorName + +# Show ACR credentials +az acr credential show -n $registryName \ No newline at end of file From 47978017aaa176550fa41184f6b1d695e2847c04 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Sat, 6 May 2017 17:04:24 -0400 Subject: [PATCH 21/32] 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 22/32] 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 23/32] 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 24/32] 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 25/32] 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 26/32] 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 27/32] 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 28/32] 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 29/32] 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 30/32] 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 31/32] 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 32/32] 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; + } } }