From e3719a7777bfe738160fbd4de381e7b3fccaa596 Mon Sep 17 00:00:00 2001 From: daniv-msft <48293037+daniv-msft@users.noreply.github.com> Date: Wed, 24 Apr 2019 10:35:25 -0700 Subject: [PATCH] Add BikeSharingApp to samples --- samples/BikeSharingApp/.gitattributes | 1 + samples/BikeSharingApp/.gitignore | 258 +++++++++ .../BikeSharingWeb/.dockerignore | 11 + .../BikeSharingApp/BikeSharingWeb/.gitignore | 64 +++ .../BikeSharingApp/BikeSharingWeb/Dockerfile | 10 + .../BikeSharingWeb/Dockerfile.static | 3 + .../BikeSharingApp/BikeSharingWeb/azds.yaml | 35 ++ .../charts/bikesharingweb/.helmignore | 21 + .../charts/bikesharingweb/Chart.yaml | 5 + .../charts/bikesharingweb/templates/NOTES.txt | 19 + .../bikesharingweb/templates/_helpers.tpl | 32 ++ .../bikesharingweb/templates/deployment.yaml | 74 +++ .../bikesharingweb/templates/ingress.yaml | 39 ++ .../bikesharingweb/templates/secrets.yaml | 12 + .../bikesharingweb/templates/service.yaml | 19 + .../charts/bikesharingweb/values.yaml | 65 +++ .../BikeSharingWeb/components/BikeCard.js | 49 ++ .../BikeSharingWeb/components/Content.js | 19 + .../BikeSharingWeb/components/ErrorPanel.js | 65 +++ .../BikeSharingWeb/components/Field.js | 29 ++ .../BikeSharingWeb/components/Footer.js | 34 ++ .../BikeSharingWeb/components/FormButton.js | 46 ++ .../BikeSharingWeb/components/FormNote.js | 17 + .../BikeSharingWeb/components/FormTextbox.js | 33 ++ .../BikeSharingWeb/components/Header.js | 62 +++ .../BikeSharingWeb/components/Logo.js | 15 + .../BikeSharingWeb/components/Map.js | 21 + .../BikeSharingWeb/components/Page.js | 32 ++ .../components/ReviewControl.js | 45 ++ .../components/SigninFormLayout.js | 21 + .../BikeSharingWeb/package.json | 35 ++ .../BikeSharingWeb/pages/complete-return.js | 175 +++++++ .../BikeSharingWeb/pages/current-ride.js | 227 ++++++++ .../BikeSharingWeb/pages/devsignin.js | 95 ++++ .../BikeSharingWeb/pages/helpers.js | 93 ++++ .../BikeSharingWeb/pages/index.js | 133 +++++ .../BikeSharingWeb/pages/preview.js | 219 ++++++++ .../BikeSharingWeb/pages/review.js | 118 +++++ .../BikeSharingApp/BikeSharingWeb/server.js | 70 +++ .../BikeSharingWeb/static/awc-title.svg | 9 + .../BikeSharingWeb/static/logo.svg | 33 ++ .../BikeSharingWeb/static/sample-map.png | Bin 0 -> 9304 bytes samples/BikeSharingApp/Bikes/.dockerignore | 11 + samples/BikeSharingApp/Bikes/.gitignore | 43 ++ samples/BikeSharingApp/Bikes/Dockerfile | 10 + samples/BikeSharingApp/Bikes/azds.yaml | 31 ++ .../Bikes/charts/bikes/.helmignore | 21 + .../Bikes/charts/bikes/Chart.yaml | 5 + .../Bikes/charts/bikes/templates/NOTES.txt | 19 + .../Bikes/charts/bikes/templates/_helpers.tpl | 32 ++ .../charts/bikes/templates/deployment.yaml | 72 +++ .../Bikes/charts/bikes/templates/ingress.yaml | 39 ++ .../Bikes/charts/bikes/templates/secrets.yaml | 12 + .../Bikes/charts/bikes/templates/service.yaml | 19 + .../Bikes/charts/bikes/values.yaml | 65 +++ samples/BikeSharingApp/Bikes/package.json | 16 + samples/BikeSharingApp/Bikes/server.js | 407 +++++++++++++++ samples/BikeSharingApp/Billing/.gitignore | 4 + samples/BikeSharingApp/Billing/Dockerfile | 10 + samples/BikeSharingApp/Billing/azds.yaml | 26 + .../Billing/charts/billing/.helmignore | 21 + .../Billing/charts/billing/Chart.yaml | 5 + .../charts/billing/templates/NOTES.txt | 19 + .../charts/billing/templates/_helpers.tpl | 32 ++ .../charts/billing/templates/deployment.yaml | 72 +++ .../charts/billing/templates/ingress.yaml | 39 ++ .../charts/billing/templates/secrets.yaml | 12 + .../charts/billing/templates/service.yaml | 19 + .../Billing/charts/billing/values.yaml | 65 +++ samples/BikeSharingApp/Billing/customer.go | 62 +++ samples/BikeSharingApp/Billing/db.go | 299 +++++++++++ samples/BikeSharingApp/Billing/errors.go | 32 ++ .../BikeSharingApp/Billing/httphandlers.go | 469 +++++++++++++++++ samples/BikeSharingApp/Billing/invoice.go | 68 +++ samples/BikeSharingApp/Billing/logger.go | 46 ++ samples/BikeSharingApp/Billing/main.go | 169 ++++++ samples/BikeSharingApp/Billing/vendor.go | 56 ++ samples/BikeSharingApp/Databases/azds.yaml | 31 ++ .../Databases/charts/databases/.helmignore | 21 + .../Databases/charts/databases/Chart.yaml | 5 + .../charts/databases/templates/_helpers.tpl | 32 ++ .../charts/databases/templates/mongo.yaml | 25 + .../databases/templates/mongoService.yaml | 17 + .../charts/databases/templates/sql.yaml | 33 ++ .../charts/databases/templates/sqlSecret.yaml | 14 + .../databases/templates/sqlService.yaml | 17 + .../Databases/charts/databases/values.yaml | 9 + samples/BikeSharingApp/Gateway/.dockerignore | 14 + samples/BikeSharingApp/Gateway/.gitignore | 255 +++++++++ samples/BikeSharingApp/Gateway/Constants.cs | 22 + .../Gateway/Controllers/BikeController.cs | 184 +++++++ .../Gateway/Controllers/BillingController.cs | 140 +++++ .../Gateway/Controllers/HelloController.cs | 18 + .../Controllers/ReservationController.cs | 266 ++++++++++ .../Gateway/Controllers/UserController.cs | 489 ++++++++++++++++++ .../Gateway/CustomConfiguration.cs | 24 + samples/BikeSharingApp/Gateway/Dockerfile | 18 + .../BikeSharingApp/Gateway/Dockerfile.develop | 15 + .../ReservationNotFoundException.cs | 14 + .../ReservationRequestFailedException.cs | 16 + .../Gateway/HttpClientExtensions.cs | 18 + samples/BikeSharingApp/Gateway/HttpHelper.cs | 78 +++ .../Gateway/Logging/LogUtility.cs | 33 ++ .../Middleware/OperationContextMiddleware.cs | 29 ++ .../Middleware/RequestLoggingMiddleware.cs | 49 ++ .../Models/Bikes/AddUpdateBikeRequest.cs | 43 ++ .../Gateway/Models/Bikes/Bike.cs | 40 ++ .../Gateway/Models/Billing/Customer.cs | 14 + .../Gateway/Models/Billing/ICustomer.cs | 25 + .../Gateway/Models/Billing/IVendor.cs | 22 + .../Gateway/Models/Billing/Invoice.cs | 28 + .../Gateway/Models/Billing/Vendor.cs | 13 + samples/BikeSharingApp/Gateway/Models/Job.cs | 13 + .../Models/Reservations/Reservation.cs | 40 ++ .../Models/Reservations/ReservationState.cs | 14 + .../Models/Users/CreateCustomerRequest.cs | 19 + .../Models/Users/CreateVendorRequest.cs | 18 + .../Gateway/Models/Users/IUser.cs | 43 ++ .../Gateway/Models/Users/User.cs | 15 + .../Gateway/Models/Users/UserResponse.cs | 15 + .../Gateway/OperationContext.cs | 33 ++ samples/BikeSharingApp/Gateway/Program.cs | 18 + .../Gateway/Properties/launchSettings.json | 22 + samples/BikeSharingApp/Gateway/Startup.cs | 72 +++ samples/BikeSharingApp/Gateway/app.csproj | 23 + .../Gateway/appsettings.Development.json | 19 + .../BikeSharingApp/Gateway/appsettings.json | 18 + samples/BikeSharingApp/Gateway/azds.yaml | 41 ++ .../Gateway/charts/gateway/.helmignore | 21 + .../Gateway/charts/gateway/Chart.yaml | 5 + .../charts/gateway/templates/NOTES.txt | 19 + .../charts/gateway/templates/_helpers.tpl | 32 ++ .../charts/gateway/templates/deployment.yaml | 72 +++ .../charts/gateway/templates/ingress.yaml | 39 ++ .../charts/gateway/templates/secrets.yaml | 12 + .../charts/gateway/templates/service.yaml | 19 + .../Gateway/charts/gateway/values.yaml | 72 +++ .../PopulateDatabase/.dockerignore | 14 + .../PopulateDatabase/Dockerfile | 18 + .../PopulateDatabase/Dockerfile.develop | 15 + .../PopulateDatabase/Program.cs | 133 +++++ .../PopulateDatabase/app.csproj | 30 ++ .../PopulateDatabase/appsettings.json | 7 + .../BikeSharingApp/PopulateDatabase/azds.yaml | 40 ++ .../charts/populatedatabase/.helmignore | 21 + .../charts/populatedatabase/Chart.yaml | 5 + .../populatedatabase/templates/NOTES.txt | 19 + .../populatedatabase/templates/_helpers.tpl | 32 ++ .../populatedatabase/templates/ingress.yaml | 39 ++ .../populatedatabase/templates/job.yaml | 24 + .../populatedatabase/templates/secrets.yaml | 12 + .../populatedatabase/templates/service.yaml | 19 + .../charts/populatedatabase/values.yaml | 74 +++ .../BikeSharingApp/PopulateDatabase/data.json | 145 ++++++ samples/BikeSharingApp/README.md | 135 +++++ samples/BikeSharingApp/Reservation/.gitignore | 24 + samples/BikeSharingApp/Reservation/Dockerfile | 16 + .../Reservation/app/connectionConfig.go | 10 + .../Reservation/app/handlers.go | 93 ++++ .../BikeSharingApp/Reservation/app/logger.go | 24 + .../BikeSharingApp/Reservation/app/main.go | 103 ++++ .../Reservation/app/mongohelper.go | 125 +++++ .../Reservation/app/reservationdetails.go | 55 ++ samples/BikeSharingApp/Reservation/azds.yaml | 26 + .../charts/reservation/.helmignore | 21 + .../Reservation/charts/reservation/Chart.yaml | 5 + .../charts/reservation/templates/NOTES.txt | 19 + .../charts/reservation/templates/_helpers.tpl | 32 ++ .../reservation/templates/deployment.yaml | 72 +++ .../charts/reservation/templates/ingress.yaml | 39 ++ .../charts/reservation/templates/secrets.yaml | 12 + .../charts/reservation/templates/service.yaml | 19 + .../charts/reservation/values.yaml | 66 +++ .../ReservationEngine/.dockerignore | 14 + .../ReservationEngine/.gitignore | 254 +++++++++ .../ReservationEngine/BikesHelper.cs | 54 ++ .../ReservationEngine/BillingHelper.cs | 69 +++ .../ReservationEngine/Constants.cs | 22 + .../Controllers/HelloController.cs | 18 + .../ReservationEngineController.cs | 130 +++++ .../ReservationEngine/CustomConfiguration.cs | 29 ++ .../ReservationEngine/Dockerfile | 18 + .../ReservationEngine/Dockerfile.develop | 15 + .../ReservationEngine/HttpHelper.cs | 64 +++ .../ReservationEngine/LogUtility.cs | 36 ++ .../ReservationEngine/Models/Bikes/Bike.cs | 34 ++ .../Models/Billing/Invoice.cs | 25 + .../Models/Billing/ReservationStatus.cs | 14 + .../Models/Reservations/Reservation.cs | 40 ++ .../Models/Reservations/ReservationState.cs | 14 + .../ReservationEngine/MongoHelper.cs | 77 +++ .../ReservationEngine/Program.cs | 34 ++ .../ReservationEngine/Startup.cs | 43 ++ .../ReservationEngine/app.csproj | 27 + .../ReservationEngine/appsettings.json | 13 + .../ReservationEngine/azds.yaml | 40 ++ .../charts/reservationengine/.helmignore | 21 + .../charts/reservationengine/Chart.yaml | 5 + .../reservationengine/templates/NOTES.txt | 19 + .../reservationengine/templates/_helpers.tpl | 32 ++ .../templates/deployment.yaml | 72 +++ .../reservationengine/templates/ingress.yaml | 39 ++ .../reservationengine/templates/secrets.yaml | 12 + .../reservationengine/templates/service.yaml | 19 + .../charts/reservationengine/values.yaml | 70 +++ samples/BikeSharingApp/Users/.dockerignore | 11 + samples/BikeSharingApp/Users/.gitignore | 37 ++ .../Users/ConnectionConfig.json | 20 + samples/BikeSharingApp/Users/Dockerfile | 10 + samples/BikeSharingApp/Users/azds.yaml | 31 ++ .../Users/charts/users/.helmignore | 21 + .../Users/charts/users/Chart.yaml | 5 + .../Users/charts/users/templates/NOTES.txt | 19 + .../Users/charts/users/templates/_helpers.tpl | 32 ++ .../charts/users/templates/deployment.yaml | 72 +++ .../Users/charts/users/templates/ingress.yaml | 39 ++ .../Users/charts/users/templates/secrets.yaml | 12 + .../Users/charts/users/templates/service.yaml | 19 + .../Users/charts/users/values.yaml | 68 +++ samples/BikeSharingApp/Users/package.json | 33 ++ samples/BikeSharingApp/Users/server.js | 264 ++++++++++ samples/BikeSharingApp/charts/.gitignore | 2 + samples/BikeSharingApp/charts/.helmignore | 21 + samples/BikeSharingApp/charts/Chart.yaml | 5 + .../BikeSharingApp/charts/requirements.yaml | 28 + samples/BikeSharingApp/charts/values.yaml | 16 + 226 files changed, 11156 insertions(+) create mode 100644 samples/BikeSharingApp/.gitattributes create mode 100644 samples/BikeSharingApp/.gitignore create mode 100644 samples/BikeSharingApp/BikeSharingWeb/.dockerignore create mode 100644 samples/BikeSharingApp/BikeSharingWeb/.gitignore create mode 100644 samples/BikeSharingApp/BikeSharingWeb/Dockerfile create mode 100644 samples/BikeSharingApp/BikeSharingWeb/Dockerfile.static create mode 100644 samples/BikeSharingApp/BikeSharingWeb/azds.yaml create mode 100644 samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/.helmignore create mode 100644 samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/Chart.yaml create mode 100644 samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/NOTES.txt create mode 100644 samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/_helpers.tpl create mode 100644 samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/deployment.yaml create mode 100644 samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/ingress.yaml create mode 100644 samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/secrets.yaml create mode 100644 samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/service.yaml create mode 100644 samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/values.yaml create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/BikeCard.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/Content.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/ErrorPanel.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/Field.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/Footer.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/FormButton.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/FormNote.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/FormTextbox.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/Header.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/Logo.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/Map.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/Page.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/ReviewControl.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/components/SigninFormLayout.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/package.json create mode 100644 samples/BikeSharingApp/BikeSharingWeb/pages/complete-return.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/pages/current-ride.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/pages/devsignin.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/pages/helpers.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/pages/index.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/pages/preview.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/pages/review.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/server.js create mode 100644 samples/BikeSharingApp/BikeSharingWeb/static/awc-title.svg create mode 100644 samples/BikeSharingApp/BikeSharingWeb/static/logo.svg create mode 100644 samples/BikeSharingApp/BikeSharingWeb/static/sample-map.png create mode 100644 samples/BikeSharingApp/Bikes/.dockerignore create mode 100644 samples/BikeSharingApp/Bikes/.gitignore create mode 100644 samples/BikeSharingApp/Bikes/Dockerfile create mode 100644 samples/BikeSharingApp/Bikes/azds.yaml create mode 100644 samples/BikeSharingApp/Bikes/charts/bikes/.helmignore create mode 100644 samples/BikeSharingApp/Bikes/charts/bikes/Chart.yaml create mode 100644 samples/BikeSharingApp/Bikes/charts/bikes/templates/NOTES.txt create mode 100644 samples/BikeSharingApp/Bikes/charts/bikes/templates/_helpers.tpl create mode 100644 samples/BikeSharingApp/Bikes/charts/bikes/templates/deployment.yaml create mode 100644 samples/BikeSharingApp/Bikes/charts/bikes/templates/ingress.yaml create mode 100644 samples/BikeSharingApp/Bikes/charts/bikes/templates/secrets.yaml create mode 100644 samples/BikeSharingApp/Bikes/charts/bikes/templates/service.yaml create mode 100644 samples/BikeSharingApp/Bikes/charts/bikes/values.yaml create mode 100644 samples/BikeSharingApp/Bikes/package.json create mode 100644 samples/BikeSharingApp/Bikes/server.js create mode 100644 samples/BikeSharingApp/Billing/.gitignore create mode 100644 samples/BikeSharingApp/Billing/Dockerfile create mode 100644 samples/BikeSharingApp/Billing/azds.yaml create mode 100644 samples/BikeSharingApp/Billing/charts/billing/.helmignore create mode 100644 samples/BikeSharingApp/Billing/charts/billing/Chart.yaml create mode 100644 samples/BikeSharingApp/Billing/charts/billing/templates/NOTES.txt create mode 100644 samples/BikeSharingApp/Billing/charts/billing/templates/_helpers.tpl create mode 100644 samples/BikeSharingApp/Billing/charts/billing/templates/deployment.yaml create mode 100644 samples/BikeSharingApp/Billing/charts/billing/templates/ingress.yaml create mode 100644 samples/BikeSharingApp/Billing/charts/billing/templates/secrets.yaml create mode 100644 samples/BikeSharingApp/Billing/charts/billing/templates/service.yaml create mode 100644 samples/BikeSharingApp/Billing/charts/billing/values.yaml create mode 100644 samples/BikeSharingApp/Billing/customer.go create mode 100644 samples/BikeSharingApp/Billing/db.go create mode 100644 samples/BikeSharingApp/Billing/errors.go create mode 100644 samples/BikeSharingApp/Billing/httphandlers.go create mode 100644 samples/BikeSharingApp/Billing/invoice.go create mode 100644 samples/BikeSharingApp/Billing/logger.go create mode 100644 samples/BikeSharingApp/Billing/main.go create mode 100644 samples/BikeSharingApp/Billing/vendor.go create mode 100644 samples/BikeSharingApp/Databases/azds.yaml create mode 100644 samples/BikeSharingApp/Databases/charts/databases/.helmignore create mode 100644 samples/BikeSharingApp/Databases/charts/databases/Chart.yaml create mode 100644 samples/BikeSharingApp/Databases/charts/databases/templates/_helpers.tpl create mode 100644 samples/BikeSharingApp/Databases/charts/databases/templates/mongo.yaml create mode 100644 samples/BikeSharingApp/Databases/charts/databases/templates/mongoService.yaml create mode 100644 samples/BikeSharingApp/Databases/charts/databases/templates/sql.yaml create mode 100644 samples/BikeSharingApp/Databases/charts/databases/templates/sqlSecret.yaml create mode 100644 samples/BikeSharingApp/Databases/charts/databases/templates/sqlService.yaml create mode 100644 samples/BikeSharingApp/Databases/charts/databases/values.yaml create mode 100644 samples/BikeSharingApp/Gateway/.dockerignore create mode 100644 samples/BikeSharingApp/Gateway/.gitignore create mode 100644 samples/BikeSharingApp/Gateway/Constants.cs create mode 100644 samples/BikeSharingApp/Gateway/Controllers/BikeController.cs create mode 100644 samples/BikeSharingApp/Gateway/Controllers/BillingController.cs create mode 100644 samples/BikeSharingApp/Gateway/Controllers/HelloController.cs create mode 100644 samples/BikeSharingApp/Gateway/Controllers/ReservationController.cs create mode 100644 samples/BikeSharingApp/Gateway/Controllers/UserController.cs create mode 100644 samples/BikeSharingApp/Gateway/CustomConfiguration.cs create mode 100644 samples/BikeSharingApp/Gateway/Dockerfile create mode 100644 samples/BikeSharingApp/Gateway/Dockerfile.develop create mode 100644 samples/BikeSharingApp/Gateway/Exceptions/ReservationNotFoundException.cs create mode 100644 samples/BikeSharingApp/Gateway/Exceptions/ReservationRequestFailedException.cs create mode 100644 samples/BikeSharingApp/Gateway/HttpClientExtensions.cs create mode 100644 samples/BikeSharingApp/Gateway/HttpHelper.cs create mode 100644 samples/BikeSharingApp/Gateway/Logging/LogUtility.cs create mode 100644 samples/BikeSharingApp/Gateway/Middleware/OperationContextMiddleware.cs create mode 100644 samples/BikeSharingApp/Gateway/Middleware/RequestLoggingMiddleware.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Bikes/AddUpdateBikeRequest.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Bikes/Bike.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Billing/Customer.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Billing/ICustomer.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Billing/IVendor.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Billing/Invoice.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Billing/Vendor.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Job.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Reservations/Reservation.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Reservations/ReservationState.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Users/CreateCustomerRequest.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Users/CreateVendorRequest.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Users/IUser.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Users/User.cs create mode 100644 samples/BikeSharingApp/Gateway/Models/Users/UserResponse.cs create mode 100644 samples/BikeSharingApp/Gateway/OperationContext.cs create mode 100644 samples/BikeSharingApp/Gateway/Program.cs create mode 100644 samples/BikeSharingApp/Gateway/Properties/launchSettings.json create mode 100644 samples/BikeSharingApp/Gateway/Startup.cs create mode 100644 samples/BikeSharingApp/Gateway/app.csproj create mode 100644 samples/BikeSharingApp/Gateway/appsettings.Development.json create mode 100644 samples/BikeSharingApp/Gateway/appsettings.json create mode 100644 samples/BikeSharingApp/Gateway/azds.yaml create mode 100644 samples/BikeSharingApp/Gateway/charts/gateway/.helmignore create mode 100644 samples/BikeSharingApp/Gateway/charts/gateway/Chart.yaml create mode 100644 samples/BikeSharingApp/Gateway/charts/gateway/templates/NOTES.txt create mode 100644 samples/BikeSharingApp/Gateway/charts/gateway/templates/_helpers.tpl create mode 100644 samples/BikeSharingApp/Gateway/charts/gateway/templates/deployment.yaml create mode 100644 samples/BikeSharingApp/Gateway/charts/gateway/templates/ingress.yaml create mode 100644 samples/BikeSharingApp/Gateway/charts/gateway/templates/secrets.yaml create mode 100644 samples/BikeSharingApp/Gateway/charts/gateway/templates/service.yaml create mode 100644 samples/BikeSharingApp/Gateway/charts/gateway/values.yaml create mode 100644 samples/BikeSharingApp/PopulateDatabase/.dockerignore create mode 100644 samples/BikeSharingApp/PopulateDatabase/Dockerfile create mode 100644 samples/BikeSharingApp/PopulateDatabase/Dockerfile.develop create mode 100644 samples/BikeSharingApp/PopulateDatabase/Program.cs create mode 100644 samples/BikeSharingApp/PopulateDatabase/app.csproj create mode 100644 samples/BikeSharingApp/PopulateDatabase/appsettings.json create mode 100644 samples/BikeSharingApp/PopulateDatabase/azds.yaml create mode 100644 samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/.helmignore create mode 100644 samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/Chart.yaml create mode 100644 samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/NOTES.txt create mode 100644 samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/_helpers.tpl create mode 100644 samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/ingress.yaml create mode 100644 samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/job.yaml create mode 100644 samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/secrets.yaml create mode 100644 samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/service.yaml create mode 100644 samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/values.yaml create mode 100644 samples/BikeSharingApp/PopulateDatabase/data.json create mode 100644 samples/BikeSharingApp/README.md create mode 100644 samples/BikeSharingApp/Reservation/.gitignore create mode 100644 samples/BikeSharingApp/Reservation/Dockerfile create mode 100644 samples/BikeSharingApp/Reservation/app/connectionConfig.go create mode 100644 samples/BikeSharingApp/Reservation/app/handlers.go create mode 100644 samples/BikeSharingApp/Reservation/app/logger.go create mode 100644 samples/BikeSharingApp/Reservation/app/main.go create mode 100644 samples/BikeSharingApp/Reservation/app/mongohelper.go create mode 100644 samples/BikeSharingApp/Reservation/app/reservationdetails.go create mode 100644 samples/BikeSharingApp/Reservation/azds.yaml create mode 100644 samples/BikeSharingApp/Reservation/charts/reservation/.helmignore create mode 100644 samples/BikeSharingApp/Reservation/charts/reservation/Chart.yaml create mode 100644 samples/BikeSharingApp/Reservation/charts/reservation/templates/NOTES.txt create mode 100644 samples/BikeSharingApp/Reservation/charts/reservation/templates/_helpers.tpl create mode 100644 samples/BikeSharingApp/Reservation/charts/reservation/templates/deployment.yaml create mode 100644 samples/BikeSharingApp/Reservation/charts/reservation/templates/ingress.yaml create mode 100644 samples/BikeSharingApp/Reservation/charts/reservation/templates/secrets.yaml create mode 100644 samples/BikeSharingApp/Reservation/charts/reservation/templates/service.yaml create mode 100644 samples/BikeSharingApp/Reservation/charts/reservation/values.yaml create mode 100644 samples/BikeSharingApp/ReservationEngine/.dockerignore create mode 100644 samples/BikeSharingApp/ReservationEngine/.gitignore create mode 100644 samples/BikeSharingApp/ReservationEngine/BikesHelper.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/BillingHelper.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/Constants.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/Controllers/HelloController.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/Controllers/ReservationEngineController.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/CustomConfiguration.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/Dockerfile create mode 100644 samples/BikeSharingApp/ReservationEngine/Dockerfile.develop create mode 100644 samples/BikeSharingApp/ReservationEngine/HttpHelper.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/LogUtility.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/Models/Bikes/Bike.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/Models/Billing/Invoice.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/Models/Billing/ReservationStatus.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/Models/Reservations/Reservation.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/Models/Reservations/ReservationState.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/MongoHelper.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/Program.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/Startup.cs create mode 100644 samples/BikeSharingApp/ReservationEngine/app.csproj create mode 100644 samples/BikeSharingApp/ReservationEngine/appsettings.json create mode 100644 samples/BikeSharingApp/ReservationEngine/azds.yaml create mode 100644 samples/BikeSharingApp/ReservationEngine/charts/reservationengine/.helmignore create mode 100644 samples/BikeSharingApp/ReservationEngine/charts/reservationengine/Chart.yaml create mode 100644 samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/NOTES.txt create mode 100644 samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/_helpers.tpl create mode 100644 samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/deployment.yaml create mode 100644 samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/ingress.yaml create mode 100644 samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/secrets.yaml create mode 100644 samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/service.yaml create mode 100644 samples/BikeSharingApp/ReservationEngine/charts/reservationengine/values.yaml create mode 100644 samples/BikeSharingApp/Users/.dockerignore create mode 100644 samples/BikeSharingApp/Users/.gitignore create mode 100644 samples/BikeSharingApp/Users/ConnectionConfig.json create mode 100644 samples/BikeSharingApp/Users/Dockerfile create mode 100644 samples/BikeSharingApp/Users/azds.yaml create mode 100644 samples/BikeSharingApp/Users/charts/users/.helmignore create mode 100644 samples/BikeSharingApp/Users/charts/users/Chart.yaml create mode 100644 samples/BikeSharingApp/Users/charts/users/templates/NOTES.txt create mode 100644 samples/BikeSharingApp/Users/charts/users/templates/_helpers.tpl create mode 100644 samples/BikeSharingApp/Users/charts/users/templates/deployment.yaml create mode 100644 samples/BikeSharingApp/Users/charts/users/templates/ingress.yaml create mode 100644 samples/BikeSharingApp/Users/charts/users/templates/secrets.yaml create mode 100644 samples/BikeSharingApp/Users/charts/users/templates/service.yaml create mode 100644 samples/BikeSharingApp/Users/charts/users/values.yaml create mode 100644 samples/BikeSharingApp/Users/package.json create mode 100644 samples/BikeSharingApp/Users/server.js create mode 100644 samples/BikeSharingApp/charts/.gitignore create mode 100644 samples/BikeSharingApp/charts/.helmignore create mode 100644 samples/BikeSharingApp/charts/Chart.yaml create mode 100644 samples/BikeSharingApp/charts/requirements.yaml create mode 100644 samples/BikeSharingApp/charts/values.yaml diff --git a/samples/BikeSharingApp/.gitattributes b/samples/BikeSharingApp/.gitattributes new file mode 100644 index 000000000..103045ba5 --- /dev/null +++ b/samples/BikeSharingApp/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf \ No newline at end of file diff --git a/samples/BikeSharingApp/.gitignore b/samples/BikeSharingApp/.gitignore new file mode 100644 index 000000000..bd8c69f02 --- /dev/null +++ b/samples/BikeSharingApp/.gitignore @@ -0,0 +1,258 @@ +contoso-bikerental/charts/* + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +*.sln + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs +.vscode +package-lock.json + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml diff --git a/samples/BikeSharingApp/BikeSharingWeb/.dockerignore b/samples/BikeSharingApp/BikeSharingWeb/.dockerignore new file mode 100644 index 000000000..1a764bc71 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/.dockerignore @@ -0,0 +1,11 @@ +.dockerignore +.git +.gitignore +.vs +.vscode +azds.yaml +charts +Dockerfile +node_modules +secrets.dev.yaml +values.dev.yaml \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/.gitignore b/samples/BikeSharingApp/BikeSharingWeb/.gitignore new file mode 100644 index 000000000..1af6290bc --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/.gitignore @@ -0,0 +1,64 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next +.next +*.log +html \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/Dockerfile b/samples/BikeSharingApp/BikeSharingWeb/Dockerfile new file mode 100644 index 000000000..6bbe856f9 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/Dockerfile @@ -0,0 +1,10 @@ +FROM node:lts +ENV PORT 80 +EXPOSE 80 + +WORKDIR /app +COPY package.json . +RUN npm install +COPY . . + +CMD ["npm", "start"] \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/Dockerfile.static b/samples/BikeSharingApp/BikeSharingWeb/Dockerfile.static new file mode 100644 index 000000000..345203502 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/Dockerfile.static @@ -0,0 +1,3 @@ +FROM nginx +EXPOSE 80 +COPY html/ /usr/share/nginx/html \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/azds.yaml b/samples/BikeSharingApp/BikeSharingWeb/azds.yaml new file mode 100644 index 000000000..6c4c0272b --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/azds.yaml @@ -0,0 +1,35 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: . + dockerfile: Dockerfile +install: + chart: charts/bikesharingweb + values: + - values.dev.yaml? + - secrets.dev.yaml? + set: + replicaCount: 1 + image: + repository: bikesharingweb + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + # This expands to [space.s.][rootSpace.]bikesharingweb...azds.io + - $(spacePrefix)$(rootSpacePrefix)bikesharingweb$(hostSuffix) +configurations: + develop: + build: + useGitIgnore: true + container: + sync: + - "!**/package.json" + command: + - npm + - run + - dev + iterate: + processesToKill: [node] \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/.helmignore b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/Chart.yaml b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/Chart.yaml new file mode 100644 index 000000000..3f49eb6ab --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: bikesharingweb +version: 0.1.0 diff --git a/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/NOTES.txt b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/NOTES.txt new file mode 100644 index 000000000..12bb29667 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "bikesharingweb.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "bikesharingweb.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "bikesharingweb.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "bikesharingweb.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/_helpers.tpl b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/_helpers.tpl new file mode 100644 index 000000000..0eb183704 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "bikesharingweb.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bikesharingweb.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bikesharingweb.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/deployment.yaml b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/deployment.yaml new file mode 100644 index 000000000..8b7b0d094 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: {{ template "bikesharingweb.fullname" . }} + labels: + app: {{ template "bikesharingweb.name" . }} + chart: {{ template "bikesharingweb.chart" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "bikesharingweb.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "bikesharingweb.name" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + annotations: + buildID: {{ .Values.buildID }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + {{- if .Values.probes.enabled }} + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + {{- end }} + env: + - name: API_NAME + value: {{ .Values.apiName | quote }} + {{- $root := . }} + {{- range $ref, $values := .Values.secrets }} + {{- range $key, $value := $values }} + - name: {{ $ref }}_{{ $key }} + valueFrom: + secretKeyRef: + name: {{ template "bikesharingweb.fullname" $root }}-{{ $ref | lower }} + key: {{ $key }} + {{- end }} + {{- end }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/ingress.yaml b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/ingress.yaml new file mode 100644 index 000000000..88c0d3c22 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "bikesharingweb.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "bikesharingweb.name" . }} + chart: {{ template "bikesharingweb.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/secrets.yaml b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/secrets.yaml new file mode 100644 index 000000000..aae2523b5 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/secrets.yaml @@ -0,0 +1,12 @@ +{{- $root := . }} +{{- range $name, $values := .Values.secrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "bikesharingweb.fullname" $root }}-{{ $name | lower }} +data: + {{- range $key, $value := $values }} + {{ $key }}: {{ $value | b64enc }} + {{- end }} +--- +{{- end }} diff --git a/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/service.yaml b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/service.yaml new file mode 100644 index 000000000..eebb6b256 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "bikesharingweb.fullname" . }} + labels: + app: {{ template "bikesharingweb.name" . }} + chart: {{ template "bikesharingweb.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app: {{ template "bikesharingweb.name" . }} + release: {{ .Release.Name }} diff --git a/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/values.yaml b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/values.yaml new file mode 100644 index 000000000..22a9455ad --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/charts/bikesharingweb/values.yaml @@ -0,0 +1,65 @@ +# Default values for bikesharingweb. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +apiName: gateway + +fullnameOverride: bikesharingweb +replicaCount: 1 +image: + repository: azdspublic/bikesharing-bikesharingweb + tag: build.20190418.2 + pullPolicy: IfNotPresent +imagePullSecrets: [] + # Optionally specify an array of imagePullSecrets. + # Secrets must be manually created in the namespace. + # ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod + # + # This uses credentials from secret "myRegistryKeySecretName". + # - name: myRegistryKeySecretName +service: + type: ClusterIP + port: 80 + +probes: + enabled: false + +ingress: + enabled: true + annotations: + kubernetes.io/ingress.class: addon-http-application-routing + path: / + hosts: + - dev.bikesharingweb. + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local +secrets: {} + # Optionally specify a set of secret objects whose values + # will be injected as environment variables by default. + # You should add this section to a file like secrets.yaml + # that is explicitly NOT committed to source code control + # and then include it as part of your helm install step. + # ref: https://kubernetes.io/docs/concepts/configuration/secret/ + # + # This creates a secret "mysecret" and injects "mypassword" + # as the environment variable mysecret_mypassword=password. + # mysecret: + # mypassword: password +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi +nodeSelector: {} + +tolerations: [] + +affinity: {} \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/BikeCard.js b/samples/BikeSharingApp/BikeSharingWeb/components/BikeCard.js new file mode 100644 index 000000000..2e4fe86d6 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/BikeCard.js @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const BikeCard = (props) => ( +
+
+ photo of bike +
+
{props.name}
+
{props.address}
+
${props.rate}/hour
+
+
+ +
+) + +export default BikeCard \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/Content.js b/samples/BikeSharingApp/BikeSharingWeb/components/Content.js new file mode 100644 index 000000000..48e8f4403 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/Content.js @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const Content = (props) => ( +
+ {props.children} +



+ +
+) + +export default Content \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/ErrorPanel.js b/samples/BikeSharingApp/BikeSharingWeb/components/ErrorPanel.js new file mode 100644 index 000000000..2f37468b8 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/ErrorPanel.js @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import React, { Component } from 'react' + +export default class ErrorPanel extends Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
+ {this.props.errorMessage != null && +
+
+
{this.props.errorMessage}
+
+ } + +
+ ) + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/Field.js b/samples/BikeSharingApp/BikeSharingWeb/components/Field.js new file mode 100644 index 000000000..85b2a2931 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/Field.js @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const Field = (props) => ( +
+
{props.label}
+
{props.value}
+ +
+) + +export default Field \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/Footer.js b/samples/BikeSharingApp/BikeSharingWeb/components/Footer.js new file mode 100644 index 000000000..cfc64456e --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/Footer.js @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const Footer = (props) => ( +
+
+ {props.children} +
+ +
+) + +export default Footer \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/FormButton.js b/samples/BikeSharingApp/BikeSharingWeb/components/FormButton.js new file mode 100644 index 000000000..e1f447354 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/FormButton.js @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Component } from 'react' + +export default class FormButton extends Component { + + constructor (props) { + super(props); + } + + render() { + return ( +
+ + +
+ ) + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/FormNote.js b/samples/BikeSharingApp/BikeSharingWeb/components/FormNote.js new file mode 100644 index 000000000..49e5bfe59 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/FormNote.js @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const FormNote = (props) => ( +
{props.text} + +
+) + +export default FormNote \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/FormTextbox.js b/samples/BikeSharingApp/BikeSharingWeb/components/FormTextbox.js new file mode 100644 index 000000000..cbe21d880 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/FormTextbox.js @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Component } from 'react' + +export default class FormTextbox extends Component { + + constructor (props) { + super(props); + } + + render() { + return ( +
+
+ + +
+ +
+ ) + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/Header.js b/samples/BikeSharingApp/BikeSharingWeb/components/Header.js new file mode 100644 index 000000000..47da38d86 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/Header.js @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import Link from 'next/link' + +const Header = (props) => ( +
+
+
+ + Adventure Works Cycles + +
+
+ {props.userName != null && + + Hi {props.userName} | Sign out + + } +
+ +
+) + +export default Header \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/Logo.js b/samples/BikeSharingApp/BikeSharingWeb/components/Logo.js new file mode 100644 index 000000000..7bdda0c54 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/Logo.js @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const Logo = (props) => ( +
+ Adventure Works Cycles + +
+) + +export default Logo \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/Map.js b/samples/BikeSharingApp/BikeSharingWeb/components/Map.js new file mode 100644 index 000000000..9e873829d --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/Map.js @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const Map = (props) => ( +
+ map of bike location + +
+) + +export default Map \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/Page.js b/samples/BikeSharingApp/BikeSharingWeb/components/Page.js new file mode 100644 index 000000000..89911cc80 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/Page.js @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import Head from 'next/head' + +const Page = (props) => ( +
+ + Adventure Works Cycles + + + + + + + + {props.children} + +
+) + +export default Page; \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/ReviewControl.js b/samples/BikeSharingApp/BikeSharingWeb/components/ReviewControl.js new file mode 100644 index 000000000..4911034ab --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/ReviewControl.js @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Component } from 'react' +import Rating from 'react-rating' + +export default class ReviewControl extends Component { + + constructor(props) { + super(props); + this.state = {value: 0}; + this.handleClick = this.handleClick.bind(this); + } + + handleClick(event) { + this.setState({value: event}); + } + + render() { + return ( +

+ } + fullSymbol={} + /> + +

+ ) + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/components/SigninFormLayout.js b/samples/BikeSharingApp/BikeSharingWeb/components/SigninFormLayout.js new file mode 100644 index 000000000..862cffbfb --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/components/SigninFormLayout.js @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const SigninFormLayout = (props) => ( +
+ {props.children} + +
+) + +export default SigninFormLayout \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/package.json b/samples/BikeSharingApp/BikeSharingWeb/package.json new file mode 100644 index 000000000..12167bb2b --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/package.json @@ -0,0 +1,35 @@ +{ + "name": "bikesharingweb", + "version": "1.0.0", + "description": "BikeSharing web app", + "main": "server.js", + "dependencies": { + "express": "^4.16.4", + "isomorphic-fetch": "^2.2.1", + "next": "^7.0.2", + "react": "^16.7.0", + "react-dom": "^16.7.0", + "react-rating": "^1.6.2", + "react-responsive": "^6.0.1", + "universal-cookie": "^3.0.7" + }, + "devDependencies": { + "nodemon": "^1.18.10" + }, + "scripts": { + "dev": "nodemon server.js -w server.js -w server -w universal -w next.config.js -w config.js -w package.json", + "export": "next export -o html", + "start": "next build && NODE_ENV=production node server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ContosoBikeRental/BikeSharingSampleApp.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/ContosoBikeRental/BikeSharingSampleApp/issues" + }, + "homepage": "https://github.com/ContosoBikeRental/BikeSharingSampleApp#readme" +} diff --git a/samples/BikeSharingApp/BikeSharingWeb/pages/complete-return.js b/samples/BikeSharingApp/BikeSharingWeb/pages/complete-return.js new file mode 100644 index 000000000..4c9e7e178 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/pages/complete-return.js @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import React, { Component } from 'react' +import Page from "../components/Page" +import Header from "../components/Header" +import Content from "../components/Content" +import Field from "../components/Field" +import FormNote from "../components/FormNote" +import FormButton from "../components/FormButton" +import Map from "../components/Map" +import Footer from '../components/Footer' +import MediaQuery from 'react-responsive' +import helpers from './helpers'; +import Router from 'next/router' +import ErrorPanel from '../components/ErrorPanel' + +export default class CompleteReturn extends Component { + + constructor(props) { + super(props); + this.state = { + userId: undefined, + userName: undefined, + reservation: {}, + bike: {}, + invoice: {}, + errorMessage: undefined + }; + } + + async componentDidMount() { + let user = null; + try { + this.apiHost = await helpers.getApiHostAsync(); + user = await helpers.verifyUserAsync(this.apiHost); + if (!user) { + Router.push('/devsignin'); + return; + } + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving current user's data. Make sure that your Gateway and Users services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + userId: user.id, + userName: user.name + }); + + let reservation = null; + try { + // get reservation + var state = "Completed"; + reservation = await helpers.getReservationForUserAsync(user.id, this.apiHost, state); + if (!reservation) { + // Error, something's gone wrong, go home + console.error("couldn't find " + state + " reservation, going to Index"); + Router.push("/"); + return; + } + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving current reservation's data. Make sure that your Gateway and Reservation services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + reservation: reservation + }); + + let bike = null; + try { + // get bike + bike = await helpers.getBikeAsync(reservation.bikeId, this.apiHost); + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving bike's data. Make sure that your Gateway and Bikes services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + bike: bike + }); + + let invoice = null; + try { + // get invoice + invoice = await helpers.getInvoiceAsync(reservation.invoiceId, this.apiHost); + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving invoice's data. Make sure that your Gateway and Billing services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + invoice: invoice + }); + } + + // handle return bike + async handleClick(context) { + // return bike + console.log("completing return..."); + + // navigate to review + Router.push("/review"); + } + + render() { + return ( + +
+ +
+ +
{this.state.bike.model ? `You're returning a ${this.state.bike.model}` : ``}
+ +
+
+ + +
+
+ +
+
+ +
+ Confirm return +
+
+
+ +
+ +
+ Confirm return +
+
+ + + ) + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/pages/current-ride.js b/samples/BikeSharingApp/BikeSharingWeb/pages/current-ride.js new file mode 100644 index 000000000..aeeca6848 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/pages/current-ride.js @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Component } from 'react' +import Page from "../components/Page" +import Header from "../components/Header" +import Content from "../components/Content" +import Field from "../components/Field" +import FormNote from "../components/FormNote" +import FormButton from "../components/FormButton" +import Map from "../components/Map" +import Footer from '../components/Footer' +import { withRouter } from 'next/router' +import fetch from 'isomorphic-fetch' +import MediaQuery from 'react-responsive' +import Router from 'next/router' +import helpers from './helpers.js' +import ErrorPanel from '../components/ErrorPanel' + +class CurrentRideBase extends Component { + + constructor(props) { + super(props); + this.state = { + userId: undefined, + userName: undefined, + reservation: {}, + bike: {}, + vendor: {}, + errorMessage: undefined + }; + } + + async componentDidMount() { + let user = null; + try { + this.apiHost = await helpers.getApiHostAsync(); + user = await helpers.verifyUserAsync(this.apiHost); + if (!user) { + Router.push('/devsignin'); + return; + } + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving current user's data. Make sure that your Gateway and Users services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + userId: user.id, + userName: user.name + }); + + let reservation = null; + try { + // get reservation + var state = "Booked"; + reservation = await helpers.getReservationForUserAsync(user.id, this.apiHost, state); + if (!reservation) { + // Error, something's gone wrong, go home + console.error("couldn't find " + state + " reservation, going to Index"); + Router.push("/"); + return; + } + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving current reservation's data. Make sure that your Gateway and Reservation services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + reservation: reservation + }); + + let bike = null; + try { + // get bike + bike = await helpers.getBikeAsync(reservation.bikeId, this.apiHost); + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving bike's data. Make sure that your Gateway and Bikes services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + bike: bike + }); + + let vendor = null; + try { + // get vendor + vendor = await helpers.getVendorAsync(bike.ownerUserId, this.apiHost); + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving bike's vendor's data. Make sure that your Gateway and Users services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + vendor: vendor + }); + } + + // handle return bike + async handleClick(context) { + // return bike + console.log("returning bike..."); + try { + var url = this.apiHost + '/api/reservation/' + this.state.reservation.reservationId; + const res = await fetch(url, { + method: 'POST', + cache: 'no-cache', + headers: { + "Content-Type": "application/json; charset=utf-8" + } + }); + + if (!res.ok) { + throw new Error(await res.text()); + } + + // return confirmation + const data = await res.json(); + console.log(data); + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while creating a reservation for the bike. Make sure that your Gateway and ReservationEngine services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + // navigate to complete-return + Router.push("/complete-return"); + } + + render() { + return ( + +
+ +
+
+ {this.state.bike.imageUrl != null && + photo of bike + } +
+
+
+ {this.state.bike.model != null && +
You've rented a {this.state.bike.model}
+ } + {this.state.vendor.name != null && +
Owned by {this.state.vendor.name}
+ } +
+
+ + +
+
+ + +
+ Return bike +
+
+
+
+
+ +
+
+ +
+ +
+ Return bike +
+
+ + + ) + } +} + +const CurrentRide = withRouter(CurrentRideBase); + +export default CurrentRide \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/pages/devsignin.js b/samples/BikeSharingApp/BikeSharingWeb/pages/devsignin.js new file mode 100644 index 000000000..10b595164 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/pages/devsignin.js @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Component } from 'react' +import Page from "../components/Page" +import Content from "../components/Content" +import SigninFormLayout from '../components/SigninFormLayout' +import Logo from '../components/Logo' +import FormButton from '../components/FormButton' +import Router from 'next/router' +import helpers from './helpers'; +import ErrorPanel from '../components/ErrorPanel' + +export default class DevSignin extends Component { + + constructor(props) { + super(props); + this.state = { + users: [], + errorMessage: undefined + }; + } + + async componentDidMount() { + try { + // Clears any login information the user may still have. + helpers.clearUserCookie(); + + // Retrieves all users that can be selected for sign-in. + this.apiHost = await helpers.getApiHostAsync(); + const usersResponse = await fetch(`${this.apiHost}/api/user/allUsers`); + let users = await usersResponse.json(); + console.log("Users retrieved", users); + + // Filtering out vendors, as we don't provide any vendors experience for now. + users = users.filter(user => user.type != "vendor"); + + if (users.length == 0) { + this.setState({errorMessage: `No users have been retrieved from the database. Make sure that your PopulateDatabase job ran successfully.`}); + return; + } + + this.setState({users: users}); + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving users to select. Make sure that your Gateway and Users services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + } + + async handleClick(context) { + const userId = arguments[0]; + const userName = arguments[1]; + console.log(`User selected: ${userName} - ${userId}`); + helpers.storeUserCookie(userId); + + // Navigate to index. + Router.push("/"); + } + + render() { + return ( + + + + +

+ {this.state.users.length > 0 && +
+

Select a test user:

+ {this.state.users.map((user, index) => ( + {user.name} ({user.type}) + ))} +
+ } +
+ +
+ +
+ ); + } +} diff --git a/samples/BikeSharingApp/BikeSharingWeb/pages/helpers.js b/samples/BikeSharingApp/BikeSharingWeb/pages/helpers.js new file mode 100644 index 000000000..2b4d78fdb --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/pages/helpers.js @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import Cookies from 'universal-cookie' + +const helpers = { + getApiHostAsync: async function() { + const apiRequest = await fetch('/api/host'); + const data = await apiRequest.json(); + console.log('apiHost: ' + data.apiHost); + return data.apiHost; + }, + verifyUserAsync: async function(apiHost) { + var cookies = new Cookies(); + var user = cookies.get('user'); + if (!user || !user.id) { + return null; + } + + if (!apiHost) { + apiHost = await this.getApiHostAsync(); + } + const url = apiHost + '/api/user/' + user.id; + const userResponse = await fetch(url); + if (userResponse.ok) { + return await userResponse.json(); + } + + // User stored locally isn't valid anymore. Let's clear the local data. + this.clearUserCookie(); + return null; + }, + storeUserCookie: function(userId) { + const cookies = new Cookies(); + cookies.set('user', { + id: userId + }, { + path: "/" + }); + }, + clearUserCookie: function() { + const cookies = new Cookies(); + cookies.remove('user'); + }, + getVendorAsync: async function(ownerUserId, apiHost) { + if (!apiHost) { + apiHost = await this.getApiHostAsync(); + } + const url = apiHost + '/api/user/' + ownerUserId; + const res = await fetch(url); + if (!res.ok) { + throw new Error(await res.text()); + } + return await res.json(); + }, + getBikeAsync: async function(bikeId, apiHost) { + if (!apiHost) { + apiHost = await this.getApiHostAsync(); + } + const url = apiHost + '/api/bike/' + bikeId; + const res = await fetch(url); + if (!res.ok) { + throw new Error(await res.text()); + } + return await res.json(); + }, + getReservationForUserAsync: async function(userId, apiHost, state) { + if (!apiHost) { + apiHost = await this.getApiHostAsync(); + } + const url = apiHost + '/api/user/' + userId + '/reservations'; + const res = await fetch(url); + if (!res.ok) { + throw new Error(await res.text()); + } + const reservations = await res.json(); + const reservation = reservations.reverse().find(function(r) { return r.state == state }); + return reservation; + }, + getInvoiceAsync: async function (invoiceId, apiHost) { + if (!apiHost) { + apiHost = await this.getApiHostAsync(); + } + const url = apiHost + '/api/billing/invoice/' + invoiceId; + const res = await fetch(url); + if (!res.ok) { + throw new Error(await res.text()); + } + return await res.json(); + }, +} + +export default helpers; \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/pages/index.js b/samples/BikeSharingApp/BikeSharingWeb/pages/index.js new file mode 100644 index 000000000..29df08d3a --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/pages/index.js @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Component } from 'react' +import Page from "../components/Page" +import Header from "../components/Header" +import Content from "../components/Content" +import Link from 'next/link' +import BikeCard from "../components/BikeCard" +import fetch from 'isomorphic-fetch' +import Router from 'next/router' +import helpers from './helpers.js' +import ErrorPanel from '../components/ErrorPanel' + +export default class Index extends Component { + + constructor(props) { + super(props); + this.state = { + userId: undefined, + userName: undefined, + bikes: [], + errorMessage: undefined + }; + } + + async componentDidMount() { + try { + this.apiHost = await helpers.getApiHostAsync(); + var user = await helpers.verifyUserAsync(this.apiHost); + if (!user) { + Router.push('/devsignin'); + return; + } + + // User exists. + this.setState({ userId: user.id, userName: user.name }); + console.log(this.state.userId); + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving current user's data. Make sure that your Gateway and Users services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + try { + // Fetch user state, then navigate appropriately. + var url = this.apiHost + '/api/user/' + this.state.userId + '/reservations'; + const reservationsResponse = await fetch(url); + const reservations = await reservationsResponse.json(); + if (reservations.findIndex(function(r) { return r.state == 'Booked' }) >= 0) { + console.log('Navigating to reserved bike...'); + Router.push("/current-ride"); + return; + } + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving bikes' reservations. Make sure that your Gateway and Reservation services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + try { + console.log("fetching list of bikes..."); + var url = this.apiHost + '/api/bike/availableBikes'; + const res = await fetch(url); + const bikes = await res.json(); + this.setState({ bikes: bikes }); + console.log(bikes); + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving bikes' data. Make sure that your Gateway and Bikes services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + } + + render() { + function isEven(index) { + return (index % 2); + } + + function isOdd(index) { + return !isEven(index); + } + + function listBikes(bikes, func) { + return ( + bikes.map(function (bike, index) { + if (func(index)) { + return ( + +
+ +
+ + ); + } + }) + ); + } + + return ( + +
+ +
Bikes available in Seattle area
+
A selection of bikes that are best suited for your preferences.
+
+
+ {listBikes(this.state.bikes, isOdd)} +
+
+ {listBikes(this.state.bikes, isEven)} +
+
+ +
+ + + ); + } +} diff --git a/samples/BikeSharingApp/BikeSharingWeb/pages/preview.js b/samples/BikeSharingApp/BikeSharingWeb/pages/preview.js new file mode 100644 index 000000000..1c6cab3e3 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/pages/preview.js @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Component } from 'react' +import Page from "../components/Page" +import Header from "../components/Header" +import Content from "../components/Content" +import Field from "../components/Field" +import FormNote from "../components/FormNote" +import FormButton from "../components/FormButton" +import Map from "../components/Map" +import Footer from '../components/Footer' +import { withRouter } from 'next/router' +import MediaQuery from 'react-responsive' +import fetch from 'isomorphic-fetch' +import Router from 'next/router' +import helpers from './helpers.js' +import ErrorPanel from '../components/ErrorPanel' + +class PreviewBase extends Component { + + constructor(props) { + super(props); + this.state = { + userId: undefined, + userName: undefined, + bike: {}, + vendor: {}, + errorMessage: undefined + }; + } + + async componentDidMount() { + let user = null; + try { + this.apiHost = await helpers.getApiHostAsync(); + user = await helpers.verifyUserAsync(this.apiHost); + if (!user) { + Router.push('/devsignin'); + return; + } + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving current user's data. Make sure that your Gateway and Users services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + userId: user.id, + userName: user.name + }); + + let bikeData = null; + try { + // get bike + bikeData = await helpers.getBikeAsync(this.props.bikeId, this.apiHost); + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving bike's data. Make sure that your Gateway and Bikes services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + bike: bikeData + }); + + let vendorData = null; + try { + // get vendor + vendorData = await helpers.getVendorAsync(bikeData.ownerUserId, this.apiHost); + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving bike's vendor's data. Make sure that your Gateway and Users services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + vendor: vendorData + }); + } + + static async getInitialProps(context) { + return { + bikeId: context.query.id + } + } + + async handleClick(context) { + // reserve bike + console.log("Reserving bike..."); + try { + var url = this.apiHost + '/api/reservation'; + const res = await fetch(url, + { + method: 'POST', + cache: 'no-cache', + headers: { + "Content-Type": "application/json; charset=utf-8" + }, + body: JSON.stringify({ + userId: this.state.userId, + bikeId: this.state.bike.id + }) + }); + + if (!res.ok) { + throw new Error(await res.text()); + } + + // confirm reservation + const data = await res.json(); + console.log("Reservation data:", data); + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while creating a reservation for the bike. Make sure that your Gateway and ReservationEngine services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + // navigate to current-ride + Router.push("/current-ride"); + } + + render() { + return ( + +
+ +
+
+ {this.state.bike.imageUrl != null && + photo of bike + } +
+
+
+
{this.state.bike.model}
+ {this.state.vendor.name != null && +
Owned by {this.state.vendor.name}
+ } + + + + + + + +
+ Rent bike + +
+
+
+
+
+ +
+
+ +
+ +
+ Rent bike + +
+
+ + + ) + } +} + +const Preview = withRouter(PreviewBase); + +export default Preview \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/pages/review.js b/samples/BikeSharingApp/BikeSharingWeb/pages/review.js new file mode 100644 index 000000000..76459ee26 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/pages/review.js @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Component } from 'react' +import Page from "../components/Page" +import Header from "../components/Header" +import Content from "../components/Content" +import FormButton from "../components/FormButton" +import Footer from '../components/Footer' +import MediaQuery from 'react-responsive' +import helpers from './helpers'; +import Router from 'next/router' +import ReviewControl from "../components/ReviewControl" +import ErrorPanel from '../components/ErrorPanel' + +export default class Review extends Component { + + constructor(props) { + super(props); + this.state = { + userName: undefined, + errorMessage: undefined + }; + } + + async componentDidMount() { + let user = null; + try { + this.apiHost = await helpers.getApiHostAsync(); + user = await helpers.verifyUserAsync(this.apiHost); + if (!user) { + Router.push('/devsignin'); + return; + } + } + catch (error) { + console.error(error); + this.setState({errorMessage: `Error while retrieving current user's data. Make sure that your Gateway and Users services are up and running (run "azds list-up"). Details: ${error.message}`}); + return; + } + + this.setState({ + userName: user.name + }); + } + + // handle return bike + async handleClick(context) { + // return bike + console.log("submitting review..."); + + // navigate to review + Router.push("/"); + } + + render() { + return ( + +
+ +
+
+ +
+
+
Comfortable
+
Good brakes
+
Easy pick-up
+
Smooth ride
+
+ + + +
+ Submit +
+
+
+ +
+ +
+ Submit +
+
+ + + ) + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/server.js b/samples/BikeSharingApp/BikeSharingWeb/server.js new file mode 100644 index 000000000..d10c439bd --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/server.js @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const express = require('express'); +const next = require('next'); +const url = require('url'); + +const port = parseInt(process.env.PORT, 10) || 3000; +const dev = process.env.NODE_ENV !== 'production'; +const app = next({ dev }); +const handle = app.getRequestHandler(); + +app.prepare() + .then(() => { + const server = express() + + server.get('/api/host', (req, res) => { + var apiHost = url.format({ + protocol: req.protocol, + hostname: getApiUrl(req.get('host')) + }); + + console.log("API_HOST = " + apiHost); + + res.status(200).send({ + apiHost: apiHost + }); + }); + + server.get('/preview/:id', (req, res) => { + return app.render(req, res, '/preview', { id: req.params.id }) + }); + + server.get('/', (req, res) => { + console.log("Serving index"); + return app.render(req, res, '/index', {}) + }); + + server.get('*', (req, res) => { + return handle(req, res) + }); + + server.listen(port, (err) => { + if (err) throw err + console.log(`> Ready on http://localhost:${port}`) + }); + }); + +function getApiUrl(host) { + // break up hostname parts into array + var hostArr = host.split("."); + + // find prefix + var prefix = ""; + if (hostArr.indexOf("s") >= 0) { + prefix = hostArr[0] + ".s." + } + + // find base hostname + var root = 0; + var start = 2; + if (prefix !== "" && prefix !== null) { + root += 2; + start += 2; + } + var baseHost = hostArr.slice(start, hostArr.length).join('.'); + + // return full URL of API service (spacePrefix + rootSpace + apiName + host) + return prefix + hostArr[root] + "." + process.env.API_NAME + "." + baseHost; +} \ No newline at end of file diff --git a/samples/BikeSharingApp/BikeSharingWeb/static/awc-title.svg b/samples/BikeSharingApp/BikeSharingWeb/static/awc-title.svg new file mode 100644 index 000000000..58b816900 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/static/awc-title.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/samples/BikeSharingApp/BikeSharingWeb/static/logo.svg b/samples/BikeSharingApp/BikeSharingWeb/static/logo.svg new file mode 100644 index 000000000..62883c331 --- /dev/null +++ b/samples/BikeSharingApp/BikeSharingWeb/static/logo.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/BikeSharingApp/BikeSharingWeb/static/sample-map.png b/samples/BikeSharingApp/BikeSharingWeb/static/sample-map.png new file mode 100644 index 0000000000000000000000000000000000000000..1905a013660f55ebcd5ebb9406482a922415b45f GIT binary patch literal 9304 zcmb_?^;Z@wzh!y4wGd5-}3t;NXy|t10W_;5<@%Xg2}yA1D!bfzU%m z=&5GngM&j%^}q56Cnt~oVepZU{(nk1b)(FC4;?%QMXiTr@KoX($P*kKjstaNMZi7?nOvP=-Or2{jIK%MW1{OhA756<@;=3Y{FV~f{3lDT&~5;SH)VI7CfDeFUe zr6m;Vs}T-@{9$ISX5{fDD=$Foe|U*P4+D@3N1ZksZASe|W#kI2 z`RhY#aU0s#7c1eipEnK>0SM=zY^9N95=M_ zY(2P-e;&=ph^W}UL0yl`*M+0mJ|0B)Y!^z7-nw6ebb4+1q01;0T5H$w6*iZ9!Yywu zPmqM{DNYC3cXCzQH4|j$0CIVLUw(Q1b9VOwNDJGWy<0BPns{Q?8M9ypqm?VRNlb6y$R9wb)9z# zMeo@O{nE(p@Dn+Q(G5+sy~og>HN3FP;~#z*79;-(2$+y>^rG zW3SP()z(%29IXG(7zq~Ns&j*ORO;4~=!GH-5`N2GrgZBQlJ^rq*6M{my;`nVdm3EDlfSp175|2J19qCIVmDMFY!mU}(OL zXWwSPBCV9yuiv`uEttaSsj$T&`+YCCXQo9;Hkc=)7XB@pEoz-yF&nRs*dO`~-BI+W zIXQ#Wc_Ny;uJMoY{W!Yky|pv_GKqox{Zo>+CAIx?EqWCeW5(Rqp*!TNJ#zut`T(nJ ze8br90S&L^(L?P@RKjnMrP2W(oxGsNM@Cs3qegLNHh7y~PQp8iAICq3it!c9{6qomjl>ogAK?&?qGL{6q0(kOZ{2MZ|-Av8ZOj@Z7@)4S2PYP$o?wiuyG zDw_MxM&kn5t%Dr-WqP5^Q?M~uRJ2cZan_q2P@X;9g}Vph>p%h_A^^M~o2lP@DLJ&J5&|<@ zugfW!Q1S+Iy$cheqLFrzg=rwVjYC~^&Tz>`$Mu_*A;_ONCX&v_?@&l^z@T6#_rWfy zGMPqd+~_0gYV9vAPsFH?rdr6g`Fmu@=9I^UUw%MoIJ5f0&{a%BY8z`=jZ(&zq zf~q@PCo_aSa?lV3#3=7L_Ud%U=&>`(h zyzUp4R+Z7aK?LXMI>ZZwcza39v&n}gYllzJQ7IyK)#j6ODQo1SpR)4@TWN?Lg5hF# z@Ueen@QeLrkH$KFd6VF<(9srPIgo(zbqt=^KFDa3KI?(0>i9U^mt5)l-xFEFoJ1lq zupr|`IH7N*RG!D4y22USaLo>O2~8-1hza|Bd-*tm{3U5LceXPhz>jt{?`Qv+ijJV* zV_2eimD!Tu|MrW0(L>5!)~M{W&hO+IB^2cCHN)|zW7x;$;U2RijYF>f_V*JcJo+M6 zn)W5De;P&fXX?Wweh*`ZPzX=3*Ff+81cYMStuMBPJi=WuzxXfXGQA4v*ZxQToVmN6 zNRZdQh~4?!a|hzthnYdbetEj55XZ;Yu!1#SIObd3efZS*zZ#Rr7+Bk8;=`%=Ka~?| zjm~F_k!)0mQ2c+R$shTv+8Za5A2`rUru;uTl5<5|sqaD`>y_l1JxC-b8L^GERidu8 zaG^&vH`&%7(|m7 zOE6{nV2r=HMYHx)nJ#>%UA?4)yCC|rv^Iru#kCEkCjbm4O!u_Z2MCvOEizm`s z45ZHk&Cix~MG{7GlORQi_cqB~yl#~_d~NPe*V+e{IyC<-Z+Ep|s}}4pvtdUQ+rb_8 z_os#T%+&V=4uDh`?t0w))hQJFh(f8R-^K583AOfOh7Y zzAI8V*?Cl17_YIAfR@cmL7ANz^goMyx-<4$T)F&i*JL)I-@S`~6~I(^&KwY9Y-W_DmIxeIxw zH~)N3^hXCo`?Cqq@n%bIj03$YRXz&I{=)am>K#ylQ5(8!%in95FkKwXl*o!z{*%^O zJHgK#6btb!J}t6}CV+#olqmYlQE!-lk%@}ev~0hq#|#KkQr?M>a6IYa<#?tHU|r{A z*B9dJ8oZO*lu*08ZhWsh}z;u8Zp-S$S+quC7qC!vAElwu3{&w>mcJPl< zv5(M&ymfwzMY-?hGbea;8yLd7JJjymbSz9ax2s`FT$JLs+H>yW0`t*^?-ehy+L+8> zZKmwRCdaqd;!7kr(s~be5&p#reL~5PV8VRswA@CRb;X;_zreChmg`yD!+$KQb34|5 zyfcqFYNrQQCA%e*rvI5Wk5qI7&v=QiktIyI)al$;Xb(J_^PE#%+Ucr*#KOm#8M^2F zW#MNP6;Ai&sr9XZAx09Lh|X=`XDQz0IKQ8wzZx#ddzh1ic6)c)(xWP6d%VfPn#CjU z7X;bb-O)RKA2A=Robp@B=rK#>dQstxS`$BS?-3bixx{xnG8a>QIV~&ph2USwUV8$O zH^%WrZ-{s(OhDrelw4op+2}}5x;^^_mo7KT#fLcgUlh-m6$Tq`wX`DQp=J*Ix6L&9 z^Y_xde#{^S3e;Z)LrKe0VR90}+`i9A1R_5cuope|<|fe)UfbX5o8%rQf!(>~mt+G6 zWVKY%h$pcS5{FNK>&St|6~z*%p}c$+KyIQ!G;&|TkDAN(X3YQFnqRD!ED5EeyqZ9! zvHD0`x50*K;HzmY{|5sTVUq^?L5A1~6TF#U)s&_;shD`KN`({Zb>JEpO58Q-9Z1sw zI-b^W1MQO_<0szXiO0UJU~-X-L(_9=l8okj`#@_JxRY3858$(Sw9$iyqBFK6Mm z7|X9F@4j89WhFKzT*6e@kFe2EC8S8<>vd&vMMXz)_8NIS&tam8h3B)}Mo{ICq0Zk* z=Z`CoPyfg4H;^0fIg$0jaoPRLVk?1$@~*wxYJ|ycqM|M_wGyxI-9DUG(9sba;@zUYbQgZ?e9 zgkc2&Z*&CfWyHl`%3Z@%c$z+`^s$hCcDedE;vu9bQ^wX?wC60}E$jrhAmCl9ZOgzK-we*zgDh1*hwxxe(o{z<nE2HaHL8n2Wi_@D-rvWN^ZiGS*pt5eWfzJ_HAAo!xKxNz>OCATb9ACx^j1{Ac?oQ{Ob_wk3pE@14Vgg5J|+ncnGZIfXV| zH&6b+2LCP7K02}XJ5&ysvm8E!{TkU~)d*p#w@GkyGQP*I+?!E0*{A**>8WRu@APfS zZSDwQIt%f+_UX<4N_+80Qjf-_mDpymlC;gqZjXL3V|H%sAGES9p$>RdD`=#L9!dKRh{pkEWLw%jgZ9Ih(;@q-C8>Pk#2r4_C;0|DUYVV>J9V{SGBSJuhwBPf zm-d9T3{4{QmiURQS;-v2sS5@yQ?(=Z=iPt3;`>`Wi`jo#^lPwU$S?f$oi}jE0kiM} z?IkZABb8jd84mIZOcBD*C?qUy`D0pecXrTY*;)fb)(=Vj&Ga$TvDh|n>{iQ*;cg*SJG!_s8BvvQYk5j6vX7}l7yHsE-oJ)fsDoOhiHj0=Zue-m4^zaE>JBE&w;Lm+-7#f8|M)z5bo);D5^V*A-Wz z&pMLOYYUF=yO^p1JB{X2vwppi4+kLeW~IYYC8Qy8VUFbo=M36Iqza#GjAlghz8tsv zr$<@Hs4lanIyE|7uvCz1*b!f1*O^nc1W2y5Ww`tC>FB6PG8snF2xpu|>1Zk7X?6eH z03b%)#84Q46mwTmmOk16d)Rmw&P$De2{_E|T}l>K9ML4^7qVrQ)Jo(R3Rb=~2JO&p zJdN9=ry72j=^)qAP0Bu(%Q7H7pn%&eVD}_D)QE-S;$kr=jfH<(*sVf{ryf^}5<20X z)Qs0ZiF+E>dB`s^ds}tq4vs?c%?TyDD4Yy(nOz`QOM|d8zj_=gI*Bp^z_fBPXDTGS z-sq`cnSN|+{&ldUFML?WgEa?Bj~A?y%|;WiHWxnZK@a(7S!i=`!m8RPO(|MY0#016 z2ZG);@RxT`AtR9!4LWmJn^HbP(xIvmpMv|lvFQw{4 zmd$&kM#&HUtWHG5o>vn$McTM`#9svHm{BA`V6SM&R&38Nr^20?WEi!eQeOg z_yhrteUnJE3PN!U3F-*bTFyJr9z5sbHXSY!v5&B38g>$_uN^8)>Z&v44k4|~c_(^) zx4unJDB2mCVYKQasEIoT5%iX=l?nZrNrV~cewjx^nJQphqmyv+t?aMho9nx&sp5cB`>6&trdO`? zBD6MyO5lRAFMImHE|_s|*!|L_iO&cr$LNC!o5gZvoibB*po_hR-P8G)n#nmF@n7<4 z7PcU|(}vE15HSv!YE%CJRdCP$!f81oi!r&u13$xhWPL1kqU#RVyaUWl?&}?&?5v`Z z=zh6K2DM?zyewov?vx+Rw{o$iYodOMV2bHWVxL>G4m1H;d5yM3_+>>S0H_IIBNOzD z{2`XJj3YY?wfL1z3*ac+ zY2-QH$U;e3eQ+1>h1kB~<5J|jyTf|y3<2eg?fvqZiz~%Om}8#&#QpG)Tyi+sDGZm< zAagO8E!9mHP6se;uF1=s(-h@r8ZI#BxS)w2WA1fW{#bP&$B-Ua4w!b z_t+NYZ{^(E`k@nV+W9*R@KHUz?ROV6r_z~S20$&MV5Vb+#d=rK2pu*qxfYDjSO{Wm$j(H_t#L1T=S5JLx|Waf7}Lw4VBcQ4|J+rz z(oxu_?#4hpioW$;QdLiKDYh;AzpNSWR;ai>8$2h#f=HTm`pMISkYN-`J~5d|m}i4L zZjLxMw$#F-0W+VxsiCGz~1~k>$v)sFONNc{32nr8(=&`};b2BC#YQdUF5;3y~xAw*WfZpVd zI5cd`zZgN6)GnLNW;SP(ryE`*os!Y*o7)2elV|SS%kgI>cw0#9rPp7Je?U_RDf7Fd zNjiZQnN^yomD@+hsi`{smf`mX(d&9 z)g{+hudfKMp#~je6?>D*f$)^BA*j&@*{JW&UWEzO%czq->+^DLbM#DkfJ6JN-MhfB zw&DE-hQ}!q61M}$#cm__Kl_#1HVT~3rr3oxg_LPlkHsTvcTXCP2Fe6=3nr*XG+=1v zUoC&l%jgEDG_}bnnZvkSNV=D%l1(}|nqA1~a~y@fwj6kgp5RDNtGf#xS90KQ41_j* zoUa-%dM5U*z2f8A3;%IC#qV#xehj5KW&iCR`twAZT4C=WRRx9df1>=Pb^xSY8XfF!g$AA_`Kkj0q<@d@ zPkZS<#}~n1o2}et9BzE534`{Lxu-9W46JPuQt~{3m0!SXKR+4zVXsS+_jF%K&2lBI zt~JPD1hz41l?8>>Jp_uoZR9B8vfdzY-k0&}R0j*bVU~OMaH@Wzk#(&k%Q6!(rw^)# z%J+(<8wa+8N?Ej2L>tU~%EB%DBcq9Wt4-I|+n5Ek`OQs6DW704NqUg}S;r=;$5f!_ zl@As}*NfE9`NS3H|6gpDGqu{y=;Gv+VP~-f`H;ccHSdxvoJYXM?f`1ftI?q8D9|c- zHcd3Mb0H%D=suK}4@b}$53tF1ihd9lEJ{_{^JIKiooE0jxSTEKS9Xi~*A08R{FRHI zw>;YG&^`F64E3vw@;@whXPByho3mY%TX%4q&hiV2`8s6rkc}EgbRE8VpOWthsOs+5 zV(DCt&u>s%hqVx#!vM%GLH`(nZ*6Dyi&lCL9qY@3Maj&#n&Ka(we(c!%#rP2kso-b zg0^?hp3J!?NACr-I+5GZAJObDL!Y7*ti=CAczcxi5{*Wbn(9Oh|~Cx&v+BLpXUU)HCez0a!|hhAuHbWfw*px$Gs z?kT}XJ~5!buh=FDQWn`BGe$j8!=?lV7`q)mJkQvoGS^dHZQNP>3*U|5ZMn46e_ZA~ zWl$omq69afyVmt@8){-Lg}fV>P58jA)86A`MH!)`7^6&=_t>QJ8a+H|e zZ+iLb@3p@_rd0c1PjR)b#x9yi1s{~WyZ~P>*=Mkq@2<%2rihFpw|0stuMYUB$-+eV z%=%9*GG}+NFxVDgCuGpQ4uvDDe$JgXF@0(~-;Gdmf#;M1HVk8O6NpRv5?>db$G`Yg z`NHLwWt~Mv!}mSQ>!;PRScRXi6akDB6vOQ|;a)siW0|G{7__pZ*5FoRD~Q z0DXRRqS@P$NZ*0*GYl z4asGH!Gixm1qVr0ZaVAoUzp)BP z;-y|(mN3@TB$7hv%i572Zc#xo)bhHAx>C3n&Od3&_D_S%m(BB4vWy8ZLDLr_*2Q^IxT z$&Pfj!Ic#Ic8q1KMwraT#Z8o9K~ZdLUyT&l;EqJB@+*#@%HGs4&NH)$MXDLK z&b~cwt?7`n)PH19qiNB24`V1sDPMiUqNPZE|9BX*)9eJpVIzUx51RR(2S}j`F-h5f z?}9{CYMRVnE0zzjJgt|kHx%)a)aUrx)I@#2$iYGh7DnZOx2^M5tXAlJ8jP`3kgoBMs~pcRq-A%=Ud_*a zmTh*)jJToAN;MbPIODK4U69w4mKAD|?nsnL6yMt`4laLx@OoJMJJ-DSk$Bo48ab<) zdpxaNqjTqO27&bPRhDpPL9@Db-LHnUMMOf;;)Fxks3E%QMX z{V3+hf;cVeMxp8@IXL;Wxea-YlIhZv59?&SMSEoc*2 z&iI&szYGfvhAr;q31ID^ujb^g>Kr4v-M+@dA|3%>lsY<3)BU}_g?zWjXs11S_y~lf MuA;46r(_lRf3oIt!T..azds.io + - $(spacePrefix)$(rootSpacePrefix)bikes$(hostSuffix) +configurations: + develop: + build: + useGitIgnore: true + container: + sync: + - "!**/package.json" + iterate: + processesToKill: [node] \ No newline at end of file diff --git a/samples/BikeSharingApp/Bikes/charts/bikes/.helmignore b/samples/BikeSharingApp/Bikes/charts/bikes/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/samples/BikeSharingApp/Bikes/charts/bikes/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/samples/BikeSharingApp/Bikes/charts/bikes/Chart.yaml b/samples/BikeSharingApp/Bikes/charts/bikes/Chart.yaml new file mode 100644 index 000000000..c0cfd1d0d --- /dev/null +++ b/samples/BikeSharingApp/Bikes/charts/bikes/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: bikes +version: 0.1.0 diff --git a/samples/BikeSharingApp/Bikes/charts/bikes/templates/NOTES.txt b/samples/BikeSharingApp/Bikes/charts/bikes/templates/NOTES.txt new file mode 100644 index 000000000..29c82f814 --- /dev/null +++ b/samples/BikeSharingApp/Bikes/charts/bikes/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "bikes.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "bikes.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "bikes.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "bikes.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/samples/BikeSharingApp/Bikes/charts/bikes/templates/_helpers.tpl b/samples/BikeSharingApp/Bikes/charts/bikes/templates/_helpers.tpl new file mode 100644 index 000000000..28666f710 --- /dev/null +++ b/samples/BikeSharingApp/Bikes/charts/bikes/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "bikes.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bikes.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bikes.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/samples/BikeSharingApp/Bikes/charts/bikes/templates/deployment.yaml b/samples/BikeSharingApp/Bikes/charts/bikes/templates/deployment.yaml new file mode 100644 index 000000000..a5afc6754 --- /dev/null +++ b/samples/BikeSharingApp/Bikes/charts/bikes/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: {{ template "bikes.fullname" . }} + labels: + app: {{ template "bikes.name" . }} + chart: {{ template "bikes.chart" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "bikes.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "bikes.name" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + annotations: + buildID: {{ .Values.buildID }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + {{- if .Values.probes.enabled }} + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + {{- end }} + env: + {{- $root := . }} + {{- range $ref, $values := .Values.secrets }} + {{- range $key, $value := $values }} + - name: {{ $ref }}_{{ $key }} + valueFrom: + secretKeyRef: + name: {{ template "bikes.fullname" $root }}-{{ $ref | lower }} + key: {{ $key }} + {{- end }} + {{- end }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/samples/BikeSharingApp/Bikes/charts/bikes/templates/ingress.yaml b/samples/BikeSharingApp/Bikes/charts/bikes/templates/ingress.yaml new file mode 100644 index 000000000..4c51e9cdd --- /dev/null +++ b/samples/BikeSharingApp/Bikes/charts/bikes/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "bikes.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "bikes.name" . }} + chart: {{ template "bikes.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/samples/BikeSharingApp/Bikes/charts/bikes/templates/secrets.yaml b/samples/BikeSharingApp/Bikes/charts/bikes/templates/secrets.yaml new file mode 100644 index 000000000..edfe56919 --- /dev/null +++ b/samples/BikeSharingApp/Bikes/charts/bikes/templates/secrets.yaml @@ -0,0 +1,12 @@ +{{- $root := . }} +{{- range $name, $values := .Values.secrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "bikes.fullname" $root }}-{{ $name | lower }} +data: + {{- range $key, $value := $values }} + {{ $key }}: {{ $value | b64enc }} + {{- end }} +--- +{{- end }} diff --git a/samples/BikeSharingApp/Bikes/charts/bikes/templates/service.yaml b/samples/BikeSharingApp/Bikes/charts/bikes/templates/service.yaml new file mode 100644 index 000000000..42fd278a6 --- /dev/null +++ b/samples/BikeSharingApp/Bikes/charts/bikes/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "bikes.fullname" . }} + labels: + app: {{ template "bikes.name" . }} + chart: {{ template "bikes.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app: {{ template "bikes.name" . }} + release: {{ .Release.Name }} diff --git a/samples/BikeSharingApp/Bikes/charts/bikes/values.yaml b/samples/BikeSharingApp/Bikes/charts/bikes/values.yaml new file mode 100644 index 000000000..fd8cb8fdd --- /dev/null +++ b/samples/BikeSharingApp/Bikes/charts/bikes/values.yaml @@ -0,0 +1,65 @@ +# Default values for bikes. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +fullnameOverride: bikes +replicaCount: 1 +image: + repository: azdspublic/bikesharing-bikes + tag: build.20190418.1 + pullPolicy: IfNotPresent +imagePullSecrets: [] + # Optionally specify an array of imagePullSecrets. + # Secrets must be manually created in the namespace. + # ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod + # + # This uses credentials from secret "myRegistryKeySecretName". + # - name: myRegistryKeySecretName +service: + type: ClusterIP + port: 80 + +probes: + enabled: false + +ingress: + enabled: false + annotations: + kubernetes.io/ingress.class: addon-http-application-routing + path: / + # hosts: + # - chart-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local +secrets: + mongo: + collection: bikes + connectionstring: mongodb://databases-mongo + # Optionally specify a set of secret objects whose values + # will be injected as environment variables by default. + # You should add this section to a file like secrets.yaml + # that is explicitly NOT committed to source code control + # and then include it as part of your helm install step. + # ref: https://kubernetes.io/docs/concepts/configuration/secret/ + # + # This creates a secret "mysecret" and injects "mypassword" + # as the environment variable mysecret_mypassword=password. + # mysecret: + # mypassword: password +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi +nodeSelector: {} + +tolerations: [] + +affinity: {} \ No newline at end of file diff --git a/samples/BikeSharingApp/Bikes/package.json b/samples/BikeSharingApp/Bikes/package.json new file mode 100644 index 000000000..afbff28b0 --- /dev/null +++ b/samples/BikeSharingApp/Bikes/package.json @@ -0,0 +1,16 @@ +{ + "name": "contoso.bikerental.bikes", + "version": "0.1.0", + "description": "Microservice for managing bikes", + "dependencies": { + "express": "^4.13.4", + "morgan": "^1.7.0", + "mongodb": "^2.2.24", + "body-parser": "^1.16.1", + "validate.js": "^0.11.1", + "async": "^2.1.5" + }, + "devDependencies": { + "nodemon": "^1.18.10" + } +} diff --git a/samples/BikeSharingApp/Bikes/server.js b/samples/BikeSharingApp/Bikes/server.js new file mode 100644 index 000000000..f77f6d72a --- /dev/null +++ b/samples/BikeSharingApp/Bikes/server.js @@ -0,0 +1,407 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +var morgan = require('morgan'); +var bodyParser = require('body-parser'); +var validate = require('validate.js'); +var MongoClient = require('mongodb').MongoClient; +var ObjectId = require('mongodb').ObjectID; +var express = require('express'); +var async = require('async'); + +var mongoDBCollection = process.env.mongo_collection; +var mongoDBConnStr = process.env.mongo_connectionstring; +console.log("Collection: " + mongoDBCollection); +console.log("MongoDB connection string: " + mongoDBConnStr); + +// Will be initialized on server startup at the bottom +// Init to prototype to enable Intellisense +var mongoDB = require('mongodb').Db.prototype; + +validate.validators.illegal = function(value, options, key, attributes) { + if (value !== undefined && options) { + return "cannot be provided"; + } +} + +var incomingBikeSchema = { + id: { + illegal: true + }, + available: { + illegal: true + }, + model: { + presence: true, + length: { minimum: 1 } + }, + hourlyCost: { + presence: true, + numericality: { greaterThan: 0, noStrings: true } + }, + imageUrl: { + presence: true, + length: { minimum: 1 } + }, + address: { + presence: true, + length: { minimum: 1 } + }, + type: { + presence: true, + inclusion: [ "mountain", "road", "tandem" ] + }, + ownerUserId: { + presence: true + }, + suitableHeightInMeters: { + presence: true, + numericality: { greaterThan: 0, noStrings: true } + }, + maximumWeightInKg: { + presence: true, + numericality: { greaterThan: 0, noStrings: true } + } +}; + +var app = express(); +app.use(requestIDParser); +app.use(morgan("dev")); +app.use(bodyParser.json()); + +var requestIDHeaderName = 'x-contoso-request-id'; +var requestIDRegex = new RegExp(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + +function requestIDParser(req, res, next) { + var reqID = req.header(requestIDHeaderName); + var test = false; + if (reqID) { + test = requestIDRegex.test(reqID); + } + if (!test && req.path != "/hello") { + res.status(400).send("Couldn't parse request id guid"); + return; + } + + console.log("RequestID start: " + reqID); + next(); + console.log("RequestID done: " + reqID); +} + +// api ------------------------------------------------------------ + +// find bike ------------------------------------------------------------ +app.get('/api/availableBikes', function (req, res) { + var requestID = req.header(requestIDHeaderName); + + var query = {}; + // BUG! Uncomment code to fix :) + // query = { available: true }; + + // Add user filter conditions + for (var queryParam in req.query) { + if (isNaN(req.query[queryParam])) { + query[queryParam] = req.query[queryParam]; + } + else { + query[queryParam] = parseFloat(req.query[queryParam]); + } + } + + var cursor = mongoDB.collection(mongoDBCollection).find(query).sort({ hourlyCost: 1 }).limit(30); + cursor.toArray(function(err, data) { + if (err) { + dbError(res, err, requestID); + return; + } + + data.forEach(function(bike) { + bike.id = bike._id; + delete bike._id; + }); + + res.send(data); + }); +}); + +app.get('/api/allbikes', function(req, res) { + var requestID = req.header(requestIDHeaderName); + + var cursor = mongoDB.collection(mongoDBCollection).find({}).sort({ hourlyCost: 1 }); + cursor.toArray(function(err, data) { + if (err) { + dbError(res, err, requestID); + return; + } + + data.forEach(function(bike) { + bike.id = bike._id; + delete bike._id; + }); + + res.send(data); + }); +}); + +// new bike ------------------------------------------------------------ +app.post('/api/bikes', function (req, res) { + var requestID = req.header(requestIDHeaderName); + var validationErrors = validate(req.body, incomingBikeSchema); + if (validationErrors) { + res.status(400).send(validationErrors); + return; + } + + var newBike = req.body; + newBike.available = true; + + mongoDB.collection(mongoDBCollection).insertOne(newBike, function(err, result) { + if (err) { + dbError(res, err, requestID); + return; + } + + newBike.id = newBike._id; + delete newBike._id; + console.log(requestID + ' - inserted new bikeId: ' + newBike.id); + res.send(newBike); + }); +}); + +// update bike ------------------------------------------------------------ +app.put('/api/bikes/:bikeId', function(req, res) { + var requestID = req.header(requestIDHeaderName); + var validationErrors = validate(req.body, incomingBikeSchema); + if (validationErrors) { + res.status(400).send(validationErrors); + return; + } + if (!ObjectId.isValid(req.params.bikeId)) + { + res.status(400).send(req.params.bikeId + ' is not a valid bikeId!'); + return; + } + + var updatedBike = req.body; + + mongoDB.collection(mongoDBCollection).updateOne({ _id: new ObjectId(req.params.bikeId) }, { $set: updatedBike }, function(err, result) { + if (err) { + dbError(res, err, requestID); + return; + } + if (!result) { + res.status(500).send('DB response was null!'); + return; + } + if (result.matchedCount === 0) { + bikeDoesNotExist(res, req.params.bikeId); + return; + } + if (result.matchedCount !== 1 && result.modifiedCount !== 1) { + var msg = 'Unexpected number of bikes modified! Matched: "' + result.matchedCount + '" Modified: "' + result.modifiedCount + '"'; + console.log(requestID + " - " + msg); + res.status(500).send(msg); + return; + } + + res.sendStatus(200); + }); +}); + +// get bike ------------------------------------------------------------ +app.get('/api/bikes/:bikeId', function(req, res) { + var requestID = req.header(requestIDHeaderName); + if (!req.params.bikeId) { + res.status(400).send('Must specify bikeId'); + return; + } + if (!ObjectId.isValid(req.params.bikeId)) + { + res.status(400).send(req.params.bikeId + ' is not a valid bikeId!'); + return; + } + + mongoDB.collection(mongoDBCollection).findOne({ _id: new ObjectId(req.params.bikeId) }, function(err, result) { + if (err) { + dbError(res, err, requestID); + return; + } + if (!result) { + bikeDoesNotExist(res, req.params.bikeId); + return; + } + + var theBike = result; + theBike.id = theBike._id; + delete theBike._id; + + res.send(theBike); + }); +}); + +// delete bike ------------------------------------------------------------ +app.delete('/api/bikes/:bikeId', function(req, res) { + var requestID = req.header(requestIDHeaderName); + if (!req.params.bikeId) { + res.status(400).send('Must specify bikeId'); + return; + } + if (!ObjectId.isValid(req.params.bikeId)) + { + res.status(400).send(req.params.bikeId + ' is not a valid bikeId!'); + return; + } + + mongoDB.collection(mongoDBCollection).deleteOne({ _id: new ObjectId(req.params.bikeId) }, function(err, result) { + if (err) { + dbError(res, err, requestID); + return; + } + if (result.deletedCount === 0) { + bikeDoesNotExist(res, req.params.bikeId); + return; + } + if (result.deletedCount !== 1) { + var msg = 'Unexpected number of bikes deleted! Deleted: "' + result.deletedCount + '"'; + console.log(requestID + " - " + msg); + res.status(500).send(msg); + return; + } + + res.sendStatus(200); + }); +}); + +// reserve bike ------------------------------------------------------------ +app.patch('/api/bikes/:bikeId/reserve', function(req, res) { + var requestID = req.header(requestIDHeaderName); + if (!req.params.bikeId) { + res.status(400).send('Must specify bikeId'); + return; + } + + processReservation(res, req.params.bikeId, false, requestID); +}); + +// clear bike ------------------------------------------------------------ +app.patch('/api/bikes/:bikeId/clear', function(req, res) { + var requestID = req.header(requestIDHeaderName); + if (!req.params.bikeId) { + res.status(400).send('Must specify bikeId'); + return; + } + + processReservation(res, req.params.bikeId, true, requestID); +}); + +function processReservation(res, bikeId, changeTo, requestID) { + if (!ObjectId.isValid(bikeId)) + { + res.status(400).send(bikeId + ' is not a valid bikeId!'); + return; + } + + mongoDB.collection(mongoDBCollection).updateOne({ _id: new ObjectId(bikeId), available: !changeTo }, { $set: { available: changeTo } }, function(err, result) { + if (err) { + dbError(res, err, requestID); + return; + } + if (result.matchedCount === 0) { + // Figure out if bike does not exist or if it was invalid reservation request + mongoDB.collection(mongoDBCollection).findOne({ _id: new ObjectId(bikeId) }, function(err, result) { + if (err) { + dbError(res, err, requestID); + return; + } + + if (!result) { + bikeDoesNotExist(res, bikeId); + } + else { + // Invalid reservation request + res.status(400).send('Invalid reservation request was made for BikeId ' + bikeId); + } + }); + + return; + } + if (result.matchedCount !== 1 && result.modifiedCount !== 1) { + var msg = 'Unexpected number of bikes changed availability! Matched: "' + result.matchedCount + '" Modified: "' + result.modifiedCount + '"'; + console.log(requestID + " - " + msg); + res.status(500).send(msg); + return; + } + + res.sendStatus(200); + }); +} + +function bikeDoesNotExist(res, bikeId) { + res.status(404).send('BikeId "' + bikeId + '" does not exist!'); +} + +function dbError(res, err, requestID) { + console.log(requestID + " - " + err); + res.status(500).send(err); +} + +app.get('/hello', function(req, res) { + res.status(200).send('hello!\n'); +}); + +// start server ------------------------------------------------------------ +var port = 80; +var server = null; + +process.on("SIGINT", () => { + console.log("Interrupted. Terminating..."); + if (server) { + server.close(); + } + var tmp = mongoDB; + mongoDB = null; + tmp.close(); +}); + +process.on("SIGTERM", () => { + console.log("Terminating..."); + if (server) { + server.close(); + } + var tmp = mongoDB; + mongoDB = null; + tmp.close(); +}); + +function tryMongoConnect(callback, results) { + MongoClient.connect(mongoDBConnStr, function(err, db) { + if (err) { + console.error("Mongo connection error!"); + console.error(err); + } + + callback(err, db); + }); +} + +async.retry({times: 10, interval: 1000}, tryMongoConnect, function(err, result) { + if (err) { + console.error("Couldn't connect to Mongo! Giving up."); + console.error(err); + process.exit(1); + } + + console.log("Connected to MongoDB"); + mongoDB = result; + mongoDB.on('close', function() { + if (mongoDB) { // SIGINT and SIGTERM + console.log('Mongo connection closed! Shutting down.'); + process.exit(1); + } + }); + + // Start server + server = app.listen(port, function () { + console.log('Listening on port ' + port); + }); +}); \ No newline at end of file diff --git a/samples/BikeSharingApp/Billing/.gitignore b/samples/BikeSharingApp/Billing/.gitignore new file mode 100644 index 000000000..2fd1e02a3 --- /dev/null +++ b/samples/BikeSharingApp/Billing/.gitignore @@ -0,0 +1,4 @@ +debug +*.exe + +.*/ \ No newline at end of file diff --git a/samples/BikeSharingApp/Billing/Dockerfile b/samples/BikeSharingApp/Billing/Dockerfile new file mode 100644 index 000000000..6074b30af --- /dev/null +++ b/samples/BikeSharingApp/Billing/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.8 + +COPY . /go/src/app +WORKDIR /go/src/app +RUN go get -d -v +RUN go install -v + +EXPOSE 80 + +ENTRYPOINT ["app"] \ No newline at end of file diff --git a/samples/BikeSharingApp/Billing/azds.yaml b/samples/BikeSharingApp/Billing/azds.yaml new file mode 100644 index 000000000..49f7a5177 --- /dev/null +++ b/samples/BikeSharingApp/Billing/azds.yaml @@ -0,0 +1,26 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: . + dockerfile: Dockerfile +install: + chart: charts/billing + values: + - values.dev.yaml? + - secrets.dev.yaml? + set: + replicaCount: 1 + image: + repository: billing + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + # This expands to [space.s.][rootSpace.]billing...azds.io + - $(spacePrefix)$(rootSpacePrefix)billing$(hostSuffix) +configurations: + develop: + build: + useGitIgnore: true \ No newline at end of file diff --git a/samples/BikeSharingApp/Billing/charts/billing/.helmignore b/samples/BikeSharingApp/Billing/charts/billing/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/samples/BikeSharingApp/Billing/charts/billing/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/samples/BikeSharingApp/Billing/charts/billing/Chart.yaml b/samples/BikeSharingApp/Billing/charts/billing/Chart.yaml new file mode 100644 index 000000000..d9ee45c1a --- /dev/null +++ b/samples/BikeSharingApp/Billing/charts/billing/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: billing +version: 0.1.0 diff --git a/samples/BikeSharingApp/Billing/charts/billing/templates/NOTES.txt b/samples/BikeSharingApp/Billing/charts/billing/templates/NOTES.txt new file mode 100644 index 000000000..837722f31 --- /dev/null +++ b/samples/BikeSharingApp/Billing/charts/billing/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "billing.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "billing.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "billing.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "billing.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/samples/BikeSharingApp/Billing/charts/billing/templates/_helpers.tpl b/samples/BikeSharingApp/Billing/charts/billing/templates/_helpers.tpl new file mode 100644 index 000000000..c4f9d3bd3 --- /dev/null +++ b/samples/BikeSharingApp/Billing/charts/billing/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "billing.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "billing.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "billing.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/samples/BikeSharingApp/Billing/charts/billing/templates/deployment.yaml b/samples/BikeSharingApp/Billing/charts/billing/templates/deployment.yaml new file mode 100644 index 000000000..a025b523f --- /dev/null +++ b/samples/BikeSharingApp/Billing/charts/billing/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: {{ template "billing.fullname" . }} + labels: + app: {{ template "billing.name" . }} + chart: {{ template "billing.chart" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "billing.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "billing.name" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + annotations: + buildID: {{ .Values.buildID }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + {{- if .Values.probes.enabled }} + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + {{- end }} + env: + {{- $root := . }} + {{- range $ref, $values := .Values.secrets }} + {{- range $key, $value := $values }} + - name: {{ $ref }}_{{ $key }} + valueFrom: + secretKeyRef: + name: {{ template "billing.fullname" $root }}-{{ $ref | lower }} + key: {{ $key }} + {{- end }} + {{- end }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/samples/BikeSharingApp/Billing/charts/billing/templates/ingress.yaml b/samples/BikeSharingApp/Billing/charts/billing/templates/ingress.yaml new file mode 100644 index 000000000..e0ff00a2e --- /dev/null +++ b/samples/BikeSharingApp/Billing/charts/billing/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "billing.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "billing.name" . }} + chart: {{ template "billing.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/samples/BikeSharingApp/Billing/charts/billing/templates/secrets.yaml b/samples/BikeSharingApp/Billing/charts/billing/templates/secrets.yaml new file mode 100644 index 000000000..08c8ece93 --- /dev/null +++ b/samples/BikeSharingApp/Billing/charts/billing/templates/secrets.yaml @@ -0,0 +1,12 @@ +{{- $root := . }} +{{- range $name, $values := .Values.secrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "billing.fullname" $root }}-{{ $name | lower }} +data: + {{- range $key, $value := $values }} + {{ $key }}: {{ $value | b64enc }} + {{- end }} +--- +{{- end }} diff --git a/samples/BikeSharingApp/Billing/charts/billing/templates/service.yaml b/samples/BikeSharingApp/Billing/charts/billing/templates/service.yaml new file mode 100644 index 000000000..29e1af2a5 --- /dev/null +++ b/samples/BikeSharingApp/Billing/charts/billing/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "billing.fullname" . }} + labels: + app: {{ template "billing.name" . }} + chart: {{ template "billing.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app: {{ template "billing.name" . }} + release: {{ .Release.Name }} diff --git a/samples/BikeSharingApp/Billing/charts/billing/values.yaml b/samples/BikeSharingApp/Billing/charts/billing/values.yaml new file mode 100644 index 000000000..d7c2107d5 --- /dev/null +++ b/samples/BikeSharingApp/Billing/charts/billing/values.yaml @@ -0,0 +1,65 @@ +# Default values for billing. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +fullnameOverride: billing +replicaCount: 1 +image: + repository: azdspublic/bikesharing-billing + tag: build.20190418.1 + pullPolicy: IfNotPresent +imagePullSecrets: [] + # Optionally specify an array of imagePullSecrets. + # Secrets must be manually created in the namespace. + # ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod + # + # This uses credentials from secret "myRegistryKeySecretName". + # - name: myRegistryKeySecretName +service: + type: ClusterIP + port: 80 + +probes: + enabled: false + +ingress: + enabled: false + annotations: + kubernetes.io/ingress.class: addon-http-application-routing + path: / + # hosts: + # - chart-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local +secrets: + mongo: + connectionstring: mongodb://databases-mongo + dbname: billing + # Optionally specify a set of secret objects whose values + # will be injected as environment variables by default. + # You should add this section to a file like secrets.yaml + # that is explicitly NOT committed to source code control + # and then include it as part of your helm install step. + # ref: https://kubernetes.io/docs/concepts/configuration/secret/ + # + # This creates a secret "mysecret" and injects "mypassword" + # as the environment variable mysecret_mypassword=password. + # mysecret: + # mypassword: password +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi +nodeSelector: {} + +tolerations: [] + +affinity: {} \ No newline at end of file diff --git a/samples/BikeSharingApp/Billing/customer.go b/samples/BikeSharingApp/Billing/customer.go new file mode 100644 index 000000000..335f87fcc --- /dev/null +++ b/samples/BikeSharingApp/Billing/customer.go @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "encoding/json" + "errors" +) + +type Customer struct { + ID string `bson:"id" json:"id"` + UserID string `bson:"userId" json:"userId"` + CCNumber string `bson:"ccNumber" json:"ccNumber"` + CCExpiry string `bson:"ccExpiry" json:"ccExpiry"` + CCCCV string `bson:"ccCCV" json:"ccCCV"` +} + +// Serialize serializes a customer to JSON +func (cust Customer) Serialize() (string, error) { + val, err := json.Marshal(cust) + if err != nil { + return "", AddMyInfoToErr(err) + } + return string(val), nil +} + +func (cust Customer) Validate() error { + return ValidateCustomer(cust) +} + +func ValidateCustomer(cust Customer) error { + var errorSlice []string + var zeroString string + + if cust.ID != zeroString { + errorSlice = append(errorSlice, "Must not specify ID string") + } + if cust.UserID == zeroString { + errorSlice = append(errorSlice, "Must specify UserID string") + } + if cust.CCNumber == zeroString { + errorSlice = append(errorSlice, "Must specify CCNumber string") + } + if cust.CCExpiry == zeroString { + errorSlice = append(errorSlice, "Must specify CCExpiry string") + } + if cust.CCCCV == zeroString { + errorSlice = append(errorSlice, "Must specify CCCCV string") + } + + if len(errorSlice) > 0 { + errorBytes, err := json.Marshal(errorSlice) + if err != nil { + return AddMyInfoToErr(err) + } + + return errors.New(string(errorBytes)) + } + + return nil +} diff --git a/samples/BikeSharingApp/Billing/db.go b/samples/BikeSharingApp/Billing/db.go new file mode 100644 index 000000000..4417a4e9d --- /dev/null +++ b/samples/BikeSharingApp/Billing/db.go @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "crypto/tls" + "fmt" + "net" + "sync" + "time" + + "strings" + + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +type MongoDbConnection struct { + Name string + session *mgo.Session + dbName string + invoiceDb *mgo.Collection + vendorDb *mgo.Collection + customerDb *mgo.Collection + shutdownWg *sync.WaitGroup + isShutdown bool +} + +type invoiceDbEntity struct { + ID bson.ObjectId `bson:"_id" json:"_id"` + Invoice Invoice `bson:"invoice" json:"invoice"` +} + +type vendorDbEntity struct { + ID bson.ObjectId `bson:"_id" json:"_id"` + Vendor Vendor `bson:"vendor" json:"vendor"` +} + +type customerDbEntity struct { + ID bson.ObjectId `bson:"_id" json:"_id"` + Customer Customer `bson:"customer" json:"customer"` +} + +const ( + InvoiceCollection = "Invoice" + VendorCollection = "Vendor" + CustomerCollection = "Customer" +) + +func (dbConn *MongoDbConnection) AddInvoice(context *RequestContext, inv Invoice) (bson.ObjectId, error) { + objectID := bson.NewObjectId() + err := insertDb(dbConn.invoiceDb, invoiceDbEntity{objectID, inv}) + if err != nil { + err = fmt.Errorf("Inserting Invoice: %v", err) + } + return objectID, err +} + +func getInvoicesWithQuery(dbConn *MongoDbConnection, context *RequestContext, query bson.M) ([]Invoice, error) { + var userInvoiceEntities []invoiceDbEntity + err := findQueryDb(dbConn.invoiceDb, query, &userInvoiceEntities) + if err != nil { + return nil, fmt.Errorf("Querying for user invoices: %v", err) + } + if userInvoiceEntities == nil { + return nil, nil + } + + userInvoices := make([]Invoice, len(userInvoiceEntities)) + for i, entity := range userInvoiceEntities { + entity.Invoice.ID = entity.ID.Hex() + userInvoices[i] = entity.Invoice + } + return userInvoices, nil +} + +func (dbConn *MongoDbConnection) GetCustomerInvoices(context *RequestContext, userID string) ([]Invoice, error) { + invoices, err := getInvoicesWithQuery(dbConn, context, bson.M{"invoice.customerId": userID}) + return invoices, err +} + +func (dbConn *MongoDbConnection) GetVendorInvoices(context *RequestContext, userID string) ([]Invoice, error) { + invoices, err := getInvoicesWithQuery(dbConn, context, bson.M{"invoice.vendorId": userID}) + return invoices, err +} + +func (dbConn *MongoDbConnection) GetInvoiceById(context *RequestContext, ID string) (Invoice, bool, error) { + var invEntity invoiceDbEntity + err := findByIDDb(dbConn.invoiceDb, ID, &invEntity) + if err != nil { + switch err { + case mgo.ErrNotFound: + return Invoice{}, false, nil + default: + return Invoice{}, false, fmt.Errorf("Getting Invoice by ID: %v", err) + } + } + + invEntity.Invoice.ID = invEntity.ID.Hex() + return invEntity.Invoice, true, nil +} + +func (dbConn *MongoDbConnection) GetInvoiceForReservationId(context *RequestContext, reservationId string) (Invoice, bool, error) { + invoices, err := getInvoicesWithQuery(dbConn, context, bson.M{"invoice.reservationId": reservationId}) + if err != nil { + return Invoice{}, false, err + } + if len(invoices) == 0 { + return Invoice{}, false, nil + } + + return invoices[0], true, err +} + +func (dbConn *MongoDbConnection) AddVendor(context *RequestContext, ven Vendor) (bson.ObjectId, error) { + objectID := bson.NewObjectId() + err := insertDb(dbConn.vendorDb, vendorDbEntity{objectID, ven}) + if err != nil { + err = fmt.Errorf("Inserting Vendor: %v", err) + } + return objectID, err +} + +func (dbConn *MongoDbConnection) UpdateVendorByUserId(context *RequestContext, ven Vendor) error { + entity := vendorDbEntity{Vendor: ven} + if err := updateDb(dbConn.vendorDb, bson.M{"vendor.userId": ven.UserID}, entity); err != nil { + return fmt.Errorf("Updating Vendor: %v", err) + } + return nil +} + +func (dbConn *MongoDbConnection) GetVendorByUserId(context *RequestContext, userID string) (Vendor, bool, error) { + var venEntity []vendorDbEntity + err := findQueryDb(dbConn.vendorDb, bson.M{"vendor.userId": userID}, &venEntity) + if err != nil { + return Vendor{}, false, fmt.Errorf("Getting Vendor by ID: %v", err) + } + if venEntity == nil { + // Vendor not found + return Vendor{}, false, nil + } + + if len(venEntity) > 1 { + LogErrFormatWithContext(context, "Found %d vendors in DB with UserID '%s'", len(venEntity), userID) + } + + venEntity[0].Vendor.ID = venEntity[0].ID.Hex() + return venEntity[0].Vendor, true, nil +} + +func (dbConn *MongoDbConnection) AddCustomer(context *RequestContext, cust Customer) (bson.ObjectId, error) { + objectID := bson.NewObjectId() + err := insertDb(dbConn.customerDb, customerDbEntity{objectID, cust}) + if err != nil { + err = fmt.Errorf("Inserting Customer: %v", err) + } + return objectID, err +} + +func (dbConn *MongoDbConnection) UpdateCustomerByUserId(context *RequestContext, cust Customer) error { + entity := customerDbEntity{Customer: cust} + if err := updateDb(dbConn.customerDb, bson.M{"customer.userId": cust.UserID}, entity); err != nil { + return fmt.Errorf("Updating Customer: %v", err) + } + return nil +} + +func (dbConn *MongoDbConnection) GetCustomerByUserId(context *RequestContext, userID string) (Customer, bool, error) { + var custEntity []customerDbEntity + err := findQueryDb(dbConn.customerDb, bson.M{"customer.userId": userID}, &custEntity) + if err != nil { + return Customer{}, false, fmt.Errorf("Getting Customer by ID: %v", err) + } + if custEntity == nil { + // Customer not found + return Customer{}, false, nil + } + + if len(custEntity) > 1 { + LogErrFormatWithContext(context, "Found %d customers in DB with UserID '%s'", len(custEntity), userID) + } + + custEntity[0].Customer.ID = custEntity[0].ID.Hex() + return custEntity[0].Customer, true, nil +} + +func (dbConn *MongoDbConnection) Ping() error { + return dbConn.session.Ping() +} + +func (dbConn *MongoDbConnection) Shutdown() { + if !dbConn.isShutdown { + dbConn.session.Close() + dbConn.log("MongoDb connection closed") + dbConn.shutdownWg.Done() + dbConn.isShutdown = true + } else { + LogErrFormat("Multiple Shutdown() called for '%s'", dbConn.Name) + } +} + +func (dbConn *MongoDbConnection) log(format string, args ...interface{}) { + logMessageTo(stdLogger, fmt.Sprintf("DB '%s': %s", dbConn.Name, format), args...) +} + +func (dbConn *MongoDbConnection) logerr(format string, args ...interface{}) { + logMessageTo(errLogger, fmt.Sprintf("DB '%s': %s", dbConn.Name, format), args...) +} + +func NewDbConnection(connectionName, connectionString, dbName string, shutdownWg *sync.WaitGroup) (*MongoDbConnection, error) { + dbConn := &MongoDbConnection{ + Name: connectionName, + dbName: dbName, + shutdownWg: shutdownWg, + isShutdown: false, + } + + editedConnectionString := strings.Replace(connectionString, "ssl=true", "", -1) // 'ssl=true' not supported by mgo + info, err := mgo.ParseURL(editedConnectionString) + if err != nil { + return nil, fmt.Errorf("Couldn't parse mongo connection string: %v", err) + } + + if connectionString != editedConnectionString { + // Define override DialServer func that connects via SSL + info.DialServer = func(addr *mgo.ServerAddr) (net.Conn, error) { + conn, err := tls.Dial("tcp", addr.String(), nil) + if err != nil { + err = AddMyInfoToErr(fmt.Errorf("Couldn't dial secure TCP connection to database: %v", err)) + } + return conn, err + } + } + + dbConn.log("Dialing MongoDb (%q)", info.Addrs) + maxTries := 5 + for i := 1; i <= maxTries; i++ { + dbConn.session, err = mgo.DialWithInfo(info) + if err == nil { + break + } + + if i < maxTries { + dbConn.logerr("%d/%d - Couldn't connect, sleeping and trying again", i, maxTries) + time.Sleep(1 * time.Second) + } else { + dbConn.logerr("%d/%d - Couldn't connect.", i, maxTries) + } + } + if err != nil { + return nil, fmt.Errorf("Failed to dial db (%s): %v", connectionString, err) + } + dbConn.log("Got MongoDb connection") + + dbConn.invoiceDb = dbConn.session.DB(dbName).C(InvoiceCollection) + dbConn.vendorDb = dbConn.session.DB(dbName).C(VendorCollection) + dbConn.customerDb = dbConn.session.DB(dbName).C(CustomerCollection) + + dbConn.shutdownWg.Add(1) + return dbConn, nil +} + +func updateDb(db *mgo.Collection, selector bson.M, entity interface{}) error { + if err := db.Update(selector, entity); err != nil { + return err + } + + return nil +} + +func insertDb(db *mgo.Collection, entity interface{}) error { + if err := db.Insert(entity); err != nil { + return err + } + + return nil +} + +func findQueryDb(db *mgo.Collection, query bson.M, result interface{}) error { + err := db.Find(query).All(result) // NOTE: may cause out of memory + if err != nil { + return err + } + + return nil +} + +func findByIDDb(db *mgo.Collection, ID string, result interface{}) error { + if !bson.IsObjectIdHex(ID) { + return fmt.Errorf("'%s' is not a valid Mongo ObjectId", ID) + } + err := db.FindId(bson.ObjectIdHex(ID)).One(result) + if err != nil { + return err + } + + return nil +} diff --git a/samples/BikeSharingApp/Billing/errors.go b/samples/BikeSharingApp/Billing/errors.go new file mode 100644 index 000000000..dab6ea346 --- /dev/null +++ b/samples/BikeSharingApp/Billing/errors.go @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "fmt" + "path/filepath" + "runtime" + "strings" +) + +// AddMyInfoToErr gets the callers info and prepends to the error +func AddMyInfoToErr(err error) error { + return AddCallerInfoToErr(err, 2) // '2' to get the calling function +} + +// AddCallerInfoToErr adds the callers filename, function name, and line number to the provided error and returns a new one +// 'numFramesToSkip' specifies the number of frames to skip for grabbing this info +func AddCallerInfoToErr(err error, numFramesToSkip int) error { + funcPtr, fileName, line, ok := runtime.Caller(numFramesToSkip) + if !ok { + LogErrFormat("couldn't get caller info") + return err + } + + _, fileName = filepath.Split(fileName) + functionName := runtime.FuncForPC(funcPtr).Name() + functionName = strings.Split(functionName, ".")[1] + + return fmt.Errorf("%s:%s:%d: %v", fileName, functionName, line, err) +} diff --git a/samples/BikeSharingApp/Billing/httphandlers.go b/samples/BikeSharingApp/Billing/httphandlers.go new file mode 100644 index 000000000..912f7667b --- /dev/null +++ b/samples/BikeSharingApp/Billing/httphandlers.go @@ -0,0 +1,469 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "gopkg.in/mgo.v2/bson" + + "github.com/gorilla/mux" + + uuid "github.com/nu7hatch/gouuid" +) + +type handlerResult struct { + Message string + ResponseCode int + Error error +} + +type helloResponse struct { + Status string +} + +type RequestContext struct { + RequestID *uuid.UUID +} + +const ( + RequestIDHeaderName = "x-contoso-request-id" +) + +type EndpointHandler func(req *http.Request, context *RequestContext) *handlerResult + +type EndpointHandlerNoContext func(req *http.Request, context *RequestContext) *handlerResult + +func (handler EndpointHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + serveHTTPInner(handler, rw, req, true) +} + +func (handler EndpointHandlerNoContext) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + serveHTTPInner(EndpointHandler(handler), rw, req, false) +} + +func serveHTTPInner(handler EndpointHandler, rw http.ResponseWriter, req *http.Request, requireContext bool) { + startTime := time.Now() + result := &handlerResult{} + + requestContext, err := getRequestContext(req) + if !requireContext && err != nil { + requestContext = &RequestContext{&uuid.UUID{}} + err = nil + } + if err != nil { + result.ResponseCode = http.StatusBadRequest + result.Message = err.Error() + } else if result = handler(req, requestContext); result == nil || (result.ResponseCode == 0 && result.Error == nil) { + panic("Handler returned bad handlerResult!") + } + + defer func() { logRequestEnd(req.Method, req.URL.Path, startTime, result.ResponseCode, requestContext) }() + + if result.Error != nil { + result.ResponseCode = http.StatusInternalServerError + result.Message = result.Error.Error() + LogErrFormatWithContext(requestContext, "Returning 500 error: %s", result.Message) + } + + rw.WriteHeader(result.ResponseCode) + + if result.Message != "" { + if _, err := fmt.Fprintf(rw, "%s\n", result.Message); err != nil { + result.ResponseCode = http.StatusInternalServerError + rw.WriteHeader(http.StatusInternalServerError) + LogErrorWithContext(requestContext, AddMyInfoToErr(err)) + LogErrFormatWithContext(requestContext, "Message: %v", result.Message) + } + } +} + +func logRequestEnd(httpMethod string, endpoint string, startTime time.Time, responseCode int, requestContext *RequestContext) { + requestID := "nil" + if requestContext != nil { + requestID = requestContext.RequestID.String() + } + + milliseconds := float64(time.Since(startTime).Nanoseconds()) / float64(time.Millisecond) + Log("%s %s - %.2fms %d - %s", httpMethod, endpoint, milliseconds, responseCode, requestID) +} + +func getRequestContext(req *http.Request) (*RequestContext, error) { + reqID := req.Header.Get(RequestIDHeaderName) + reqUUID, err := uuid.ParseHex(reqID) + if err != nil { + return nil, fmt.Errorf("Couldn't parse %s header: %v", RequestIDHeaderName, err) + } + return &RequestContext{reqUUID}, nil +} + +// HelloHandler handles the /hello endpoint +func HelloHandler(req *http.Request, context *RequestContext) *handlerResult { + data := helloResponse{Status: "Hello!"} + encodedData, err := json.Marshal(data) + if err != nil { + return &handlerResult{Error: AddMyInfoToErr(err)} + } + + return &handlerResult{ResponseCode: http.StatusOK, Message: string(encodedData)} +} + +func checkInvoicesForUser(req *http.Request, context *RequestContext, userID string, invoices []Invoice, result *handlerResult) { + if invoices == nil { + result.ResponseCode = http.StatusNotFound + result.Message = fmt.Sprintf("Could not find any invoices for vendor ID: (%s)", userID) + return + } + + invoicesBytes, err := json.Marshal(invoices) + if err != nil { + result.Error = AddMyInfoToErr(err) + return + } + + result.Message = string(invoicesBytes) + result.ResponseCode = http.StatusOK +} + +func GetInvoicesForVendorHandler(req *http.Request, context *RequestContext) (result *handlerResult) { + result = &handlerResult{} + + vars := mux.Vars(req) + userID := vars["userID"] + invoices, err := DbConnection.GetVendorInvoices(context, userID) + if err != nil { + result.Error = err + return + } + checkInvoicesForUser(req, context, userID, invoices, result) + return +} + +func GetInvoicesForCustomerHandler(req *http.Request, context *RequestContext) (result *handlerResult) { + result = &handlerResult{} + + vars := mux.Vars(req) + userID := vars["userID"] + invoices, err := DbConnection.GetCustomerInvoices(context, userID) + if err != nil { + result.Error = err + return + } + checkInvoicesForUser(req, context, userID, invoices, result) + return +} + +func GetInvoiceHandler(req *http.Request, context *RequestContext) (result *handlerResult) { + result = &handlerResult{} + + vars := mux.Vars(req) + invoiceID := vars["id"] + if !bson.IsObjectIdHex(invoiceID) { + result.ResponseCode = http.StatusBadRequest + result.Message = fmt.Sprintf("(%s) is not a valid invoiceID", invoiceID) + return + } + invoice, ok, err := DbConnection.GetInvoiceById(context, invoiceID) + if err != nil { + result.Error = err + return + } + if !ok { + result.ResponseCode = http.StatusNotFound + result.Message = fmt.Sprintf("Could not find invoice with ID: (%s)", invoiceID) + return + } + + result.Message, err = invoice.Serialize() + if err != nil { + result.Error = err + } else { + result.ResponseCode = http.StatusOK + } + return +} + +func GetInvoiceForReservationIdHandler(req *http.Request, context *RequestContext) (result *handlerResult) { + result = &handlerResult{} + + vars := mux.Vars(req) + reservationID := vars["resID"] + invoice, ok, err := DbConnection.GetInvoiceForReservationId(context, reservationID) + if err != nil { + result.Error = err + return + } + if !ok { + result.ResponseCode = http.StatusNotFound + result.Message = fmt.Sprintf("Could not find an invoice for reservation ID: (%s)", reservationID) + return + } + + result.Message, err = invoice.Serialize() + if err != nil { + result.Error = err + } else { + result.ResponseCode = http.StatusOK + } + return +} + +func GetVendorByUserIdHandler(req *http.Request, context *RequestContext) (result *handlerResult) { + result = &handlerResult{} + + vars := mux.Vars(req) + userID := vars["userID"] + vendor, ok, err := DbConnection.GetVendorByUserId(context, userID) + if err != nil { + result.Error = err + return + } + if !ok { + result.ResponseCode = http.StatusNotFound + result.Message = fmt.Sprintf("Could not find vendor with UserID: (%s)", userID) + return + } + + result.Message, err = vendor.Serialize() + if err != nil { + result.Error = err + } else { + result.ResponseCode = http.StatusOK + } + return +} + +func GetCustomerByUserIdHandler(req *http.Request, context *RequestContext) (result *handlerResult) { + result = &handlerResult{} + + vars := mux.Vars(req) + userID := vars["userID"] + customer, ok, err := DbConnection.GetCustomerByUserId(context, userID) + if err != nil { + result.Error = err + return + } + if !ok { + result.ResponseCode = http.StatusNotFound + result.Message = fmt.Sprintf("Could not find customer with UserID: (%s)", userID) + return + } + + result.Message, err = customer.Serialize() + if err != nil { + result.Error = err + } else { + result.ResponseCode = http.StatusOK + } + return +} + +// NewInvoiceHandler handles the /invoice endpoint +func NewInvoiceHandler(req *http.Request, context *RequestContext) (result *handlerResult) { + result = &handlerResult{} + + decoder := json.NewDecoder(req.Body) + inv := Invoice{} + // Deserialize Invoice + if err := decoder.Decode(&inv); err != nil { + result.ResponseCode = http.StatusBadRequest + result.Message = err.Error() + return + } + if err := inv.Validate(); err != nil { + result.ResponseCode = http.StatusBadRequest + result.Message = err.Error() + return + } + + // Add the invoice + LogWithContext(context, "Adding invoice for reservation (%s)", inv.ReservationID) + dbID, err := DbConnection.AddInvoice(context, inv) + if err != nil { + result.Error = err + return + } + inv.ID = dbID.Hex() + + LogWithContext(context, "Added invoice to db (dbID: %s), processing payment", inv.ID) + /* + * Process invoice payment here + */ + LogWithContext(context, "Payment processing done") + LogWithContext(context, "Invoice complete for reservation (%s)", inv.ReservationID) + + result.Message, err = inv.Serialize() + if err != nil { + result.Error = err + } else { + result.ResponseCode = http.StatusOK + } + return +} + +func NewVendorHandler(req *http.Request, context *RequestContext) (result *handlerResult) { + result = &handlerResult{} + + decoder := json.NewDecoder(req.Body) + ven := Vendor{} + // Deserialize Vendor + if err := decoder.Decode(&ven); err != nil { + result.ResponseCode = http.StatusBadRequest + result.Message = err.Error() + return + } + if err := ven.Validate(); err != nil { + result.ResponseCode = http.StatusBadRequest + result.Message = err.Error() + return + } + + // Add the vendor + LogWithContext(context, "Adding new vendor") + dbID, err := DbConnection.AddVendor(context, ven) + if err != nil { + result.Error = err + return + } + LogWithContext(context, "Added new vendor (dbID: %s)", dbID.Hex()) + ven.ID = dbID.Hex() + + result.Message, err = ven.Serialize() + if err != nil { + result.Error = err + } else { + result.ResponseCode = http.StatusOK + } + return +} + +func UpdateVendorHandler(req *http.Request, context *RequestContext) (result *handlerResult) { + result = &handlerResult{} + + decoder := json.NewDecoder(req.Body) + ven := Vendor{} + // Deserialize Vendor + if err := decoder.Decode(&ven); err != nil { + result.ResponseCode = http.StatusBadRequest + result.Message = err.Error() + return + } + if err := ven.Validate(); err != nil { + result.ResponseCode = http.StatusBadRequest + result.Message = err.Error() + return + } + + // Update the vendor + LogWithContext(context, "Updating vendor") + err := DbConnection.UpdateVendorByUserId(context, ven) + if err != nil { + result.Error = err + return + } + LogWithContext(context, "Updated vendor (userID: %s)", ven.UserID) + + var ok bool + ven, ok, err = DbConnection.GetVendorByUserId(context, ven.UserID) + if err != nil { + result.Error = err + return + } + if !ok { + result.Error = fmt.Errorf("Couldn't get the updated Vendor") + return + } + + result.Message, err = ven.Serialize() + if err != nil { + result.Error = err + } else { + result.ResponseCode = http.StatusOK + } + return +} + +func NewCustomerHandler(req *http.Request, context *RequestContext) (result *handlerResult) { + result = &handlerResult{} + + decoder := json.NewDecoder(req.Body) + cust := Customer{} + // Deserialize Customer + if err := decoder.Decode(&cust); err != nil { + result.ResponseCode = http.StatusBadRequest + result.Message = err.Error() + } + if err := cust.Validate(); err != nil { + result.ResponseCode = http.StatusBadRequest + result.Message = err.Error() + return + } + + // Add the Customer + LogWithContext(context, "Adding new customer") + dbID, err := DbConnection.AddCustomer(context, cust) + if err != nil { + result.Error = err + return + } + LogWithContext(context, "Added new customer (dbID: %s)", dbID.Hex()) + cust.ID = dbID.Hex() + + result.Message, err = cust.Serialize() + if err != nil { + result.Error = err + } else { + result.ResponseCode = http.StatusOK + } + return +} + +func UpdateCustomerHandler(req *http.Request, context *RequestContext) (result *handlerResult) { + result = &handlerResult{} + + decoder := json.NewDecoder(req.Body) + cust := Customer{} + // Deserialize Customer + if err := decoder.Decode(&cust); err != nil { + result.ResponseCode = http.StatusBadRequest + result.Message = err.Error() + return + } + if err := cust.Validate(); err != nil { + result.ResponseCode = http.StatusBadRequest + result.Message = err.Error() + return + } + + // Update the customer + LogWithContext(context, "Updating customer") + err := DbConnection.UpdateCustomerByUserId(context, cust) + if err != nil { + result.Error = err + return + } + LogWithContext(context, "Updated customer (userID: %s)", cust.UserID) + + var ok bool + cust, ok, err = DbConnection.GetCustomerByUserId(context, cust.UserID) + if err != nil { + result.Error = err + return + } + if !ok { + result.Error = fmt.Errorf("Couldn't get the updated Customer") + return + } + + result.Message, err = cust.Serialize() + if err != nil { + result.Error = err + } else { + result.ResponseCode = http.StatusOK + } + return +} diff --git a/samples/BikeSharingApp/Billing/invoice.go b/samples/BikeSharingApp/Billing/invoice.go new file mode 100644 index 000000000..b17677dd2 --- /dev/null +++ b/samples/BikeSharingApp/Billing/invoice.go @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "encoding/json" + "errors" +) + +// Invoice defines the expected data for Invoices +type Invoice struct { + ID string `bson:"id" json:"id"` + CustomerID string `bson:"customerId" json:"customerId"` + VendorID string `bson:"vendorId" json:"vendorId"` + BikeID string `bson:"bikeId" json:"bikeId"` + ReservationID string `bson:"reservationId" json:"reservationId"` + Amount float32 `bson:"amount" json:"amount"` +} + +// Serialize serializes an invoice to JSON +func (inv Invoice) Serialize() (string, error) { + val, err := json.Marshal(inv) + if err != nil { + return "", AddMyInfoToErr(err) + } + return string(val), nil +} + +// Validate calls ValidateInvoice(inv) for the Invoice struct +func (inv Invoice) Validate() error { + return ValidateInvoice(inv) +} + +// ValidateInvoice checks an invoice and returns a non-nil error if any issues +func ValidateInvoice(inv Invoice) error { + var errorSlice []string + var zeroString string + + if inv.ID != zeroString { + errorSlice = append(errorSlice, "Must not specify ID string") + } + if inv.BikeID == zeroString { + errorSlice = append(errorSlice, "Must specify BikeID string") + } + if inv.CustomerID == zeroString { + errorSlice = append(errorSlice, "Must specify CustomerID string") + } + if inv.ReservationID == zeroString { + errorSlice = append(errorSlice, "Must specify ReservationID string") + } + if inv.VendorID == zeroString { + errorSlice = append(errorSlice, "Must specify VendorID string") + } + + // TODO validate that passed in userIDs/customerIDs are valid + + if len(errorSlice) > 0 { + errorBytes, err := json.Marshal(errorSlice) + if err != nil { + return AddMyInfoToErr(err) + } + + return errors.New(string(errorBytes)) + } + + return nil +} diff --git a/samples/BikeSharingApp/Billing/logger.go b/samples/BikeSharingApp/Billing/logger.go new file mode 100644 index 000000000..7ec921d7f --- /dev/null +++ b/samples/BikeSharingApp/Billing/logger.go @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "fmt" + "log" + "os" +) + +var flags = log.Flags() | log.LUTC | log.Lmicroseconds | log.Lshortfile +var stdLogger = log.New(os.Stdout, "", flags) +var errLogger = log.New(os.Stderr, "Error: ", flags) + +// Log logs a standard message +func Log(format string, a ...interface{}) { + logMessageTo(stdLogger, format, a...) +} + +func LogWithContext(context *RequestContext, format string, a ...interface{}) { + logMessageTo(stdLogger, fmt.Sprintf("%s - %s", context.RequestID.String(), format), a...) +} + +// LogErrFormat logs an error message +func LogErrFormat(format string, a ...interface{}) { + logMessageTo(errLogger, format, a...) +} + +func LogErrFormatWithContext(context *RequestContext, format string, a ...interface{}) { + logMessageTo(errLogger, fmt.Sprintf("%s - %s", context.RequestID.String(), format), a...) +} + +// LogError logs an error type +func LogError(err error) { + logMessageTo(errLogger, fmt.Sprintf("%v", err)) +} + +func LogErrorWithContext(context *RequestContext, err error) { + logMessageTo(errLogger, fmt.Sprintf("%s - %v", context.RequestID.String(), err)) +} + +func logMessageTo(out *log.Logger, format string, a ...interface{}) { + output := fmt.Sprintf(format, a...) + out.Output(3, output) +} diff --git a/samples/BikeSharingApp/Billing/main.go b/samples/BikeSharingApp/Billing/main.go new file mode 100644 index 000000000..95439c52a --- /dev/null +++ b/samples/BikeSharingApp/Billing/main.go @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "time" + + "os/signal" + "syscall" + + "sync" + + "github.com/gorilla/mux" +) + +const ( + mongoDbConnectionStringEnvName = "mongo_connectionstring" + mongoDbNameEnvName = "mongo_dbname" +) + +var ( + EnvMongoDbConnectionString = os.Getenv(mongoDbConnectionStringEnvName) + EnvMongoDbName = os.Getenv(mongoDbNameEnvName) +) + +var ( + DbConnection *MongoDbConnection +) + +var envOpts = map[string]string{ + mongoDbConnectionStringEnvName: EnvMongoDbConnectionString, + mongoDbNameEnvName: EnvMongoDbName, +} + +const ( + listenPort = 80 +) + +var ShutdownSignal = sync.NewCond(&sync.Mutex{}) +var ShutdownWaitGroup = &sync.WaitGroup{} + +func init() { + Log("Billing init()") + var err error + + envOptsJSON, err := json.Marshal(envOpts) + if err != nil { + LogError(err) + shutdown() + } + Log("Environment options: %s", string(envOptsJSON)) + if EnvMongoDbConnectionString == "" || + EnvMongoDbName == "" { + requiredParams := []string{ + mongoDbConnectionStringEnvName, + mongoDbNameEnvName, + } + LogErrFormat("Must specify the following environment variables: %s", strings.Join(requiredParams, ", ")) + os.Exit(1) + } + + DbConnection, err = NewDbConnection("DbConnection", EnvMongoDbConnectionString, EnvMongoDbName, ShutdownWaitGroup) + // DbConnection.collection.remove() + if err != nil { + LogErrFormat("MongoDb connection: %v", err) + LogErrFormat("Exiting") + shutdown() + } + go listenForUnexpectedMongoDbShutdown(DbConnection) + + // Define a channel that will be called when the OS wants the program to exit + // This will be used to gracefully shutdown the consumer + osChan := make(chan os.Signal, 5) + signal.Notify(osChan, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGABRT, syscall.SIGQUIT) + go func() { + LogErrFormat("OS signal received: %v", <-osChan) + shutdown() + }() +} + +func main() { + Log("Billing startup") + + Log("Setting up HTTP handlers") + r := mux.NewRouter() + r.Handle("/hello", EndpointHandlerNoContext(HelloHandler)).Methods(http.MethodGet) + r.Handle("/api/invoice", EndpointHandler(NewInvoiceHandler)).Methods(http.MethodPost) + r.Handle("/api/invoice/{id}", EndpointHandler(GetInvoiceHandler)).Methods(http.MethodGet) + r.Handle("/api/customer", EndpointHandler(NewCustomerHandler)).Methods(http.MethodPost) + r.Handle("/api/customer", EndpointHandler(UpdateCustomerHandler)).Methods(http.MethodPatch) + r.Handle("/api/customer/{userID}", EndpointHandler(GetCustomerByUserIdHandler)).Methods(http.MethodGet) + r.Handle("/api/customer/{userID}/invoices", EndpointHandler(GetInvoicesForCustomerHandler)).Methods(http.MethodGet) + r.Handle("/api/vendor", EndpointHandler(NewVendorHandler)).Methods(http.MethodPost) + r.Handle("/api/vendor", EndpointHandler(UpdateVendorHandler)).Methods(http.MethodPatch) + r.Handle("/api/vendor/{userID}", EndpointHandler(GetVendorByUserIdHandler)).Methods(http.MethodGet) + r.Handle("/api/vendor/{userID}/invoices", EndpointHandler(GetInvoicesForVendorHandler)).Methods(http.MethodGet) + r.Handle("/api/reservation/{resID}/invoice", EndpointHandler(GetInvoiceForReservationIdHandler)).Methods(http.MethodGet) + + srv := &http.Server{ + Handler: r, + Addr: fmt.Sprintf("0.0.0.0:%d", listenPort), + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + go func() { + Log("Listening on %d...", listenPort) + LogErrFormat("Webserver shutdown unexpectedly!: %v", srv.ListenAndServe()) + shutdown() + }() + + ShutdownWaitGroup.Wait() + Log("Billing graceful shutdown.") +} + +var unexpectedShutdownOnce sync.Once + +func listenForUnexpectedMongoDbShutdown(conn *MongoDbConnection) { + shutdownChan := make(chan struct{}, 1) + go func() { + ShutdownSignal.L.Lock() + ShutdownSignal.Wait() + ShutdownSignal.L.Unlock() + shutdownChan <- struct{}{} + }() + + defer func() { + if r := recover(); r != nil { + LogErrFormat("Panic while pinging MongoDb: %v", r) + unexpectedShutdownOnce.Do(shutdown) + } + }() + +checkLoop: + for { + select { + case <-shutdownChan: + Log("'%s' graceful shutdown", conn.Name) + break checkLoop + default: + // Do nothing + } + + if err := conn.Ping(); err != nil { + LogErrFormat("'%s' MongoDbConnection shut down unexpectedly!", conn.Name) + unexpectedShutdownOnce.Do(shutdown) + } + + time.Sleep(3 * time.Second) + } +} + +func shutdown() { + Log("Shutting down!") + ShutdownSignal.Broadcast() + + if DbConnection != nil { + DbConnection.Shutdown() + } + + Log("Waiting for handlers to exit") + ShutdownWaitGroup.Wait() + Log("All handlers done.") +} diff --git a/samples/BikeSharingApp/Billing/vendor.go b/samples/BikeSharingApp/Billing/vendor.go new file mode 100644 index 000000000..8ecb1fc4b --- /dev/null +++ b/samples/BikeSharingApp/Billing/vendor.go @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "encoding/json" + "errors" +) + +type Vendor struct { + ID string `bson:"id" json:"id"` + UserID string `bson:"userId" json:"userId"` + RoutingNumber string `bson:"routingNumber" json:"routingNumber"` + AccountNumber string `bson:"accountNumber" json:"accountNumber"` +} + +// Serialize serializes a vendor to JSON +func (ven Vendor) Serialize() (string, error) { + val, err := json.Marshal(ven) + if err != nil { + return "", AddMyInfoToErr(err) + } + return string(val), nil +} + +// Validate calls ValidateVendor(ven) for the Vendor struct +func (ven Vendor) Validate() error { + return ValidateVendor(ven) +} + +func ValidateVendor(ven Vendor) error { + var errorSlice []string + var zeroString string + + if ven.UserID == zeroString { + errorSlice = append(errorSlice, "Must specify UserID string") + } + if ven.AccountNumber == zeroString { + errorSlice = append(errorSlice, "Must specify AccountNumber string") + } + if ven.RoutingNumber == zeroString { + errorSlice = append(errorSlice, "Must specify RoutingNumber string") + } + + if len(errorSlice) > 0 { + errorBytes, err := json.Marshal(errorSlice) + if err != nil { + return AddMyInfoToErr(err) + } + + return errors.New(string(errorBytes)) + } + + return nil +} diff --git a/samples/BikeSharingApp/Databases/azds.yaml b/samples/BikeSharingApp/Databases/azds.yaml new file mode 100644 index 000000000..4b2f87c4e --- /dev/null +++ b/samples/BikeSharingApp/Databases/azds.yaml @@ -0,0 +1,31 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: . + dockerfile: Dockerfile +install: + chart: charts/databases + values: + - values.dev.yaml? + - secrets.dev.yaml? + set: + replicaCount: 1 + image: + repository: databases + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + # This expands to [space.s.][rootSpace.]databases...azds.io + - $(spacePrefix)$(rootSpacePrefix)databases$(hostSuffix) +configurations: + develop: + build: + useGitIgnore: true + container: + sync: + - "!**/package.json" + iterate: + processesToKill: [node] \ No newline at end of file diff --git a/samples/BikeSharingApp/Databases/charts/databases/.helmignore b/samples/BikeSharingApp/Databases/charts/databases/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/samples/BikeSharingApp/Databases/charts/databases/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/samples/BikeSharingApp/Databases/charts/databases/Chart.yaml b/samples/BikeSharingApp/Databases/charts/databases/Chart.yaml new file mode 100644 index 000000000..29f92efb4 --- /dev/null +++ b/samples/BikeSharingApp/Databases/charts/databases/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: databases +version: 0.1.0 \ No newline at end of file diff --git a/samples/BikeSharingApp/Databases/charts/databases/templates/_helpers.tpl b/samples/BikeSharingApp/Databases/charts/databases/templates/_helpers.tpl new file mode 100644 index 000000000..8a95c2a35 --- /dev/null +++ b/samples/BikeSharingApp/Databases/charts/databases/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "databases.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "databases.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "databases.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/samples/BikeSharingApp/Databases/charts/databases/templates/mongo.yaml b/samples/BikeSharingApp/Databases/charts/databases/templates/mongo.yaml new file mode 100644 index 000000000..3fdd8dd82 --- /dev/null +++ b/samples/BikeSharingApp/Databases/charts/databases/templates/mongo.yaml @@ -0,0 +1,25 @@ +{{ if .Values.hostedMongo.enabled }} +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: {{ template "databases.fullname" . }}-mongo + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + replicas: 1 + template: + metadata: + labels: + app: {{ template "databases.fullname" . }}-mongo + spec: + containers: + - name: {{ .Chart.Name }}-mongo + image: "mongo:3.4" + imagePullPolicy: IfNotPresent + ports: + - containerPort: 27017 + resources: + requests: + cpu: 100m + memory: 512Mi +{{ end }} \ No newline at end of file diff --git a/samples/BikeSharingApp/Databases/charts/databases/templates/mongoService.yaml b/samples/BikeSharingApp/Databases/charts/databases/templates/mongoService.yaml new file mode 100644 index 000000000..86ae7e9ac --- /dev/null +++ b/samples/BikeSharingApp/Databases/charts/databases/templates/mongoService.yaml @@ -0,0 +1,17 @@ +{{ if .Values.hostedMongo.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "databases.fullname" . }}-mongo + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + type: ClusterIP + ports: + - port: 27017 + targetPort: 27017 + protocol: TCP + name: mongo + selector: + app: {{ template "databases.fullname" . }}-mongo +{{ end }} \ No newline at end of file diff --git a/samples/BikeSharingApp/Databases/charts/databases/templates/sql.yaml b/samples/BikeSharingApp/Databases/charts/databases/templates/sql.yaml new file mode 100644 index 000000000..8cb98d133 --- /dev/null +++ b/samples/BikeSharingApp/Databases/charts/databases/templates/sql.yaml @@ -0,0 +1,33 @@ +{{ if .Values.hostedSql.enabled }} +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: {{ template "databases.fullname" . }}-sql + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + replicas: 1 + template: + metadata: + labels: + app: {{ template "databases.fullname" . }}-sql + spec: + containers: + - name: {{ .Chart.Name }}-sql + image: "microsoft/mssql-server-linux" + imagePullPolicy: IfNotPresent + env: + - name: ACCEPT_EULA + value: "Y" + - name: SA_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "databases.fullname" . }}-sql + key: sql-password + ports: + - containerPort: 1433 + resources: + requests: + cpu: 400m + memory: 2Gi +{{ end }} \ No newline at end of file diff --git a/samples/BikeSharingApp/Databases/charts/databases/templates/sqlSecret.yaml b/samples/BikeSharingApp/Databases/charts/databases/templates/sqlSecret.yaml new file mode 100644 index 000000000..aa007cf0f --- /dev/null +++ b/samples/BikeSharingApp/Databases/charts/databases/templates/sqlSecret.yaml @@ -0,0 +1,14 @@ +{{ if .Values.hostedSql.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "databases.fullname" . }}-sql + labels: + app: {{ template "databases.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + sql-password: {{ .Values.hostedSql.password | b64enc | quote }} +{{ end }} \ No newline at end of file diff --git a/samples/BikeSharingApp/Databases/charts/databases/templates/sqlService.yaml b/samples/BikeSharingApp/Databases/charts/databases/templates/sqlService.yaml new file mode 100644 index 000000000..55417a57e --- /dev/null +++ b/samples/BikeSharingApp/Databases/charts/databases/templates/sqlService.yaml @@ -0,0 +1,17 @@ +{{ if .Values.hostedSql.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "databases.fullname" . }}-sql + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + type: ClusterIP + ports: + - port: 1433 + targetPort: 1433 + protocol: TCP + name: sql + selector: + app: {{ template "databases.fullname" . }}-sql +{{ end }} \ No newline at end of file diff --git a/samples/BikeSharingApp/Databases/charts/databases/values.yaml b/samples/BikeSharingApp/Databases/charts/databases/values.yaml new file mode 100644 index 000000000..42e3e2af5 --- /dev/null +++ b/samples/BikeSharingApp/Databases/charts/databases/values.yaml @@ -0,0 +1,9 @@ +# Default values for databases. +# This is a YAML-formatted file +# Declare variables to be passed into your templates. +fullnameOverride: databases +hostedSql: + enabled: true + password: "!DummyPassword123!" +hostedMongo: + enabled: true \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/.dockerignore b/samples/BikeSharingApp/Gateway/.dockerignore new file mode 100644 index 000000000..04f7b133d --- /dev/null +++ b/samples/BikeSharingApp/Gateway/.dockerignore @@ -0,0 +1,14 @@ +.dockerignore +.git +.gitignore +.vs +.vscode +**/*.*proj.user +**/azds.yaml +**/bin +**/charts +**/Dockerfile +**/Dockerfile.develop +**/obj +**/secrets.dev.yaml +**/values.dev.yaml \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/.gitignore b/samples/BikeSharingApp/Gateway/.gitignore new file mode 100644 index 000000000..f6b5ce05c --- /dev/null +++ b/samples/BikeSharingApp/Gateway/.gitignore @@ -0,0 +1,255 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +*.sln + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs +.vscode + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml diff --git a/samples/BikeSharingApp/Gateway/Constants.cs b/samples/BikeSharingApp/Gateway/Constants.cs new file mode 100644 index 000000000..5fe990207 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Constants.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app +{ + public class Constants + { + public const string UsersMicroserviceEnv = "users_dnsname"; + + public const string BikesMicroserviceEnv = "bikes_dnsname"; + + public const string ReservationsMicroserviceEnv = "reservation_dnsname"; + + public const string ReservationEngineMicroserviceEnv = "reservationengine_dnsname"; + + public const string BillingMicroserviceEnv = "billing_dnsname"; + + public const string RequestIdHeaderName = "x-contoso-request-id"; + + public const string RouteAsHeaderName = "azds-route-as"; + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/Controllers/BikeController.cs b/samples/BikeSharingApp/Gateway/Controllers/BikeController.cs new file mode 100644 index 000000000..0457002eb --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Controllers/BikeController.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using app.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace app.Controllers +{ + [Route("api/bike")] + public class BikeController : Controller + { + private string _bikesService { get; set; } + + private string _usersService { get; set; } + + private CustomConfiguration _customConfiguration { get; set; } + + public BikeController(IOptions customConfiguration) + { + _customConfiguration = customConfiguration.Value; + _bikesService = Environment.GetEnvironmentVariable(Constants.BikesMicroserviceEnv) ?? _customConfiguration.Services.Bikes; + _usersService = Environment.GetEnvironmentVariable(Constants.UsersMicroserviceEnv) ?? _customConfiguration.Services.Users; + } + + // GET: api/bike/1 + [HttpGet("{bikeId}")] + public async Task GetBike(string bikeId) + { + string getBikeUrl = $"http://{_bikesService}/api/bikes/{bikeId}"; + var response = await HttpHelper.GetAsync(getBikeUrl, this.Request); + if (response.IsSuccessStatusCode) + { + var bikeDetails = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + return new JsonResult(bikeDetails); + } + + return await HttpHelper.ReturnResponseResult(response); + } + + // PATCH: /api/bike/1 + [HttpPatch("{bikeId}")] + public async Task UpdateBike(string bikeId, [FromBody] AddUpdateBikeRequest newBikeDetails) + { + // Ensure ownerUserId for bike is valid + var checkOwnerResponse = await this._CheckOwnerUserIdIsValidVendor(newBikeDetails.OwnerUserId); + if (checkOwnerResponse != null) + { + return checkOwnerResponse; + } + + string getUpdateBikeUrl = $"http://{_bikesService}/api/bikes/{bikeId}"; + var getResponse = await HttpHelper.GetAsync(getUpdateBikeUrl, this.Request); + if (getResponse.IsSuccessStatusCode) + { + var existingBikeDetails = JsonConvert.DeserializeObject(await getResponse.Content.ReadAsStringAsync()); + var updatedBikeDetails = new AddUpdateBikeRequest + { + Model = string.IsNullOrEmpty(newBikeDetails.Model) ? existingBikeDetails.Model : newBikeDetails.Model, + HourlyCost = newBikeDetails.HourlyCost ?? existingBikeDetails.HourlyCost, + ImageUrl = newBikeDetails.ImageUrl ?? existingBikeDetails.ImageUrl, + Type = string.IsNullOrEmpty(newBikeDetails.Type) ? existingBikeDetails.Type : newBikeDetails.Type, + SuitableHeightInMeters = newBikeDetails.SuitableHeightInMeters ?? existingBikeDetails.SuitableHeightInMeters, + MaximumWeightInKg = newBikeDetails.MaximumWeightInKg ?? existingBikeDetails.MaximumWeightInKg, + OwnerUserId = existingBikeDetails.OwnerUserId + }; + + var updateResponse = await HttpHelper.PutAsync(getUpdateBikeUrl, new StringContent( + JsonConvert.SerializeObject(updatedBikeDetails), Encoding.UTF8, "application/json"), this.Request); + if (updateResponse.IsSuccessStatusCode) + { + return await GetBike(bikeId); + } + + return await HttpHelper.ReturnResponseResult(updateResponse); + } + + return await HttpHelper.ReturnResponseResult(getResponse); + } + + // DELETE: /api/bike/1 + [HttpDelete("{bikeId}")] + public async Task DeleteBike(string bikeId) + { + string getDeleteBikeUrl = $"http://{_bikesService}/api/bikes/{bikeId}"; + var getResponse = await HttpHelper.GetAsync(getDeleteBikeUrl, this.Request); + if (getResponse.IsSuccessStatusCode) + { + // Bike exists, proceed with deletion. + var deleteResponse = await HttpHelper.DeleteAsync(getDeleteBikeUrl, this.Request); + return await HttpHelper.ReturnResponseResult(deleteResponse); + } + + return await HttpHelper.ReturnResponseResult(getResponse); + } + + /// + /// Returns null on success + /// + /// + /// + private async Task _CheckOwnerUserIdIsValidVendor(string ownerUserId) + { + string getUserUrl = $"http://{_usersService}/api/users/{ownerUserId}"; + var getUserResponse = await HttpHelper.GetAsync(getUserUrl, this.Request); + if (!getUserResponse.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(getUserResponse); + } + + UserResponse user = JsonConvert.DeserializeObject(await getUserResponse.Content.ReadAsStringAsync()); + if (user == null) + { + return new ContentResult() + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = $"Unexpected object returned while checking owner user" + }; + } + if (user.Type != UserType.Vendor) + { + return BadRequest("Owner user type must be Vendor"); + } + + return null; + } + + // POST: /api/bike + [HttpPost] + public async Task AddBike([FromBody] AddUpdateBikeRequest newBikeDetails) + { + if (!ModelState.IsValid) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = JsonConvert.SerializeObject(ModelState.Values.SelectMany(v => v.Errors)) + }; + } + + // Ensure ownerUserId for bike is valid + var checkOwnerResponse = await this._CheckOwnerUserIdIsValidVendor(newBikeDetails.OwnerUserId); + if (checkOwnerResponse != null) + { + return checkOwnerResponse; + } + + string addBikeUrl = $"http://{_bikesService}/api/bikes"; + var response = await HttpHelper.PostAsync(addBikeUrl, new StringContent( + JsonConvert.SerializeObject(newBikeDetails), Encoding.UTF8, "application/json"), this.Request); + if (response.IsSuccessStatusCode) + { + var createdBike = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + return new JsonResult(createdBike); + } + + return await HttpHelper.ReturnResponseResult(response); + } + + // GET: /api/bike/availableBikes + // TODO: Add query filters. + [HttpGet("availableBikes")] + public async Task FindAvailableBikes() + { + string findAvailableBikesUrl = $"http://{_bikesService}/api/availableBikes"; + var response = await HttpHelper.GetAsync(findAvailableBikesUrl, this.Request); + if (response.IsSuccessStatusCode) + { + var foundBikes = JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); + return new JsonResult(foundBikes); + } + + return await HttpHelper.ReturnResponseResult(response); + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/Controllers/BillingController.cs b/samples/BikeSharingApp/Gateway/Controllers/BillingController.cs new file mode 100644 index 000000000..f1d85df0b --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Controllers/BillingController.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using app.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace app.Controllers +{ + [Route("api/billing")] + public class BillingController : Controller + { + private string _billingsService { get; set; } + + private CustomConfiguration _customConfiguration { get; set; } + + public BillingController(IOptions customConfiguration) + { + _customConfiguration = customConfiguration.Value; + _billingsService = Environment.GetEnvironmentVariable(Constants.BillingMicroserviceEnv) ?? _customConfiguration.Services.Billing; + } + + // GET: api/billing/invoice/1 + [HttpGet("invoice/{invoiceId}")] + public async Task GetInvoice(string invoiceId) + { + string getInvoiceUrl = $"http://{_billingsService}/api/invoice/{invoiceId}"; + var response = await HttpHelper.GetAsync(getInvoiceUrl, this.Request); + if (response.IsSuccessStatusCode) + { + var invoiceDetails = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + return new JsonResult(invoiceDetails); + } + + return await HttpHelper.ReturnResponseResult(response); + } + + // Not called from gateway. + public async Task CreateInvoice([FromBody] Invoice newInvoice) + { + string createInvoiceUrl = $"http://{_billingsService}/api/invoice"; + var response = await HttpHelper.PostAsync(createInvoiceUrl, new StringContent( + JsonConvert.SerializeObject(newInvoice), Encoding.UTF8, "application/json"), this.Request); + if (response.IsSuccessStatusCode) + { + var createdInvoice = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + return new JsonResult(createdInvoice); + } + + return await HttpHelper.ReturnResponseResult(response); + } + + // GET: api/billing/customer/1 + [HttpGet("customer/{customerId}")] + public async Task GetCustomerBillingData(string customerId) + { + string getCustomerDataUrl = $"http://{_billingsService}/api/customer/{customerId}"; + var response = await HttpHelper.GetAsync(getCustomerDataUrl, this.Request); + if (response.IsSuccessStatusCode) + { + var customerBillingDetails = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + return new JsonResult(customerBillingDetails); + } + + return await HttpHelper.ReturnResponseResult(response); + } + + // PATCH: /api/billing/customer + [HttpPatch("customer")] + public async Task UpdateCustomerBillingData([FromBody] Customer customerData) + { + if (!ModelState.IsValid) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = JsonConvert.SerializeObject(ModelState.Values.SelectMany(v => v.Errors)) + }; + } + + string updateCustomerDataUrl = $"http://{_billingsService}/api/customer"; + var response = await HttpHelper.PatchAsync(updateCustomerDataUrl, new StringContent( + JsonConvert.SerializeObject(customerData), Encoding.UTF8, "application/json"), this.Request); + if (!response.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(response); + } + + var updatedCustomerData = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + return Json(updatedCustomerData); + } + + // GET: api/billing/vendor/1 + [HttpGet("vendor/{vendorId}")] + public async Task GetVendor(string vendorId) + { + string getVendorUrl = $"http://{_billingsService}/api/vendor/{vendorId}"; + var response = await HttpHelper.GetAsync(getVendorUrl, this.Request); + if (response.IsSuccessStatusCode) + { + var vendorDetails = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + return new JsonResult(vendorDetails); + } + + return await HttpHelper.ReturnResponseResult(response); + } + + // PATCH: /api/billing/vendor + [HttpPatch("vendor")] + public async Task UpdateVendor([FromBody] Vendor vendorDetails) + { + if (!ModelState.IsValid) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = JsonConvert.SerializeObject(ModelState.Values.SelectMany(v => v.Errors)) + }; + } + + string updateVendorDataUrl = $"http://{_billingsService}/api/vendor"; + var response = await HttpHelper.PatchAsync(updateVendorDataUrl, new StringContent( + JsonConvert.SerializeObject(vendorDetails), Encoding.UTF8, "application/json"), this.Request); + if (!response.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(response); + } + + var updatedVendorData = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + return Json(updatedVendorData); + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/Controllers/HelloController.cs b/samples/BikeSharingApp/Gateway/Controllers/HelloController.cs new file mode 100644 index 000000000..2488dec1f --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Controllers/HelloController.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Mvc; + +namespace app.Controllers +{ + [Route("hello")] + public class HelloController : Controller + { + // GET: api/hello + [HttpGet] + public string Hello() + { + return "Hello!"; + } + } +} diff --git a/samples/BikeSharingApp/Gateway/Controllers/ReservationController.cs b/samples/BikeSharingApp/Gateway/Controllers/ReservationController.cs new file mode 100644 index 000000000..da00a0202 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Controllers/ReservationController.cs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using app.Models; +using app.Models.Reservations; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace app.Controllers +{ + [Route("api/reservation")] + public class ReservationController : Controller + { + private CustomConfiguration _customConfiguration { get; set; } + + private string _reservationsService { get; set; } + + private string _reservationEngineService { get; set; } + + private string _billingService { get; set; } + + private string _bikesService { get; set; } + + private string _usersService { get; set; } + + private const string DateTimeFormat = "yyyy-MM-ddTHH:mm:ss"; + + public ReservationController(IOptions customConfiguration) + { + _customConfiguration = customConfiguration.Value; + _reservationsService = Environment.GetEnvironmentVariable(Constants.ReservationsMicroserviceEnv) ?? _customConfiguration.Services.Reservations; + _reservationEngineService = Environment.GetEnvironmentVariable(Constants.ReservationEngineMicroserviceEnv) ?? _customConfiguration.Services.ReservationEngine; + _billingService = Environment.GetEnvironmentVariable(Constants.BillingMicroserviceEnv) ?? _customConfiguration.Services.Billing; + _bikesService = Environment.GetEnvironmentVariable(Constants.BikesMicroserviceEnv) ?? _customConfiguration.Services.Bikes; + _usersService = Environment.GetEnvironmentVariable(Constants.UsersMicroserviceEnv) ?? _customConfiguration.Services.Users; + } + + // POST: api/reservationengine + private async Task _UpdateReservation(Reservation reservationDetails) + { + string updateReservationUrl = $"http://{_reservationEngineService}/api/reservationengine"; + var response = await HttpHelper.PostAsync(updateReservationUrl, new StringContent( + JsonConvert.SerializeObject(reservationDetails), Encoding.UTF8, "application/json"), this.Request); + if (response.IsSuccessStatusCode) + { + return new JsonResult(reservationDetails); + } + return await HttpHelper.ReturnResponseResult(response); + } + + /// + /// Returns null on success + /// + /// + /// + internal static async Task _AddInvoiceDetailsToReservation(string billingServiceUri, HttpRequest originRequest, Reservation res) + { + string getInvoiceForResUrl = $"http://{billingServiceUri}/api/reservation/{res.ReservationId}/invoice"; + var response = await HttpHelper.GetAsync(getInvoiceForResUrl, originRequest); + if (!response.IsSuccessStatusCode) + { + if (response.StatusCode != HttpStatusCode.NotFound) + { + return await HttpHelper.ReturnResponseResult(response); + } + + // Not found - Valid case + res.InvoiceId = string.Empty; + return null; // success + } + + var invoice = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + if (invoice == null) + { + return HttpHelper.Return500Result("Unexpected object returned when getting invoice for reservation id"); + } + + res.InvoiceId = invoice.Id; + return null; // success + } + + // GET: api/reservation/1 + // TODO: add this after reservation microservice is ready. + [HttpGet("{reservationId}")] + public async Task GetReservation(string reservationId) + { + string getReservationUrl = $"http://{_reservationsService}/api/reservation/{reservationId}"; + var response = await HttpHelper.GetAsync(getReservationUrl, this.Request); + if (!response.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(response); + } + + var reservationDetails = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var addInvoiceDetailsResponse = await _AddInvoiceDetailsToReservation(_billingService, this.Request, reservationDetails); + if (addInvoiceDetailsResponse != null) + { + return addInvoiceDetailsResponse; + } + return new JsonResult(reservationDetails); + } + + // GET: api/reservation/allReservations + [HttpGet("allReservations")] + public async Task GetAllReservations() + { + string getAllReservationsUrl = $"http://{_reservationsService}/api/allReservations"; + var response = await HttpHelper.GetAsync(getAllReservationsUrl, this.Request); + if (!response.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(response); + } + + var allReservations = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()) ?? new Reservation[0]; + foreach (var res in allReservations) + { + var addInvoiceDetailsResponse = await _AddInvoiceDetailsToReservation(_billingService, this.Request, res); + if (addInvoiceDetailsResponse != null) + { + return addInvoiceDetailsResponse; + } + } + return Json(allReservations); + } + + private async Task _CreateNewReservationAsync(Reservation reservationDetails) + { + string addReservationUrl = $"http://{_reservationsService}/api/reservation"; + var response = await HttpHelper.PostAsync(addReservationUrl, new StringContent( + JsonConvert.SerializeObject(reservationDetails), Encoding.UTF8, "application/json"), this.Request); + if (response.IsSuccessStatusCode) + { + return new JsonResult(reservationDetails); + } + + return await HttpHelper.ReturnResponseResult(response); + } + + // POST: /api/reservation + [HttpPost] + public async Task CreateReservation([FromBody] Reservation reservationDetails) + { + if (!ModelState.IsValid) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = JsonConvert.SerializeObject(ModelState.Values.SelectMany(v => v.Errors)) + }; + } + + try + { + // Check valid bike + string getBikeUrl = $"http://{_bikesService}/api/bikes/{reservationDetails.BikeId}"; + var getBikeResponse = await HttpHelper.GetAsync(getBikeUrl, this.Request); + if (!getBikeResponse.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(getBikeResponse); + } + + var bike = JsonConvert.DeserializeObject(await getBikeResponse.Content.ReadAsStringAsync()); + if (bike == null) + { + return new ContentResult() + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = "Unexpected type returned from call to get bike" + }; + } + if (!bike.Available.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + return StatusCode(StatusCodes.Status400BadRequest, $"BikeId '{reservationDetails.BikeId}' is not available"); + } + + // Check valid customer + string getUserUrl = $"http://{_usersService}/api/users/{reservationDetails.UserId}"; + var getUserResponse = await HttpHelper.GetAsync(getUserUrl, this.Request); + if (!getUserResponse.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(getUserResponse); + } + + var user = JsonConvert.DeserializeObject(await getUserResponse.Content.ReadAsStringAsync()); + if (user == null) + { + return new ContentResult() + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = "Unexpected type returned from call to get customer" + }; + } + if (user.Type != UserType.Customer) + { + return BadRequest("UserId must be a customer"); + } + + reservationDetails.ReservationId = Guid.NewGuid().ToString("N"); + reservationDetails.RequestTime = DateTime.UtcNow.ToString(DateTimeFormat); + reservationDetails.StartTime = string.IsNullOrEmpty(reservationDetails.StartTime) ? DateTime.UtcNow.ToString(DateTimeFormat) : reservationDetails.StartTime; + reservationDetails.State = ReservationState.Booking.ToString(); + reservationDetails.EndTime = string.Empty; + reservationDetails.RequestId = OperationContext.CurrentContext.RequestId.ToString(); + + // Create new entry in reservation db and add job to queue for engine to process + var resResponse = await _CreateNewReservationAsync(reservationDetails); + if ((resResponse as JsonResult) == null) + { + return resResponse; + } + + // Update reservation by sending to reservation engine. Should we replace this with a queue? + var updatedResponse = await _UpdateReservation(reservationDetails); + if ((updatedResponse as JsonResult) == null) + { + return updatedResponse; + } + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.ToString()); + } + + return new JsonResult(reservationDetails); + } + + // POST: /api/reservation/1 + [HttpPost("{reservationId}")] + public async Task CompleteReservation(string reservationId) + { + try + { + string getReservationUrl = $"http://{_reservationsService}/api/reservation/{reservationId}"; + var response = await HttpHelper.GetAsync(getReservationUrl, this.Request); + if (response.IsSuccessStatusCode) + { + var reservationDetails = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + reservationDetails.State = ReservationState.Completing.ToString(); + + // Update reservation by sending to reservation engine. Should we replace this with a queue? + var updatedResponse = await _UpdateReservation(reservationDetails); + if ((updatedResponse as JsonResult) == null) + { + return updatedResponse; + } + + return new JsonResult(reservationDetails); + } + + return await HttpHelper.ReturnResponseResult(response); + } + catch (Exception e) + { + return StatusCode(Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError, e.ToString()); + } + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/Controllers/UserController.cs b/samples/BikeSharingApp/Gateway/Controllers/UserController.cs new file mode 100644 index 000000000..a8856ecb5 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Controllers/UserController.cs @@ -0,0 +1,489 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using app.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace app.Controllers +{ + [Route("api/user")] + public class UserController : Controller + { + private string _usersService { get; set; } + + private string _reservationsService { get; set; } + + private string _bikesService { get; set; } + + private string _billingService { get; set; } + + private CustomConfiguration _customConfiguration { get; set; } + + public UserController(IOptions customConfiguration) + { + _customConfiguration = customConfiguration.Value; + _usersService = Environment.GetEnvironmentVariable(Constants.UsersMicroserviceEnv) ?? _customConfiguration.Services.Users; + _reservationsService = Environment.GetEnvironmentVariable(Constants.ReservationsMicroserviceEnv) ?? _customConfiguration.Services.Reservations; + _billingService = Environment.GetEnvironmentVariable(Constants.BillingMicroserviceEnv) ?? _customConfiguration.Services.Billing; + _bikesService = Environment.GetEnvironmentVariable(Constants.BikesMicroserviceEnv) ?? _customConfiguration.Services.Bikes; + } + + // GET: api/user/1 + [HttpGet("{userId}")] + public async Task GetUser(string userId) + { + if (!CheckValidUserId(userId)) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = "Invalid userId. The user Id is a valid Guid without the hyphens." + }; + } + + string getUserUrl = $"http://{_usersService}/api/users/{userId}"; + var response = await HttpHelper.GetAsync(getUserUrl, this.Request); + if (!response.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(response); + } + + var responseBody = await response.Content.ReadAsStringAsync(); + var userDetails = JsonConvert.DeserializeObject(responseBody); + return new JsonResult(userDetails); + } + + // GET: api/user/allUsers + [HttpGet("allUsers")] + public async Task GetAllUsers() + { + string getAllUserUrl = $"http://{_usersService}/api/allUsers"; + var response = await HttpHelper.GetAsync(getAllUserUrl, this.Request); + if (!response.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(response); + } + + var responseBody = await response.Content.ReadAsStringAsync(); + var allUserDetails = JsonConvert.DeserializeObject(responseBody); + return new JsonResult(allUserDetails); + } + + /// + /// Returns null on success + /// + /// + /// + /// + private async Task _UpdateUser(string userId, IUser updatedUser) + { + string getUpdateUserUrl = $"http://{_usersService}/api/users/{userId}"; + var getResponse = await HttpHelper.GetAsync(getUpdateUserUrl, this.Request); + if (!getResponse.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(getResponse); + } + + var existingUserDetails = JsonConvert.DeserializeObject(await getResponse.Content.ReadAsStringAsync()); + if (existingUserDetails.Type != updatedUser.Type) + { + return BadRequest($"Tried to update a user who isn't a {updatedUser.Type.ToString()}"); + } + + var updatedUserDetails = new User + { + Name = string.IsNullOrEmpty(updatedUser.Name) ? existingUserDetails.Name : updatedUser.Name, + Address = string.IsNullOrEmpty(updatedUser.Address) ? existingUserDetails.Address : updatedUser.Address, + Phone = string.IsNullOrEmpty(updatedUser.Phone) ? existingUserDetails.Phone : updatedUser.Phone, + Email = string.IsNullOrEmpty(updatedUser.Email) ? existingUserDetails.Email : updatedUser.Email + }; + + // Update user + var updateResponse = await HttpHelper.PutAsync(getUpdateUserUrl, new StringContent( + JsonConvert.SerializeObject(updatedUserDetails), Encoding.UTF8, "application/json"), this.Request); + if (!updateResponse.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(updateResponse); + } + + return null; + } + + // PATCH: /api/user/vendor/{userId} + [HttpPatch("vendor/{userId}")] + public async Task UpdateVendor(string userId, [FromBody] CreateVendorRequest vendorInput) + { + if (!CheckValidUserId(userId)) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = "Invalid userId. The user Id is a valid Guid without the hyphens." + }; + } + + vendorInput.Type = UserType.Vendor; + + var updateUserResponse = await this._UpdateUser(userId, vendorInput); + if (updateUserResponse != null) + { + return updateUserResponse; + } + + // Now update Billing details + string getVendorUrl = $"http://{_billingService}/api/vendor/{userId}"; + var getResponse = await HttpHelper.GetAsync(getVendorUrl, this.Request); + if (!getResponse.IsSuccessStatusCode) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = $"Couldn't find vendor billing details: {getResponse.StatusCode} {await getResponse.Content.ReadAsStringAsync()}" + }; + } + + var currentVendor = JsonConvert.DeserializeObject(await getResponse.Content.ReadAsStringAsync()); + if (currentVendor == null) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = "Unexpected object returned while getting vendor billing data" + }; + } + + var updatedVendorDetails = new Vendor + { + UserID = userId, + AccountNumber = string.IsNullOrEmpty(vendorInput.AccountNumber) ? currentVendor.AccountNumber : vendorInput.AccountNumber, + RoutingNumber = string.IsNullOrEmpty(vendorInput.RoutingNumber) ? currentVendor.RoutingNumber : vendorInput.RoutingNumber + }; + + string updateVendorDataUrl = $"http://{_billingService}/api/vendor"; + var updateResponse = await HttpHelper.PatchAsync(updateVendorDataUrl, new StringContent( + JsonConvert.SerializeObject(updatedVendorDetails), Encoding.UTF8, "application/json"), this.Request); + if (!updateResponse.IsSuccessStatusCode) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = $"Couldn't update vendor billing details: {updateResponse.StatusCode} {await updateResponse.Content.ReadAsStringAsync()}" + }; + } + + var updatedVendor = JsonConvert.DeserializeObject(await updateResponse.Content.ReadAsStringAsync()); + if (updatedVendor == null) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = $"Unexpected object returned while updating vendor billing data" + }; + } + + return await GetUser(userId); + } + + // PATCH: /api/user/1 + // Gets user details to fill in missing fields, updates and gets the updated user again. + [HttpPatch("{userId}")] + public async Task UpdateCustomer(string userId, [FromBody] CreateCustomerRequest customerInput) + { + if (!CheckValidUserId(userId)) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = "Invalid userId. The user Id is a valid Guid without the hyphens." + }; + } + + customerInput.Type = UserType.Customer; + + var updateUserResponse = await this._UpdateUser(userId, customerInput); + if (updateUserResponse != null) + { + return updateUserResponse; + } + + // Now update Billing details + string getCustomerDataUrl = $"http://{_billingService}/api/customer/{userId}"; + var getResponse = await HttpHelper.GetAsync(getCustomerDataUrl, this.Request); + if (!getResponse.IsSuccessStatusCode) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = $"Couldn't find customer billing details: {getResponse.StatusCode} {await getResponse.Content.ReadAsStringAsync()}" + }; + } + + var currentCustomer = JsonConvert.DeserializeObject(await getResponse.Content.ReadAsStringAsync()); + if (currentCustomer == null) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = "Unexpected object returned while getting customer billing data" + }; + } + + var updatedCustomerDetails = new Customer + { + UserID = userId, + CCNumber = string.IsNullOrEmpty(customerInput.CCNumber) ? currentCustomer.CCNumber : customerInput.CCNumber, + CCCCV = string.IsNullOrEmpty(customerInput.CCCCV) ? currentCustomer.CCCCV : customerInput.CCCCV, + CCExpiry = string.IsNullOrEmpty(customerInput.CCExpiry) ? currentCustomer.CCExpiry : customerInput.CCExpiry + }; + + string updateCustomerDataUrl = $"http://{_billingService}/api/customer"; + var updateResponse = await HttpHelper.PatchAsync(updateCustomerDataUrl, new StringContent( + JsonConvert.SerializeObject(updatedCustomerDetails), Encoding.UTF8, "application/json"), this.Request); + if (!updateResponse.IsSuccessStatusCode) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = $"Couldn't update customer billing data: {updateResponse.StatusCode} {await updateResponse.Content.ReadAsStringAsync()}" + }; + } + + var updatedCustomer = JsonConvert.DeserializeObject(await updateResponse.Content.ReadAsStringAsync()); + if (updatedCustomer == null) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = "Unexpected object returned while updating customer billing data" + }; + } + + return await GetUser(userId); + } + + public async Task DeleteBike(string bikeId) + { + string getDeleteBikeUrl = $"http://{_bikesService}/api/bikes/{bikeId}"; + var getResponse = await HttpHelper.GetAsync(getDeleteBikeUrl, this.Request); + if (getResponse.IsSuccessStatusCode) + { + // Bike exists, proceed with deletion. + var deleteResponse = await HttpHelper.DeleteAsync(getDeleteBikeUrl, this.Request); + return await HttpHelper.ReturnResponseResult(deleteResponse); + } + + return await HttpHelper.ReturnResponseResult(getResponse); + } + + // DELETE: /api/user/1 + [HttpDelete("{userId}")] + public async Task DeleteUser(string userId) + { + if (!CheckValidUserId(userId)) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = "Invalid userId. The user Id is a valid Guid without the hyphens." + }; + } + + string getDeleteUserUrl = $"http://{_usersService}/api/users/{userId}"; + var getResponse = await HttpHelper.GetAsync(getDeleteUserUrl, this.Request); + if (getResponse.IsSuccessStatusCode) + { + // User exists, proceed with deletion. Delete all bikes owned by user first. + string getAllBikesUrl = $"http://{_bikesService}/api/allbikes"; + var listResponse = await HttpHelper.GetAsync(getAllBikesUrl, this.Request); + if (listResponse.IsSuccessStatusCode) + { + var foundBikes = JsonConvert.DeserializeObject>(await listResponse.Content.ReadAsStringAsync()); + var usersBikes = foundBikes.FindAll(bike => bike.OwnerUserId.Equals(userId, StringComparison.OrdinalIgnoreCase)); + var deleteBikesTasks = usersBikes.AsParallel().Select(bike => DeleteBike(bike.Id)); + await Task.WhenAll(deleteBikesTasks); + var deleteResponse = await HttpHelper.DeleteAsync(getDeleteUserUrl, this.Request); + return await HttpHelper.ReturnResponseResult(deleteResponse); + } + + return await HttpHelper.ReturnResponseResult(listResponse); + } + + return await HttpHelper.ReturnResponseResult(getResponse); + } + + private async Task> _CreateUser(User u) + { + string createUserUrl = $"http://{_usersService}/api/users"; + var userId = Guid.NewGuid().ToString("N"); + + u.Id = string.IsNullOrEmpty(u.Id) ? userId : u.Id; + Console.WriteLine("u.Id : " + u.Id); + + var response = await HttpHelper.PostAsync(createUserUrl, new StringContent( + JsonConvert.SerializeObject(u), Encoding.UTF8, "application/json"), this.Request); + return new Tuple(response, u.Id); + } + + // POST: /api/user + [HttpPost] + public async Task CreateCustomer([FromBody] CreateCustomerRequest userInput) + { + if (!ModelState.IsValid) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = JsonConvert.SerializeObject(ModelState.Values.SelectMany(v => v.Errors)) + }; + } + + userInput.Type = UserType.Customer; + string userInputJson = JsonConvert.SerializeObject(userInput); + User user = JsonConvert.DeserializeObject(userInputJson); + Customer cust = JsonConvert.DeserializeObject(userInputJson); + + var createUserResponseObj = await this._CreateUser(user); + var userResponse = createUserResponseObj.Item1; + var userId = createUserResponseObj.Item2; + if (!userResponse.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(userResponse); + } + + cust.UserID = userId; + + string addCustomerDataUrl = $"http://{_billingService}/api/customer"; + var response = await HttpHelper.PostAsync(addCustomerDataUrl, new StringContent( + JsonConvert.SerializeObject(cust), Encoding.UTF8, "application/json"), this.Request); + if (!response.IsSuccessStatusCode) + { + return new ContentResult() + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = $"Billing controller couldn't create customer billing data: {response.StatusCode} {await response.Content.ReadAsStringAsync()}" + }; + } + + return await GetUser(userId); + } + + // POST: /api/user/vendor + [HttpPost("vendor")] + public async Task CreateVendor([FromBody] CreateVendorRequest vendorInput) + { + if (!ModelState.IsValid) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = JsonConvert.SerializeObject(ModelState.Values.SelectMany(v => v.Errors)) + }; + } + + vendorInput.Type = UserType.Vendor; + string vendorInputJson = JsonConvert.SerializeObject(vendorInput); + User user = JsonConvert.DeserializeObject(vendorInputJson); + Vendor vendor = JsonConvert.DeserializeObject(vendorInputJson); + + var createUserResponseObj = await this._CreateUser(user); + var userResponse = createUserResponseObj.Item1; + var userId = createUserResponseObj.Item2; + if (!userResponse.IsSuccessStatusCode) + { + return await HttpHelper.ReturnResponseResult(userResponse); + } + + vendor.UserID = userId; + + string addVendorUrl = $"http://{_billingService}/api/vendor"; + var response = await HttpHelper.PostAsync(addVendorUrl, new StringContent( + JsonConvert.SerializeObject(vendor), Encoding.UTF8, "application/json"), this.Request); + if (!response.IsSuccessStatusCode) + { + return new ContentResult() + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = $"Billing controller couldn't create vendor billing data: {response.StatusCode} {await response.Content.ReadAsStringAsync()}" + }; + } + + return await GetUser(userId); + } + + // GET: /api/user/{userId}/bikes + [HttpGet(@"{userId}/bikes")] + public async Task GetAllBikes(string userId) + { + if (!CheckValidUserId(userId)) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = "Invalid userId. The user Id is a valid Guid without the hyphens." + }; + } + + string getAllBikesUrl = $"http://{_bikesService}/api/allbikes"; + var response = await HttpHelper.GetAsync(getAllBikesUrl, this.Request); + if (response.IsSuccessStatusCode) + { + var foundBikes = JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); + var usersBikes = foundBikes.FindAll(bike => bike.OwnerUserId.Equals(userId, StringComparison.OrdinalIgnoreCase)); + return new JsonResult(usersBikes); + } + + return await HttpHelper.ReturnResponseResult(response); + } + + [HttpGet(@"{userId}/reservations")] + public async Task ListReservations(string userId) + { + if (!CheckValidUserId(userId)) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.BadRequest, + Content = "Invalid userId. The user Id is a valid Guid without the hyphens." + }; + } + + string listReservationsUrl = $"http://{_reservationsService}/api/user/{userId}/reservations"; + var response = await HttpHelper.GetAsync(listReservationsUrl, this.Request); + if (response.IsSuccessStatusCode) + { + var responseBody = await response.Content.ReadAsStringAsync(); + var reservations = JsonConvert.DeserializeObject>(responseBody) ?? new List(); + foreach (var res in reservations) + { + var addInvoiceDetailsResponse = await ReservationController._AddInvoiceDetailsToReservation(_billingService, this.Request, res); + if (addInvoiceDetailsResponse != null) + { + return addInvoiceDetailsResponse; + } + } + return new JsonResult(reservations); + } + + return await HttpHelper.ReturnResponseResult(response); + } + + private static bool CheckValidUserId(string userId) + { + if (string.IsNullOrEmpty(userId) || !Guid.TryParseExact(userId, "N", out Guid temp)) + { + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/CustomConfiguration.cs b/samples/BikeSharingApp/Gateway/CustomConfiguration.cs new file mode 100644 index 000000000..40a5bdac6 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/CustomConfiguration.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app +{ + public class Services + { + public string Users { get; set; } + + public string Bikes { get; set; } + + public string Reservations { get; set; } + + public string ReservationEngine { get; set; } + + public string Billing { get; set; } + } + + public class CustomConfiguration + { + public Services Services { get; set; } + + } +} diff --git a/samples/BikeSharingApp/Gateway/Dockerfile b/samples/BikeSharingApp/Gateway/Dockerfile new file mode 100644 index 000000000..8aedd040b --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Dockerfile @@ -0,0 +1,18 @@ +FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base +WORKDIR /app +EXPOSE 80 + +FROM microsoft/dotnet:2.1-sdk AS build +WORKDIR /src +COPY ["app.csproj", "."] +RUN dotnet restore "app.csproj" +COPY . . +RUN dotnet build "app.csproj" -c Release -o /app + +FROM build AS publish +RUN dotnet publish "app.csproj" -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "app.dll"] \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/Dockerfile.develop b/samples/BikeSharingApp/Gateway/Dockerfile.develop new file mode 100644 index 000000000..b4b6eb418 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Dockerfile.develop @@ -0,0 +1,15 @@ +FROM microsoft/dotnet:2.1-sdk +ARG BUILD_CONFIGURATION=Debug +ENV ASPNETCORE_ENVIRONMENT=Development +ENV DOTNET_USE_POLLING_FILE_WATCHER=true +EXPOSE 80 + +WORKDIR /src +COPY ["app.csproj", "./"] +RUN dotnet restore "app.csproj" +COPY . . +RUN dotnet build --no-restore -c $BUILD_CONFIGURATION + +RUN echo "exec dotnet run --no-build --no-launch-profile -c $BUILD_CONFIGURATION -- \"\$@\"" > /entrypoint.sh + +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/Exceptions/ReservationNotFoundException.cs b/samples/BikeSharingApp/Gateway/Exceptions/ReservationNotFoundException.cs new file mode 100644 index 000000000..a6db126d0 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Exceptions/ReservationNotFoundException.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace app.Exceptions +{ + public class ReservationNotFoundException : Exception + { + public ReservationNotFoundException(string reservationId) + : base($"Couldn't find reservation with ID: {reservationId}") + {} + } +} diff --git a/samples/BikeSharingApp/Gateway/Exceptions/ReservationRequestFailedException.cs b/samples/BikeSharingApp/Gateway/Exceptions/ReservationRequestFailedException.cs new file mode 100644 index 000000000..c8c1013e5 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Exceptions/ReservationRequestFailedException.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using app.Models; +using app.Models.Reservations; + +namespace app.Exceptions +{ + public class ReservationRequestFailedException : Exception + { + public ReservationRequestFailedException(Reservation reservationDetails, ReservationState expectedState) + : base($"Reservation request failed for reservationID '{reservationDetails.ReservationId}'. Expected state: '{expectedState.ToString()}'. Actual state: '{reservationDetails.State}'") + {} + } +} diff --git a/samples/BikeSharingApp/Gateway/HttpClientExtensions.cs b/samples/BikeSharingApp/Gateway/HttpClientExtensions.cs new file mode 100644 index 000000000..ff5f2e56a --- /dev/null +++ b/samples/BikeSharingApp/Gateway/HttpClientExtensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Net.Http; +using Microsoft.AspNetCore.Http; + +namespace app +{ + public static class HttpClientExtensions + { + public static HttpRequestMessage AddOutboundHeaders(this HttpRequestMessage message, HttpRequest originRequest) + { + message.Headers.Add(Constants.RequestIdHeaderName, OperationContext.CurrentContext.RequestId.ToString()); + message.Headers.Add(Constants.RouteAsHeaderName, originRequest.Headers[Constants.RouteAsHeaderName].ToArray()); + return message; + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/HttpHelper.cs b/samples/BikeSharingApp/Gateway/HttpHelper.cs new file mode 100644 index 000000000..33c665aa6 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/HttpHelper.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using app.Logging; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace app +{ + public static class HttpHelper + { + private static HttpClient httpClient = new HttpClient(); + + public static Task GetAsync(string url, HttpRequest originRequest) + { + return doHttpAndLog(new HttpRequestMessage(HttpMethod.Get, url).AddOutboundHeaders(originRequest)); + } + + public static Task PostAsync(string url, HttpContent content, HttpRequest originRequest) + { + return doHttpAndLog(new HttpRequestMessage(HttpMethod.Post, url) { Content = content }.AddOutboundHeaders(originRequest)); + } + + public static Task PutAsync(string url, HttpContent content, HttpRequest originRequest) + { + return doHttpAndLog(new HttpRequestMessage(HttpMethod.Put, url) { Content = content }.AddOutboundHeaders(originRequest)); + } + + public static Task DeleteAsync(string url, HttpRequest originRequest) + { + return doHttpAndLog(new HttpRequestMessage(HttpMethod.Delete, url).AddOutboundHeaders(originRequest)); + } + + public static Task PatchAsync(string url, HttpContent content, HttpRequest originRequest) + { + return doHttpAndLog(new HttpRequestMessage(new HttpMethod("PATCH"), url) { Content = content }.AddOutboundHeaders(originRequest)); + } + + private static async Task doHttpAndLog(HttpRequestMessage message) + { + Stopwatch stopWatch = Stopwatch.StartNew(); + + var response = await httpClient.SendAsync(message); + + Action logFunc = LogUtility.LogWithContext; + if (!response.IsSuccessStatusCode) + { + logFunc = LogUtility.LogErrorWithContext; + } + + logFunc($"called: {message.Method.ToString()} {message.RequestUri.ToString()} - {(int)response.StatusCode} - {stopWatch.ElapsedMilliseconds}ms", null); + return response; + } + + public static async Task ReturnResponseResult(HttpResponseMessage response) + { + return new ContentResult + { + StatusCode = (int)response.StatusCode, + Content = await response.Content.ReadAsStringAsync() + }; + } + + public static ContentResult Return500Result(string errorMsg) + { + return new ContentResult + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Content = errorMsg + }; + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/Logging/LogUtility.cs b/samples/BikeSharingApp/Gateway/Logging/LogUtility.cs new file mode 100644 index 000000000..88168f897 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Logging/LogUtility.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Extensions.Logging; + +namespace app.Logging +{ + public static class LogUtility + { + public static void Log(string format, params string[] args) + { + Console.WriteLine(string.Format($"{DateTime.UtcNow.ToString()}: {format}", args)); + } + + public static void LogWithContext(string format, params string[] args) + { + string newFormat = $"{DateTime.UtcNow.ToString()}: {OperationContext.CurrentContext.RequestId.ToString()} - {format}"; + OperationContext.CurrentContext.Logger.LogInformation(newFormat, args); + } + + public static void LogError(string format, params string[] args) + { + Console.Error.WriteLine(string.Format($"{DateTime.UtcNow.ToString()}: {format}", args)); + } + + public static void LogErrorWithContext(string format, params string[] args) + { + string newFormat = $"{DateTime.UtcNow.ToString()}: {OperationContext.CurrentContext.RequestId.ToString()} - {format}"; + OperationContext.CurrentContext.Logger.LogError(newFormat, args); + } + } +} diff --git a/samples/BikeSharingApp/Gateway/Middleware/OperationContextMiddleware.cs b/samples/BikeSharingApp/Gateway/Middleware/OperationContextMiddleware.cs new file mode 100644 index 000000000..7ec2a5eef --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Middleware/OperationContextMiddleware.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace app.Middleware +{ + public class OperationContextMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public OperationContextMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public Task Invoke(HttpContext context) + { + OperationContext.CurrentContext.RequestId = Guid.NewGuid(); + OperationContext.CurrentContext.Logger = _logger; + return this._next(context); + } + } +} diff --git a/samples/BikeSharingApp/Gateway/Middleware/RequestLoggingMiddleware.cs b/samples/BikeSharingApp/Gateway/Middleware/RequestLoggingMiddleware.cs new file mode 100644 index 000000000..623fdfbb0 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Middleware/RequestLoggingMiddleware.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using app.Logging; +using Microsoft.AspNetCore.Http; + +namespace app.Middleware +{ + public class RequestLoggingMiddleware + { + private readonly RequestDelegate _next; + + public RequestLoggingMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + Stopwatch stopWatch = Stopwatch.StartNew(); + var responseStream = context.Response.Body; + string responseBody = null; + using (MemoryStream memStream = new MemoryStream()) + { + context.Response.Body = memStream; + + await this._next(context); + + context.Response.Body = responseStream; + memStream.Seek(0, SeekOrigin.Begin); + using (StreamReader reader = new StreamReader(memStream)) + { + responseBody = await reader.ReadToEndAsync(); + } + await context.Response.WriteAsync(responseBody); + } + + if (context.Response.StatusCode >= 500) + { + LogUtility.LogErrorWithContext("Returning error status code!: " + responseBody); + } + + LogUtility.LogWithContext($"{context.Request.Path.Value} - {context.Response.StatusCode} - {stopWatch.ElapsedMilliseconds}ms"); + } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Bikes/AddUpdateBikeRequest.cs b/samples/BikeSharingApp/Gateway/Models/Bikes/AddUpdateBikeRequest.cs new file mode 100644 index 000000000..cd250beee --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Bikes/AddUpdateBikeRequest.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; + +namespace app.Models +{ + public class AddUpdateBikeRequest + { + [JsonProperty("model")] + [Required] + public string Model { get; set; } + + [JsonProperty("hourlyCost")] + [Required] + public float? HourlyCost { get; set; } + + [JsonProperty("imageUrl")] + [Required] + public string ImageUrl { get; set; } + + [JsonProperty("address")] + [Required] + public string Address { get; set; } + + [JsonProperty("type")] + [Required] + public string Type { get; set; } + + [JsonProperty("ownerUserId")] + [Required] + public string OwnerUserId { get; set; } + + [JsonProperty("suitableHeightInMeters")] + [Required] + public float? SuitableHeightInMeters { get; set; } + + [JsonProperty("maximumWeightInKg")] + [Required] + public float? MaximumWeightInKg { get; set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Bikes/Bike.cs b/samples/BikeSharingApp/Gateway/Models/Bikes/Bike.cs new file mode 100644 index 000000000..8f8cfba18 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Bikes/Bike.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; + +namespace app.Models +{ + public class Bike + { + [JsonProperty("id")] + public string Id { get; private set; } + + [JsonProperty("available")] + public string Available { get; private set; } + + [JsonProperty("model")] + public string Model { get; private set; } + + [JsonProperty("hourlyCost")] + public float HourlyCost { get; private set; } + + [JsonProperty("imageUrl")] + public string ImageUrl { get; private set; } + + [JsonProperty("address")] + public string Address { get; private set; } + + [JsonProperty("type")] + public string Type { get; private set; } + + [JsonProperty("ownerUserId")] + public string OwnerUserId { get; private set; } + + [JsonProperty("suitableHeightInMeters")] + public float SuitableHeightInMeters { get; private set; } + + [JsonProperty("maximumWeightInKg")] + public float MaximumWeightInKg { get; private set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Billing/Customer.cs b/samples/BikeSharingApp/Gateway/Models/Billing/Customer.cs new file mode 100644 index 000000000..71e7e8419 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Billing/Customer.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app.Models +{ + public class Customer : ICustomer + { + public string Id { get; set; } + public string UserID { get; set; } + public string CCNumber { get; set; } + public string CCExpiry { get; set; } + public string CCCCV { get; set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Billing/ICustomer.cs b/samples/BikeSharingApp/Gateway/Models/Billing/ICustomer.cs new file mode 100644 index 000000000..b2da414f3 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Billing/ICustomer.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; + +namespace app.Models +{ + public interface ICustomer + { + [JsonProperty("id")] + string Id { get; set; } + + [JsonProperty("userId")] + string UserID { get; set; } + + [JsonProperty("ccNumber")] + string CCNumber { get; set; } + + [JsonProperty("ccExpiry")] + string CCExpiry { get; set; } + + [JsonProperty("ccCCV")] + string CCCCV { get; set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Billing/IVendor.cs b/samples/BikeSharingApp/Gateway/Models/Billing/IVendor.cs new file mode 100644 index 000000000..3674e8db1 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Billing/IVendor.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; + +namespace app.Models +{ + public interface IVendor + { + [JsonProperty("id")] + string Id { get; set; } + + [JsonProperty("userId")] + string UserID { get; set; } + + [JsonProperty("routingNumber")] + string RoutingNumber { get; set; } + + [JsonProperty("accountNumber")] + string AccountNumber { get; set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Billing/Invoice.cs b/samples/BikeSharingApp/Gateway/Models/Billing/Invoice.cs new file mode 100644 index 000000000..9f7929424 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Billing/Invoice.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; + +namespace app.Models +{ + public class Invoice + { + [JsonProperty("id")] + public string Id { get; private set; } + + [JsonProperty("reservationId")] + public string ReservationId { get; private set; } + + [JsonProperty("customerId")] + public string CustomerId { get; private set; } + + [JsonProperty("vendorId")] + public string VendorId { get; private set; } + + [JsonProperty("bikeId")] + public string BikeId { get; private set; } + + [JsonProperty("amount")] + public string Amount { get; private set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Billing/Vendor.cs b/samples/BikeSharingApp/Gateway/Models/Billing/Vendor.cs new file mode 100644 index 000000000..9a0570ca2 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Billing/Vendor.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app.Models +{ + public class Vendor : IVendor + { + public string Id { get; set; } + public string UserID { get; set; } + public string RoutingNumber { get; set; } + public string AccountNumber { get; set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Job.cs b/samples/BikeSharingApp/Gateway/Models/Job.cs new file mode 100644 index 000000000..20fa1cca4 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Job.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; + +namespace app.Models +{ + public class Job + { + [JsonProperty("id")] + public string Id {get; set;} + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Reservations/Reservation.cs b/samples/BikeSharingApp/Gateway/Models/Reservations/Reservation.cs new file mode 100644 index 000000000..582a4948f --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Reservations/Reservation.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; + +namespace app.Models +{ + public class Reservation + { + [JsonProperty("reservationId")] + public string ReservationId { get; set; } + + [JsonProperty("userId")] + [Required] + public string UserId { get; set; } + + [JsonProperty("bikeId")] + [Required] + public string BikeId { get; set; } + + [JsonProperty("state")] + public string State {get;set;} + + [JsonProperty("requestTime")] + public string RequestTime {get;set;} + + [JsonProperty("startTime")] + public string StartTime {get;set;} + + [JsonProperty("endTime")] + public string EndTime {get;set;} + + [JsonProperty("invoiceId")] + public string InvoiceId { get; set; } + + [JsonProperty("requestId")] + public string RequestId { get; set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Reservations/ReservationState.cs b/samples/BikeSharingApp/Gateway/Models/Reservations/ReservationState.cs new file mode 100644 index 000000000..d1a497c8c --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Reservations/ReservationState.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app.Models.Reservations +{ + public enum ReservationState + { + Booking = 1, + Completing = 2, + Booked = 3, + Completed = 4, + Failed = 5 + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Users/CreateCustomerRequest.cs b/samples/BikeSharingApp/Gateway/Models/Users/CreateCustomerRequest.cs new file mode 100644 index 000000000..b455afcc1 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Users/CreateCustomerRequest.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app.Models +{ + public class CreateCustomerRequest : IUser, ICustomer + { + public string Id { get; set; } + public string Name { get; set; } + public string Address { get; set; } + public string Phone { get; set; } + public string Email { get; set; } + public string UserID { get; set; } + public string CCNumber { get; set; } + public string CCExpiry { get; set; } + public string CCCCV { get; set; } + public UserType Type { get; set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Users/CreateVendorRequest.cs b/samples/BikeSharingApp/Gateway/Models/Users/CreateVendorRequest.cs new file mode 100644 index 000000000..dc609c83b --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Users/CreateVendorRequest.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app.Models +{ + public class CreateVendorRequest : IUser, IVendor + { + public string Id { get; set; } + public string Name { get; set; } + public string Address { get; set; } + public string Phone { get; set; } + public string Email { get; set; } + public string UserID { get; set; } + public string RoutingNumber { get; set; } + public string AccountNumber { get; set; } + public UserType Type { get; set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Users/IUser.cs b/samples/BikeSharingApp/Gateway/Models/Users/IUser.cs new file mode 100644 index 000000000..edfef47f7 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Users/IUser.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace app.Models +{ + public interface IUser + { + [JsonProperty("id")] + string Id { get; set; } + + [JsonProperty("name")] + [Required] + string Name { get; set; } + + [JsonProperty("address")] + [Required] + string Address { get; set; } + + [JsonProperty("phone")] + string Phone { get; set; } + + [JsonProperty("email")] + [Required] + string Email { get; set; } + + [JsonProperty("type")] + [JsonConverter(typeof(StringEnumConverter))] + UserType Type { get; set; } + } + + public enum UserType + { + [EnumMember(Value = "customer")] + Customer, + [EnumMember(Value = "vendor")] + Vendor + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Users/User.cs b/samples/BikeSharingApp/Gateway/Models/Users/User.cs new file mode 100644 index 000000000..bed3926fe --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Users/User.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app.Models +{ + public class User : IUser + { + public string Id { get; set; } + public string Name { get; set; } + public string Address { get; set; } + public string Phone { get; set; } + public string Email { get; set; } + public UserType Type { get; set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/Models/Users/UserResponse.cs b/samples/BikeSharingApp/Gateway/Models/Users/UserResponse.cs new file mode 100644 index 000000000..6717bdeb7 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Models/Users/UserResponse.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app.Models +{ + public class UserResponse : IUser + { + public string Id { get; set; } + public string Name { get; set; } + public string Address { get; set; } + public string Phone { get; set; } + public string Email { get; set; } + public UserType Type { get; set; } + } +} diff --git a/samples/BikeSharingApp/Gateway/OperationContext.cs b/samples/BikeSharingApp/Gateway/OperationContext.cs new file mode 100644 index 000000000..5ab1dc38e --- /dev/null +++ b/samples/BikeSharingApp/Gateway/OperationContext.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using Microsoft.Extensions.Logging; + +namespace app +{ + public class OperationContext + { + public Guid RequestId { get; set; } + + public ILogger Logger { get; set; } + + private static AsyncLocal asyncLocal = new AsyncLocal(); + /// + /// Gets the operation context on the current logical thread of execution. + /// + public static OperationContext CurrentContext + { + get + { + if (asyncLocal.Value == null) + { + asyncLocal.Value = new OperationContext(); + } + + return asyncLocal.Value; + } + } + } +} diff --git a/samples/BikeSharingApp/Gateway/Program.cs b/samples/BikeSharingApp/Gateway/Program.cs new file mode 100644 index 000000000..ca0d70fed --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Program.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace app +{ + public class Program + { + public static void Main(string[] args) + { + var host = WebHost.CreateDefaultBuilder(args).Build(); + host.Run(); + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/Properties/launchSettings.json b/samples/BikeSharingApp/Gateway/Properties/launchSettings.json new file mode 100644 index 000000000..b67181a36 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Properties/launchSettings.json @@ -0,0 +1,22 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5000/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "app": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/Startup.cs b/samples/BikeSharingApp/Gateway/Startup.cs new file mode 100644 index 000000000..89aa9abde --- /dev/null +++ b/samples/BikeSharingApp/Gateway/Startup.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using app.Logging; +using app.Middleware; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace app +{ + public class Startup + { + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Add framework services. + services.AddMvc() + .AddJsonOptions(o => + { + o.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; + }); + + services.AddOptions(); + + // Enable CORS + services.AddCors(options => + options.AddPolicy("MyPolicy", builder => + { + builder.AllowAnyHeader() + .AllowAnyMethod() + .AllowAnyOrigin(); + } + ) + ); + + services.Configure(Configuration.GetSection("CustomConfiguration")); + } + + // 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, IApplicationLifetime lifetime) + { + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + + app.UseMiddleware(typeof(OperationContextMiddleware)); + app.UseMiddleware(typeof(RequestLoggingMiddleware)); + app.UseCors("MyPolicy"); + app.UseMvc(); + + lifetime.ApplicationStopping.Register(() => + { + LogUtility.Log("Graceful shutdown."); + }); + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/app.csproj b/samples/BikeSharingApp/Gateway/app.csproj new file mode 100644 index 000000000..8942c74e6 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/app.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp2.1 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/appsettings.Development.json b/samples/BikeSharingApp/Gateway/appsettings.Development.json new file mode 100644 index 000000000..4c1574b07 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/appsettings.Development.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "CustomConfiguration": { + "Services": { + "Users": "localhost:8080", + "Bikes": "localhost:8080", + "Reservations": "localhost:8080", + "ReservationEngine": "localhost:8080", + "Billing": "localhost:8080" + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/appsettings.json b/samples/BikeSharingApp/Gateway/appsettings.json new file mode 100644 index 000000000..43f0004d5 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Information", + "Microsoft": "Error" + } + }, + "CustomConfiguration": { + "Services": { + "Users": "localhost:8080", + "Bikes": "localhost:8080", + "Reservations": "localhost:8080", + "ReservationEngine": "localhost:8080", + "Billing": "localhost:8080" + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Gateway/azds.yaml b/samples/BikeSharingApp/Gateway/azds.yaml new file mode 100644 index 000000000..6da7fd5ec --- /dev/null +++ b/samples/BikeSharingApp/Gateway/azds.yaml @@ -0,0 +1,41 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: . + dockerfile: Dockerfile.develop + # dockerfile: Dockerfile +install: + chart: charts/gateway + values: + - values.dev.yaml? + - secrets.dev.yaml? + set: + replicaCount: 1 + image: + repository: gateway + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + # This expands to [space.s.][rootSpace.]gateway...azds.io + - $(spacePrefix)$(rootSpacePrefix)gateway$(hostSuffix) +configurations: + develop: + build: + dockerfile: Dockerfile.develop + useGitIgnore: true + args: + BUILD_CONFIGURATION: ${BUILD_CONFIGURATION:-Debug} + container: + sync: + - "**/Pages/**" + - "**/Views/**" + - "**/wwwroot/**" + - "!**/*.{sln,csproj}" + command: [dotnet, run, --no-restore, --no-build, --no-launch-profile, -c, "${BUILD_CONFIGURATION:-Debug}"] + iterate: + processesToKill: [dotnet, vsdbg] + buildCommands: + - [dotnet, build, --no-restore, -c, "${BUILD_CONFIGURATION:-Debug}"] diff --git a/samples/BikeSharingApp/Gateway/charts/gateway/.helmignore b/samples/BikeSharingApp/Gateway/charts/gateway/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/charts/gateway/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/samples/BikeSharingApp/Gateway/charts/gateway/Chart.yaml b/samples/BikeSharingApp/Gateway/charts/gateway/Chart.yaml new file mode 100644 index 000000000..fa83d86d6 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/charts/gateway/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: gateway +version: 0.1.0 diff --git a/samples/BikeSharingApp/Gateway/charts/gateway/templates/NOTES.txt b/samples/BikeSharingApp/Gateway/charts/gateway/templates/NOTES.txt new file mode 100644 index 000000000..14a8ce7cf --- /dev/null +++ b/samples/BikeSharingApp/Gateway/charts/gateway/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "gateway.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "gateway.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "gateway.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "gateway.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/samples/BikeSharingApp/Gateway/charts/gateway/templates/_helpers.tpl b/samples/BikeSharingApp/Gateway/charts/gateway/templates/_helpers.tpl new file mode 100644 index 000000000..a500357da --- /dev/null +++ b/samples/BikeSharingApp/Gateway/charts/gateway/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "gateway.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "gateway.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "gateway.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/samples/BikeSharingApp/Gateway/charts/gateway/templates/deployment.yaml b/samples/BikeSharingApp/Gateway/charts/gateway/templates/deployment.yaml new file mode 100644 index 000000000..4fe2562cb --- /dev/null +++ b/samples/BikeSharingApp/Gateway/charts/gateway/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: {{ template "gateway.fullname" . }} + labels: + app: {{ template "gateway.name" . }} + chart: {{ template "gateway.chart" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "gateway.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "gateway.name" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + annotations: + buildID: {{ .Values.buildID }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + {{- if .Values.probes.enabled }} + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + {{- end }} + env: + {{- $root := . }} + {{- range $ref, $values := .Values.secrets }} + {{- range $key, $value := $values }} + - name: {{ $ref }}_{{ $key }} + valueFrom: + secretKeyRef: + name: {{ template "gateway.fullname" $root }}-{{ $ref | lower }} + key: {{ $key }} + {{- end }} + {{- end }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/samples/BikeSharingApp/Gateway/charts/gateway/templates/ingress.yaml b/samples/BikeSharingApp/Gateway/charts/gateway/templates/ingress.yaml new file mode 100644 index 000000000..b5fc4fde2 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/charts/gateway/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "gateway.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "gateway.name" . }} + chart: {{ template "gateway.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/samples/BikeSharingApp/Gateway/charts/gateway/templates/secrets.yaml b/samples/BikeSharingApp/Gateway/charts/gateway/templates/secrets.yaml new file mode 100644 index 000000000..d49240f20 --- /dev/null +++ b/samples/BikeSharingApp/Gateway/charts/gateway/templates/secrets.yaml @@ -0,0 +1,12 @@ +{{- $root := . }} +{{- range $name, $values := .Values.secrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "gateway.fullname" $root }}-{{ $name | lower }} +data: + {{- range $key, $value := $values }} + {{ $key }}: {{ $value | b64enc }} + {{- end }} +--- +{{- end }} diff --git a/samples/BikeSharingApp/Gateway/charts/gateway/templates/service.yaml b/samples/BikeSharingApp/Gateway/charts/gateway/templates/service.yaml new file mode 100644 index 000000000..ec370ed0c --- /dev/null +++ b/samples/BikeSharingApp/Gateway/charts/gateway/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "gateway.fullname" . }} + labels: + app: {{ template "gateway.name" . }} + chart: {{ template "gateway.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app: {{ template "gateway.name" . }} + release: {{ .Release.Name }} diff --git a/samples/BikeSharingApp/Gateway/charts/gateway/values.yaml b/samples/BikeSharingApp/Gateway/charts/gateway/values.yaml new file mode 100644 index 000000000..f5829b71d --- /dev/null +++ b/samples/BikeSharingApp/Gateway/charts/gateway/values.yaml @@ -0,0 +1,72 @@ +# Default values for gateway. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +fullnameOverride: gateway +replicaCount: 1 +image: + repository: azdspublic/bikesharing-gateway + tag: build.20190418.2 + pullPolicy: IfNotPresent +imagePullSecrets: [] + # Optionally specify an array of imagePullSecrets. + # Secrets must be manually created in the namespace. + # ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod + # + # This uses credentials from secret "myRegistryKeySecretName". + # - name: myRegistryKeySecretName +service: + type: LoadBalancer + port: 80 + +probes: + enabled: false + +ingress: + enabled: true + annotations: + kubernetes.io/ingress.class: addon-http-application-routing + path: / + hosts: + - dev.gateway. + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local +secrets: + users: + dnsname: users + bikes: + dnsname: bikes + reservation: + dnsname: reservation + reservationengine: + dnsname: reservationengine + billing: + dnsname: billing + # Optionally specify a set of secret objects whose values + # will be injected as environment variables by default. + # You should add this section to a file like secrets.yaml + # that is explicitly NOT committed to source code control + # and then include it as part of your helm install step. + # ref: https://kubernetes.io/docs/concepts/configuration/secret/ + # + # This creates a secret "mysecret" and injects "mypassword" + # as the environment variable mysecret_mypassword=password. + # mysecret: + # mypassword: password +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi +nodeSelector: {} + +tolerations: [] + +affinity: {} \ No newline at end of file diff --git a/samples/BikeSharingApp/PopulateDatabase/.dockerignore b/samples/BikeSharingApp/PopulateDatabase/.dockerignore new file mode 100644 index 000000000..04f7b133d --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/.dockerignore @@ -0,0 +1,14 @@ +.dockerignore +.git +.gitignore +.vs +.vscode +**/*.*proj.user +**/azds.yaml +**/bin +**/charts +**/Dockerfile +**/Dockerfile.develop +**/obj +**/secrets.dev.yaml +**/values.dev.yaml \ No newline at end of file diff --git a/samples/BikeSharingApp/PopulateDatabase/Dockerfile b/samples/BikeSharingApp/PopulateDatabase/Dockerfile new file mode 100644 index 000000000..8aedd040b --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/Dockerfile @@ -0,0 +1,18 @@ +FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base +WORKDIR /app +EXPOSE 80 + +FROM microsoft/dotnet:2.1-sdk AS build +WORKDIR /src +COPY ["app.csproj", "."] +RUN dotnet restore "app.csproj" +COPY . . +RUN dotnet build "app.csproj" -c Release -o /app + +FROM build AS publish +RUN dotnet publish "app.csproj" -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "app.dll"] \ No newline at end of file diff --git a/samples/BikeSharingApp/PopulateDatabase/Dockerfile.develop b/samples/BikeSharingApp/PopulateDatabase/Dockerfile.develop new file mode 100644 index 000000000..b4b6eb418 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/Dockerfile.develop @@ -0,0 +1,15 @@ +FROM microsoft/dotnet:2.1-sdk +ARG BUILD_CONFIGURATION=Debug +ENV ASPNETCORE_ENVIRONMENT=Development +ENV DOTNET_USE_POLLING_FILE_WATCHER=true +EXPOSE 80 + +WORKDIR /src +COPY ["app.csproj", "./"] +RUN dotnet restore "app.csproj" +COPY . . +RUN dotnet build --no-restore -c $BUILD_CONFIGURATION + +RUN echo "exec dotnet run --no-build --no-launch-profile -c $BUILD_CONFIGURATION -- \"\$@\"" > /entrypoint.sh + +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] \ No newline at end of file diff --git a/samples/BikeSharingApp/PopulateDatabase/Program.cs b/samples/BikeSharingApp/PopulateDatabase/Program.cs new file mode 100644 index 000000000..701cbab27 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/Program.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace app +{ + public class Program + { + private static readonly HttpClient _httpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(10) }; + private static readonly string _gatewayUrl = $"http://{Environment.GetEnvironmentVariable("gateway_dnsname")}"; + private static readonly string _bikesUrl = $"http://{Environment.GetEnvironmentVariable("bikes_dnsname")}"; + private static readonly string _usersUrl = $"http://{Environment.GetEnvironmentVariable("users_dnsname")}"; + + public static void Main(string[] args) + { + using (_httpClient) + { + _WaitForServiceReadiness().Wait(); + _PopulateDatabase().Wait(); + Console.WriteLine("Shutting down."); + } + } + + private async static Task _WaitForServiceReadiness() + { + Console.WriteLine($"gatewayUrl: {_gatewayUrl}"); + Console.WriteLine($"bikesUrl: {_bikesUrl}"); + Console.WriteLine($"usersUrl: {_usersUrl}"); + + while (true) + { + Console.WriteLine("Checking to see if services are up..."); + + // Check bikes + bool bikesReady = false; + try { bikesReady = (await _httpClient.GetAsync($"{_bikesUrl}/hello")).IsSuccessStatusCode; } catch { } + if (!bikesReady) + { + Console.WriteLine("Bikes not ready :("); + Console.WriteLine($"{_bikesUrl}/hello"); + } + + // Check users + bool usersReady = false; + try { usersReady = (await _httpClient.GetAsync($"{_usersUrl}/hello")).IsSuccessStatusCode; } catch { } + if (!usersReady) + { + Console.WriteLine("Users not ready :("); + Console.WriteLine($"{_usersUrl}/hello"); + } + + // Check gateway + bool gatewayReady = false; + try { gatewayReady = (await _httpClient.GetAsync($"{_gatewayUrl}/hello")).IsSuccessStatusCode; } catch { } + if (!gatewayReady) + { + Console.WriteLine("Gateway not ready :("); + Console.WriteLine($"{_gatewayUrl}/hello"); + } + + if (bikesReady && usersReady && gatewayReady) + { + // Success! + Console.WriteLine("Services are up!"); + break; + } + + var sleep = TimeSpan.FromSeconds(10); + Console.WriteLine($"Sleeping for {sleep.TotalSeconds} seconds and trying again..."); + await Task.Delay(sleep); + } + } + + private async static Task _PopulateDatabase() + { + Console.WriteLine("Populating databases..."); + + // Read JSON directly from a file + JObject data = JObject.Parse(File.ReadAllText(@"data.json")); + JToken customers = (JToken)data.SelectToken("customers"); + JToken vendors = (JToken)data.SelectToken("vendors"); + JToken bikes = (JToken)data.SelectToken("bikes"); + + // Add users and bikes + Console.WriteLine("\n********************************************\nADDING USERS"); + foreach (var customer in customers) + { + Console.WriteLine("Adding user: " + customer.ToString()); + var response = await _httpClient.PostAsync(_gatewayUrl + "/api/user/", + new StringContent(customer.ToString(), Encoding.UTF8, "application/json")); + if (!response.IsSuccessStatusCode) + { + Console.WriteLine("Failed to add : " + customer.ToString()); + Console.WriteLine(response.StatusCode + " " + await response.Content.ReadAsStringAsync()); + } + } + + Console.WriteLine("\n********************************************\nADDING VENDORS"); + foreach (var vendor in vendors) + { + Console.WriteLine("Adding vendor: " + vendor.ToString()); + var response = await _httpClient.PostAsync(_gatewayUrl + "/api/user/vendor", + new StringContent(vendor.ToString(), Encoding.UTF8, "application/json")); + if (!response.IsSuccessStatusCode) + { + Console.WriteLine("Failed to add : " + vendor.ToString()); + Console.WriteLine(response.StatusCode + " " + await response.Content.ReadAsStringAsync()); + } + } + + Console.WriteLine("\n********************************************\nADDING BIKES"); + foreach (var bike in bikes) + { + Console.WriteLine("Adding bike: " + bike.ToString()); + var response = await _httpClient.PostAsync(_gatewayUrl + "/api/bike", + new StringContent(bike.ToString(), Encoding.UTF8, "application/json")); + if (!response.IsSuccessStatusCode) + { + Console.WriteLine("Failed to add : " + bike.ToString()); + Console.WriteLine(response.StatusCode + " " + await response.Content.ReadAsStringAsync()); + } + } + + Console.WriteLine("Finished populating databases."); + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/PopulateDatabase/app.csproj b/samples/BikeSharingApp/PopulateDatabase/app.csproj new file mode 100644 index 000000000..0bacf76d4 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/app.csproj @@ -0,0 +1,30 @@ + + + + Exe + netcoreapp2.1 + + + + + + + + + + + + + + + + + + Always + + + Always + + + + \ No newline at end of file diff --git a/samples/BikeSharingApp/PopulateDatabase/appsettings.json b/samples/BikeSharingApp/PopulateDatabase/appsettings.json new file mode 100644 index 000000000..a79da7fd4 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/appsettings.json @@ -0,0 +1,7 @@ +{ + "CustomConfiguration": { + "Services": { + "Gateway": "gateway" + } + } + } \ No newline at end of file diff --git a/samples/BikeSharingApp/PopulateDatabase/azds.yaml b/samples/BikeSharingApp/PopulateDatabase/azds.yaml new file mode 100644 index 000000000..15c77ea4a --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/azds.yaml @@ -0,0 +1,40 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: . + dockerfile: Dockerfile +install: + chart: charts/populatedatabase + values: + - values.dev.yaml? + - secrets.dev.yaml? + set: + replicaCount: 1 + image: + repository: populatedatabase + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + # This expands to [space.s.][rootSpace.]populatedatabase...azds.io + - $(spacePrefix)$(rootSpacePrefix)populatedatabase$(hostSuffix) +configurations: + develop: + build: + dockerfile: Dockerfile.develop + useGitIgnore: true + args: + BUILD_CONFIGURATION: ${BUILD_CONFIGURATION:-Debug} + container: + sync: + - "**/Pages/**" + - "**/Views/**" + - "**/wwwroot/**" + - "!**/*.{sln,csproj}" + command: [dotnet, run, --no-restore, --no-build, --no-launch-profile, -c, "${BUILD_CONFIGURATION:-Debug}"] + iterate: + processesToKill: [dotnet, vsdbg] + buildCommands: + - [dotnet, build, --no-restore, -c, "${BUILD_CONFIGURATION:-Debug}"] diff --git a/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/.helmignore b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/Chart.yaml b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/Chart.yaml new file mode 100644 index 000000000..a14d728ef --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: populatedatabase +version: 0.1.0 diff --git a/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/NOTES.txt b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/NOTES.txt new file mode 100644 index 000000000..4887be0f7 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "populatedatabase.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "populatedatabase.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "populatedatabase.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "populatedatabase.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/_helpers.tpl b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/_helpers.tpl new file mode 100644 index 000000000..b74a46919 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "populatedatabase.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "populatedatabase.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "populatedatabase.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/ingress.yaml b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/ingress.yaml new file mode 100644 index 000000000..a611ba1b2 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "populatedatabase.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "populatedatabase.name" . }} + chart: {{ template "populatedatabase.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/job.yaml b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/job.yaml new file mode 100644 index 000000000..b2bf134c1 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/job.yaml @@ -0,0 +1,24 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ template "populatedatabase.fullname" . }} +spec: + template: + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + env: +{{ toYaml .Values.env | indent 8 }} + {{- $root := . }} + {{- range $ref, $values := .Values.secrets }} + {{- range $key, $value := $values }} + - name: {{ $ref | upper }}_{{ $key | upper }} + valueFrom: + secretKeyRef: + name: {{ template "populatedatabase.fullname" $root }}-{{ $ref | lower }} + key: {{ $key }} + {{- end }} + {{- end }} + restartPolicy: {{ .Values.restartPolicy }} + backoffLimit: {{ .Values.backOffLimit }} \ No newline at end of file diff --git a/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/secrets.yaml b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/secrets.yaml new file mode 100644 index 000000000..51ac03da4 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/secrets.yaml @@ -0,0 +1,12 @@ +{{- $root := . }} +{{- range $name, $values := .Values.secrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "populatedatabase.fullname" $root }}-{{ $name | lower }} +data: + {{- range $key, $value := $values }} + {{ $key }}: {{ $value | b64enc }} + {{- end }} +--- +{{- end }} diff --git a/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/service.yaml b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/service.yaml new file mode 100644 index 000000000..e7e5907a6 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "populatedatabase.fullname" . }} + labels: + app: {{ template "populatedatabase.name" . }} + chart: {{ template "populatedatabase.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app: {{ template "populatedatabase.name" . }} + release: {{ .Release.Name }} diff --git a/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/values.yaml b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/values.yaml new file mode 100644 index 000000000..e5fb11ce9 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/charts/populatedatabase/values.yaml @@ -0,0 +1,74 @@ +# Default values for populatedatabase. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +fullnameOverride: populatedatabase +replicaCount: 1 +image: + repository: azdspublic/bikesharing-populatedatabase + tag: build.20190418.2 + pullPolicy: IfNotPresent +imagePullSecrets: [] + # Optionally specify an array of imagePullSecrets. + # Secrets must be manually created in the namespace. + # ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod + # + # This uses credentials from secret "myRegistryKeySecretName". + # - name: myRegistryKeySecretName + +env: + - name: gateway_dnsname + value: gateway + - name: bikes_dnsname + value: bikes + - name: users_dnsname + value: users + +restartPolicy: Never +backOffLimit: 4 + +service: + type: ClusterIP + port: 80 + +probes: + enabled: false + +ingress: + enabled: false + annotations: + kubernetes.io/ingress.class: addon-http-application-routing + path: / + # hosts: + # - chart-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local +secrets: {} + # Optionally specify a set of secret objects whose values + # will be injected as environment variables by default. + # You should add this section to a file like secrets.yaml + # that is explicitly NOT committed to source code control + # and then include it as part of your helm install step. + # ref: https://kubernetes.io/docs/concepts/configuration/secret/ + # + # This creates a secret "mysecret" and injects "mypassword" + # as the environment variable mysecret_mypassword=password. + # mysecret: + # mypassword: password +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi +nodeSelector: {} + +tolerations: [] + +affinity: {} \ No newline at end of file diff --git a/samples/BikeSharingApp/PopulateDatabase/data.json b/samples/BikeSharingApp/PopulateDatabase/data.json new file mode 100644 index 000000000..957e50805 --- /dev/null +++ b/samples/BikeSharingApp/PopulateDatabase/data.json @@ -0,0 +1,145 @@ +{ + "customers": [ + { + "name": "Aurelia Briggs", + "address": "404 E Harrison St, Seattle, WA 98102", + "phone": "2025550128", + "email": "aurelia.briggs@contoso.com", + "ccNumber": "1234567890123456", + "ccExpiry": "2022-01-01", + "ccccv": "1234" + }, + { + "name": "Terrence Freeland", + "address": "3600 157th Ave NE, Redmond, WA 98052", + "phone": "2025550163", + "email": "terrence.freeland@contoso.com", + "ccNumber": "1234567890123456", + "ccExpiry": "2022-01-01", + "ccccv": "1234" + } + ], + "vendors": [ + { + "id": "16a46619738a4865a5afcb5e18ffb138", + "name": "Fanny Melton", + "address": "100 108th Ave NE, Bellevue, WA 98004", + "phone": "2025550195", + "email": "fanny.melton@contoso.com", + "routingNumber": "0987", + "accountNumber": "6543" + } + ], + "bikes": [ + { + "model": "Women's Cruiser", + "hourlyCost": 1.00, + "type": "tandem", + "address": "1907 18th Ave S, Seattle, WA 98144", + "ownerUserId": "16a46619738a4865a5afcb5e18ffb138", + "suitableHeightInMeters": 1.7, + "maximumWeightInKg": 150, + "imageUrl": "https://contosobikerental.blob.core.windows.net/public/images/sample-bike-02.jpg" + }, + { + "model": "Men's Comfort", + "hourlyCost": 1.50, + "type": "tandem", + "address": "2100 Queen Anne Ave N, Seattle, WA 98109", + "ownerUserId": "16a46619738a4865a5afcb5e18ffb138", + "suitableHeightInMeters": 1.7, + "maximumWeightInKg": 150, + "imageUrl": "https://contosobikerental.blob.core.windows.net/public/images/sample-bike-11.jpg" + }, + { + "model": "Men's Cruiser", + "hourlyCost": 1.00, + "type": "tandem", + "address": "8431 SE 39th St, Mercer Island, WA 98040", + "ownerUserId": "16a46619738a4865a5afcb5e18ffb138", + "suitableHeightInMeters": 1.7, + "maximumWeightInKg": 150, + "imageUrl": "https://contosobikerental.blob.core.windows.net/public/images/sample-bike-01.jpg" + }, + { + "model": "Men's Comfort", + "hourlyCost": 1.50, + "type": "tandem", + "address": "3401 California Ave SW, Seattle, WA 98116", + "ownerUserId": "16a46619738a4865a5afcb5e18ffb138", + "suitableHeightInMeters": 1.7, + "maximumWeightInKg": 150, + "imageUrl": "https://contosobikerental.blob.core.windows.net/public/images/sample-bike-14.jpg" + }, + { + "model": "Men's Cruiser", + "hourlyCost": 1.00, + "type": "tandem", + "address": "283 NW Market St, Seattle, WA 98107", + "ownerUserId": "16a46619738a4865a5afcb5e18ffb138", + "suitableHeightInMeters": 1.7, + "maximumWeightInKg": 150, + "imageUrl": "https://contosobikerental.blob.core.windows.net/public/images/sample-bike-03.jpg" + }, + { + "model": "Girl's Cruiser", + "hourlyCost": 1.00, + "type": "tandem", + "address": "500 17th Ave, Seattle, WA 98122", + "ownerUserId": "16a46619738a4865a5afcb5e18ffb138", + "suitableHeightInMeters": 1.2, + "maximumWeightInKg": 75, + "imageUrl": "https://contosobikerental.blob.core.windows.net/public/images/sample-bike-09.jpg" + }, + { + "model": "Women's Cruiser", + "hourlyCost": 1.00, + "type": "tandem", + "address": "1302 Market St, Kirkland, WA 98033", + "ownerUserId": "16a46619738a4865a5afcb5e18ffb138", + "suitableHeightInMeters": 1.7, + "maximumWeightInKg": 150, + "imageUrl": "https://contosobikerental.blob.core.windows.net/public/images/sample-bike-10.jpg" + }, + { + "model": "Women's Cruiser", + "hourlyCost": 1.00, + "type": "tandem", + "address": "8049 18th Ave NW, Seattle, WA 98117", + "ownerUserId": "16a46619738a4865a5afcb5e18ffb138", + "suitableHeightInMeters": 1.7, + "maximumWeightInKg": 150, + "imageUrl": "https://contosobikerental.blob.core.windows.net/public/images/sample-bike-08.jpg" + }, + { + "model": "Men's Racing", + "hourlyCost": 2.00, + "type": "tandem", + "address": "7516 135th Ave SE, Newcastle, WA 98059", + "ownerUserId": "16a46619738a4865a5afcb5e18ffb138", + "suitableHeightInMeters": 1.7, + "maximumWeightInKg": 150, + "imageUrl": "https://contosobikerental.blob.core.windows.net/public/images/sample-bike-15.jpg" + }, + { + "model": "Men's Cruiser", + "hourlyCost": 1.00, + "type": "tandem", + "address": "8638 22nd Ave SW, Seattle, WA 98106", + "ownerUserId": "16a46619738a4865a5afcb5e18ffb138", + "suitableHeightInMeters": 1.7, + "maximumWeightInKg": 150, + "imageUrl": "https://contosobikerental.blob.core.windows.net/public/images/sample-bike-04.jpg" + }, + { + "model": "Women's Racer", + "hourlyCost": 2.00, + "type": "tandem", + "address": "14505 NE 91st St, Redmond, WA 98052", + "ownerUserId": "16a46619738a4865a5afcb5e18ffb138", + "suitableHeightInMeters": 1.7, + "maximumWeightInKg": 150, + "imageUrl": "https://contosobikerental.blob.core.windows.net/public/images/sample-bike-05.jpg" + } + ] + } diff --git a/samples/BikeSharingApp/README.md b/samples/BikeSharingApp/README.md new file mode 100644 index 000000000..538f28af9 --- /dev/null +++ b/samples/BikeSharingApp/README.md @@ -0,0 +1,135 @@ +# Bike Sharing Sample: Iteratively Develop and Debug Microservices in Kubenertes + +## Prerequisites +1. An Azure subscription. If you don't have one, you can create a [free account](https://azure.microsoft.com/free). +1. [Visual Studio Code](https://code.visualstudio.com/download). +1. [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest) version 2.0.43 or higher. +1. [Helm](https://github.com/helm/helm/blob/master/docs/install.md) + + +## Setup + +1. Create an AKS resource group, specifying its name and region: + + ``` + az group create --name bikesharinggroup --location eastus2 + ``` + +1. Create a Kubernetes cluster in AKS: + + ``` + az aks create -g bikesharinggroup -n bikesharingcluster --location eastus2 --node-vm-size Standard_DS2_v2 --node-count 1 --generate-ssh-keys --disable-rbac + ``` + +1. Enable Azure Dev Spaces for the cluster we just created: + + ``` + az aks use-dev-spaces -g bikesharinggroup -n bikesharingcluster --space dev --yes + ``` + + We now have an AKS cluster named `bikesharingcluster` in resource group `bikesharinggroup` in the `eastus2` region with Azure Dev Spaces enabled. + +1. We now need to retrieve the host suffix. To do this, run the following command and note the `HostSuffix` field's value: + + ``` + azds show-context + ``` + +1. We have to update some of our code with this host suffix. Open the `./charts/values.yaml` file and replace the occurrences of `` in this file with the host suffix. + +1. From the source repository's root folder, navigate to the `charts` folder: + + ``` + cd charts + ``` + +1. Run the app's API and Data services: + + ``` + helm init --wait + helm install -n bikesharing . --dep-up --namespace dev --wait + ``` + +1. Run `azds list-uris` to display the web frontend's url, and open it in a browser. Select one of the sample customer accounts to sign into the web app. + + +## Set the app's state for the demo +If you want to demo finding and fixing a bug: bikes are still (incorrectly) displayed as available even if the bike is currently in use. +1. Open the webapp in the browser, and select one of the sample user accounts (e.g. *Aurelia Briggs*). +1. Select a bike and rent it. Remember the bike, as we'll refer to it later. +1. Navigate to the sign-in page by appending '/devsignin' to the root URL. + +## Add multiple dev spaces to the same cluster +We'll demonstrate how multiple developers on a team can use the same cluster, so let's create multiple child dev spaces: +``` +azds space select -n dev/stephen +azds space select -n dev/lisa +azds space select -n dev/john +``` + +## Walkthrough + +1. Our team is building a Bike Sharing app where you can view available bikes in your area, rent a bike, and then later return it to a designated location. You're billed for the time you used the bike when you return it. + 1. Select another use to sign into the web app, for example **Terrence Freeland** + 1. Select a bike, then click "Rent". + 1. Click through the experience for returning a bike: click "Return bike", then "Confirm return". Optionally submit a review. +1. Most of the time this process works, but we currently seem to have a bug where, sometimes, a bike can't be rented. + 1. Select the bike that *Aurelia* has checked out. + 1. Click "Rent" - nothing happens: no confirmation, no error. +1. Our app consists of several services -- users, bikes, reservations, billing, reviews, etc -- and I've been asked to track the bug down. I'll start with the *`Bikes`* service, as maybe I can glean some clues about what's different about this particular problem bike. First, let's connect to the cluster where the full app is running: + 1. Open a terminal window, and run: `az aks use-dev-spaces -g bikesharinggroup -n bikesharingcluster` (Your resource group and cluster name may be different.) + 1. When prompted, select a child dev space, for example: `dev/john` (you can always change selection via `azds space select`). + +1. Now let's debug the `bikes` service: + 1. Open VS Code on the `./Bikes` folder. If prompted by the Dev Spaces extension, click 'Yes' to generate debug configuration. + 1. Set a debug breakpoint in `server.js` inside `GET bike` (around line 228). + ``` javascript + // get bike ------------------------------------------------------------ + app.get('/api/bikes/:bikeId', function(req, res) { + ``` + 1. Enzure the AZDS debugger profile is selected: **Attach to server (AZDS)**. + 1. Hit F5 - this syncs code to AKS, builds the container image, deploys it in a pod, and attaches the debugger to it. + 1. Open the browser to the page that displays available bikes, and then select the "problem bike". Our debug breakpoint is not hit - that's a good sign that shows how anyone else working in the same cluster will be unaffected by our activity in our own dev space. + 1. To reach our specific instance of the `Bikes` service, prefix the web app's URL with `.s.` For example: `http://john.s.dev.bikesharingweb...aksapp.io`. This will route requests to `http://bikes` to our version of the `bikes` service running in the `john` namespace. + 1. Prefix the URL appropriately in the browser and refresh the page - the debug breakpoint in VS Code will activate. + 1. Step through the code - even though the container we're debugging is running in AKS, you'll notice that the debugger is reasonably responsive. And, we have access to the full richness of data in the debugger: local variables, call stacks, streaming logs, etc. + 1. Stepping through the code we notice that a Mongo database request is made to retrieve bike details. We can inspect the local variable `theBike` to glean detailed bike info. + 1. Set another breakpoint on the last line of code (because it's part of a separate callback function): + ``` javascript + res.send(theBike); + ``` + 1. Continue execution to hit this last breakpoint. + 1. Inspecting `theBike` in the debugger, we notice something doesn't seem right: `theBike.available = false`. +1. It may be that this bike shouldn't have been displayed as an *available bike* in the app's preceeding page. + 1. Navigate to the function that handles `GET availableBikes` (around line 99): + ``` javascript + // find bike ------------------------------------------------------------ + app.get('/api/availableBikes', function (req, res) { + ``` + 1. Set a breakpoint on the last line of code for that function: + ``` javascript + res.send(data); + ``` + 1. In the web app, navigate to view the list of available bikes (click on the app's logo in the header). + 1. In the debugger, view the `data` variable. We notice that aside from our "problem bike", the other bikes are `availabe = true`. +1. Let's experiment with a fix. + 1. Uncomment line 104 that modifies the `query` filter object that is passed to mongo. + ``` javascript + // BUG! Uncomment code to fix :) + query = { available: true }; + ``` + 1. Save the code file. + 1. Refresh the page in the browser. Our problem bike is filtered out! Notice how seeing the updated behavior is fast - the container image didn't need to be recreated; instead, the updated code was synced directly to the running container and `nodemon` was restarted (for compiled languages like C# or Java then a re-compilation is kicked off inside the container instance). + 1. Notice that if we remove the `john.s.` prefix in the browser's URL, then we see the original behavior (our modified `bikes` service in `dev/john` is not hit). + +Our next step would be to continue testing our fix, then commit and push to the source repo. If we have a CI/CD pipeline set up, it will be triggered to update the team's baseline (the 'dev' namespace). At that point, everyone working in our AKS cluster will automatically see the updated behavior (this is another benefit of working in a shared team cluster, because the team always work with up to date dependencies). + +## Clean up cloud resources +Delete the AKS cluster's **resource group** to permanently delete all Azure resources created in this walkthrough. +```bash +# View AKS clusters +az aks list -o table + +# Delete all resources in group `bikesharinggroup` +az group delete -n bikesharinggroup --no-wait +``` \ No newline at end of file diff --git a/samples/BikeSharingApp/Reservation/.gitignore b/samples/BikeSharingApp/Reservation/.gitignore new file mode 100644 index 000000000..daf913b1b --- /dev/null +++ b/samples/BikeSharingApp/Reservation/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/samples/BikeSharingApp/Reservation/Dockerfile b/samples/BikeSharingApp/Reservation/Dockerfile new file mode 100644 index 000000000..8dc264f08 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.8 + +# Bundle app source +RUN mkdir /app +ADD /app /app +WORKDIR /app + +# Get dependencies and build +RUN go get -d -v +RUN go build -o main . + +# Bind to port 80 +EXPOSE 80 + +# Start Go server +ENTRYPOINT ["/app/main"] diff --git a/samples/BikeSharingApp/Reservation/app/connectionConfig.go b/samples/BikeSharingApp/Reservation/app/connectionConfig.go new file mode 100644 index 000000000..143dac628 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/app/connectionConfig.go @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +const ( + reservationMongoDBConnectionString string = `dummyconnectionstring` + reservationMongoDBDatabase string = `dummydatabase` + reservationMongoDBCollection string = `dummycollection` +) diff --git a/samples/BikeSharingApp/Reservation/app/handlers.go b/samples/BikeSharingApp/Reservation/app/handlers.go new file mode 100644 index 000000000..4fa2e52fe --- /dev/null +++ b/samples/BikeSharingApp/Reservation/app/handlers.go @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "fmt" + "net/http" + + "gopkg.in/mgo.v2/bson" + + "encoding/json" + + "github.com/gorilla/mux" +) + +func HelloHandler(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "It's-a-me Mario.") +} + +func addReservationHandler(w http.ResponseWriter, req *http.Request) { + jsonDecoder := json.NewDecoder(req.Body) + reservationDetails := ReservationDetails{} + if err := jsonDecoder.Decode(&reservationDetails); err != nil { + http.Error(w, "BadRequest: Invalid request body.", http.StatusBadRequest) + return + } + + if err := reservationDetails.Validate(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + LogInfo("Inserting reservation document for reservationId: %s", reservationDetails.ReservationID) + InsertDocument(reservationDetails) +} + +func getReservationHandler(w http.ResponseWriter, req *http.Request) { + varsMap := mux.Vars(req) + reservationID := varsMap["reservationId"] + var queryResult ReservationDetails + query := bson.M{"reservationId": reservationID} + LogInfo("Querying for reservationId: %s", reservationID) + if err := QueryOne(query, &queryResult); err != nil { + LogError("Couldn't get reservation for reservationId: %s. Reason: %v", reservationID, err) + http.Error(w, fmt.Sprintf("BadRequest: %v", err), http.StatusBadRequest) + return + } + + if queryResult == (ReservationDetails{}) { + LogError("No reservation found for reservationId: %s", reservationID) + http.Error(w, fmt.Sprintf("BadRequest: No reservation found for reservationId: %s", reservationID), http.StatusBadRequest) + return + } + + LogInfo("Reservation found for reservationId: %s", reservationID) + w.Header().Set("Content-Type", "application/json") + jsonResponse, _ := json.Marshal(queryResult) + fmt.Fprintf(w, string(jsonResponse)) +} + +func getAllReservationsHandler(w http.ResponseWriter, req *http.Request) { + var queryResult []ReservationDetails + LogInfo("Getting all reservations") + if err := QueryAll(bson.M{}, &queryResult); err != nil { + LogError("Couldn't get all reservations. Reason: %v", err) + http.Error(w, fmt.Sprintf("InternalServerError: %v", err), http.StatusInternalServerError) + return + } + + LogInfo("Returning %d reservations", len(queryResult)) + w.Header().Set("Content-Type", "application/json") + jsonResponse, _ := json.Marshal(queryResult) + fmt.Fprintf(w, string(jsonResponse)) +} + +func listReservationsHandler(w http.ResponseWriter, req *http.Request) { + varsMap := mux.Vars(req) + userID := varsMap["userId"] + var queryResult []ReservationDetails + query := bson.M{"userId": userID} + LogInfo("Querying reservations for userId: %s", userID) + if err := QueryAll(query, &queryResult); err != nil { + LogError("Couldn't get reservations for userId: %s. Reason: %v", userID, err) + http.Error(w, fmt.Sprintf("BadRequest: %v", err), http.StatusBadRequest) + return + } + + LogInfo("Found %d reservations for userId: %s", len(queryResult), userID) + w.Header().Set("Content-Type", "application/json") + jsonResponse, _ := json.Marshal(queryResult) + fmt.Fprintf(w, string(jsonResponse)) +} diff --git a/samples/BikeSharingApp/Reservation/app/logger.go b/samples/BikeSharingApp/Reservation/app/logger.go new file mode 100644 index 000000000..f7c981cb4 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/app/logger.go @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "fmt" + "log" + "os" +) + +var flags = log.Flags() | log.LUTC | log.Lmicroseconds | log.Lshortfile +var stdLogger = log.New(os.Stdout, "Reservation Service: ", flags) +var errLogger = log.New(os.Stderr, "Reservation Service Error: ", flags) + +func LogInfo(format string, a ...interface{}) { + str := fmt.Sprintf(format, a...) + stdLogger.Output(3, str) +} + +func LogError(format string, a ...interface{}) { + str := fmt.Sprintf(format, a...) + errLogger.Output(3, str) +} diff --git a/samples/BikeSharingApp/Reservation/app/main.go b/samples/BikeSharingApp/Reservation/app/main.go new file mode 100644 index 000000000..3a9424e1e --- /dev/null +++ b/samples/BikeSharingApp/Reservation/app/main.go @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "net/http" + "os/signal" + "syscall" + "time" + + "fmt" + "os" + "sync" + + "github.com/gorilla/mux" +) + +var ( + DbConnection *MongoHelper + ShutdownSignal = sync.NewCond(&sync.Mutex{}) + ShutdownWg = &sync.WaitGroup{} + Port = 80 +) + +func init() { + DbConnection = CreateMongoConnection() + ShutdownWg.Add(1) + go listenForUnexpectedMongoDbShutdown(DbConnection) + + // Define a channel that will be called when the OS wants the program to exit + // This will be used to gracefully shutdown the app + osChan := make(chan os.Signal, 5) + signal.Notify(osChan, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGABRT, syscall.SIGQUIT) + go func() { + fmt.Fprintf(os.Stderr, "OS signal received: %v\n", <-osChan) + shutdown() + }() +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/hello", HelloHandler).Methods(http.MethodGet) + r.HandleFunc("/api/allReservations", getAllReservationsHandler).Methods(http.MethodGet) + r.HandleFunc("/api/reservation", addReservationHandler).Methods(http.MethodPost) + r.HandleFunc("/api/reservation/{reservationId}", getReservationHandler).Methods(http.MethodGet) + r.HandleFunc("/api/user/{userId}/reservations", listReservationsHandler).Methods(http.MethodGet) + go func() { + LogInfo("Listening on port: %d", Port) + fmt.Fprintf(os.Stderr, "%v\n", http.ListenAndServe(fmt.Sprintf(":%d", Port), r)) + shutdown() + }() + + ShutdownWg.Wait() + fmt.Println("Graceful shutdown.") +} + +var unexpectedShutdownOnce sync.Once + +func listenForUnexpectedMongoDbShutdown(conn *MongoHelper) { + shutdownChan := make(chan struct{}, 1) + go func() { + ShutdownSignal.L.Lock() + ShutdownSignal.Wait() + ShutdownSignal.L.Unlock() + shutdownChan <- struct{}{} + }() + + defer func() { + if r := recover(); r != nil { + fmt.Fprintf(os.Stderr, "Panic while pinging MongoDb: %v\n", r) + unexpectedShutdownOnce.Do(shutdown) + } + }() + +checkLoop: + for { + timer := time.NewTimer(3 * time.Second) + select { + case <-shutdownChan: + timer.Stop() + break checkLoop + case <-timer.C: + // Do nothing + } + + if err := conn.session.Ping(); err != nil { + fmt.Fprintln(os.Stderr, "MongoDbConnection shut down unexpectedly!") + unexpectedShutdownOnce.Do(shutdown) + } + } +} + +func shutdown() { + fmt.Println("Shutting down!") + ShutdownSignal.Broadcast() + + if DbConnection != nil { + DbConnection.session.Close() + } + + ShutdownWg.Done() +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Reservation/app/mongohelper.go b/samples/BikeSharingApp/Reservation/app/mongohelper.go new file mode 100644 index 000000000..f84b54cb8 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/app/mongohelper.go @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "crypto/tls" + "fmt" + "net" + "os" + "strings" + "time" + + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +const ( + mongoDbConnectionStringEnvName = "mongo_connectionstring" + mongoDbNameEnvName = "mongo_dbname" + mongoDbCollectionEnvName = "mongo_collection" +) + +// MongoDB details with session, db and collection +type MongoHelper struct { + session *mgo.Session + database *mgo.Database + collection *mgo.Collection +} + +// Connect to the MongoDB +func CreateMongoConnection() *MongoHelper { + uri := os.Getenv(mongoDbConnectionStringEnvName) + if uri == "" { + uri = reservationMongoDBConnectionString + } + + useSsl := false + if strings.Contains(uri, "?ssl=true") { + uri = strings.TrimSuffix(uri, "?ssl=true") + useSsl = true + } + + dialInfo, err := mgo.ParseURL(uri) + if err != nil { + fmt.Println("failed to parse URI: ", err) + os.Exit(1) + } + + if useSsl { + tlsConfig := &tls.Config{} + tlsConfig.InsecureSkipVerify = true + dialInfo.DialServer = func(addr *mgo.ServerAddr) (net.Conn, error) { + conn, err := tls.Dial("tcp", addr.String(), tlsConfig) + return conn, err + } + } + + var mongoSession *mgo.Session + maxTries := 5 + fmt.Printf("Connecting to Mongo: %s\n", uri) + for i := 1; i <= maxTries; i++ { + mongoSession, err = mgo.DialWithInfo(dialInfo) + if err == nil { + break + } + + if i < maxTries { + fmt.Printf("%d/%d - Couldn't connect, sleeping and trying again\n", i, maxTries) + time.Sleep(1 * time.Second) + } else { + fmt.Printf("%d/%d - Couldn't connect.\n", i, maxTries) + } + } + if err != nil { + fmt.Fprintf(os.Stderr, "failed to connect to mongodb: %s\n", err) + os.Exit(1) + } + fmt.Println("Connected to Mongo") + + db := os.Getenv(mongoDbNameEnvName) + if db == "" { + db = reservationMongoDBDatabase + } + + collection := os.Getenv(mongoDbCollectionEnvName) + if collection == "" { + collection = reservationMongoDBCollection + } + + mongoHelper := &MongoHelper{ + session: mongoSession, + database: mongoSession.DB(db), + collection: mongoSession.DB(db).C(collection), + } + + return mongoHelper +} + +func InsertDocument(doc interface{}) error { + mongoHelper := DbConnection + if err := mongoHelper.collection.Insert(doc); err != nil { + return err + } + + return nil +} + +func QueryOne(query bson.M, result interface{}) error { + mongoHelper := DbConnection + if err := mongoHelper.collection.Find(query).One(result); err != nil { + return err + } + + return nil +} + +func QueryAll(query bson.M, result interface{}) error { + mongoHelper := DbConnection + if err := mongoHelper.collection.Find(query).All(result); err != nil { + return err + } + + return nil +} diff --git a/samples/BikeSharingApp/Reservation/app/reservationdetails.go b/samples/BikeSharingApp/Reservation/app/reservationdetails.go new file mode 100644 index 000000000..ac0d6cd0c --- /dev/null +++ b/samples/BikeSharingApp/Reservation/app/reservationdetails.go @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "encoding/json" + "errors" +) + +// ReservationDetails for bike reservations as read from the mongoDB. +type ReservationDetails struct { + ReservationID string `bson:"reservationId" json:"reservationId"` + BikeID string `bson:"bikeId" json:"bikeId"` + UserID string `bson:"userId" json:"userId"` + RequestTime string `bson:"requestTime" json:"requestTime"` + StartTime string `bson:"startTime" json:"startTime"` + EndTime string `bson:"endTime" json:"endTime"` + State string `bson:"state" json:"state"` + RequestId string `bson:"requestId" json:"requestId"` +} + +func (reservationDetails ReservationDetails) Validate() error { + var errorSlice []string + var zeroString string + + if reservationDetails.ReservationID == zeroString { + errorSlice = append(errorSlice, "Must specify reservationId string") + } + if reservationDetails.BikeID == zeroString { + errorSlice = append(errorSlice, "Must specify bikeId string") + } + if reservationDetails.UserID == zeroString { + errorSlice = append(errorSlice, "Must specify userId string") + } + if reservationDetails.RequestTime == zeroString { + errorSlice = append(errorSlice, "Must specify requestTime string") + } + if reservationDetails.StartTime == zeroString { + errorSlice = append(errorSlice, "Must specify startTime string") + } + if reservationDetails.State == zeroString { + errorSlice = append(errorSlice, "Must specify state string") + } + if reservationDetails.RequestId == zeroString { + errorSlice = append(errorSlice, "Must specify requestId string") + } + + if len(errorSlice) > 0 { + errorBytes, _ := json.Marshal(errorSlice) + return errors.New(string(errorBytes)) + } + + return nil +} diff --git a/samples/BikeSharingApp/Reservation/azds.yaml b/samples/BikeSharingApp/Reservation/azds.yaml new file mode 100644 index 000000000..1eb5aa250 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/azds.yaml @@ -0,0 +1,26 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: . + dockerfile: Dockerfile +install: + chart: charts/reservation + values: + - values.dev.yaml? + - secrets.dev.yaml? + set: + replicaCount: 1 + image: + repository: reservation + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + # This expands to [space.s.][rootSpace.]reservation...azds.io + - $(spacePrefix)$(rootSpacePrefix)reservation$(hostSuffix) +configurations: + develop: + build: + useGitIgnore: true \ No newline at end of file diff --git a/samples/BikeSharingApp/Reservation/charts/reservation/.helmignore b/samples/BikeSharingApp/Reservation/charts/reservation/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/charts/reservation/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/samples/BikeSharingApp/Reservation/charts/reservation/Chart.yaml b/samples/BikeSharingApp/Reservation/charts/reservation/Chart.yaml new file mode 100644 index 000000000..a43ab449e --- /dev/null +++ b/samples/BikeSharingApp/Reservation/charts/reservation/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: reservation +version: 0.1.0 diff --git a/samples/BikeSharingApp/Reservation/charts/reservation/templates/NOTES.txt b/samples/BikeSharingApp/Reservation/charts/reservation/templates/NOTES.txt new file mode 100644 index 000000000..fb7f00416 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/charts/reservation/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "reservation.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "reservation.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "reservation.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "reservation.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/samples/BikeSharingApp/Reservation/charts/reservation/templates/_helpers.tpl b/samples/BikeSharingApp/Reservation/charts/reservation/templates/_helpers.tpl new file mode 100644 index 000000000..95f2fc337 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/charts/reservation/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "reservation.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "reservation.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "reservation.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/samples/BikeSharingApp/Reservation/charts/reservation/templates/deployment.yaml b/samples/BikeSharingApp/Reservation/charts/reservation/templates/deployment.yaml new file mode 100644 index 000000000..c245ef63e --- /dev/null +++ b/samples/BikeSharingApp/Reservation/charts/reservation/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: {{ template "reservation.fullname" . }} + labels: + app: {{ template "reservation.name" . }} + chart: {{ template "reservation.chart" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "reservation.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "reservation.name" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + annotations: + buildID: {{ .Values.buildID }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + {{- if .Values.probes.enabled }} + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + {{- end }} + env: + {{- $root := . }} + {{- range $ref, $values := .Values.secrets }} + {{- range $key, $value := $values }} + - name: {{ $ref }}_{{ $key }} + valueFrom: + secretKeyRef: + name: {{ template "reservation.fullname" $root }}-{{ $ref | lower }} + key: {{ $key }} + {{- end }} + {{- end }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/samples/BikeSharingApp/Reservation/charts/reservation/templates/ingress.yaml b/samples/BikeSharingApp/Reservation/charts/reservation/templates/ingress.yaml new file mode 100644 index 000000000..249291a83 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/charts/reservation/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "reservation.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "reservation.name" . }} + chart: {{ template "reservation.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/samples/BikeSharingApp/Reservation/charts/reservation/templates/secrets.yaml b/samples/BikeSharingApp/Reservation/charts/reservation/templates/secrets.yaml new file mode 100644 index 000000000..c61d03f93 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/charts/reservation/templates/secrets.yaml @@ -0,0 +1,12 @@ +{{- $root := . }} +{{- range $name, $values := .Values.secrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "reservation.fullname" $root }}-{{ $name | lower }} +data: + {{- range $key, $value := $values }} + {{ $key }}: {{ $value | b64enc }} + {{- end }} +--- +{{- end }} diff --git a/samples/BikeSharingApp/Reservation/charts/reservation/templates/service.yaml b/samples/BikeSharingApp/Reservation/charts/reservation/templates/service.yaml new file mode 100644 index 000000000..9838782a7 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/charts/reservation/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "reservation.fullname" . }} + labels: + app: {{ template "reservation.name" . }} + chart: {{ template "reservation.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app: {{ template "reservation.name" . }} + release: {{ .Release.Name }} diff --git a/samples/BikeSharingApp/Reservation/charts/reservation/values.yaml b/samples/BikeSharingApp/Reservation/charts/reservation/values.yaml new file mode 100644 index 000000000..f470d8af8 --- /dev/null +++ b/samples/BikeSharingApp/Reservation/charts/reservation/values.yaml @@ -0,0 +1,66 @@ +# Default values for reservation. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +fullnameOverride: reservation +replicaCount: 1 +image: + repository: azdspublic/bikesharing-reservation + tag: build.20190418.1 + pullPolicy: IfNotPresent +imagePullSecrets: [] + # Optionally specify an array of imagePullSecrets. + # Secrets must be manually created in the namespace. + # ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod + # + # This uses credentials from secret "myRegistryKeySecretName". + # - name: myRegistryKeySecretName +service: + type: ClusterIP + port: 80 + +probes: + enabled: false + +ingress: + enabled: false + annotations: + kubernetes.io/ingress.class: addon-http-application-routing + path: / + # hosts: + # - chart-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local +secrets: + mongo: + connectionstring: mongodb://databases-mongo + dbname: resdb + collection: reservation + # Optionally specify a set of secret objects whose values + # will be injected as environment variables by default. + # You should add this section to a file like secrets.yaml + # that is explicitly NOT committed to source code control + # and then include it as part of your helm install step. + # ref: https://kubernetes.io/docs/concepts/configuration/secret/ + # + # This creates a secret "mysecret" and injects "mypassword" + # as the environment variable mysecret_mypassword=password. + # mysecret: + # mypassword: password +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi +nodeSelector: {} + +tolerations: [] + +affinity: {} \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/.dockerignore b/samples/BikeSharingApp/ReservationEngine/.dockerignore new file mode 100644 index 000000000..04f7b133d --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/.dockerignore @@ -0,0 +1,14 @@ +.dockerignore +.git +.gitignore +.vs +.vscode +**/*.*proj.user +**/azds.yaml +**/bin +**/charts +**/Dockerfile +**/Dockerfile.develop +**/obj +**/secrets.dev.yaml +**/values.dev.yaml \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/.gitignore b/samples/BikeSharingApp/ReservationEngine/.gitignore new file mode 100644 index 000000000..5ddbe03f5 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/.gitignore @@ -0,0 +1,254 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +*.sln + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml diff --git a/samples/BikeSharingApp/ReservationEngine/BikesHelper.cs b/samples/BikeSharingApp/ReservationEngine/BikesHelper.cs new file mode 100644 index 000000000..04591487b --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/BikesHelper.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using app.Models; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; + +namespace app +{ + public static class BikesHelper + { + private static string _bikesService { get; set; } + + public static void Init(CustomConfiguration customConfiguration) + { + LogUtility.Log("BikesHelper init start"); + _bikesService = Environment.GetEnvironmentVariable(Constants.BikesMicroserviceEnv) ?? customConfiguration.Services.Bikes; + LogUtility.Log("BikesHelper init end"); + } + + public static async Task ReserveBike(Guid requestId, string bikeId, HttpRequest originRequest) + { + LogUtility.LogWithContext(requestId, "Reserving BikeID " + bikeId); + string reserveBikeUrl = $"http://{_bikesService}/api/bikes/{bikeId}/reserve"; + var response = await HttpHelper.PatchAsync(requestId, reserveBikeUrl, null, originRequest); + return response; + } + + public static async Task FreeBike(Guid requestId, string bikeId, HttpRequest originRequest) + { + LogUtility.LogWithContext(requestId, "Freeing BikeID " + bikeId); + string freeBikeUrl = $"http://{_bikesService}/api/bikes/{bikeId}/clear"; + var response = await HttpHelper.PatchAsync(requestId, freeBikeUrl, null, originRequest); + return response; + } + + public static async Task GetBike(Guid requestId, string bikeId, HttpRequest originRequest) + { + LogUtility.LogWithContext(requestId, "Getting BikeID " + bikeId); + string getBikeUrl = $"http://{_bikesService}/api/bikes/{bikeId}"; + var response = await HttpHelper.GetAsync(requestId, getBikeUrl, originRequest); + if (response.IsSuccessStatusCode) + { + var bikeDetails = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + return bikeDetails; + } + + return null; + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/BillingHelper.cs b/samples/BikeSharingApp/ReservationEngine/BillingHelper.cs new file mode 100644 index 000000000..bc233e533 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/BillingHelper.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using app.Models; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace app +{ + public static class BillingHelper + { + private static string _billingService { get; set; } + + private const string DateTimeFormat = "yyyy-MM-ddTHH:mm:ss"; + + public static void Init(CustomConfiguration customConfiguration) + { + LogUtility.Log("BillingHelper init start"); + _billingService = Environment.GetEnvironmentVariable(Constants.BillingMicroserviceEnv) ?? customConfiguration.Services.Billing; + LogUtility.Log("BillingHelper init end"); + } + + public static async Task CreateInvoice(Guid requestId, Reservation reservationDetails, HttpRequest originRequest) + { + LogUtility.LogWithContext(requestId, "Creating an invoice"); + var bikeDetails = await BikesHelper.GetBike(requestId, reservationDetails.BikeId, originRequest); + var startTime = DateTime.ParseExact(reservationDetails.StartTime, DateTimeFormat, null); + var endTime = DateTime.ParseExact(reservationDetails.EndTime, DateTimeFormat, null); + double amount = 0; + if (endTime > startTime) + { + var totalHours = Math.Ceiling((endTime - startTime).TotalHours); + amount = totalHours * bikeDetails.HourlyCost; + } + + var createInvoiceUrl = $"http://{_billingService}/api/invoice"; + var invoice = new Invoice + { + Amount = (float)amount, + BikeId = reservationDetails.BikeId, + CustomerId = reservationDetails.UserId, + VendorId = bikeDetails.OwnerUserId, + ReservationId = reservationDetails.ReservationId + }; + + var response = await HttpHelper.PostAsync(requestId, createInvoiceUrl, new StringContent( + JsonConvert.SerializeObject(invoice), Encoding.UTF8, "application/json"), originRequest); + var result = new BillingResponse() { HttpResponse = response }; + if (response.IsSuccessStatusCode) + { + var obj = JObject.Parse(await response.Content.ReadAsStringAsync()); + result.InvoiceId = obj["id"].Value(); + } + return result; + } + + public class BillingResponse + { + public HttpResponseMessage HttpResponse { get; set; } + + public string InvoiceId { get; set; } + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/Constants.cs b/samples/BikeSharingApp/ReservationEngine/Constants.cs new file mode 100644 index 000000000..9f01081d5 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Constants.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app +{ + public class Constants + { + public const string MongoDbConnectionStringEnv = "mongo_connectionstring"; + + public const string MongoDbDatabaseEnv = "mongo_dbname"; + + public const string MongoDbCollectionEnv = "mongo_collection"; + + public const string BikesMicroserviceEnv = "bikes_dnsname"; + + public const string BillingMicroserviceEnv = "billing_dnsname"; + + public const string RequestIdHeaderName = "x-contoso-request-id"; + + public const string RouteAsHeaderName = "azds-route-as"; + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/Controllers/HelloController.cs b/samples/BikeSharingApp/ReservationEngine/Controllers/HelloController.cs new file mode 100644 index 000000000..f5f4b4161 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Controllers/HelloController.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Mvc; + +namespace app.Controllers +{ + [Route("hello")] + [ApiController] + public class HelloController : ControllerBase + { + [HttpGet] + public string Get() + { + return "hello!"; + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/Controllers/ReservationEngineController.cs b/samples/BikeSharingApp/ReservationEngine/Controllers/ReservationEngineController.cs new file mode 100644 index 000000000..168d4ed8a --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Controllers/ReservationEngineController.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using app.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace app.Controllers +{ + [Route("api/reservationengine")] + [ApiController] + public class ReservationEngineController : ControllerBase + { + private static CustomConfiguration _customConfiguration { get; set; } + + private const string DateTimeFormat = "yyyy-MM-ddTHH:mm:ss"; + + // POST api/reservationengine + [HttpPost] + public async Task UpdateReservation([FromBody] Reservation reservationDetails) + { + var requestId = new Guid(reservationDetails.RequestId); + LogUtility.LogWithContext(requestId, "Updating booking"); + + try + { + if (reservationDetails.State.Equals(ReservationStatus.Booking.ToString(), StringComparison.OrdinalIgnoreCase)) + { + await _createBooking(requestId, reservationDetails, this.Request); + } + else if (reservationDetails.State.Equals(ReservationStatus.Completing.ToString(), StringComparison.OrdinalIgnoreCase)) + { + await _completeBooking(requestId, reservationDetails, this.Request); + } + + LogUtility.LogWithContext(requestId, "End"); + } + catch (Exception e) + { + LogUtility.LogError("Error processing message: " + e.ToString()); + } + + LogUtility.LogWithContext(requestId, "Updating booking succeeded"); + return new JsonResult(reservationDetails); + } + + private static async Task _createBooking(Guid requestId, Reservation reservationDetails, HttpRequest originRequest) + { + LogUtility.LogWithContext(requestId, "Creating a booking"); + reservationDetails.EndTime = string.Empty; + + var createBookingResponse = await BikesHelper.ReserveBike(requestId, reservationDetails.BikeId, originRequest); + if (!createBookingResponse.IsSuccessStatusCode) + { + LogUtility.LogErrorWithContext(requestId, "Error response from Bikes! ResponseCode: {0}, Content: {1}", createBookingResponse.StatusCode.ToString(), await createBookingResponse.Content.ReadAsStringAsync()); + await _failBooking(requestId, reservationDetails); + return; + } + + reservationDetails.State = ReservationStatus.Booked.ToString(); + var result = await MongoHelper.UpdateReservationStateAndEndTime(requestId, reservationDetails); + + if (result.ModifiedCount == 0) + { + LogUtility.LogErrorWithContext(requestId, "Reservation not updated! MatchedCount: {0}, ModifiedCount: {1}", result.MatchedCount.ToString(), result.ModifiedCount.ToString()); + await _failBooking(requestId, reservationDetails); + return; + } + + LogUtility.LogWithContext(requestId, "Create booking succeeded"); + } + + private static async Task _completeBooking(Guid requestId, Reservation reservationDetails, HttpRequest originRequest) + { + LogUtility.LogWithContext(requestId, "Completing a booking"); + reservationDetails.State = ReservationStatus.Completing.ToString(); + reservationDetails.EndTime = string.Empty; + + var reservationCompletingResult = await MongoHelper.UpdateReservationStateAndEndTime(requestId, reservationDetails); + if (reservationCompletingResult.ModifiedCount == 0) + { + LogUtility.LogErrorWithContext(requestId, "Reservation not updated to 'Completing'! MatchedCount: {0}, ModifiedCount: {1}", reservationCompletingResult.MatchedCount.ToString(), reservationCompletingResult.ModifiedCount.ToString()); + await _failBooking(requestId, reservationDetails); + return; + } + + var freeBikeResponse = await BikesHelper.FreeBike(requestId, reservationDetails.BikeId, originRequest); + if (!freeBikeResponse.IsSuccessStatusCode) + { + LogUtility.LogErrorWithContext(requestId, "Error response from Bikes! ResponseCode: {0}, Content: {1}", freeBikeResponse.StatusCode.ToString(), await freeBikeResponse.Content.ReadAsStringAsync()); + await _failBooking(requestId, reservationDetails); + return; + } + + reservationDetails.State = ReservationStatus.Completed.ToString(); + reservationDetails.EndTime = DateTime.UtcNow.ToString(DateTimeFormat); + var createInvoiceResponse = await BillingHelper.CreateInvoice(requestId, reservationDetails, originRequest); + if (!createInvoiceResponse.HttpResponse.IsSuccessStatusCode) + { + LogUtility.LogErrorWithContext(requestId, "Couldn't create invoice, rolling back!"); + // Rollback + reservationDetails.EndTime = string.Empty; + await BikesHelper.ReserveBike(requestId, reservationDetails.BikeId, originRequest); + await _failBooking(requestId, reservationDetails); + return; + } + reservationDetails.InvoiceId = createInvoiceResponse.InvoiceId; + + var reservationCompletedResult = await MongoHelper.UpdateReservationStateAndEndTime(requestId, reservationDetails); + if (reservationCompletedResult.ModifiedCount == 0) + { + LogUtility.LogErrorWithContext(requestId, "Reservation not updated to 'Completed'! MatchedCount: {0}, ModifiedCount: {1}", reservationCompletedResult.MatchedCount.ToString(), reservationCompletedResult.ModifiedCount.ToString()); + await _failBooking(requestId, reservationDetails); + return; + } + + LogUtility.LogWithContext(requestId, "Complete booking succeeded"); + } + + private static async Task _failBooking(Guid requestId, Reservation reservationDetails) + { + LogUtility.LogErrorWithContext(requestId, "_failBooking start"); + reservationDetails.State = ReservationStatus.Failed.ToString(); + await MongoHelper.UpdateReservationStateAndEndTime(requestId, reservationDetails); + LogUtility.LogErrorWithContext(requestId, "_failBooking end"); + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/CustomConfiguration.cs b/samples/BikeSharingApp/ReservationEngine/CustomConfiguration.cs new file mode 100644 index 000000000..847c08d82 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/CustomConfiguration.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app +{ + + public class MongoDBConnectionInfo + { + public string ConnectionString { get; set; } + + public string Database { get; set; } + + public string Collection { get; set; } + } + + public class Services + { + public string Bikes { get; set; } + + public string Billing { get; set; } + } + + public class CustomConfiguration + { + public MongoDBConnectionInfo MongoDBConnectionInfo { get; set; } + + public Services Services { get; set; } + } +} diff --git a/samples/BikeSharingApp/ReservationEngine/Dockerfile b/samples/BikeSharingApp/ReservationEngine/Dockerfile new file mode 100644 index 000000000..8aedd040b --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Dockerfile @@ -0,0 +1,18 @@ +FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base +WORKDIR /app +EXPOSE 80 + +FROM microsoft/dotnet:2.1-sdk AS build +WORKDIR /src +COPY ["app.csproj", "."] +RUN dotnet restore "app.csproj" +COPY . . +RUN dotnet build "app.csproj" -c Release -o /app + +FROM build AS publish +RUN dotnet publish "app.csproj" -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "app.dll"] \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/Dockerfile.develop b/samples/BikeSharingApp/ReservationEngine/Dockerfile.develop new file mode 100644 index 000000000..b4b6eb418 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Dockerfile.develop @@ -0,0 +1,15 @@ +FROM microsoft/dotnet:2.1-sdk +ARG BUILD_CONFIGURATION=Debug +ENV ASPNETCORE_ENVIRONMENT=Development +ENV DOTNET_USE_POLLING_FILE_WATCHER=true +EXPOSE 80 + +WORKDIR /src +COPY ["app.csproj", "./"] +RUN dotnet restore "app.csproj" +COPY . . +RUN dotnet build --no-restore -c $BUILD_CONFIGURATION + +RUN echo "exec dotnet run --no-build --no-launch-profile -c $BUILD_CONFIGURATION -- \"\$@\"" > /entrypoint.sh + +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/HttpHelper.cs b/samples/BikeSharingApp/ReservationEngine/HttpHelper.cs new file mode 100644 index 000000000..3260fd3b8 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/HttpHelper.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace app +{ + public static class HttpHelper + { + private static HttpClient _httpClient = new HttpClient(); + + public static Task GetAsync(Guid requestId, string url, HttpRequest originRequest) + { + var request = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(url) + }; + + return SendAndLogAsync(requestId, request, originRequest); + } + + public static Task PostAsync(Guid requestId, string url, HttpContent content, HttpRequest originRequest) + { + var request = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri(url), + Content = content + }; + + return SendAndLogAsync(requestId, request, originRequest); + } + + public static Task PatchAsync(Guid requestId, string url, HttpContent content, HttpRequest originRequest) + { + var request = new HttpRequestMessage + { + Method = new HttpMethod("PATCH"), + RequestUri = new Uri(url), + Content = content + }; + + return SendAndLogAsync(requestId, request, originRequest); + } + + private static async Task SendAndLogAsync(Guid requestId, HttpRequestMessage request, HttpRequest originRequest) + { + Stopwatch stopWatch = Stopwatch.StartNew(); + request.Headers.Add(Constants.RequestIdHeaderName, requestId.ToString()); + if (originRequest.Headers.ContainsKey(Constants.RouteAsHeaderName)) + { + request.Headers.Add(Constants.RouteAsHeaderName, originRequest.Headers[Constants.RouteAsHeaderName].ToArray()); + } + var response = await _httpClient.SendAsync(request); + LogUtility.LogWithContext(requestId, "Dependency: {0} {1} - {2} - {3}ms", request.Method.Method, request.RequestUri.ToString(), response.StatusCode.ToString(), stopWatch.ElapsedMilliseconds.ToString()); + return response; + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/LogUtility.cs b/samples/BikeSharingApp/ReservationEngine/LogUtility.cs new file mode 100644 index 000000000..c03fe3cf3 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/LogUtility.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace app +{ + public static class LogUtility + { + public static void Log(string format, params string[] args) + { + string msg = $"{DateTime.UtcNow.ToString()}: {format}"; + Console.WriteLine(string.Format(msg, args)); + } + + public static void LogWithContext(Guid requestId, string format, params string[] args) + { + string msg = $"{DateTime.UtcNow.ToString()}: {requestId.ToString()}: {format}"; + Console.WriteLine(string.Format(msg, args)); + } + + public static void LogError(string format, params string[] args) + { + string msg = $"{DateTime.UtcNow.ToString()}: {format}"; + Console.Error.WriteLine(string.Format(msg, args)); + } + + public static void LogErrorWithContext(Guid requestId, string format, params string[] args) + { + string msg = $"{DateTime.UtcNow.ToString()}: {requestId.ToString()}: {format}"; + Console.Error.WriteLine(string.Format(msg, args)); + } + } +} diff --git a/samples/BikeSharingApp/ReservationEngine/Models/Bikes/Bike.cs b/samples/BikeSharingApp/ReservationEngine/Models/Bikes/Bike.cs new file mode 100644 index 000000000..723ee76a9 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Models/Bikes/Bike.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; + +namespace app.Models +{ + public class Bike + { + [JsonProperty("id")] + public string Id { get; private set; } + + [JsonProperty("available")] + public string Available { get; private set; } + + [JsonProperty("model")] + public string Model { get; private set; } + + [JsonProperty("hourlyCost")] + public float HourlyCost { get; private set; } + + [JsonProperty("type")] + public string Type { get; private set; } + + [JsonProperty("ownerUserId")] + public string OwnerUserId { get; private set; } + + [JsonProperty("suitableHeightInMeters")] + public float SuitableHeightInMeters { get; private set; } + + [JsonProperty("maximumWeightInKg")] + public float MaximumWeightInKg { get; private set; } + } +} diff --git a/samples/BikeSharingApp/ReservationEngine/Models/Billing/Invoice.cs b/samples/BikeSharingApp/ReservationEngine/Models/Billing/Invoice.cs new file mode 100644 index 000000000..27aeebe1a --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Models/Billing/Invoice.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; + +namespace app.Models +{ + class Invoice + { + [JsonProperty("reservationId")] + public string ReservationId { get; set; } + + [JsonProperty("bikeId")] + public string BikeId { get; set; } + + [JsonProperty("customerId")] + public string CustomerId { get; set; } + + [JsonProperty("vendorId")] + public string VendorId { get; set; } + + [JsonProperty("amount")] + public float Amount { get; set; } + } +} diff --git a/samples/BikeSharingApp/ReservationEngine/Models/Billing/ReservationStatus.cs b/samples/BikeSharingApp/ReservationEngine/Models/Billing/ReservationStatus.cs new file mode 100644 index 000000000..6cf7588d2 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Models/Billing/ReservationStatus.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app.Models +{ + public enum ReservationStatus + { + Booking = 1, + Completing = 2, + Booked = 3, + Completed = 4, + Failed = 5 + } +} diff --git a/samples/BikeSharingApp/ReservationEngine/Models/Reservations/Reservation.cs b/samples/BikeSharingApp/ReservationEngine/Models/Reservations/Reservation.cs new file mode 100644 index 000000000..582a4948f --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Models/Reservations/Reservation.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; + +namespace app.Models +{ + public class Reservation + { + [JsonProperty("reservationId")] + public string ReservationId { get; set; } + + [JsonProperty("userId")] + [Required] + public string UserId { get; set; } + + [JsonProperty("bikeId")] + [Required] + public string BikeId { get; set; } + + [JsonProperty("state")] + public string State {get;set;} + + [JsonProperty("requestTime")] + public string RequestTime {get;set;} + + [JsonProperty("startTime")] + public string StartTime {get;set;} + + [JsonProperty("endTime")] + public string EndTime {get;set;} + + [JsonProperty("invoiceId")] + public string InvoiceId { get; set; } + + [JsonProperty("requestId")] + public string RequestId { get; set; } + } +} diff --git a/samples/BikeSharingApp/ReservationEngine/Models/Reservations/ReservationState.cs b/samples/BikeSharingApp/ReservationEngine/Models/Reservations/ReservationState.cs new file mode 100644 index 000000000..d1a497c8c --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Models/Reservations/ReservationState.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace app.Models.Reservations +{ + public enum ReservationState + { + Booking = 1, + Completing = 2, + Booked = 3, + Completed = 4, + Failed = 5 + } +} diff --git a/samples/BikeSharingApp/ReservationEngine/MongoHelper.cs b/samples/BikeSharingApp/ReservationEngine/MongoHelper.cs new file mode 100644 index 000000000..07847e54f --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/MongoHelper.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using app.Models; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace app +{ + public static class MongoHelper + { + private static string _connectionString { get; set; } + + private static string _database { get; set; } + + private static string _collection { get; set; } + + private static MongoClient _mongoClient; + + public static void Init(CustomConfiguration customConfiguration) + { + LogUtility.Log("MongoHelper init start"); + _connectionString = Environment.GetEnvironmentVariable(Constants.MongoDbConnectionStringEnv) ?? customConfiguration.MongoDBConnectionInfo.ConnectionString; + _database = Environment.GetEnvironmentVariable(Constants.MongoDbDatabaseEnv) ?? customConfiguration.MongoDBConnectionInfo.Database; + _collection = Environment.GetEnvironmentVariable(Constants.MongoDbCollectionEnv) ?? customConfiguration.MongoDBConnectionInfo.Collection; + _mongoClient = new MongoClient(_connectionString); + CheckConnectionLoop(); + LogUtility.Log("MongoHelper init end"); + } + + private static async void CheckConnectionLoop() + { + var db = _mongoClient.GetDatabase(_database); + + while (true) + { + try + { + BsonDocument isMongoLive = await db.RunCommandAsync((Command)"{ping:1}"); + if (!isMongoLive.Contains("ok") || isMongoLive["ok"].AsDouble != 1.0) + { + Console.Error.WriteLine("Mongo connection dead! Shutting down."); + Environment.Exit(1); + } + } + catch (Exception e) + { + Console.Error.WriteLine("Exception pinging Mongo:"); + Console.Error.WriteLine(e.ToString()); + Environment.Exit(1); + } + + await Task.Delay(3000); + } + } + + /// + /// Updates ONLY the State field of the reservation in the DB + /// + /// + /// + public static async Task UpdateReservationStateAndEndTime(Guid requestId, Reservation reservationDetails) + { + LogUtility.LogWithContext(requestId, "Updating reservationID " + reservationDetails.ReservationId); + var db = _mongoClient.GetDatabase(_database); + var collection = db.GetCollection(_collection).WithWriteConcern(new WriteConcern("majority")); + + var filter = Builders.Filter.Eq("reservationId", reservationDetails.ReservationId); + var update = Builders.Update.Set("state", reservationDetails.State).Set("endTime", reservationDetails.EndTime); + + var result = await collection.UpdateOneAsync(filter, update); + return result; + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/Program.cs b/samples/BikeSharingApp/ReservationEngine/Program.cs new file mode 100644 index 000000000..88ff57c2e --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Program.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; + +namespace app +{ + public class Program + { + private static CustomConfiguration _customConfiguration { get; set; } + + public static void Main(string[] args) + { + IConfiguration Configuration; + var builder = new ConfigurationBuilder().AddJsonFile($"appsettings.json", true, true); + Configuration = builder.Build(); + _customConfiguration = new CustomConfiguration(); + Configuration.GetSection("CustomConfiguration").Bind(_customConfiguration); + + BikesHelper.Init(_customConfiguration); + BillingHelper.Init(_customConfiguration); + MongoHelper.Init(_customConfiguration); + + var host = WebHost.CreateDefaultBuilder(args).Build(); + host.Run(); + LogUtility.Log("Reservation engine service running."); + + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/Startup.cs b/samples/BikeSharingApp/ReservationEngine/Startup.cs new file mode 100644 index 000000000..3dc8eb061 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/Startup.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace app +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc() + .AddJsonOptions(o => + { + o.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; + }); + + services.AddOptions(); + + services.Configure(Configuration.GetSection("CustomConfiguration")); + services.AddMvc() + .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + app.UseMvc(); + } + } +} diff --git a/samples/BikeSharingApp/ReservationEngine/app.csproj b/samples/BikeSharingApp/ReservationEngine/app.csproj new file mode 100644 index 000000000..419c79d10 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/app.csproj @@ -0,0 +1,27 @@ + + + + Exe + netcoreapp2.1 + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/samples/BikeSharingApp/ReservationEngine/appsettings.json b/samples/BikeSharingApp/ReservationEngine/appsettings.json new file mode 100644 index 000000000..c26cd0266 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/appsettings.json @@ -0,0 +1,13 @@ +{ + "CustomConfiguration": { + "MongoDBConnectionInfo": { + "ConnectionString": "dummyconnectionstring", + "Database": "dummydatabase", + "Collection": "dummycollection" + }, + "Services": { + "Bikes": "bikes", + "Billing": "billing" + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/ReservationEngine/azds.yaml b/samples/BikeSharingApp/ReservationEngine/azds.yaml new file mode 100644 index 000000000..e3b1ae394 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/azds.yaml @@ -0,0 +1,40 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: . + dockerfile: Dockerfile +install: + chart: charts/reservationengine + values: + - values.dev.yaml? + - secrets.dev.yaml? + set: + replicaCount: 1 + image: + repository: reservationengine + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + # This expands to [space.s.][rootSpace.]reservationengine...azds.io + - $(spacePrefix)$(rootSpacePrefix)reservationengine$(hostSuffix) +configurations: + develop: + build: + dockerfile: Dockerfile.develop + useGitIgnore: true + args: + BUILD_CONFIGURATION: ${BUILD_CONFIGURATION:-Debug} + container: + sync: + - "**/Pages/**" + - "**/Views/**" + - "**/wwwroot/**" + - "!**/*.{sln,csproj}" + command: [dotnet, run, --no-restore, --no-build, --no-launch-profile, -c, "${BUILD_CONFIGURATION:-Debug}"] + iterate: + processesToKill: [dotnet, vsdbg] + buildCommands: + - [dotnet, build, --no-restore, -c, "${BUILD_CONFIGURATION:-Debug}"] diff --git a/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/.helmignore b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/Chart.yaml b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/Chart.yaml new file mode 100644 index 000000000..c6f2ef563 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: reservationengine +version: 0.1.0 diff --git a/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/NOTES.txt b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/NOTES.txt new file mode 100644 index 000000000..64c717068 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "reservationengine.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "reservationengine.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "reservationengine.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "reservationengine.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/_helpers.tpl b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/_helpers.tpl new file mode 100644 index 000000000..6ef070145 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "reservationengine.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "reservationengine.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "reservationengine.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/deployment.yaml b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/deployment.yaml new file mode 100644 index 000000000..932a61d5f --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: {{ template "reservationengine.fullname" . }} + labels: + app: {{ template "reservationengine.name" . }} + chart: {{ template "reservationengine.chart" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "reservationengine.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "reservationengine.name" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + annotations: + buildID: {{ .Values.buildID }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + {{- if .Values.probes.enabled }} + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + {{- end }} + env: + {{- $root := . }} + {{- range $ref, $values := .Values.secrets }} + {{- range $key, $value := $values }} + - name: {{ $ref }}_{{ $key }} + valueFrom: + secretKeyRef: + name: {{ template "reservationengine.fullname" $root }}-{{ $ref | lower }} + key: {{ $key }} + {{- end }} + {{- end }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/ingress.yaml b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/ingress.yaml new file mode 100644 index 000000000..f9a74b272 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "reservationengine.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "reservationengine.name" . }} + chart: {{ template "reservationengine.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/secrets.yaml b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/secrets.yaml new file mode 100644 index 000000000..0b3cdd14d --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/secrets.yaml @@ -0,0 +1,12 @@ +{{- $root := . }} +{{- range $name, $values := .Values.secrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "reservationengine.fullname" $root }}-{{ $name | lower }} +data: + {{- range $key, $value := $values }} + {{ $key }}: {{ $value | b64enc }} + {{- end }} +--- +{{- end }} diff --git a/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/service.yaml b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/service.yaml new file mode 100644 index 000000000..9217887f0 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "reservationengine.fullname" . }} + labels: + app: {{ template "reservationengine.name" . }} + chart: {{ template "reservationengine.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app: {{ template "reservationengine.name" . }} + release: {{ .Release.Name }} diff --git a/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/values.yaml b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/values.yaml new file mode 100644 index 000000000..c837089a9 --- /dev/null +++ b/samples/BikeSharingApp/ReservationEngine/charts/reservationengine/values.yaml @@ -0,0 +1,70 @@ +# Default values for reservationengine. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +fullnameOverride: reservationengine +replicaCount: 1 +image: + repository: azdspublic/bikesharing-reservationengine + tag: build.20190418.2 + pullPolicy: IfNotPresent +imagePullSecrets: [] + # Optionally specify an array of imagePullSecrets. + # Secrets must be manually created in the namespace. + # ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod + # + # This uses credentials from secret "myRegistryKeySecretName". + # - name: myRegistryKeySecretName +service: + type: ClusterIP + port: 80 + +probes: + enabled: false + +ingress: + enabled: false + annotations: + kubernetes.io/ingress.class: addon-http-application-routing + path: / + # hosts: + # - chart-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local +secrets: + bikes: + dnsname: bikes + billing: + dnsname: billing + mongo: + connectionstring: mongodb://databases-mongo + dbname: resdb + collection: reservation + # Optionally specify a set of secret objects whose values + # will be injected as environment variables by default. + # You should add this section to a file like secrets.yaml + # that is explicitly NOT committed to source code control + # and then include it as part of your helm install step. + # ref: https://kubernetes.io/docs/concepts/configuration/secret/ + # + # This creates a secret "mysecret" and injects "mypassword" + # as the environment variable mysecret_mypassword=password. + # mysecret: + # mypassword: password +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi +nodeSelector: {} + +tolerations: [] + +affinity: {} \ No newline at end of file diff --git a/samples/BikeSharingApp/Users/.dockerignore b/samples/BikeSharingApp/Users/.dockerignore new file mode 100644 index 000000000..1a764bc71 --- /dev/null +++ b/samples/BikeSharingApp/Users/.dockerignore @@ -0,0 +1,11 @@ +.dockerignore +.git +.gitignore +.vs +.vscode +azds.yaml +charts +Dockerfile +node_modules +secrets.dev.yaml +values.dev.yaml \ No newline at end of file diff --git a/samples/BikeSharingApp/Users/.gitignore b/samples/BikeSharingApp/Users/.gitignore new file mode 100644 index 000000000..5148e527a --- /dev/null +++ b/samples/BikeSharingApp/Users/.gitignore @@ -0,0 +1,37 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history diff --git a/samples/BikeSharingApp/Users/ConnectionConfig.json b/samples/BikeSharingApp/Users/ConnectionConfig.json new file mode 100644 index 000000000..594f78602 --- /dev/null +++ b/samples/BikeSharingApp/Users/ConnectionConfig.json @@ -0,0 +1,20 @@ +{ + "config": { + "authentication": { + "type": "default", + "options": { + } + }, + "options": { + "encrypt": true, + "useColumnNames": true, + "debug": { + "data": true, + "payload": true, + "token": false, + "packet": true, + "log": true + } + } + } +} \ No newline at end of file diff --git a/samples/BikeSharingApp/Users/Dockerfile b/samples/BikeSharingApp/Users/Dockerfile new file mode 100644 index 000000000..c5c3d6d23 --- /dev/null +++ b/samples/BikeSharingApp/Users/Dockerfile @@ -0,0 +1,10 @@ +FROM node:lts +ENV PORT 80 +EXPOSE 80 + +WORKDIR /app +COPY package.json . +RUN npm install +COPY . . + +CMD ["npm", "start"] diff --git a/samples/BikeSharingApp/Users/azds.yaml b/samples/BikeSharingApp/Users/azds.yaml new file mode 100644 index 000000000..2494d98a3 --- /dev/null +++ b/samples/BikeSharingApp/Users/azds.yaml @@ -0,0 +1,31 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: . + dockerfile: Dockerfile +install: + chart: charts/users + values: + - values.dev.yaml? + - secrets.dev.yaml? + set: + replicaCount: 1 + image: + repository: users + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + # This expands to [space.s.][rootSpace.]users...azds.io + - $(spacePrefix)$(rootSpacePrefix)users$(hostSuffix) +configurations: + develop: + build: + useGitIgnore: true + container: + sync: + - "!**/package.json" + iterate: + processesToKill: [node] \ No newline at end of file diff --git a/samples/BikeSharingApp/Users/charts/users/.helmignore b/samples/BikeSharingApp/Users/charts/users/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/samples/BikeSharingApp/Users/charts/users/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/samples/BikeSharingApp/Users/charts/users/Chart.yaml b/samples/BikeSharingApp/Users/charts/users/Chart.yaml new file mode 100644 index 000000000..402f875d5 --- /dev/null +++ b/samples/BikeSharingApp/Users/charts/users/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: users +version: 0.1.0 diff --git a/samples/BikeSharingApp/Users/charts/users/templates/NOTES.txt b/samples/BikeSharingApp/Users/charts/users/templates/NOTES.txt new file mode 100644 index 000000000..a11e5d008 --- /dev/null +++ b/samples/BikeSharingApp/Users/charts/users/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "users.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "users.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "users.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "users.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/samples/BikeSharingApp/Users/charts/users/templates/_helpers.tpl b/samples/BikeSharingApp/Users/charts/users/templates/_helpers.tpl new file mode 100644 index 000000000..203912254 --- /dev/null +++ b/samples/BikeSharingApp/Users/charts/users/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "users.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "users.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "users.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/samples/BikeSharingApp/Users/charts/users/templates/deployment.yaml b/samples/BikeSharingApp/Users/charts/users/templates/deployment.yaml new file mode 100644 index 000000000..253e390b4 --- /dev/null +++ b/samples/BikeSharingApp/Users/charts/users/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: {{ template "users.fullname" . }} + labels: + app: {{ template "users.name" . }} + chart: {{ template "users.chart" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "users.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "users.name" . }} + draft: {{ default "draft-app" .Values.draft }} + release: {{ .Release.Name }} + annotations: + buildID: {{ .Values.buildID }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + {{- if .Values.probes.enabled }} + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + {{- end }} + env: + {{- $root := . }} + {{- range $ref, $values := .Values.secrets }} + {{- range $key, $value := $values }} + - name: {{ $ref }}_{{ $key }} + valueFrom: + secretKeyRef: + name: {{ template "users.fullname" $root }}-{{ $ref | lower }} + key: {{ $key }} + {{- end }} + {{- end }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/samples/BikeSharingApp/Users/charts/users/templates/ingress.yaml b/samples/BikeSharingApp/Users/charts/users/templates/ingress.yaml new file mode 100644 index 000000000..123413763 --- /dev/null +++ b/samples/BikeSharingApp/Users/charts/users/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "users.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "users.name" . }} + chart: {{ template "users.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/samples/BikeSharingApp/Users/charts/users/templates/secrets.yaml b/samples/BikeSharingApp/Users/charts/users/templates/secrets.yaml new file mode 100644 index 000000000..3eb29fbee --- /dev/null +++ b/samples/BikeSharingApp/Users/charts/users/templates/secrets.yaml @@ -0,0 +1,12 @@ +{{- $root := . }} +{{- range $name, $values := .Values.secrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "users.fullname" $root }}-{{ $name | lower }} +data: + {{- range $key, $value := $values }} + {{ $key }}: {{ $value | b64enc }} + {{- end }} +--- +{{- end }} diff --git a/samples/BikeSharingApp/Users/charts/users/templates/service.yaml b/samples/BikeSharingApp/Users/charts/users/templates/service.yaml new file mode 100644 index 000000000..f8f2f3908 --- /dev/null +++ b/samples/BikeSharingApp/Users/charts/users/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "users.fullname" . }} + labels: + app: {{ template "users.name" . }} + chart: {{ template "users.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app: {{ template "users.name" . }} + release: {{ .Release.Name }} diff --git a/samples/BikeSharingApp/Users/charts/users/values.yaml b/samples/BikeSharingApp/Users/charts/users/values.yaml new file mode 100644 index 000000000..795162a9e --- /dev/null +++ b/samples/BikeSharingApp/Users/charts/users/values.yaml @@ -0,0 +1,68 @@ +# Default values for users. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +fullnameOverride: users +replicaCount: 1 +image: + repository: azdspublic/bikesharing-users + tag: build.20190418.1 + pullPolicy: IfNotPresent +imagePullSecrets: [] + # Optionally specify an array of imagePullSecrets. + # Secrets must be manually created in the namespace. + # ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod + # + # This uses credentials from secret "myRegistryKeySecretName". + # - name: myRegistryKeySecretName +service: + type: ClusterIP + port: 80 + +probes: + enabled: false + +ingress: + enabled: false + annotations: + kubernetes.io/ingress.class: addon-http-application-routing + path: / + # hosts: + # - chart-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local +secrets: + sql: + username: SA + password: "!DummyPassword123!" + server: databases-sql + database: tempdb + table: myTable + # Optionally specify a set of secret objects whose values + # will be injected as environment variables by default. + # You should add this section to a file like secrets.yaml + # that is explicitly NOT committed to source code control + # and then include it as part of your helm install step. + # ref: https://kubernetes.io/docs/concepts/configuration/secret/ + # + # This creates a secret "mysecret" and injects "mypassword" + # as the environment variable mysecret_mypassword=password. + # mysecret: + # mypassword: password +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi +nodeSelector: {} + +tolerations: [] + +affinity: {} \ No newline at end of file diff --git a/samples/BikeSharingApp/Users/package.json b/samples/BikeSharingApp/Users/package.json new file mode 100644 index 000000000..074dce6b1 --- /dev/null +++ b/samples/BikeSharingApp/Users/package.json @@ -0,0 +1,33 @@ +{ + "name": "users", + "version": "1.0.0", + "description": "Management microservice for users.", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ContosoBikeRental/BikeSharingSampleApp.git" + }, + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.14.0", + "morgan": "^1.8.0", + "express-api-validator": "0.1.0", + "body-parser": "^1.16.1", + "validate.js": "^0.11.1", + "tedious": "4.1.1", + "util": "0.10.3", + "async": "^2.1.5" + }, + "devDependencies": { + "nodemon": "^1.18.10" + }, + "bugs": { + "url": "https://github.com/ContosoBikeRental/BikeSharingSampleApp/issues" + }, + "homepage": "https://github.com/ContosoBikeRental/BikeSharingSampleApp#readme" +} diff --git a/samples/BikeSharingApp/Users/server.js b/samples/BikeSharingApp/Users/server.js new file mode 100644 index 000000000..e92a2074b --- /dev/null +++ b/samples/BikeSharingApp/Users/server.js @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +var express = require('express'); +var morgan = require('morgan'); +var bodyParser = require('body-parser'); +var Connection = require('tedious').Connection; +var fs = require('fs'); +var validate = require('validate.js'); +var util = require('util'); +var async = require('async'); + +var port = 80; +var serviceName = "User Management Service"; +var app = express(); +app.use(morgan("dev")); +app.use(bodyParser.json()); + +var Request = require('tedious').Request; +var TYPES = require('tedious').TYPES; +var config = JSON.parse(fs.readFileSync('ConnectionConfig.json', 'utf8')).config; +config.authentication.options.userName = process.env.sql_username; +config.authentication.options.password = process.env.sql_password; +config.server = process.env.sql_server; +config.options.database = process.env.sql_database; +var dbConnection = Connection.prototype; // Will be initialized below + +var tableName = process.env.sql_table || 'Users' +var jsonColumnName = "JSON_F52E2B61-18A1-11d1-B105-00805F49916B" + +var userSchema = { + id: { + presence: true + }, + name: { + presence: true, + length: { minimum: 1 } + }, + address: { + presence: true, + length: { minimum: 1 } + }, + email: { + presence: true + }, + type: { + presence: true, + inclusion: [ "vendor", "customer" ] + }, + phone: { + format: { + pattern: "[0-9]+", + length: { minimum: 8, maximum: 15 } + } + } +}; + +function execInsert(params, callbackAffectedRows) { + var sqlStatement = util.format( + "INSERT INTO %s (Id, Name, Address, Phone, Email, Type) VALUES (@Id, @Name, @Address, @Phone, @Email, @Type)", + tableName) + var request = new Request(sqlStatement, function (err, rowCount) { + if (err) { + console.log('Statement failed: ' + err); + callbackAffectedRows(rowCount, err); + return; + } + callbackAffectedRows(rowCount); + }); + request.addParameter('Id', TYPES.NVarChar, params.id); + request.addParameter('Name', TYPES.NVarChar, params.name); + request.addParameter('Address', TYPES.NVarChar, params.address); + request.addParameter('Phone', TYPES.NVarChar, params.phone); + request.addParameter('Email', TYPES.NVarChar, params.email); + request.addParameter('Type', TYPES.NVarChar, params.type); + dbConnection.execSql(request); +} + +function execUpdate(sqlStatement, callbackAffectedRows) { + var request = new Request(sqlStatement, function (err, rowCount) { + if (err) { + console.log('Statement failed: ' + err); + callbackAffectedRows(rowCount, err); + } + callbackAffectedRows(rowCount); + }); + dbConnection.execSql(request); +} + +function execSelect(statement, callbackReturnResult) { + var sqlStatement = statement.toString() + ' FOR JSON PATH'; + var request = new Request(sqlStatement, function (err, rowCount) { + if (err) { + console.log('Statement failed: ' + err); + callbackAffectedRows(rowCount, err); + } + if (rowCount == 0) { + callbackReturnResult(null); + } + }); + dbConnection.execSql(request); + var result = null; + request.on('row', function (columns) { + result = JSON.parse(columns[jsonColumnName].value); + callbackReturnResult(result); + }); +} + +function execStatement(sqlStatement, callbackAffectedRows) { + var request = new Request(sqlStatement, function (err, rowCount) { + if (err) { + console.log('Statement failed: ' + err); + callbackAffectedRows(rowCount, err); + } + callbackAffectedRows(rowCount); + }); + dbConnection.execSql(request); +} + +function createTableIfNotExists(callbackFunc) { + sqlStatement = util.format( + "IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].%s') AND type in (N'U')) BEGIN CREATE TABLE %s(Id NVARCHAR(100) NOT NULL PRIMARY KEY, Name NVARCHAR(100) NOT NULL, Address NVARCHAR(500) NOT NULL, Phone NVARCHAR(22) NULL, Email NVARCHAR(100) NOT NULL, Type NVARCHAR(20) NOT NULL)END", + tableName, tableName) + var request = new Request(sqlStatement, function (err, rowCount) { + if (err) { + console.log('Statement failed: ' + err); + } + + callbackFunc(err); + }); + + dbConnection.execSql(request); +} + +// api ------------------------------------------------------------- + +app.get('/hello', function (req, res) { + console.log("saying hello..."); + res.send("Hello from Users") +}); + +app.get('/api/users/:userId', function (req, res) { + // get user details + var selectStatement = util.format("SELECT Id,Name,Address,Phone,Email,Type FROM %s WHERE Id='%s'", tableName, req.params.userId); + execSelect(selectStatement, function (result, err) { + if (err) { + res.status(500).send(err); + return; + } + if (result == null) { + console.log("No records found.") + res.status(404).send("User not found"); + } else { + console.log(JSON.stringify(result, null, 2)); + res.status(200).send(result[0]); + } + }); +}); + +app.get('/api/allUsers', function (req, res) { + var selectStatement = util.format("SELECT Id,Name,Address,Phone,Email,Type FROM %s", tableName); + execSelect(selectStatement, function (result, err) { + if (err) { + res.status(500).send(err); + return; + } + if (result == null) { + console.log("No records found."); + res.status(200).send([]); + } else { + res.status(200).send(result); + } + }); +}); + +app.post('/api/users', function (req, res) { + // add user details + var validationErrors = validate(req.body, userSchema); + if (validationErrors) { + res.status(400).send(validationErrors); + return; + } + + execInsert(req.body, function (rowCount, err) { + if (err) { + res.status(500).send(err); + return; + } + console.log("Affected Row(s): " + rowCount); + if (rowCount == 0) { + res.status(400).send("Bad Request."); + } else { + res.status(200).send(); + }; + }); +}); + +app.put('/api/users/:userId', function (req, res) { + // update user + var sqlStatement = util.format("UPDATE %s SET Name='%s',Address='%s',Phone='%s',Email='%s' WHERE Id='%s'", tableName, req.body.name, req.body.address, req.body.phone, req.body.email, req.params.userId); + execUpdate(sqlStatement, function (rowCount, err) { + if (err) { + res.status(500).send(err); + return; + } + console.log("Affected Row(s): " + rowCount); + if (rowCount == 0) { + res.status(400).send("Bad Request."); + } else { + res.status(200).send(); + }; + }); +}); + +app.delete('/api/users/:userId', function (req, res) { + var deleteStatement = util.format("DELETE FROM %s WHERE Id='%s'", tableName, req.params.userId); + execStatement(deleteStatement, function (rowCount, err) { + if (err) { + res.status(500).send(err); + return; + } + if (rowCount == 0) { + res.status(400).send("Bad Request."); + } else { + res.status(202).send("Accepted."); + }; + }); +}); + +// application ------------------------------------------------------------- + +function trySqlConnect(callback) { + dbConnection = new Connection(config); + dbConnection.on('connect', function (err) { + if (err) { + console.error('Can\'t connect to the database: ' + err); + callback(err); + } else { + console.log('Connected to the database'); + createTableIfNotExists(function(err) { + if (err) { + console.error('Can\'t create table: ' + err); + } + callback(err); + }); + } + }); + dbConnection.on('end', function () { + console.error('Lost SQL connection! Shutting down.'); + process.exit(1); + }); +} + +async.retry({times: 20, interval: 1000}, trySqlConnect, function(err) { + if (err) { + console.error("Couldn't connect to SQL! Giving up."); + console.error(err); + process.exit(1); + } + + app.listen(port, function () { + console.log(serviceName + ' listening on port ' + port); + }); +}); \ No newline at end of file diff --git a/samples/BikeSharingApp/charts/.gitignore b/samples/BikeSharingApp/charts/.gitignore new file mode 100644 index 000000000..698018d31 --- /dev/null +++ b/samples/BikeSharingApp/charts/.gitignore @@ -0,0 +1,2 @@ +*.tgz +requirements.lock \ No newline at end of file diff --git a/samples/BikeSharingApp/charts/.helmignore b/samples/BikeSharingApp/charts/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/samples/BikeSharingApp/charts/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/samples/BikeSharingApp/charts/Chart.yaml b/samples/BikeSharingApp/charts/Chart.yaml new file mode 100644 index 000000000..e4d2ded0b --- /dev/null +++ b/samples/BikeSharingApp/charts/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: The master chart for BikeSharing sample app +name: bikesharingsampleapp +version: 0.1.0 diff --git a/samples/BikeSharingApp/charts/requirements.yaml b/samples/BikeSharingApp/charts/requirements.yaml new file mode 100644 index 000000000..3c92468e4 --- /dev/null +++ b/samples/BikeSharingApp/charts/requirements.yaml @@ -0,0 +1,28 @@ +dependencies: + - name: bikes + version: 0.1.0 + repository: "file://../Bikes/charts/bikes" + - name: bikesharingweb + version: 0.1.0 + repository: "file://../BikeSharingWeb/charts/bikesharingweb" + - name: billing + version: 0.1.0 + repository: "file://../Billing/charts/billing" + - name: databases + version: 0.1.0 + repository: "file://../Databases/charts/databases" + - name: gateway + version: 0.1.0 + repository: "file://../Gateway/charts/gateway" + - name: populatedatabase + version: 0.1.0 + repository: "file://../PopulateDatabase/charts/populatedatabase" + - name: reservation + version: 0.1.0 + repository: "file://../Reservation/charts/reservation" + - name: reservationengine + version: 0.1.0 + repository: "file://../ReservationEngine/charts/reservationengine" + - name: users + version: 0.1.0 + repository: "file://../Users/charts/users" \ No newline at end of file diff --git a/samples/BikeSharingApp/charts/values.yaml b/samples/BikeSharingApp/charts/values.yaml new file mode 100644 index 000000000..99dee8ff5 --- /dev/null +++ b/samples/BikeSharingApp/charts/values.yaml @@ -0,0 +1,16 @@ +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +bikesharingweb: + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds # Dev Spaces-specific + hosts: + - dev.bikesharingweb. # Assumes deployment to the 'dev' space + +gateway: + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds # Dev Spaces-specific + hosts: + - dev.gateway. # Assumes deployment to the 'dev' space \ No newline at end of file