Merge from Dev
This commit is contained in:
commit
83a33bc8fd
134
KUBERNETES.md
Normal file
134
KUBERNETES.md
Normal file
@ -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/)
|
28
README.k8s.md
Normal file
28
README.k8s.md
Normal file
@ -0,0 +1,28 @@
|
||||
# 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.
|
||||
* 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`.
|
||||
* 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. 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
|
||||
>```
|
||||
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)
|
20
README.md
20
README.md
@ -92,6 +92,26 @@ https://github.com/dotnet/eShopOnContainers/wiki/04.-Setting-eShopOnContainer-so
|
||||
The <b>Windows Containers scenario is currently being implemented/tested yet</b>. 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**
|
||||
|
6
cli-mac/build-bits.sh
Normal file → Executable file
6
cli-mac/build-bits.sh
Normal file → Executable file
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 rebuild node-sass && popd && dotnet restore ./eShopOnContainers-ServicesAndWebApps.sln && dotnet publish ./eShopOnContainers-ServicesAndWebApps.sln -c Release -o ./obj/Docker/publish"
|
||||
|
@ -56,7 +56,11 @@ services:
|
||||
- CatalogUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101
|
||||
- OrderingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102
|
||||
- IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105.
|
||||
- BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103
|
||||
- BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103
|
||||
- CatalogUrlHC=http://catalog.api:5101/hc
|
||||
- OrderingUrlHC=http://ordering.api:5102/hc
|
||||
- IdentityUrlHC=http://identity.api:5105/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser.
|
||||
- BasketUrlHC=http://basket.api:5103/hc
|
||||
ports:
|
||||
- "5104:5104"
|
||||
|
||||
|
@ -61,7 +61,11 @@ services:
|
||||
- CatalogUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5101
|
||||
- OrderingUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5102
|
||||
- IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105.
|
||||
- BasketUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5103
|
||||
- BasketUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5103
|
||||
- CatalogUrlHC=http://catalog.api:5101/hc
|
||||
- OrderingUrlHC=http://ordering.api:5102/hc
|
||||
- IdentityUrlHC=http://identity.api:5105/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser.
|
||||
- BasketUrlHC=http://basket.api:5103/hc
|
||||
ports:
|
||||
- "5104:5104"
|
||||
|
||||
|
@ -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
|
||||
|
80
k8s/deploy.ps1
Normal file
80
k8s/deploy.ps1
Normal file
@ -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, rabbitmq, frontend deployments
|
||||
kubectl create configmap config-files --from-file=nginx-conf=nginx.conf
|
||||
kubectl label configmap config-files app=eshop
|
||||
kubectl create -f sql-data.yaml -f rabbitmq.yaml -f services.yaml -f frontend.yaml
|
||||
|
||||
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
|
||||
|
||||
Write-Host "Pushing images to $registry..." -ForegroundColor Yellow
|
||||
$services = ("basket.api", "catalog.api", "identity.api", "ordering.api", "webmvc", "webspa")
|
||||
foreach ($service in $services) {
|
||||
docker tag eshop/$service $registry/eshop/$service
|
||||
docker push $registry/eshop/$service
|
||||
}
|
||||
|
||||
Write-Host "Waiting for frontend's external ip..." -ForegroundColor Yellow
|
||||
while ($true) {
|
||||
$frontendUrl = kubectl get svc frontend -o=jsonpath="{.status.loadBalancer.ingress[0].ip}"
|
||||
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
|
||||
|
||||
Write-Host "Creating deployments..."
|
||||
kubectl apply -f deployments.yaml
|
||||
|
||||
# update deployments with the private registry before k8s tries to pull images
|
||||
# (deployment templating, or Helm, would obviate this)
|
||||
kubectl set image -f deployments.yaml `
|
||||
basket=$registry/eshop/basket.api `
|
||||
catalog=$registry/eshop/catalog.api `
|
||||
identity=$registry/eshop/identity.api `
|
||||
ordering=$registry/eshop/ordering.api `
|
||||
webmvc=$registry/eshop/webmvc `
|
||||
webspa=$registry/eshop/webspa
|
||||
kubectl rollout resume -f deployments.yaml
|
||||
|
||||
Write-Host "WebSPA is exposed at http://$frontendUrl, WebMVC at http://$frontendUrl/webmvc" -ForegroundColor Yellow
|
236
k8s/deployments.yaml
Normal file
236
k8s/deployments.yaml
Normal file
@ -0,0 +1,236 @@
|
||||
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_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:
|
||||
name: urls
|
||||
key: IdentityUrl
|
||||
ports:
|
||||
- containerPort: 80
|
||||
- name: basket-data
|
||||
image: redis:3.2-alpine
|
||||
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_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:
|
||||
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_URLS
|
||||
value: http://0.0.0.0:80/identity
|
||||
- name: ConnectionStrings__DefaultConnection
|
||||
value: "Server=sql-data;Initial Catalog=Microsoft.eShopOnContainers.Services.IdentityDb;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_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:
|
||||
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_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_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
|
47
k8s/frontend.yaml
Normal file
47
k8s/frontend.yaml
Normal file
@ -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
|
25
k8s/gen-k8s-env.ps1
Normal file
25
k8s/gen-k8s-env.ps1
Normal file
@ -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
|
74
k8s/nginx.conf
Normal file
74
k8s/nginx.conf
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
30
k8s/rabbitmq.yaml
Normal file
30
k8s/rabbitmq.yaml
Normal file
@ -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
|
83
k8s/services.yaml
Normal file
83
k8s/services.yaml
Normal file
@ -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
|
33
k8s/sql-data.yaml
Normal file
33
k8s/sql-data.yaml
Normal file
@ -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
|
@ -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;
|
||||
}
|
||||
|
@ -10,7 +10,14 @@ namespace Microsoft.Extensions.HealthChecks
|
||||
{
|
||||
// Numeric checks
|
||||
|
||||
public static HealthCheckBuilder AddMinValueCheck<T>(this HealthCheckBuilder builder, string name, T minValue, Func<T> currentValueFunc)
|
||||
public static HealthCheckBuilder AddMinValueCheck<T>(this HealthCheckBuilder builder, string name, T minValue, Func<T> currentValueFunc) where T : IComparable<T>
|
||||
{
|
||||
Guard.ArgumentNotNull(nameof(builder), builder);
|
||||
|
||||
return AddMinValueCheck(builder, name, minValue, currentValueFunc, builder.DefaultCacheDuration);
|
||||
}
|
||||
|
||||
public static HealthCheckBuilder AddMinValueCheck<T>(this HealthCheckBuilder builder, string name, T minValue, Func<T> currentValueFunc, TimeSpan cacheDuration)
|
||||
where T : IComparable<T>
|
||||
{
|
||||
Guard.ArgumentNotNull(nameof(builder), builder);
|
||||
@ -26,12 +33,19 @@ namespace Microsoft.Extensions.HealthChecks
|
||||
$"min={minValue}, current={currentValue}",
|
||||
new Dictionary<string, object> { { "min", minValue }, { "current", currentValue } }
|
||||
);
|
||||
});
|
||||
}, cacheDuration);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static HealthCheckBuilder AddMaxValueCheck<T>(this HealthCheckBuilder builder, string name, T maxValue, Func<T> currentValueFunc)
|
||||
public static HealthCheckBuilder AddMaxValueCheck<T>(this HealthCheckBuilder builder, string name, T maxValue, Func<T> currentValueFunc) where T : IComparable<T>
|
||||
{
|
||||
Guard.ArgumentNotNull(nameof(builder), builder);
|
||||
|
||||
return AddMaxValueCheck(builder, name, maxValue, currentValueFunc, builder.DefaultCacheDuration);
|
||||
}
|
||||
|
||||
public static HealthCheckBuilder AddMaxValueCheck<T>(this HealthCheckBuilder builder, string name, T maxValue, Func<T> currentValueFunc, TimeSpan cacheDuration)
|
||||
where T : IComparable<T>
|
||||
{
|
||||
Guard.ArgumentNotNull(nameof(builder), builder);
|
||||
@ -47,7 +61,7 @@ namespace Microsoft.Extensions.HealthChecks
|
||||
$"max={maxValue}, current={currentValue}",
|
||||
new Dictionary<string, object> { { "max", maxValue }, { "current", currentValue } }
|
||||
);
|
||||
});
|
||||
}, cacheDuration);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
||||
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url,
|
||||
Func<HttpResponseMessage, IHealthCheckResult> checkFunc)
|
||||
{
|
||||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc);
|
||||
Guard.ArgumentNotNull(nameof(builder), builder);
|
||||
|
||||
return AddUrlCheck(builder, url, response => new ValueTask<IHealthCheckResult>(checkFunc(response)));
|
||||
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<HttpResponseMessage, IHealthCheckResult> checkFunc)
|
||||
{
|
||||
Guard.ArgumentNotNull(nameof(builder), builder);
|
||||
|
||||
return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration);
|
||||
}
|
||||
|
||||
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url,
|
||||
Func<HttpResponseMessage, Task<IHealthCheckResult>> checkFunc)
|
||||
Func<HttpResponseMessage, IHealthCheckResult> checkFunc,
|
||||
TimeSpan cacheDuration)
|
||||
{
|
||||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc);
|
||||
|
||||
return AddUrlCheck(builder, url, response => new ValueTask<IHealthCheckResult>(checkFunc(response)));
|
||||
return AddUrlCheck(builder, url, response => new ValueTask<IHealthCheckResult>(checkFunc(response)), cacheDuration);
|
||||
}
|
||||
|
||||
// Func returning Task<IHealthCheckResult>
|
||||
|
||||
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, Func<HttpResponseMessage, Task<IHealthCheckResult>> checkFunc)
|
||||
{
|
||||
Guard.ArgumentNotNull(nameof(builder), builder);
|
||||
|
||||
return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration);
|
||||
}
|
||||
|
||||
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url,
|
||||
Func<HttpResponseMessage, ValueTask<IHealthCheckResult>> checkFunc)
|
||||
Func<HttpResponseMessage, Task<IHealthCheckResult>> checkFunc,
|
||||
TimeSpan cacheDuration)
|
||||
{
|
||||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc);
|
||||
|
||||
return AddUrlCheck(builder, url, response => new ValueTask<IHealthCheckResult>(checkFunc(response)), cacheDuration);
|
||||
}
|
||||
|
||||
// Func returning ValueTask<IHealthCheckResult>
|
||||
|
||||
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, Func<HttpResponseMessage, ValueTask<IHealthCheckResult>> checkFunc)
|
||||
{
|
||||
Guard.ArgumentNotNull(nameof(builder), builder);
|
||||
|
||||
return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration);
|
||||
}
|
||||
|
||||
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url,
|
||||
Func<HttpResponseMessage, ValueTask<IHealthCheckResult>> 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;
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
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<string> GetStringAsync(string uri);
|
||||
Task<HttpResponseMessage> PostAsync<T>(string uri, T item);
|
||||
Task<HttpResponseMessage> DeleteAsync(string uri);
|
||||
Task<string> GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer");
|
||||
|
||||
Task<HttpResponseMessage> PostAsync<T>(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer");
|
||||
|
||||
Task<HttpResponseMessage> DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer");
|
||||
|
||||
Task<HttpResponseMessage> PutAsync<T>(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer");
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http
|
||||
{
|
||||
public class ResiliencePolicy
|
||||
{
|
||||
}
|
||||
}
|
@ -3,9 +3,12 @@ using Newtonsoft.Json;
|
||||
using Polly;
|
||||
using Polly.Wrap;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http
|
||||
@ -17,48 +20,140 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http
|
||||
/// </summary>
|
||||
public class ResilientHttpClient : IHttpClient
|
||||
{
|
||||
private HttpClient _client;
|
||||
private PolicyWrap _policyWrapper;
|
||||
private ILogger<ResilientHttpClient> _logger;
|
||||
public HttpClient Inst => _client;
|
||||
private readonly HttpClient _client;
|
||||
private readonly ILogger<ResilientHttpClient> _logger;
|
||||
private readonly Func<string, IEnumerable<Policy>> _policyCreator;
|
||||
private ConcurrentDictionary<string, PolicyWrap> _policyWrappers;
|
||||
|
||||
public ResilientHttpClient(Policy[] policies, ILogger<ResilientHttpClient> logger)
|
||||
public ResilientHttpClient(Func<string, IEnumerable<Policy>> policyCreator, ILogger<ResilientHttpClient> logger)
|
||||
{
|
||||
_client = new HttpClient();
|
||||
_logger = logger;
|
||||
_policyCreator = policyCreator;
|
||||
_policyWrappers = new ConcurrentDictionary<string, PolicyWrap>();
|
||||
}
|
||||
|
||||
// Add Policies to be applied
|
||||
_policyWrapper = Policy.WrapAsync(policies);
|
||||
}
|
||||
|
||||
public Task<string> GetStringAsync(string uri) =>
|
||||
HttpInvoker(() =>
|
||||
_client.GetStringAsync(uri));
|
||||
public Task<HttpResponseMessage> PostAsync<T>(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer")
|
||||
{
|
||||
return DoPostPutAsync(HttpMethod.Post, uri, item, authorizationToken, requestId, authorizationMethod);
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PostAsync<T>(string uri, T item) =>
|
||||
// a new StringContent must be created for each retry
|
||||
// as it is disposed after each call
|
||||
HttpInvoker(() =>
|
||||
public Task<HttpResponseMessage> PutAsync<T>(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer")
|
||||
{
|
||||
return DoPostPutAsync(HttpMethod.Put, uri, item, authorizationToken, requestId, authorizationMethod);
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer")
|
||||
{
|
||||
var origin = GetOriginFromUri(uri);
|
||||
|
||||
return HttpInvoker(origin, async () =>
|
||||
{
|
||||
var response = _client.PostAsync(uri, new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"));
|
||||
// raise exception if HttpResponseCode 500
|
||||
// needed for circuit breaker to track fails
|
||||
if (response.Result.StatusCode == HttpStatusCode.InternalServerError)
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Delete, uri);
|
||||
|
||||
if (authorizationToken != null)
|
||||
{
|
||||
throw new HttpRequestException();
|
||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken);
|
||||
}
|
||||
|
||||
return response;
|
||||
if (requestId != null)
|
||||
{
|
||||
requestMessage.Headers.Add("x-requestid", requestId);
|
||||
}
|
||||
|
||||
return await _client.SendAsync(requestMessage);
|
||||
});
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> DeleteAsync(string uri) =>
|
||||
HttpInvoker(() => _client.DeleteAsync(uri));
|
||||
public Task<string> GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer")
|
||||
{
|
||||
var origin = GetOriginFromUri(uri);
|
||||
|
||||
return HttpInvoker(origin, async () =>
|
||||
{
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
private Task<HttpResponseMessage> DoPostPutAsync<T>(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
|
||||
var origin = GetOriginFromUri(uri);
|
||||
|
||||
return HttpInvoker(origin, () =>
|
||||
{
|
||||
var requestMessage = new HttpRequestMessage(method, 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 = _client.SendAsync(requestMessage).Result;
|
||||
|
||||
// raise exception if HttpResponseCode 500
|
||||
// needed for circuit breaker to track fails
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.InternalServerError)
|
||||
{
|
||||
throw new HttpRequestException();
|
||||
}
|
||||
|
||||
return Task.FromResult(response);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<T> HttpInvoker<T>(string origin, Func<Task<T>> action)
|
||||
{
|
||||
var normalizedOrigin = NormalizeOrigin(origin);
|
||||
|
||||
if (!_policyWrappers.TryGetValue(normalizedOrigin, out PolicyWrap policyWrap))
|
||||
{
|
||||
policyWrap = Policy.Wrap(_policyCreator(normalizedOrigin).ToArray());
|
||||
_policyWrappers.TryAdd(normalizedOrigin, policyWrap);
|
||||
}
|
||||
|
||||
private Task<T> HttpInvoker<T>(Func<Task<T>> action) =>
|
||||
// Executes the action applying all
|
||||
// the policies defined in the wrapper
|
||||
_policyWrapper.ExecuteAsync(() => action());
|
||||
}
|
||||
return await policyWrap.Execute(action, new Context(normalizedOrigin));
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
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 +12,90 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http
|
||||
{
|
||||
private HttpClient _client;
|
||||
private ILogger<StandardHttpClient> _logger;
|
||||
public HttpClient Inst => _client;
|
||||
|
||||
public StandardHttpClient(ILogger<StandardHttpClient> logger)
|
||||
{
|
||||
_client = new HttpClient();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<string> GetStringAsync(string uri) =>
|
||||
_client.GetStringAsync(uri);
|
||||
|
||||
public Task<HttpResponseMessage> PostAsync<T>(string uri, T item)
|
||||
public async Task<string> 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<HttpResponseMessage> DeleteAsync(string uri) =>
|
||||
_client.DeleteAsync(uri);
|
||||
private async Task<HttpResponseMessage> DoPostPutAsync<T>(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
|
||||
|
||||
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<HttpResponseMessage> PostAsync<T>(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<HttpResponseMessage> PutAsync<T>(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<HttpResponseMessage> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,4 +111,3 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,12 @@
|
||||
|
||||
services.AddHealthChecks(checks =>
|
||||
{
|
||||
checks.AddSqlCheck("CatalogDb", Configuration["ConnectionString"]);
|
||||
var minutes = 1;
|
||||
if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed))
|
||||
{
|
||||
minutes = minutesParsed;
|
||||
}
|
||||
checks.AddSqlCheck("CatalogDb", Configuration["ConnectionString"], TimeSpan.FromMinutes(minutes));
|
||||
});
|
||||
|
||||
services.AddMvc(options =>
|
||||
|
@ -60,7 +60,7 @@ namespace Identity.API.Configuration
|
||||
AllowAccessTokensViaBrowser = true,
|
||||
RedirectUris = { clientsUrl["Xamarin"] },
|
||||
RequireConsent = false,
|
||||
PostLogoutRedirectUris = { "http://13.88.8.119:5105/Account/Redirecting", "http://10.6.1.234:5105/Account/Redirecting" },
|
||||
PostLogoutRedirectUris = { $"{clientsUrl["Xamarin"]}/Account/Redirecting" },
|
||||
AllowedCorsOrigins = { "http://eshopxamarin" },
|
||||
AllowedScopes =
|
||||
{
|
||||
@ -84,15 +84,11 @@ namespace Identity.API.Configuration
|
||||
AllowOfflineAccess = true,
|
||||
RedirectUris = new List<string>
|
||||
{
|
||||
$"{clientsUrl["Mvc"]}/signin-oidc",
|
||||
"http://104.40.62.65:5100/signin-oidc",
|
||||
"http://localhost:5100/signin-oidc",
|
||||
"http://13.88.8.119:5100/signin-oidc"
|
||||
$"{clientsUrl["Mvc"]}/signin-oidc"
|
||||
},
|
||||
PostLogoutRedirectUris = new List<string>
|
||||
{
|
||||
$"{clientsUrl["Mvc"]}/signout-callback-oidc",
|
||||
"http://localhost:5100/signout-callback-oidc"
|
||||
$"{clientsUrl["Mvc"]}/signout-callback-oidc"
|
||||
},
|
||||
AllowedScopes = new List<string>
|
||||
{
|
||||
|
@ -66,7 +66,12 @@ namespace eShopOnContainers.Identity
|
||||
|
||||
services.AddHealthChecks(checks =>
|
||||
{
|
||||
checks.AddSqlCheck("Identity_Db", Configuration.GetConnectionString("DefaultConnection"));
|
||||
var minutes = 1;
|
||||
if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed))
|
||||
{
|
||||
minutes = minutesParsed;
|
||||
}
|
||||
checks.AddSqlCheck("Identity_Db", Configuration.GetConnectionString("DefaultConnection"), TimeSpan.FromMinutes(minutes));
|
||||
});
|
||||
|
||||
services.AddTransient<IEmailSender, AuthMessageSender>();
|
||||
|
@ -1,13 +1,14 @@
|
||||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public interface IOrderQueries
|
||||
{
|
||||
Task<dynamic> GetOrderAsync(int id);
|
||||
|
||||
Task<dynamic> GetOrdersAsync();
|
||||
Task<IEnumerable<dynamic>> GetOrdersAsync();
|
||||
|
||||
Task<dynamic> GetCardTypesAsync();
|
||||
Task<IEnumerable<dynamic>> GetCardTypesAsync();
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<dynamic> GetOrdersAsync()
|
||||
public async Task<IEnumerable<dynamic>> GetOrdersAsync()
|
||||
{
|
||||
using (var connection = new SqlConnection(_connectionString))
|
||||
{
|
||||
@ -58,7 +58,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<dynamic> GetCardTypesAsync()
|
||||
public async Task<IEnumerable<dynamic>> GetCardTypesAsync()
|
||||
{
|
||||
using (var connection = new SqlConnection(_connectionString))
|
||||
{
|
||||
|
@ -30,25 +30,21 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> 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<CreateOrderCommand, bool>(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 ? (IActionResult)Ok() : (IActionResult)BadRequest();
|
||||
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
[Route("{orderId:int}")]
|
||||
@ -72,8 +68,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetOrders()
|
||||
{
|
||||
var orders = await _orderQueries
|
||||
.GetOrdersAsync();
|
||||
var orderTask = _orderQueries.GetOrdersAsync();
|
||||
|
||||
var orders = await orderTask;
|
||||
|
||||
return Ok(orders);
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ordering.API.Infrastructure.Middlewares
|
||||
{
|
||||
public class FailingStartupFilter : IStartupFilter
|
||||
{
|
||||
public FailingStartupFilter()
|
||||
{
|
||||
}
|
||||
|
||||
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
|
||||
{
|
||||
return app =>
|
||||
{
|
||||
app.UseFailingMiddleware();
|
||||
next(app);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Ordering.API.Infrastructure.Middlewares;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Hosting
|
||||
{
|
||||
public static class WebHostBuildertExtensions
|
||||
{
|
||||
public static IWebHostBuilder UseFailing(this IWebHostBuilder builder, string path)
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<IStartupFilter>(new FailingStartupFilter());
|
||||
});
|
||||
return builder;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API
|
||||
{
|
||||
var host = new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.UseFailing("/Failing")
|
||||
.UseHealthChecks("/hc")
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
|
@ -61,7 +61,12 @@
|
||||
|
||||
services.AddHealthChecks(checks =>
|
||||
{
|
||||
checks.AddSqlCheck("OrderingDb", Configuration["ConnectionString"]);
|
||||
var minutes = 1;
|
||||
if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed))
|
||||
{
|
||||
minutes = minutesParsed;
|
||||
}
|
||||
checks.AddSqlCheck("OrderingDb", Configuration["ConnectionString"], TimeSpan.FromMinutes(minutes));
|
||||
});
|
||||
|
||||
services.AddEntityFrameworkSqlServer()
|
||||
@ -143,8 +148,6 @@
|
||||
|
||||
app.UseCors("CorsPolicy");
|
||||
|
||||
app.UseFailingMiddleware();
|
||||
|
||||
ConfigureAuth(app);
|
||||
|
||||
app.UseMvcWithDefaultRoute();
|
||||
|
@ -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())
|
||||
|
@ -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);
|
||||
}
|
||||
|
68
src/Web/WebMVC/Infrastructure/API.cs
Normal file
68
src/Web/WebMVC/Infrastructure/API.cs
Normal file
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Polly;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.WebMVC.Infrastructure
|
||||
@ -17,14 +14,13 @@ namespace Microsoft.eShopOnContainers.WebMVC.Infrastructure
|
||||
=>_logger = logger;
|
||||
|
||||
public ResilientHttpClient CreateResilientHttpClient()
|
||||
=> new ResilientHttpClient(CreatePolicies(), _logger);
|
||||
|
||||
=> new ResilientHttpClient((origin) => CreatePolicies(), _logger);
|
||||
|
||||
private Policy[] CreatePolicies()
|
||||
=> new Policy[]
|
||||
{
|
||||
Policy.Handle<HttpRequestException>()
|
||||
.WaitAndRetryAsync(
|
||||
Policy.Handle<HttpRequestException>()
|
||||
.WaitAndRetry(
|
||||
// number of retries
|
||||
6,
|
||||
// exponential backofff
|
||||
@ -40,7 +36,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Infrastructure
|
||||
_logger.LogDebug(msg);
|
||||
}),
|
||||
Policy.Handle<HttpRequestException>()
|
||||
.CircuitBreakerAsync(
|
||||
.CircuitBreaker(
|
||||
// number of exceptions before breaking circuit
|
||||
5,
|
||||
// time circuit opened before retry
|
||||
@ -54,6 +50,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Infrastructure
|
||||
{
|
||||
// on circuit closed
|
||||
_logger.LogTrace("Circuit breaker reset");
|
||||
})};
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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<Basket> 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<Basket>(dataString) ??
|
||||
var response = JsonConvert.DeserializeObject<Basket>(dataString) ??
|
||||
new Basket()
|
||||
{
|
||||
BuyerId = user.Id
|
||||
@ -47,14 +44,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
|
||||
|
||||
public async Task<Basket> 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<string> GetUserTokenAsync()
|
||||
{
|
||||
var context = _httpContextAccesor.HttpContext;
|
||||
return await context.Authentication.GetTokenAsync("access_token");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<AppSettings> _settings;
|
||||
private IHttpClient _apiClient;
|
||||
private readonly IHttpClient _apiClient;
|
||||
private readonly ILogger<CatalogService> _logger;
|
||||
|
||||
private readonly string _remoteServiceBaseUrl;
|
||||
|
||||
public CatalogService(IOptionsSnapshot<AppSettings> settings, ILoggerFactory loggerFactory, IHttpClient httpClient) {
|
||||
_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<Catalog> GetCatalogItems(int page,int take, int? brand, int? type)
|
||||
|
||||
public CatalogService(IOptionsSnapshot<AppSettings> settings, IHttpClient httpClient, ILogger<CatalogService> logger)
|
||||
{
|
||||
var itemsQs = $"items?pageIndex={page}&pageSize={take}";
|
||||
var filterQs = "";
|
||||
_settings = settings;
|
||||
_apiClient = httpClient;
|
||||
_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}";
|
||||
}
|
||||
_remoteServiceBaseUrl = $"{_settings.Value.CatalogUrl}/api/v1/catalog/";
|
||||
}
|
||||
|
||||
var catalogUrl = $"{_remoteServiceBaseUrl}items{filterQs}?pageIndex={page}&pageSize={take}";
|
||||
public async Task<Catalog> GetCatalogItems(int page, int take, int? brand, int? type)
|
||||
{
|
||||
var allcatalogItemsUri = API.Catalog.GetAllCatalogItems(_remoteServiceBaseUrl, page, take, brand, type);
|
||||
|
||||
var dataString = "";
|
||||
|
||||
//
|
||||
// Using a HttpClient wrapper with Retry and Exponential Backoff
|
||||
//
|
||||
dataString = await _apiClient.GetStringAsync(catalogUrl);
|
||||
var dataString = await _apiClient.GetStringAsync(allcatalogItemsUri);
|
||||
|
||||
var response = JsonConvert.DeserializeObject<Catalog>(dataString);
|
||||
|
||||
@ -52,14 +41,16 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
|
||||
|
||||
public async Task<IEnumerable<SelectListItem>> 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<SelectListItem>();
|
||||
items.Add(new SelectListItem() { Value = null, Text = "All", Selected = true });
|
||||
|
||||
JArray brands = JArray.Parse(dataString);
|
||||
foreach (JObject brand in brands.Children<JObject>())
|
||||
var brands = JArray.Parse(dataString);
|
||||
|
||||
foreach (var brand in brands.Children<JObject>())
|
||||
{
|
||||
items.Add(new SelectListItem()
|
||||
{
|
||||
@ -73,14 +64,15 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
|
||||
|
||||
public async Task<IEnumerable<SelectListItem>> 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<SelectListItem>();
|
||||
items.Add(new SelectListItem() { Value = null, Text = "All", Selected = true });
|
||||
|
||||
JArray brands = JArray.Parse(dataString);
|
||||
foreach (JObject brand in brands.Children<JObject>())
|
||||
var brands = JArray.Parse(dataString);
|
||||
foreach (var brand in brands.Children<JObject>())
|
||||
{
|
||||
items.Add(new SelectListItem()
|
||||
{
|
||||
|
@ -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.Extensions.Options;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http;
|
||||
using Microsoft.eShopOnContainers.WebMVC.ViewModels;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
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<Order> GetOrder(ApplicationUser user, string Id)
|
||||
async public Task<Order> 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<Order>(dataString);
|
||||
|
||||
return response;
|
||||
@ -43,16 +40,13 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
|
||||
|
||||
async public Task<List<Order>> 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<List<Order>>(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");
|
||||
var token = await GetUserTokenAsync();
|
||||
var requestId = order.RequestId.ToString();
|
||||
var addNewOrderUri = API.Order.AddNewOrder(_remoteServiceBaseUrl);
|
||||
|
||||
_apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
_apiClient.Inst.DefaultRequestHeaders.Add("x-requestid", order.RequestId.ToString());
|
||||
|
||||
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<string> GetUserTokenAsync()
|
||||
{
|
||||
var context = _httpContextAccesor.HttpContext;
|
||||
|
||||
return await context.Authentication.GetTokenAsync("access_token");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,29 +54,34 @@ namespace Microsoft.eShopOnContainers.WebMVC
|
||||
|
||||
services.AddHealthChecks(checks =>
|
||||
{
|
||||
checks.AddUrlCheck(Configuration["CatalogUrl"]);
|
||||
checks.AddUrlCheck(Configuration["OrderingUrl"]);
|
||||
checks.AddUrlCheck(Configuration["BasketUrl"]);
|
||||
checks.AddUrlCheck(Configuration["IdentityUrl"]);
|
||||
var minutes = 1;
|
||||
if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed))
|
||||
{
|
||||
minutes = minutesParsed;
|
||||
}
|
||||
checks.AddUrlCheck(Configuration["CatalogUrl"] + "/hc", TimeSpan.FromMinutes(minutes));
|
||||
checks.AddUrlCheck(Configuration["OrderingUrl"] + "/hc", TimeSpan.FromMinutes(minutes));
|
||||
checks.AddUrlCheck(Configuration["BasketUrl"] + "/hc", TimeSpan.FromMinutes(minutes));
|
||||
checks.AddUrlCheck(Configuration["IdentityUrl"] + "/hc", TimeSpan.FromMinutes(minutes));
|
||||
});
|
||||
|
||||
// Add application services.
|
||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddTransient<ICatalogService, CatalogService>();
|
||||
services.AddTransient<IOrderingService, OrderingService>();
|
||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddTransient<ICatalogService, CatalogService>();
|
||||
services.AddTransient<IOrderingService, OrderingService>();
|
||||
services.AddTransient<IBasketService, BasketService>();
|
||||
services.AddTransient<IIdentityParser<ApplicationUser>, IdentityParser>();
|
||||
|
||||
if (Configuration.GetValue<string>("UseResilientHttp") == bool.TrueString)
|
||||
{
|
||||
services.AddTransient<IResilientHttpClientFactory, ResilientHttpClientFactory>();
|
||||
services.AddTransient<IHttpClient, ResilientHttpClient>(sp => sp.GetService<IResilientHttpClientFactory>().CreateResilientHttpClient());
|
||||
services.AddSingleton<IResilientHttpClientFactory, ResilientHttpClientFactory>();
|
||||
services.AddSingleton<IHttpClient, ResilientHttpClient>(sp => sp.GetService<IResilientHttpClientFactory>().CreateResilientHttpClient());
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddTransient<IHttpClient, StandardHttpClient>();
|
||||
services.AddSingleton<IHttpClient, StandardHttpClient>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
|
||||
@ -113,10 +118,10 @@ namespace Microsoft.eShopOnContainers.WebMVC
|
||||
AuthenticationScheme = "oidc",
|
||||
SignInScheme = "Cookies",
|
||||
Authority = identityUrl.ToString(),
|
||||
PostLogoutRedirectUri = callBackUrl.ToString(),
|
||||
PostLogoutRedirectUri = callBackUrl.ToString(),
|
||||
ClientId = "mvc",
|
||||
ClientSecret = "secret",
|
||||
ResponseType = "code id_token",
|
||||
ResponseType = "code id_token",
|
||||
SaveTokens = true,
|
||||
GetClaimsFromUserInfoEndpoint = true,
|
||||
RequireHttpsMetadata = false,
|
||||
|
@ -55,7 +55,7 @@
|
||||
</section>
|
||||
|
||||
<section class="col-sm-6">
|
||||
<div class="esh-app-footer-text hidden-xs"> e-ShoponContainers. All right reserved </div>
|
||||
<div class="esh-app-footer-text hidden-xs"> e-ShoponContainers. By Microsoft Corp. </div>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
|
@ -13,12 +13,12 @@
|
||||
<DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--<ItemGroup>
|
||||
<Compile Remove="wwwroot\lib\bootstrap\**" />
|
||||
<Content Remove="wwwroot\lib\bootstrap\**" />
|
||||
<EmbeddedResource Remove="wwwroot\lib\bootstrap\**" />
|
||||
<None Remove="wwwroot\lib\bootstrap\**" />
|
||||
</ItemGroup>
|
||||
</ItemGroup>-->
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="1.1.0" />
|
||||
|
2
src/Web/WebMVC/wwwroot/css/site.min.css
vendored
2
src/Web/WebMVC/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
@ -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; }
|
||||
|
@ -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();
|
||||
|
@ -33,6 +33,9 @@ namespace eShopConContainers.WebSPA
|
||||
}
|
||||
|
||||
Configuration = builder.Build();
|
||||
|
||||
var localPath = new Uri(Configuration["ASPNETCORE_URLS"])?.LocalPath ?? "/";
|
||||
Configuration["BaseUrl"] = localPath;
|
||||
}
|
||||
|
||||
public static IConfigurationRoot Configuration { get; set;}
|
||||
@ -42,10 +45,16 @@ namespace eShopConContainers.WebSPA
|
||||
{
|
||||
services.AddHealthChecks(checks =>
|
||||
{
|
||||
checks.AddUrlCheck(Configuration["CatalogUrl"]);
|
||||
checks.AddUrlCheck(Configuration["OrderingUrl"]);
|
||||
checks.AddUrlCheck(Configuration["BasketUrl"]);
|
||||
checks.AddUrlCheck(Configuration["IdentityUrl"]);
|
||||
var minutes = 1;
|
||||
if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed))
|
||||
{
|
||||
minutes = minutesParsed;
|
||||
}
|
||||
|
||||
checks.AddUrlCheck(Configuration["CatalogUrlHC"], TimeSpan.FromMinutes(minutes));
|
||||
checks.AddUrlCheck(Configuration["OrderingUrlHC"], TimeSpan.FromMinutes(minutes));
|
||||
checks.AddUrlCheck(Configuration["BasketUrlHC"], TimeSpan.FromMinutes(minutes));
|
||||
checks.AddUrlCheck(Configuration["IdentityUrlHC"], TimeSpan.FromMinutes(minutes));
|
||||
});
|
||||
|
||||
services.Configure<AppSettings>(Configuration);
|
||||
|
@ -72,6 +72,15 @@
|
||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.0-msbuild3-final" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- workaround for https://github.com/aspnet/websdk/issues/114 -->
|
||||
<!--
|
||||
<Target Name="AddGeneratedContentItems" BeforeTargets="AssignTargetPaths" DependsOnTargets="PrepareForPublish">
|
||||
<ItemGroup>
|
||||
<Content Include="wwwroot/**" CopyToPublishDirectory="PreserveNewest" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder);@(Content)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\BuildingBlocks\HealthChecks\src\Microsoft.AspNetCore.HealthChecks\Microsoft.AspNetCore.HealthChecks.csproj" />
|
||||
<ProjectReference Include="..\..\BuildingBlocks\HealthChecks\src\Microsoft.Extensions.HealthChecks\Microsoft.Extensions.HealthChecks.csproj" />
|
||||
|
@ -28,6 +28,7 @@ namespace WebStatus.Controllers
|
||||
data.AddResult(checkResult.Key, checkResult.Value);
|
||||
}
|
||||
|
||||
ViewBag.RefreshSeconds = 60;
|
||||
return View(data);
|
||||
}
|
||||
|
||||
|
@ -8,11 +8,11 @@ namespace WebStatus.Extensions
|
||||
{
|
||||
public static class HealthCheckBuilderExtensions
|
||||
{
|
||||
public static HealthCheckBuilder AddUrlCheckIfNotNull(this HealthCheckBuilder builder, string url)
|
||||
public static HealthCheckBuilder AddUrlCheckIfNotNull(this HealthCheckBuilder builder, string url, TimeSpan cacheDuration)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
builder.AddUrlCheck(url);
|
||||
builder.AddUrlCheck(url, cacheDuration);
|
||||
}
|
||||
|
||||
return builder;
|
||||
|
@ -32,12 +32,18 @@ namespace WebStatus
|
||||
// Add framework services.
|
||||
services.AddHealthChecks(checks =>
|
||||
{
|
||||
checks.AddUrlCheckIfNotNull(Configuration["OrderingUrl"]);
|
||||
checks.AddUrlCheckIfNotNull(Configuration["BasketUrl"]);
|
||||
checks.AddUrlCheckIfNotNull(Configuration["CatalogUrl"]);
|
||||
checks.AddUrlCheckIfNotNull(Configuration["IdentityUrl"]);
|
||||
checks.AddUrlCheckIfNotNull(Configuration["mvc"]);
|
||||
checks.AddUrlCheckIfNotNull(Configuration["spa"]);
|
||||
var minutes = 1;
|
||||
if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed))
|
||||
{
|
||||
minutes = minutesParsed;
|
||||
}
|
||||
|
||||
checks.AddUrlCheckIfNotNull(Configuration["OrderingUrl"], TimeSpan.FromMinutes(minutes));
|
||||
checks.AddUrlCheckIfNotNull(Configuration["BasketUrl"], TimeSpan.FromMinutes(minutes));
|
||||
checks.AddUrlCheckIfNotNull(Configuration["CatalogUrl"], TimeSpan.FromMinutes(minutes));
|
||||
checks.AddUrlCheckIfNotNull(Configuration["IdentityUrl"], TimeSpan.FromMinutes(minutes));
|
||||
checks.AddUrlCheckIfNotNull(Configuration["mvc"], TimeSpan.FromMinutes(minutes));
|
||||
checks.AddUrlCheckIfNotNull(Configuration["spa"], TimeSpan.FromMinutes(minutes));
|
||||
});
|
||||
services.AddMvc();
|
||||
}
|
||||
|
@ -16,6 +16,12 @@
|
||||
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
|
||||
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
|
||||
</environment>
|
||||
|
||||
@if (ViewBag.RefreshSeconds != null && ViewBag.RefreshSeconds > 0)
|
||||
{
|
||||
<meta http-equiv="refresh" content="@ViewBag.RefreshSeconds">
|
||||
}
|
||||
|
||||
@Html.Raw(JavaScriptSnippet.FullScript)
|
||||
</head>
|
||||
<body>
|
||||
@ -42,9 +48,10 @@
|
||||
<div class="container body-content">
|
||||
@RenderBody()
|
||||
</div>
|
||||
<footer class="container footer">
|
||||
<p class="center">© 2017 - WebStatus</p>
|
||||
</footer>
|
||||
<br />
|
||||
<div id="footer">
|
||||
<p> © 2017 - WebStatus</p>
|
||||
</div>
|
||||
|
||||
|
||||
<environment names="Development">
|
||||
|
@ -32,9 +32,9 @@ namespace FunctionalTests.Services.Catalog
|
||||
}
|
||||
}
|
||||
|
||||
public static class Post
|
||||
public static class Put
|
||||
{
|
||||
public static string UpdateCatalogProduct = "api/v1/catalog/update";
|
||||
public static string UpdateCatalogProduct = "api/v1/catalog/items";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ namespace FunctionalTests.Services
|
||||
var itemToModify = basket.Items[2];
|
||||
var oldPrice = itemToModify.UnitPrice;
|
||||
var newPrice = oldPrice + priceModification;
|
||||
var pRes = await catalogClient.PostAsync(CatalogScenariosBase.Post.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice, originalCatalogProducts), UTF8Encoding.UTF8, "application/json"));
|
||||
var pRes = await catalogClient.PutAsync(CatalogScenariosBase.Put.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice, originalCatalogProducts), UTF8Encoding.UTF8, "application/json"));
|
||||
|
||||
var modifiedCatalogProducts = await GetCatalogAsync(catalogClient);
|
||||
|
||||
|
@ -93,7 +93,7 @@
|
||||
string BuildOrderWithInvalidExperationTime()
|
||||
{
|
||||
var order = new CreateOrderCommand(
|
||||
null,
|
||||
new List<OrderItemDTO>(),
|
||||
cardExpiration: DateTime.UtcNow.AddYears(-1),
|
||||
cardNumber: "5145-555-5555",
|
||||
cardHolderName: "Jhon Senna",
|
||||
|
@ -14,6 +14,8 @@ namespace UnitTest.Ordering.Application
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand;
|
||||
|
||||
public class NewOrderRequestHandlerTest
|
||||
{
|
||||
private readonly Mock<IOrderRepository> _orderRepositoryMock;
|
||||
@ -72,7 +74,7 @@ namespace UnitTest.Ordering.Application
|
||||
private CreateOrderCommand FakeOrderRequestWithBuyer(Dictionary<string, object> args = null)
|
||||
{
|
||||
return new CreateOrderCommand(
|
||||
null,
|
||||
new List<OrderItemDTO>(),
|
||||
city: args != null && args.ContainsKey("city") ? (string)args["city"] : null,
|
||||
street: args != null && args.ContainsKey("street") ? (string)args["street"] : null,
|
||||
state: args != null && args.ContainsKey("state") ? (string)args["state"] : null,
|
||||
|
@ -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<object>();
|
||||
_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<object>();
|
||||
_orderQueriesMock.Setup(x => x.GetCardTypesAsync())
|
||||
.Returns(Task.FromResult(fakeDynamicResult));
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user