├── .github └── workflows │ └── rng-build.yml ├── .gitignore ├── CNAME ├── LICENSE ├── README.md ├── _config.yml ├── hackathon ├── README.md ├── solution-part-1 │ ├── .gitignore │ ├── db │ │ └── postgres │ │ │ ├── Dockerfile │ │ │ └── init-products-db.sh │ ├── docker-compose.yml │ ├── products-api │ │ └── java │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.v2 │ │ │ ├── build.sh │ │ │ ├── pom.xml │ │ │ ├── restore.sh │ │ │ └── src │ │ │ └── main │ │ │ ├── java │ │ │ └── com │ │ │ │ └── widgetario │ │ │ │ ├── Application.java │ │ │ │ ├── configuration │ │ │ │ └── RegistryConfiguration.java │ │ │ │ ├── controllers │ │ │ │ └── ProductsController.java │ │ │ │ ├── models │ │ │ │ └── Product.java │ │ │ │ ├── repositories │ │ │ │ └── ProductRepository.java │ │ │ │ └── startup │ │ │ │ └── ApplicationStartup.java │ │ │ └── resources │ │ │ └── application.properties │ ├── stock-api │ │ └── golang │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile.v2 │ │ │ ├── build.sh │ │ │ ├── restore.sh │ │ │ └── src │ │ │ ├── go.mod │ │ │ ├── handlers │ │ │ └── handlers.go │ │ │ ├── main.go │ │ │ ├── middleware │ │ │ └── prometheus.go │ │ │ ├── models │ │ │ └── models.go │ │ │ └── router │ │ │ └── router.go │ └── web │ │ └── dotnet │ │ ├── Dockerfile │ │ ├── Dockerfile.v2 │ │ ├── Widgetario.Web │ │ ├── Controllers │ │ │ ├── HomeController.cs │ │ │ └── UpController.cs │ │ ├── Models │ │ │ ├── ErrorViewModel.cs │ │ │ ├── Product.cs │ │ │ ├── ProductStock.cs │ │ │ └── ProductViewModel.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── Services │ │ │ ├── ProductService.cs │ │ │ └── StockService.cs │ │ ├── Startup.cs │ │ ├── Views │ │ │ ├── Home │ │ │ │ └── Index.cshtml │ │ │ ├── Shared │ │ │ │ ├── Error.cshtml │ │ │ │ ├── _Layout.cshtml │ │ │ │ └── _ValidationScriptsPartial.cshtml │ │ │ ├── _ViewImports.cshtml │ │ │ └── _ViewStart.cshtml │ │ ├── Widgetario.Web.csproj │ │ ├── appsettings.json │ │ ├── config │ │ │ └── serilog.json │ │ └── wwwroot │ │ │ ├── css │ │ │ ├── site.css │ │ │ └── themes │ │ │ │ ├── dark.css │ │ │ │ └── light.css │ │ │ ├── favicon.ico │ │ │ ├── img │ │ │ ├── logo.png │ │ │ ├── logo2-small.png │ │ │ └── logo2.png │ │ │ ├── js │ │ │ └── site.js │ │ │ └── lib │ │ │ ├── bootstrap │ │ │ ├── LICENSE │ │ │ └── dist │ │ │ │ ├── css │ │ │ │ ├── bootstrap-grid.css │ │ │ │ ├── bootstrap-grid.css.map │ │ │ │ ├── bootstrap-grid.min.css │ │ │ │ ├── bootstrap-grid.min.css.map │ │ │ │ ├── bootstrap-reboot.css │ │ │ │ ├── bootstrap-reboot.css.map │ │ │ │ ├── bootstrap-reboot.min.css │ │ │ │ ├── bootstrap-reboot.min.css.map │ │ │ │ ├── bootstrap.css │ │ │ │ ├── bootstrap.css.map │ │ │ │ ├── bootstrap.min.css │ │ │ │ └── bootstrap.min.css.map │ │ │ │ └── js │ │ │ │ ├── bootstrap.bundle.js │ │ │ │ ├── bootstrap.bundle.js.map │ │ │ │ ├── bootstrap.bundle.min.js │ │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ │ ├── bootstrap.js │ │ │ │ ├── bootstrap.js.map │ │ │ │ ├── bootstrap.min.js │ │ │ │ └── bootstrap.min.js.map │ │ │ ├── jquery-validation-unobtrusive │ │ │ ├── LICENSE.txt │ │ │ ├── jquery.validate.unobtrusive.js │ │ │ └── jquery.validate.unobtrusive.min.js │ │ │ ├── jquery-validation │ │ │ ├── LICENSE.md │ │ │ └── dist │ │ │ │ ├── additional-methods.js │ │ │ │ ├── additional-methods.min.js │ │ │ │ ├── jquery.validate.js │ │ │ │ └── jquery.validate.min.js │ │ │ └── jquery │ │ │ ├── LICENSE.txt │ │ │ └── dist │ │ │ ├── jquery.js │ │ │ ├── jquery.min.js │ │ │ └── jquery.min.map │ │ ├── build.sh │ │ └── restore.sh ├── solution-part-2 │ └── docker-compose.yml ├── solution-part-3 │ ├── config │ │ └── web │ │ │ └── logging.json │ └── docker-compose.yml ├── solution-part-4 │ ├── config │ │ └── web │ │ │ └── logging.json │ └── docker-compose.yml └── src │ ├── .gitignore │ ├── db │ └── postgres │ │ ├── Dockerfile │ │ └── init-products-db.sh │ ├── docker-compose.yml │ ├── products-api │ └── java │ │ ├── Dockerfile │ │ ├── build.sh │ │ ├── pom.xml │ │ ├── restore.sh │ │ └── src │ │ └── main │ │ ├── java │ │ └── com │ │ │ └── widgetario │ │ │ ├── Application.java │ │ │ ├── configuration │ │ │ └── RegistryConfiguration.java │ │ │ ├── controllers │ │ │ └── ProductsController.java │ │ │ ├── models │ │ │ └── Product.java │ │ │ ├── repositories │ │ │ └── ProductRepository.java │ │ │ └── startup │ │ │ └── ApplicationStartup.java │ │ └── resources │ │ └── application.properties │ ├── stock-api │ └── golang │ │ ├── Dockerfile │ │ ├── build.sh │ │ ├── restore.sh │ │ └── src │ │ ├── go.mod │ │ ├── handlers │ │ └── handlers.go │ │ ├── main.go │ │ ├── middleware │ │ └── prometheus.go │ │ ├── models │ │ └── models.go │ │ └── router │ │ └── router.go │ └── web │ └── dotnet │ ├── Dockerfile │ ├── Widgetario.Web │ ├── Controllers │ │ ├── HomeController.cs │ │ └── UpController.cs │ ├── Models │ │ ├── ErrorViewModel.cs │ │ ├── Product.cs │ │ ├── ProductStock.cs │ │ └── ProductViewModel.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Services │ │ ├── ProductService.cs │ │ └── StockService.cs │ ├── Startup.cs │ ├── Views │ │ ├── Home │ │ │ └── Index.cshtml │ │ ├── Shared │ │ │ ├── Error.cshtml │ │ │ ├── _Layout.cshtml │ │ │ └── _ValidationScriptsPartial.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ ├── Widgetario.Web.csproj │ ├── appsettings.json │ ├── config │ │ └── serilog.json │ └── wwwroot │ │ ├── css │ │ ├── site.css │ │ └── themes │ │ │ ├── dark.css │ │ │ └── light.css │ │ ├── favicon.ico │ │ ├── img │ │ ├── logo.png │ │ ├── logo2-small.png │ │ └── logo2.png │ │ ├── js │ │ └── site.js │ │ └── lib │ │ ├── bootstrap │ │ ├── LICENSE │ │ └── dist │ │ │ ├── css │ │ │ ├── bootstrap-grid.css │ │ │ ├── bootstrap-grid.css.map │ │ │ ├── bootstrap-grid.min.css │ │ │ ├── bootstrap-grid.min.css.map │ │ │ ├── bootstrap-reboot.css │ │ │ ├── bootstrap-reboot.css.map │ │ │ ├── bootstrap-reboot.min.css │ │ │ ├── bootstrap-reboot.min.css.map │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ │ └── js │ │ │ ├── bootstrap.bundle.js │ │ │ ├── bootstrap.bundle.js.map │ │ │ ├── bootstrap.bundle.min.js │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.js.map │ │ │ ├── bootstrap.min.js │ │ │ └── bootstrap.min.js.map │ │ ├── jquery-validation-unobtrusive │ │ ├── LICENSE.txt │ │ ├── jquery.validate.unobtrusive.js │ │ └── jquery.validate.unobtrusive.min.js │ │ ├── jquery-validation │ │ ├── LICENSE.md │ │ └── dist │ │ │ ├── additional-methods.js │ │ │ ├── additional-methods.min.js │ │ │ ├── jquery.validate.js │ │ │ └── jquery.validate.min.js │ │ └── jquery │ │ ├── LICENSE.txt │ │ └── dist │ │ ├── jquery.js │ │ ├── jquery.min.js │ │ └── jquery.min.map │ ├── build.sh │ └── restore.sh ├── img ├── docker-desktop-kubernetes.png ├── widgetario-architecture.png ├── widgetario-solution-1.png └── widgetario-solution-2.png ├── index.md ├── labs ├── compose-build │ ├── README.md │ ├── hints.md │ ├── rng │ │ ├── amd64.yml │ │ ├── args.yml │ │ ├── arm64.yml │ │ ├── build.yml │ │ ├── compose.yml │ │ ├── config │ │ │ └── prod │ │ │ │ ├── api │ │ │ │ └── override.json │ │ │ │ └── logging.env │ │ ├── core.yml │ │ ├── dev.yml │ │ ├── docker-compose.yml │ │ ├── docker │ │ │ ├── api │ │ │ │ └── Dockerfile │ │ │ └── web │ │ │ │ └── Dockerfile │ │ ├── push-manifests.ps1 │ │ ├── release.yml │ │ └── src │ │ │ ├── Numbers.Api │ │ │ ├── Controllers │ │ │ │ ├── HealthController.cs │ │ │ │ └── RngController.cs │ │ │ ├── Numbers.Api.csproj │ │ │ ├── Program.cs │ │ │ ├── Startup.cs │ │ │ ├── Status.cs │ │ │ ├── appsettings.Development.json │ │ │ └── appsettings.json │ │ │ └── Numbers.Web │ │ │ ├── App.razor │ │ │ ├── Numbers.Web.csproj │ │ │ ├── Pages │ │ │ ├── Error.razor │ │ │ ├── Index.razor │ │ │ └── _Host.cshtml │ │ │ ├── Program.cs │ │ │ ├── Services │ │ │ └── RandomNumberService.cs │ │ │ ├── Shared │ │ │ └── MainLayout.razor │ │ │ ├── Startup.cs │ │ │ ├── _Imports.razor │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.json │ │ │ └── wwwroot │ │ │ ├── css │ │ │ ├── bootstrap │ │ │ │ ├── bootstrap.min.css │ │ │ │ └── bootstrap.min.css.map │ │ │ ├── open-iconic │ │ │ │ ├── FONT-LICENSE │ │ │ │ ├── ICON-LICENSE │ │ │ │ ├── README.md │ │ │ │ └── font │ │ │ │ │ ├── css │ │ │ │ │ └── open-iconic-bootstrap.min.css │ │ │ │ │ └── fonts │ │ │ │ │ ├── open-iconic.eot │ │ │ │ │ ├── open-iconic.otf │ │ │ │ │ ├── open-iconic.svg │ │ │ │ │ ├── open-iconic.ttf │ │ │ │ │ └── open-iconic.woff │ │ │ └── site.css │ │ │ └── favicon.ico │ └── solution.md ├── compose-limits │ ├── README.md │ ├── hints.md │ ├── rng │ │ ├── v1.yml │ │ ├── v2.yml │ │ ├── v3.yml │ │ └── v4.yml │ └── solution.md ├── compose-model │ ├── README.md │ ├── hints.md │ ├── lab │ │ ├── .env │ │ └── compose.yml │ ├── rng │ │ ├── config │ │ │ ├── dev │ │ │ │ ├── api │ │ │ │ │ └── override.json │ │ │ │ ├── logging.json │ │ │ │ └── web │ │ │ │ │ └── override.json │ │ │ ├── logging.env │ │ │ └── test │ │ │ │ ├── api │ │ │ │ └── override.json │ │ │ │ ├── logging.json │ │ │ │ └── web │ │ │ │ └── override.json │ │ ├── core.yml │ │ ├── dev.yml │ │ ├── test.yml │ │ ├── v1.yml │ │ ├── v2.yml │ │ └── v3.yml │ └── solution.md ├── compose │ ├── README.md │ ├── hints.md │ ├── lab │ │ └── compose.yml │ ├── nginx │ │ └── docker-compose.yml │ ├── rng │ │ ├── v1.yml │ │ └── v2.yml │ └── solution.md ├── containers │ ├── README.md │ ├── hints.md │ └── solution.md ├── env │ ├── README.md │ ├── exercises.env │ ├── hints.md │ ├── html │ │ └── index.html │ ├── scripts │ │ └── print-network.sh │ └── solution.md ├── images │ ├── README.md │ ├── base │ │ └── Dockerfile │ ├── curl │ │ ├── Dockerfile │ │ └── Dockerfile.v2 │ ├── hints.md │ ├── java │ │ ├── HelloWorld.class │ │ └── HelloWorld.java │ ├── lab │ │ └── Dockerfile │ ├── solution.md │ └── web │ │ ├── Dockerfile │ │ └── index.html ├── kubernetes │ ├── README.md │ ├── pods │ │ ├── sleep.yaml │ │ └── whoami.yaml │ └── services │ │ └── whoami-nodeport.yaml ├── multi-stage │ ├── README.md │ ├── hints.md │ ├── simple │ │ └── Dockerfile │ ├── solution.md │ └── whoami │ │ ├── Dockerfile │ │ ├── app.go │ │ └── go.mod ├── networking │ ├── README.md │ ├── compose-network.yml │ ├── compose.yml │ ├── hints.md │ ├── scripts │ │ └── network-info.sh │ └── solution.md ├── orchestration │ ├── README.md │ ├── config │ │ ├── api.json │ │ ├── logging.json │ │ └── web.json │ ├── rng-v1.yml │ └── rng-v2.yml ├── registries │ ├── README.md │ ├── hints.md │ └── solution.md └── troubleshooting │ ├── .env │ ├── README.md │ ├── compose.yml │ ├── config │ ├── api │ │ └── override.json │ └── logging.env │ ├── hints.md │ ├── lab │ └── solution.yml │ └── solution.md └── setup └── README.md /.github/workflows/rng-build.yml: -------------------------------------------------------------------------------- 1 | name: RNG App Docker Image Weekly Build 2 | on: 3 | workflow_dispatch: 4 | push: 5 | paths: 6 | - ".github/workflows/rng-build.yml" 7 | - "labs/compose-build/rng/**" 8 | schedule: 9 | - cron: "0 4 * * 1" 10 | 11 | env: 12 | RELEASE: 21.05 13 | BUILD_NUMBER: ${{ github.run_number }} 14 | 15 | jobs: 16 | image-build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Docker Hub login 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 25 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 26 | 27 | - name: Build and push with tag ${{ env.RELEASE }}-${{ env.BUILD_NUMBER }} 28 | working-directory: labs/compose-build/rng 29 | run: | 30 | docker-compose -f core.yml -f build.yml -f args.yml build --pull 31 | docker-compose -f core.yml -f build.yml -f args.yml push 32 | 33 | - name: Build and push with tag ${{ env.RELEASE }} 34 | working-directory: labs/compose-build/rng 35 | run: | 36 | docker-compose -f core.yml -f build.yml -f args.yml -f release.yml build --pull 37 | docker-compose -f core.yml -f build.yml -f args.yml -f release.yml push 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | hackathon/**/logs/web/app.log 3 | labs/compose-model/rng/lab.yml 4 | labs/compose-model/.env 5 | labs/troubleshooting/solution.yml 6 | labs/compose/rng/lab.yml 7 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | docker.courselabs.co -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Course Labs 2 | 3 | Labs and exercises to help you learn Docker. 4 | 5 | Live at https://docker.courselabs.co. -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: Docker Course Labs 2 | markdown: CommonMarkGhPages 3 | google_analytics: UA-66499982-9 -------------------------------------------------------------------------------- /hackathon/solution-part-1/db/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:11.6-alpine 2 | 3 | COPY ./init-products-db.sh /docker-entrypoint-initdb.d/ 4 | 5 | ENV POSTGRES_PASSWORD=widgetario -------------------------------------------------------------------------------- /hackathon/solution-part-1/db/postgres/init-products-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL 5 | 6 | CREATE TABLE public.products ( 7 | id bigint NOT NULL, 8 | name character varying(255) NULL, 9 | price numeric(19,2) NULL, 10 | stock bigint NOT NULL 11 | ); 12 | 13 | ALTER TABLE public.products ADD CONSTRAINT products_pkey PRIMARY KEY (id); 14 | 15 | INSERT INTO "public"."products" ("id", "name", "price", "stock") 16 | VALUES (1, 'Arm64 SoC', 30.00, 600); 17 | 18 | INSERT INTO "public"."products" ("id", "name", "price", "stock") 19 | VALUES (2, 'IoT breakout board', 8.00, 40); 20 | 21 | INSERT INTO "public"."products" ("id", "name", "price", "stock") 22 | VALUES (3, 'DAC extension board', 15.50, 750); 23 | 24 | INSERT INTO "public"."products" ("id", "name", "price", "stock") 25 | VALUES (4, 'Mars comms unit', 6000.00, 0); 26 | 27 | EOSQL -------------------------------------------------------------------------------- /hackathon/solution-part-1/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | products-db: 4 | image: hackathon/products-db 5 | build: 6 | context: db/postgres 7 | 8 | products-api: 9 | image: hackathon/products-api 10 | build: 11 | context: products-api/java 12 | 13 | stock-api: 14 | image: hackathon/stock-api 15 | build: 16 | context: stock-api/golang 17 | 18 | web: 19 | image: hackathon/web 20 | build: 21 | context: web/dotnet -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.6.3-jdk-11 AS builder 2 | 3 | WORKDIR /usr/src/api 4 | COPY . . 5 | 6 | RUN chmod +x restore.sh && ./restore.sh 7 | RUN chmod +x build.sh && ./build.sh 8 | 9 | # app 10 | FROM openjdk:11.0.12-jre-slim-buster 11 | 12 | WORKDIR /app 13 | COPY --from=builder /usr/src/api/target/products-api-0.1.0.jar . 14 | 15 | EXPOSE 80 16 | ENTRYPOINT ["java", "-jar", "/app/products-api-0.1.0.jar"] 17 | 18 | ENV JRE_VERSION="11.0.12" \ 19 | APP_VERSION="0.4.0" -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/Dockerfile.v2: -------------------------------------------------------------------------------- 1 | FROM maven:3.6.3-jdk-11 AS builder 2 | 3 | WORKDIR /usr/src/api 4 | COPY pom.xml . 5 | RUN mvn -B dependency:go-offline 6 | 7 | COPY . . 8 | RUN mvn package 9 | 10 | # app 11 | FROM openjdk:11.0.12-jre-slim-buster 12 | 13 | WORKDIR /app 14 | COPY --from=builder /usr/src/api/target/products-api-0.1.0.jar . 15 | 16 | EXPOSE 80 17 | ENTRYPOINT ["java", "-jar", "/app/products-api-0.1.0.jar"] 18 | 19 | ENV JRE_VERSION="11.0.12" \ 20 | APP_VERSION="0.4.0" -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mvn package -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.widgetario 7 | products-api 8 | 0.1.0 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 2.4.3 14 | 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-web 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-data-jpa 25 | 26 | 27 | org.springdoc 28 | springdoc-openapi-ui 29 | 1.4.5 30 | 31 | 32 | org.postgresql 33 | postgresql 34 | 42.2.16 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-actuator 39 | 40 | 41 | io.micrometer 42 | micrometer-registry-prometheus 43 | 1.5.4 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-test 48 | test 49 | 50 | 51 | com.jayway.jsonpath 52 | json-path 53 | test 54 | 55 | 56 | 57 | 58 | 1.8 59 | 60 | 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-maven-plugin 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mvn -B dependency:go-offline -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/src/main/java/com/widgetario/Application.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @SpringBootApplication 10 | @RestController 11 | public class Application { 12 | 13 | @Autowired 14 | ProductRepository repository; 15 | 16 | @RequestMapping("/") 17 | public String home() { 18 | return "Nothing to see here, try /products"; 19 | } 20 | 21 | public static void main(String[] args) { 22 | SpringApplication.run(Application.class, args); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/src/main/java/com/widgetario/configuration/RegistryConfiguration.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 6 | import io.micrometer.core.aop.TimedAspect; 7 | import io.micrometer.core.instrument.MeterRegistry; 8 | 9 | @Configuration 10 | @EnableAspectJAutoProxy 11 | public class RegistryConfiguration { 12 | 13 | @Bean 14 | TimedAspect timedAspect(MeterRegistry registry) { 15 | return new TimedAspect(registry); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/src/main/java/com/widgetario/controllers/ProductsController.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import io.micrometer.core.annotation.Timed; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | 6 | import java.math.BigDecimal; 7 | import java.math.MathContext; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Value; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RequestParam; 18 | import org.springframework.web.bind.annotation.RestController; 19 | import org.springframework.web.client.RestTemplate; 20 | 21 | @RestController 22 | public class ProductsController { 23 | private static final Logger log = LoggerFactory.getLogger(ProductsController.class); 24 | 25 | @Autowired 26 | ProductRepository repository; 27 | 28 | @Autowired 29 | MeterRegistry registry; 30 | 31 | @Value("${price.factor}") 32 | private String priceFactor; 33 | 34 | @RequestMapping("/products") 35 | @Timed() 36 | public List get() { 37 | log.debug("** GET /products called, using price factor: " + priceFactor); 38 | registry.counter("products_data_load_total", "status", "called").increment(); 39 | List products = null; 40 | try { 41 | products = repository.findAll(); 42 | BigDecimal factor = new BigDecimal(priceFactor); 43 | MathContext mc = new MathContext(2); 44 | for (Product p:products) { 45 | 46 | p.setPrice(p.getPrice().multiply(factor, mc)); 47 | } 48 | } 49 | catch (Exception ex) { 50 | log.debug("** GET /products failed!"); 51 | registry.counter("products_data_load_total", "status", "failure").increment(); 52 | } 53 | return products; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/src/main/java/com/widgetario/models/Product.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import java.io.Serializable; 4 | import java.math.BigDecimal; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import javax.persistence.Table; 12 | 13 | @Entity 14 | @Table(name = "products") 15 | public class Product implements Serializable { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.AUTO) 19 | private long id; 20 | 21 | @Column(name = "name") 22 | private String name; 23 | 24 | @Column(name = "price") 25 | private BigDecimal price; 26 | 27 | public Product() {} 28 | 29 | public Product(String name, BigDecimal price) { 30 | setName(name); 31 | setPrice(price); 32 | } 33 | 34 | public long getId() { 35 | return id; 36 | } 37 | 38 | public void setId(long id) { 39 | this.id = id; 40 | } 41 | 42 | public String getName() { 43 | return name; 44 | } 45 | 46 | public void setName(String name) { 47 | this.name = name; 48 | } 49 | 50 | public BigDecimal getPrice() { 51 | return price; 52 | } 53 | 54 | public void setPrice(BigDecimal price) { 55 | this.price = price; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/src/main/java/com/widgetario/repositories/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import java.util.List; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import org.springframework.data.repository.CrudRepository; 9 | 10 | public interface ProductRepository extends CrudRepository { 11 | List findAll(); 12 | } 13 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/src/main/java/com/widgetario/startup/ApplicationStartup.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import io.micrometer.core.instrument.MeterRegistry; 4 | import io.micrometer.core.instrument.Tags; 5 | 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.ApplicationArguments; 10 | import org.springframework.boot.ApplicationRunner; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class ApplicationStartup implements ApplicationRunner { 15 | 16 | private AtomicInteger appInfoGaugeValue = new AtomicInteger(1); 17 | 18 | @Autowired 19 | MeterRegistry registry; 20 | 21 | @Override 22 | public void run(ApplicationArguments args) throws Exception { 23 | registry.gauge("app.info", Tags.of("version", System.getenv("APP_VERSION"), "java.version", System.getenv("JRE_VERSION")), appInfoGaugeValue); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/products-api/java/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | price.factor=${PRICE_FACTOR:1} 2 | logging.level.widgetario.products=DEBUG 3 | management.endpoints.web.exposure.include=prometheus 4 | server.port=80 5 | spring.jpa.show-sql=true 6 | spring.jpa.generate-ddl=true 7 | spring.jpa.hibernate.ddl-auto=update 8 | spring.jpa.database=POSTGRESQL 9 | spring.datasource.platform=postgres 10 | spring.datasource.url=jdbc:postgresql://products-db:5432/postgres 11 | spring.datasource.username=postgres 12 | spring.datasource.password=widgetario -------------------------------------------------------------------------------- /hackathon/solution-part-1/stock-api/golang/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.15.14-alpine3.14 AS builder 2 | ENV CGO_ENABLED=0 3 | 4 | WORKDIR /go/stock-api 5 | COPY . . 6 | 7 | RUN chmod +x restore.sh && ./restore.sh 8 | RUN chmod +x build.sh && ./build.sh 9 | 10 | # app 11 | FROM alpine:3.14 12 | 13 | ENV POSTGRES_CONNECTION_STRING="host=products-db port=5432 user=postgres password=widgetario dbname=postgres sslmode=disable" \ 14 | CACHE_EXPIRY_SECONDS="45" \ 15 | GOLANG_VERSION="1.15.14" \ 16 | APP_VERSION="0.4.0" 17 | 18 | EXPOSE 8080 19 | CMD ["/app/server"] 20 | 21 | WORKDIR /cache 22 | WORKDIR /app 23 | COPY --from=builder /server . -------------------------------------------------------------------------------- /hackathon/solution-part-1/stock-api/golang/Dockerfile.v2: -------------------------------------------------------------------------------- 1 | FROM golang:1.15.14-alpine3.14 AS builder 2 | ENV CGO_ENABLED=0 3 | 4 | WORKDIR /go/stock-api 5 | COPY ./src/go.mod . 6 | RUN go mod download 7 | 8 | COPY ./src . 9 | RUN go build -o /server 10 | 11 | # app 12 | FROM alpine:3.14 13 | 14 | ENV POSTGRES_CONNECTION_STRING="host=products-db port=5432 user=postgres password=widgetario dbname=postgres sslmode=disable" \ 15 | CACHE_EXPIRY_SECONDS="45" \ 16 | GOLANG_VERSION="1.15.14" \ 17 | APP_VERSION="0.4.0" 18 | 19 | EXPOSE 8080 20 | CMD ["/app/server"] 21 | 22 | WORKDIR /cache 23 | WORKDIR /app 24 | COPY --from=builder /server . -------------------------------------------------------------------------------- /hackathon/solution-part-1/stock-api/golang/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd src 4 | go build -o /server -------------------------------------------------------------------------------- /hackathon/solution-part-1/stock-api/golang/restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd src 4 | go mod download -------------------------------------------------------------------------------- /hackathon/solution-part-1/stock-api/golang/src/go.mod: -------------------------------------------------------------------------------- 1 | module stock-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.0 7 | github.com/lib/pq v1.9.0 8 | github.com/prometheus/client_golang v1.9.0 9 | ) 10 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/stock-api/golang/src/handlers/handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | "time" 12 | "github.com/gorilla/mux" 13 | _ "github.com/lib/pq" 14 | "stock-api/models" 15 | "io/ioutil" 16 | ) 17 | 18 | type response struct { 19 | ID int64 `json:"id,omitempty"` 20 | Message string `json:"message,omitempty"` 21 | } 22 | 23 | func createConnection() *sql.DB { 24 | db, err := sql.Open("postgres", os.Getenv("POSTGRES_CONNECTION_STRING")) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | err = db.Ping() 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | return db 35 | } 36 | 37 | func GetHealth(w http.ResponseWriter, r *http.Request) { 38 | w.Write([]byte("OK")) 39 | } 40 | 41 | func GetProductStock(w http.ResponseWriter, r *http.Request) { 42 | params := mux.Vars(r) 43 | id,_ := strconv.Atoi(params["id"]) 44 | var cachedProduct models.CachedProduct 45 | var cacheIsValid bool 46 | 47 | cacheFile := fmt.Sprintf("/cache/product-%v.json", id); 48 | if _, err := os.Stat(cacheFile); err == nil { 49 | content,_ := ioutil.ReadFile(cacheFile) 50 | _ = json.Unmarshal(content, &cachedProduct) 51 | cacheIsValid = time.Now().Unix() < cachedProduct.ExpiresAt 52 | if cacheIsValid { 53 | log.Printf("Loaded stock from cache for product ID: %v", id) 54 | } else { 55 | log.Printf("Cache expired for product ID: %v", id) 56 | os.Remove(cacheFile) 57 | } 58 | } 59 | 60 | if !cacheIsValid { 61 | product,_ := getProductStock(int64(id)) 62 | log.Printf("Fetched stock from DB for product ID: %v", id) 63 | cacheExpiry,_ := strconv.Atoi(os.Getenv("CACHE_EXPIRY_SECONDS")) 64 | cachedProduct = models.CachedProduct{ 65 | Product: product, 66 | ExpiresAt: time.Now().Unix() + int64(cacheExpiry), 67 | } 68 | data, _ := json.MarshalIndent(cachedProduct, "", " ") 69 | err := ioutil.WriteFile(cacheFile, data, 0644) 70 | if err != nil { 71 | log.Printf("ERR 1046 - failed to write to cache file: %v", cacheFile) 72 | } 73 | } 74 | 75 | w.Header().Add("Content-Type", "application/json") 76 | json.NewEncoder(w).Encode(cachedProduct.Product) 77 | } 78 | 79 | func SetProductStock(w http.ResponseWriter, r *http.Request) { 80 | params := mux.Vars(r) 81 | id,_ := strconv.Atoi(params["id"]) 82 | 83 | var product models.Product 84 | err := json.NewDecoder(r.Body).Decode(&product) 85 | if err != nil { 86 | http.Error(w, err.Error(), http.StatusBadRequest) 87 | return 88 | } 89 | 90 | log.Printf("Setting stock to : %v, for product ID: %v", product.Stock, id) 91 | setProductStock(int64(id), product.Stock) 92 | log.Printf("Updated stock for product ID: %v", id) 93 | 94 | res := response{ 95 | ID: int64(id), 96 | Message: "Stock updated", 97 | } 98 | 99 | w.Header().Add("Content-Type", "application/json") 100 | json.NewEncoder(w).Encode(res) 101 | } 102 | 103 | func getProductStock(id int64) (models.Product, error) { 104 | db := createConnection() 105 | defer db.Close() 106 | 107 | sql := `SELECT id, stock FROM "public"."products" WHERE id=$1` 108 | row := db.QueryRow(sql, id) 109 | 110 | var product models.Product 111 | err := row.Scan(&product.ID, &product.Stock) 112 | 113 | if err != nil { 114 | log.Fatalf("Error fetching product. %v", err) 115 | } 116 | 117 | return product, err 118 | } 119 | 120 | func setProductStock(id int64, stock int64) { 121 | db := createConnection() 122 | defer db.Close() 123 | 124 | sql := `UPDATE "public"."products" SET stock = $2 WHERE id=$1` 125 | _, err := db.Exec(sql, id, stock) 126 | 127 | if err != nil { 128 | log.Fatalf("Error updating product. %v", err) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/stock-api/golang/src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "stock-api/router" 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/prometheus/client_golang/prometheus/promauto" 10 | ) 11 | 12 | var ( 13 | appInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{ 14 | Name: "app_info", 15 | Help: "Application info", 16 | }, []string{"version", "goversion"}) 17 | ) 18 | 19 | func main() { 20 | appInfo.WithLabelValues(os.Getenv("APP_VERSION"), os.Getenv("GOLANG_VERSION")).Set(1) 21 | 22 | r := router.Router() 23 | log.Println("Starting server on port 8080...") 24 | log.Fatal(http.ListenAndServe(":8080", r)) 25 | } 26 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/stock-api/golang/src/middleware/prometheus.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/prometheus/client_golang/prometheus/promauto" 9 | ) 10 | 11 | var ( 12 | activeRequests = promauto.NewGauge(prometheus.GaugeOpts{ 13 | Name: "http_requests_in_progress", 14 | Help: "Active HTTP requests", 15 | }) 16 | ) 17 | 18 | var ( 19 | httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ 20 | Name: "http_request_duration_seconds", 21 | Help: "Duration of HTTP requests", 22 | }, []string{"path"}) 23 | ) 24 | 25 | func Prometheus(next http.Handler) http.Handler { 26 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | activeRequests.Inc() 28 | route := mux.CurrentRoute(r) 29 | path, _ := route.GetPathTemplate() 30 | timer := prometheus.NewTimer(httpDuration.WithLabelValues(path)) 31 | next.ServeHTTP(w, r) 32 | timer.ObserveDuration() 33 | activeRequests.Dec() 34 | }) 35 | } -------------------------------------------------------------------------------- /hackathon/solution-part-1/stock-api/golang/src/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Product struct { 4 | ID int64 `json:"id"` 5 | Stock int64 `json:"stock"` 6 | } 7 | 8 | type CachedProduct struct { 9 | Product Product `json:"product"` 10 | ExpiresAt int64 `json:"expiresAt"` 11 | } -------------------------------------------------------------------------------- /hackathon/solution-part-1/stock-api/golang/src/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | "github.com/prometheus/client_golang/prometheus/promhttp" 6 | "stock-api/handlers" 7 | "stock-api/middleware" 8 | ) 9 | 10 | func Router() *mux.Router { 11 | router := mux.NewRouter() 12 | router.Use(middleware.Prometheus) 13 | 14 | router.Path("/metrics").Handler(promhttp.Handler()) 15 | router.HandleFunc("/healthz", handlers.GetHealth).Methods("GET") 16 | router.HandleFunc("/stock/{id}", handlers.GetProductStock).Methods("GET", "OPTIONS") 17 | router.HandleFunc("/stock/{id}", handlers.SetProductStock).Methods("PUT", "OPTIONS") 18 | 19 | return router 20 | } 21 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine AS builder 2 | 3 | WORKDIR /src 4 | COPY . . 5 | 6 | RUN chmod +x restore.sh && ./restore.sh 7 | RUN chmod +x build.sh && ./build.sh 8 | 9 | # app image 10 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine 11 | 12 | ENV Widgetario__ProductsApi__Url="http://products-api/products" \ 13 | Widgetario__StockApi__Url="http://stock-api:8080/stock" \ 14 | DOTNET_VERSION="3.1" \ 15 | APP_VERSION="1.0.0" 16 | 17 | ENTRYPOINT ["dotnet", "/app/Widgetario.Web.dll"] 18 | 19 | WORKDIR /app 20 | COPY --from=builder /out/ . -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Dockerfile.v2: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine AS builder 2 | 3 | WORKDIR /src 4 | COPY Widgetario.Web/Widgetario.Web.csproj . 5 | RUN dotnet restore 6 | 7 | COPY Widgetario.Web/ . 8 | RUN dotnet publish -c Release -o /out Widgetario.Web.csproj --no-restore 9 | 10 | # app image 11 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine 12 | 13 | ENV Widgetario__ProductsApi__Url="http://products-api/products" \ 14 | Widgetario__StockApi__Url="http://stock-api:8080/stock" \ 15 | DOTNET_VERSION="3.1" \ 16 | APP_VERSION="1.0.0" 17 | 18 | ENTRYPOINT ["dotnet", "/app/Widgetario.Web.dll"] 19 | 20 | WORKDIR /app 21 | COPY --from=builder /out/ . -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Logging; 4 | using OpenTracing; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Threading.Tasks; 9 | using Widgetario.Web.Models; 10 | using Widgetario.Web.Services; 11 | 12 | namespace Widgetario.Web.Controllers 13 | { 14 | public class HomeController : Controller 15 | { 16 | private readonly IConfiguration _config; 17 | private readonly ITracer _tracer; 18 | private readonly ILogger _logger; 19 | private readonly ProductService _productsService; 20 | private readonly StockService _stockService; 21 | 22 | public HomeController(ProductService productsService, StockService stockService, ITracer tracer, IConfiguration config, ILogger logger) 23 | { 24 | _productsService = productsService; 25 | _stockService = stockService; 26 | _tracer = tracer; 27 | _config = config; 28 | _logger = logger; 29 | } 30 | 31 | public async Task Index() 32 | { 33 | var stopwatch = Stopwatch.StartNew(); 34 | _logger.LogDebug($"Loading products & stock"); 35 | var model = new ProductViewModel(); 36 | using (var loadScope = _tracer.BuildSpan("api-load").StartActive()) 37 | { 38 | using (var productLoadScope = _tracer.BuildSpan("product-api-load").StartActive()) 39 | { 40 | model.Products = await _productsService.GetProducts(); 41 | _logger.LogTrace($"Loaded: {model.Products.Count()} products from API"); 42 | } 43 | foreach (var product in model.Products) 44 | { 45 | using (var stockLoadScope = _tracer.BuildSpan("stock-api-load").StartActive()) 46 | { 47 | var productStock = await _stockService.GetStock(product.Id); 48 | product.Stock = productStock.Stock; 49 | _logger.LogTrace($"Fetched stock count: {product.Stock} for product ID: {product.Id} from API"); 50 | } 51 | } 52 | if (model.Products.Sum(x=>x.Stock) == 0) 53 | { 54 | _logger.LogWarning("No stock for any products!"); 55 | } 56 | _logger.LogDebug($"Products & stock load took: {stopwatch.Elapsed.TotalMilliseconds}ms"); 57 | } 58 | 59 | if (_config.GetValue("Widgetario:Debug")) 60 | { 61 | ViewData["Environment"] = $"{_config["Widgetario:Environment"]} @ {Dns.GetHostName()}"; 62 | } 63 | else 64 | { 65 | ViewData["Environment"] = $"{_config["Widgetario:Environment"]}"; 66 | } 67 | 68 | ViewData["Theme"] = _config.GetValue("Widgetario:Theme") ?? "light"; 69 | 70 | _logger.LogInformation($"Returning: {model.Products.Count()} products"); 71 | return View(model); 72 | } 73 | 74 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 75 | public IActionResult Error() 76 | { 77 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Controllers/UpController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Widgetario.Web.Controllers 5 | { 6 | public class UpController : Controller 7 | { 8 | private readonly ILogger _logger; 9 | 10 | public UpController(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | public IActionResult Index() 16 | { 17 | _logger.LogTrace($"/up called"); 18 | return Ok(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Models/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Widgetario.Web.Models 4 | { 5 | public class ErrorViewModel 6 | { 7 | public string RequestId { get; set; } 8 | 9 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Models/Product.cs: -------------------------------------------------------------------------------- 1 | namespace Widgetario.Web.Models 2 | { 3 | public class Product 4 | { 5 | public long Id { get; set; } 6 | 7 | public string Name { get; set; } 8 | 9 | public double Price { get; set; } 10 | 11 | public int Stock { get; set; } 12 | 13 | public string StockMessage 14 | { 15 | get 16 | { 17 | var message = "Plenty"; 18 | if (Stock == 0) 19 | { 20 | message = "SOLD OUT!"; 21 | } 22 | else if (Stock < 50) 23 | { 24 | message = "Last few..."; 25 | } 26 | return message; 27 | } 28 | } 29 | 30 | 31 | public string DisplayPrice 32 | { 33 | get 34 | { 35 | return $"${Price.ToString("#.00")}"; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Models/ProductStock.cs: -------------------------------------------------------------------------------- 1 | namespace Widgetario.Web.Models 2 | { 3 | public class ProductStock 4 | { 5 | public long Id { get; set; } 6 | 7 | public int Stock { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Models/ProductViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Widgetario.Web.Models 4 | { 5 | public class ProductViewModel 6 | { 7 | public IEnumerable Products { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | using Prometheus; 10 | using Serilog; 11 | 12 | namespace Widgetario.Web 13 | { 14 | public class Program 15 | { 16 | private static readonly Gauge _InfoGauge = 17 | Metrics.CreateGauge("app_info", "Application info", "dotnet_version", "assembly_name", "app_version"); 18 | 19 | public static void Main(string[] args) 20 | { 21 | var appVersion = Environment.GetEnvironmentVariable("APP_VERSION"); 22 | var dotnetVersion = Environment.GetEnvironmentVariable("DOTNET_VERSION"); 23 | _InfoGauge.Labels(dotnetVersion, "Widgetario.Web", appVersion).Set(1); 24 | CreateHostBuilder(args).Build().Run(); 25 | } 26 | 27 | public static IHostBuilder CreateHostBuilder(string[] args) => 28 | Host.CreateDefaultBuilder(args) 29 | .UseSerilog((builderContext, config) => 30 | { 31 | config.ReadFrom.Configuration(builderContext.Configuration); 32 | }) 33 | .ConfigureAppConfiguration((builderContext, config) => 34 | { 35 | config.AddJsonFile("appsettings.json") 36 | .AddEnvironmentVariables() 37 | .AddJsonFile("config/serilog.json", optional: true, reloadOnChange: true) 38 | .AddJsonFile("config/logging.json", optional: true, reloadOnChange: true) 39 | .AddJsonFile("secrets/api.json", optional: true, reloadOnChange: true); 40 | }) 41 | .ConfigureWebHostDefaults(webBuilder => 42 | { 43 | webBuilder.UseStartup(); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54605", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Widgetario.Web": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Services/ProductService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using RestSharp; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using Widgetario.Web.Models; 7 | 8 | namespace Widgetario.Web.Services 9 | { 10 | public class ProductService 11 | { 12 | private readonly IConfiguration _config; 13 | 14 | public string ApiUrl { get; private set; } 15 | 16 | public ProductService(IConfiguration config) 17 | { 18 | _config = config; 19 | ApiUrl = _config["Widgetario:ProductsApi:Url"]; 20 | } 21 | 22 | public async Task> GetProducts() 23 | { 24 | var client = new RestClient(ApiUrl); 25 | var request = new RestRequest(); 26 | var response = await client.ExecuteGetAsync>(request); 27 | if (!response.IsSuccessful) 28 | { 29 | throw new Exception($"Service call failed, status: {response.StatusCode}, message: {response.ErrorMessage}"); 30 | } 31 | return response.Data; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Services/StockService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using RestSharp; 3 | using System; 4 | using System.Threading.Tasks; 5 | using Widgetario.Web.Models; 6 | 7 | namespace Widgetario.Web.Services 8 | { 9 | public class StockService 10 | { 11 | private readonly IConfiguration _config; 12 | 13 | public string ApiUrl { get; private set; } 14 | 15 | public StockService(IConfiguration config) 16 | { 17 | _config = config; 18 | ApiUrl = _config["Widgetario:StockApi:Url"]; 19 | } 20 | 21 | public async Task GetStock(long productId) 22 | { 23 | var client = new RestClient(ApiUrl); 24 | var request = new RestRequest($"{productId}"); 25 | var response = await client.ExecuteGetAsync(request); 26 | if (!response.IsSuccessful) 27 | { 28 | throw new Exception($"Service call failed, status: {response.StatusCode}, message: {response.ErrorMessage}"); 29 | } 30 | return response.Data; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Startup.cs: -------------------------------------------------------------------------------- 1 | using Jaeger; 2 | using Jaeger.Reporters; 3 | using Jaeger.Samplers; 4 | using Jaeger.Senders.Thrift; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | using OpenTracing; 12 | using OpenTracing.Util; 13 | using Prometheus; 14 | using Widgetario.Web.Services; 15 | 16 | namespace Widgetario.Web 17 | { 18 | public class Startup 19 | { 20 | public Startup(IConfiguration configuration) 21 | { 22 | Configuration = configuration; 23 | } 24 | 25 | public IConfiguration Configuration { get; } 26 | 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddControllersWithViews(); 30 | services.AddScoped(); 31 | services.AddScoped(); 32 | 33 | if (Configuration.GetValue("Widgetario:Tracing:Enabled")) 34 | { 35 | services.AddSingleton(serviceProvider => 36 | { 37 | var loggerFactory = serviceProvider.GetRequiredService(); 38 | var sampler = new ConstSampler(sample: true); 39 | 40 | var reporter = new RemoteReporter.Builder() 41 | .WithLoggerFactory(loggerFactory) 42 | .WithSender(new UdpSender(Configuration["Widgetario:Tracing:Target"], 6831, 0)) 43 | .Build(); 44 | 45 | var tracer = new Tracer.Builder("Widgetario.Web") 46 | .WithLoggerFactory(loggerFactory) 47 | .WithSampler(sampler) 48 | .WithReporter(reporter) 49 | .Build(); 50 | 51 | GlobalTracer.Register(tracer); 52 | return tracer; 53 | }); 54 | services.AddOpenTracing(); 55 | } 56 | else 57 | { 58 | services.AddSingleton(GlobalTracer.Instance); 59 | } 60 | } 61 | 62 | 63 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 64 | { 65 | if (env.IsDevelopment()) 66 | { 67 | app.UseDeveloperExceptionPage(); 68 | } 69 | else 70 | { 71 | app.UseExceptionHandler("/Error"); 72 | } 73 | 74 | app.UseStaticFiles(); 75 | app.UseRouting(); 76 | 77 | app.UseMetricServer(); 78 | app.UseHttpMetrics(); 79 | 80 | app.UseAuthorization(); 81 | app.UseEndpoints(endpoints => 82 | { 83 | endpoints.MapControllerRoute( 84 | name: "default", 85 | pattern: "{controller=Home}/{action=Index}/{id?}"); 86 | }); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model ProductViewModel 2 | @{ 3 | ViewData["Title"] = "Products"; 4 | } 5 | 6 | @if (ViewData["Theme"] != null) 7 | { 8 | var css = "/css/themes/" + ViewData["Theme"] + ".css"; 9 | 10 | } 11 | 12 | @if (ViewData["Environment"] != null) 13 | { 14 |
15 |
16 |

@ViewData["Environment"]

17 |
18 |
19 | } 20 | 21 | 22 | 23 | @if (@Model.Products == null) 24 | { 25 |

Loading...

26 | } 27 | else 28 | { 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | @foreach (var product in @Model.Products) 39 | { 40 | 41 | 42 | 43 | 44 | 45 | } 46 | 47 |
Your widgetJustAvailability
@product.Name@product.DisplayPrice@product.StockMessage
48 | } -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | @{ 3 | ViewData["Title"] = "Error"; 4 | } 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (Model.ShowRequestId) 10 | { 11 |

12 | Request ID: @Model.RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - Widgetario.Web 7 | 8 | 9 | 10 | 11 |
12 |
13 | @RenderBody() 14 |
15 |
16 | 17 | 18 | 19 | @RenderSection("Scripts", required: false) 20 | 21 | 22 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Widgetario.Web 2 | @using Widgetario.Web.Models 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/Widgetario.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.File" ], 4 | "MinimumLevel": "Information", 5 | "WriteTo": [ 6 | { 7 | "Name": "File", 8 | "Args": { "path": "/logs/app.log" } 9 | } 10 | ], 11 | "Enrich": [ "FromLogContext", "WithMachineName" ], 12 | "Properties": { 13 | "Application": "Widgetario.Web" 14 | } 15 | }, 16 | "AllowedHosts": "*", 17 | "Widgetario" :{ 18 | "Theme": "light", 19 | "ProductsApi": { 20 | "Url": "http://localhost:8080/products" 21 | }, 22 | "StockApi": { 23 | "Url": "http://localhost:8088/stock" 24 | }, 25 | "Tracing": { 26 | "Enabled": false, 27 | "Target": "jaeger" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/config/serilog.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.File" ], 4 | "MinimumLevel": "Information", 5 | "WriteTo": [ 6 | { 7 | "Name": "File", 8 | "Args": { "path": "/logs/app.log" } 9 | } 10 | ], 11 | "Enrich": [ "FromLogContext", "WithMachineName" ], 12 | "Properties": { 13 | "Application": "Widgetario.Web" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | 4 | a.navbar-brand { 5 | white-space: normal; 6 | text-align: center; 7 | word-break: break-all; 8 | } 9 | 10 | /* Provide sufficient contrast against white background */ 11 | a { 12 | color: #0366d6; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .nav-pills .nav-link.active, .nav-pills .show > .nav-link { 22 | color: #fff; 23 | background-color: #1b6ec2; 24 | border-color: #1861ac; 25 | } 26 | 27 | /* Sticky footer styles 28 | -------------------------------------------------- */ 29 | html { 30 | font-size: 14px; 31 | } 32 | @media (min-width: 768px) { 33 | html { 34 | font-size: 16px; 35 | } 36 | } 37 | 38 | .border-top { 39 | border-top: 1px solid #e5e5e5; 40 | } 41 | .border-bottom { 42 | border-bottom: 1px solid #e5e5e5; 43 | } 44 | 45 | .box-shadow { 46 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 47 | } 48 | 49 | button.accept-policy { 50 | font-size: 1rem; 51 | line-height: inherit; 52 | } 53 | 54 | /* Sticky footer styles 55 | -------------------------------------------------- */ 56 | html { 57 | position: relative; 58 | min-height: 100%; 59 | } 60 | 61 | body { 62 | /* Margin bottom by footer height */ 63 | margin-bottom: 60px; 64 | } 65 | 66 | .footer { 67 | position: absolute; 68 | bottom: 0; 69 | width: 100%; 70 | white-space: nowrap; 71 | line-height: 60px; /* Vertically center the text there */ 72 | } 73 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/css/themes/dark.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 22px; 3 | font-family: Georgia; 4 | background-color: darkslategray; 5 | color: #f9fffd; 6 | } 7 | 8 | .table { 9 | color: #f9fffd; 10 | } -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/css/themes/light.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 22px; 3 | font-family: Georgia; 4 | background-color: #f9fffd; 5 | color: darkslategray; 6 | } 7 | 8 | .table { 9 | color: darkslategray; 10 | } -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/img/logo.png -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/img/logo2-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/img/logo2-small.png -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/img/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/img/logo2.png -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your Javascript code. 5 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2018 Twitter, Inc. 4 | Copyright (c) 2011-2018 The Bootstrap Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/Widgetario.Web/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, https://js.foundation/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd Widgetario.Web 4 | dotnet publish -c Release -o /out Widgetario.Web.csproj --no-restore -------------------------------------------------------------------------------- /hackathon/solution-part-1/web/dotnet/restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd Widgetario.Web 4 | dotnet restore -------------------------------------------------------------------------------- /hackathon/solution-part-2/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | products-db: 4 | image: hackathon/products-db 5 | networks: 6 | - app-net 7 | 8 | products-api: 9 | image: hackathon/products-api 10 | networks: 11 | - app-net 12 | 13 | stock-api: 14 | image: hackathon/stock-api 15 | networks: 16 | - app-net 17 | 18 | web: 19 | image: hackathon/web 20 | ports: 21 | - "8080:80" 22 | networks: 23 | - app-net 24 | 25 | networks: 26 | app-net: -------------------------------------------------------------------------------- /hackathon/solution-part-3/config/web/logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /hackathon/solution-part-3/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | products-db: 4 | image: hackathon/products-db 5 | ports: 6 | - "5432:5432" 7 | networks: 8 | - app-net 9 | 10 | products-api: 11 | image: hackathon/products-api 12 | environment: 13 | - PRICE_FACTOR=1.5 14 | ports: 15 | - "8081:80" 16 | networks: 17 | - app-net 18 | 19 | stock-api: 20 | image: hackathon/stock-api 21 | ports: 22 | - "8082:8080" 23 | networks: 24 | - app-net 25 | 26 | web: 27 | image: hackathon/web 28 | environment: 29 | - Widgetario__Theme=dark 30 | volumes: 31 | - ./config/web:/app/config 32 | - ./logs/web:/logs 33 | ports: 34 | - "8080:80" 35 | networks: 36 | - app-net 37 | 38 | networks: 39 | app-net: -------------------------------------------------------------------------------- /hackathon/solution-part-4/config/web/logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /hackathon/solution-part-4/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | products-db: 4 | image: hackathon/products-db 5 | ports: 6 | - "5432:5432" 7 | networks: 8 | - app-net 9 | restart: always 10 | scale: 1 11 | cpus: 1 12 | mem_limit: 250m 13 | 14 | products-api: 15 | image: hackathon/products-api 16 | environment: 17 | - PRICE_FACTOR=1.5 18 | networks: 19 | - app-net 20 | depends_on: 21 | - products-db 22 | restart: always 23 | scale: 2 24 | cpus: 0.5 25 | mem_limit: 400m 26 | 27 | stock-api: 28 | image: hackathon/stock-api 29 | networks: 30 | - app-net 31 | depends_on: 32 | - products-db 33 | restart: always 34 | scale: 3 35 | cpus: 0.25 36 | mem_limit: 100m 37 | 38 | web: 39 | image: hackathon/web 40 | environment: 41 | - Widgetario__Theme=dark 42 | volumes: 43 | - ./config/web:/app/config 44 | - ./logs/web:/logs 45 | ports: 46 | - "8080:80" 47 | networks: 48 | - app-net 49 | depends_on: 50 | - products-api 51 | - stock-api 52 | restart: always 53 | scale: 1 54 | cpus: 0.5 55 | mem_limit: 300m 56 | 57 | networks: 58 | app-net: -------------------------------------------------------------------------------- /hackathon/src/db/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | # We use Postgres for the database, version 11.6 - it can use a minimal OS. 4 | 5 | # There's an init script in this folder which populates the reference data: 6 | # init-products-db.sh 7 | 8 | # Your base image should make it easy to run that script as part of the container startup. -------------------------------------------------------------------------------- /hackathon/src/db/postgres/init-products-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL 5 | 6 | CREATE TABLE public.products ( 7 | id bigint NOT NULL, 8 | name character varying(255) NULL, 9 | price numeric(19,2) NULL, 10 | stock bigint NOT NULL 11 | ); 12 | 13 | ALTER TABLE public.products ADD CONSTRAINT products_pkey PRIMARY KEY (id); 14 | 15 | INSERT INTO "public"."products" ("id", "name", "price", "stock") 16 | VALUES (1, 'Arm64 SoC', 30.00, 600); 17 | 18 | INSERT INTO "public"."products" ("id", "name", "price", "stock") 19 | VALUES (2, 'IoT breakout board', 8.00, 40); 20 | 21 | INSERT INTO "public"."products" ("id", "name", "price", "stock") 22 | VALUES (3, 'DAC extension board', 15.50, 750); 23 | 24 | INSERT INTO "public"."products" ("id", "name", "price", "stock") 25 | VALUES (4, 'Mars comms unit', 6000.00, 0); 26 | 27 | EOSQL -------------------------------------------------------------------------------- /hackathon/src/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | products-db: 4 | image: hackathon/products-db 5 | 6 | products-api: 7 | image: hackathon/products-api 8 | 9 | stock-api: 10 | image: hackathon/stock-api 11 | 12 | web: 13 | image: hackathon/web -------------------------------------------------------------------------------- /hackathon/src/products-api/java/Dockerfile: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | # We use Maven version 3.6.3 for the build, with JDK 11. 4 | 5 | # Run restore.sh and then build.sh - you'll need to make 6 | # the files executable first with chmod +x . 7 | 8 | # Build output is a single JAR file: 9 | # /usr/src/api/target/products-api-0.1.0.jar 10 | 11 | # The app should run on OpenJDK 11.0.12, it can use a minimal OS. 12 | 13 | # We need to set two environment variables - 14 | # JRE_VERSION and APP_VERSION. 15 | 16 | # The startup command needs to run the JAR file from the build: 17 | # java -jar products-api-0.1.0.jar -------------------------------------------------------------------------------- /hackathon/src/products-api/java/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mvn -B dependency:go-offline 4 | mvn package 5 | -------------------------------------------------------------------------------- /hackathon/src/products-api/java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.widgetario 7 | products-api 8 | 0.1.0 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 2.4.3 14 | 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-web 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-data-jpa 25 | 26 | 27 | org.springdoc 28 | springdoc-openapi-ui 29 | 1.4.5 30 | 31 | 32 | org.postgresql 33 | postgresql 34 | 42.2.16 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-actuator 39 | 40 | 41 | io.micrometer 42 | micrometer-registry-prometheus 43 | 1.5.4 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-test 48 | test 49 | 50 | 51 | com.jayway.jsonpath 52 | json-path 53 | test 54 | 55 | 56 | 57 | 58 | 1.8 59 | 60 | 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-maven-plugin 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /hackathon/src/products-api/java/restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mvn -B dependency:go-offline 4 | mvn package 5 | -------------------------------------------------------------------------------- /hackathon/src/products-api/java/src/main/java/com/widgetario/Application.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @SpringBootApplication 10 | @RestController 11 | public class Application { 12 | 13 | @Autowired 14 | ProductRepository repository; 15 | 16 | @RequestMapping("/") 17 | public String home() { 18 | return "Nothing to see here, try /products"; 19 | } 20 | 21 | public static void main(String[] args) { 22 | SpringApplication.run(Application.class, args); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /hackathon/src/products-api/java/src/main/java/com/widgetario/configuration/RegistryConfiguration.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 6 | import io.micrometer.core.aop.TimedAspect; 7 | import io.micrometer.core.instrument.MeterRegistry; 8 | 9 | @Configuration 10 | @EnableAspectJAutoProxy 11 | public class RegistryConfiguration { 12 | 13 | @Bean 14 | TimedAspect timedAspect(MeterRegistry registry) { 15 | return new TimedAspect(registry); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /hackathon/src/products-api/java/src/main/java/com/widgetario/controllers/ProductsController.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import io.micrometer.core.annotation.Timed; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | 6 | import java.math.BigDecimal; 7 | import java.math.MathContext; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Value; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RequestParam; 18 | import org.springframework.web.bind.annotation.RestController; 19 | import org.springframework.web.client.RestTemplate; 20 | 21 | @RestController 22 | public class ProductsController { 23 | private static final Logger log = LoggerFactory.getLogger(ProductsController.class); 24 | 25 | @Autowired 26 | ProductRepository repository; 27 | 28 | @Autowired 29 | MeterRegistry registry; 30 | 31 | @Value("${price.factor}") 32 | private String priceFactor; 33 | 34 | @RequestMapping("/products") 35 | @Timed() 36 | public List get() { 37 | log.debug("** GET /products called, using price factor: " + priceFactor); 38 | registry.counter("products_data_load_total", "status", "called").increment(); 39 | List products = null; 40 | try { 41 | products = repository.findAll(); 42 | BigDecimal factor = new BigDecimal(priceFactor); 43 | MathContext mc = new MathContext(2); 44 | for (Product p:products) { 45 | 46 | p.setPrice(p.getPrice().multiply(factor, mc)); 47 | } 48 | } 49 | catch (Exception ex) { 50 | log.debug("** GET /products failed!"); 51 | registry.counter("products_data_load_total", "status", "failure").increment(); 52 | } 53 | return products; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /hackathon/src/products-api/java/src/main/java/com/widgetario/models/Product.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import java.io.Serializable; 4 | import java.math.BigDecimal; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import javax.persistence.Table; 12 | 13 | @Entity 14 | @Table(name = "products") 15 | public class Product implements Serializable { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.AUTO) 19 | private long id; 20 | 21 | @Column(name = "name") 22 | private String name; 23 | 24 | @Column(name = "price") 25 | private BigDecimal price; 26 | 27 | public Product() {} 28 | 29 | public Product(String name, BigDecimal price) { 30 | setName(name); 31 | setPrice(price); 32 | } 33 | 34 | public long getId() { 35 | return id; 36 | } 37 | 38 | public void setId(long id) { 39 | this.id = id; 40 | } 41 | 42 | public String getName() { 43 | return name; 44 | } 45 | 46 | public void setName(String name) { 47 | this.name = name; 48 | } 49 | 50 | public BigDecimal getPrice() { 51 | return price; 52 | } 53 | 54 | public void setPrice(BigDecimal price) { 55 | this.price = price; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /hackathon/src/products-api/java/src/main/java/com/widgetario/repositories/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import java.util.List; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import org.springframework.data.repository.CrudRepository; 9 | 10 | public interface ProductRepository extends CrudRepository { 11 | List findAll(); 12 | } 13 | -------------------------------------------------------------------------------- /hackathon/src/products-api/java/src/main/java/com/widgetario/startup/ApplicationStartup.java: -------------------------------------------------------------------------------- 1 | package widgetario.products; 2 | 3 | import io.micrometer.core.instrument.MeterRegistry; 4 | import io.micrometer.core.instrument.Tags; 5 | 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.ApplicationArguments; 10 | import org.springframework.boot.ApplicationRunner; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class ApplicationStartup implements ApplicationRunner { 15 | 16 | private AtomicInteger appInfoGaugeValue = new AtomicInteger(1); 17 | 18 | @Autowired 19 | MeterRegistry registry; 20 | 21 | @Override 22 | public void run(ApplicationArguments args) throws Exception { 23 | registry.gauge("app.info", Tags.of("version", System.getenv("APP_VERSION"), "java.version", System.getenv("JRE_VERSION")), appInfoGaugeValue); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /hackathon/src/products-api/java/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | price.factor=${PRICE_FACTOR:1} 2 | logging.level.widgetario.products=DEBUG 3 | management.endpoints.web.exposure.include=prometheus 4 | server.port=80 5 | spring.jpa.show-sql=true 6 | spring.jpa.generate-ddl=true 7 | spring.jpa.hibernate.ddl-auto=update 8 | spring.jpa.database=POSTGRESQL 9 | spring.datasource.platform=postgres 10 | spring.datasource.url=jdbc:postgresql://products-db:5432/postgres 11 | spring.datasource.username=postgres 12 | spring.datasource.password=widgetario -------------------------------------------------------------------------------- /hackathon/src/stock-api/golang/Dockerfile: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | # We use Golang version 1.15.14 for the build, with the 4 | # environment variable CGO_ENABLED set to 0. 5 | 6 | # Run restore.sh and then build.sh - you'll need to make 7 | # the files executable first with chmod +x . 8 | 9 | # Build output is a single executable file: 10 | # /server 11 | 12 | # The app should run on a minimal OS. 13 | 14 | # We need to set four environment variables - 15 | # GOLANG_VERSION and APP_VERSION 16 | # CACHE_EXPIRY_SECONDS set to 45 17 | # POSTGRES_CONNECTION_STRING set to "host=products-db port=5432 user=postgres password=widgetario dbname=postgres sslmode=disable" 18 | 19 | # The startup command needs to run the executable from the build: 20 | # /server 21 | 22 | # We need to create an empty directory at /cache. 23 | -------------------------------------------------------------------------------- /hackathon/src/stock-api/golang/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd src 4 | go build -o /server -------------------------------------------------------------------------------- /hackathon/src/stock-api/golang/restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd src 4 | go mod download -------------------------------------------------------------------------------- /hackathon/src/stock-api/golang/src/go.mod: -------------------------------------------------------------------------------- 1 | module stock-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.0 7 | github.com/lib/pq v1.9.0 8 | github.com/prometheus/client_golang v1.9.0 9 | ) 10 | -------------------------------------------------------------------------------- /hackathon/src/stock-api/golang/src/handlers/handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | "time" 12 | "github.com/gorilla/mux" 13 | _ "github.com/lib/pq" 14 | "stock-api/models" 15 | "io/ioutil" 16 | ) 17 | 18 | type response struct { 19 | ID int64 `json:"id,omitempty"` 20 | Message string `json:"message,omitempty"` 21 | } 22 | 23 | func createConnection() *sql.DB { 24 | db, err := sql.Open("postgres", os.Getenv("POSTGRES_CONNECTION_STRING")) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | err = db.Ping() 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | return db 35 | } 36 | 37 | func GetHealth(w http.ResponseWriter, r *http.Request) { 38 | w.Write([]byte("OK")) 39 | } 40 | 41 | func GetProductStock(w http.ResponseWriter, r *http.Request) { 42 | params := mux.Vars(r) 43 | id,_ := strconv.Atoi(params["id"]) 44 | var cachedProduct models.CachedProduct 45 | var cacheIsValid bool 46 | 47 | cacheFile := fmt.Sprintf("/cache/product-%v.json", id); 48 | if _, err := os.Stat(cacheFile); err == nil { 49 | content,_ := ioutil.ReadFile(cacheFile) 50 | _ = json.Unmarshal(content, &cachedProduct) 51 | cacheIsValid = time.Now().Unix() < cachedProduct.ExpiresAt 52 | if cacheIsValid { 53 | log.Printf("Loaded stock from cache for product ID: %v", id) 54 | } else { 55 | log.Printf("Cache expired for product ID: %v", id) 56 | os.Remove(cacheFile) 57 | } 58 | } 59 | 60 | if !cacheIsValid { 61 | product,_ := getProductStock(int64(id)) 62 | log.Printf("Fetched stock from DB for product ID: %v", id) 63 | cacheExpiry,_ := strconv.Atoi(os.Getenv("CACHE_EXPIRY_SECONDS")) 64 | cachedProduct = models.CachedProduct{ 65 | Product: product, 66 | ExpiresAt: time.Now().Unix() + int64(cacheExpiry), 67 | } 68 | data, _ := json.MarshalIndent(cachedProduct, "", " ") 69 | err := ioutil.WriteFile(cacheFile, data, 0644) 70 | if err != nil { 71 | log.Printf("ERR 1046 - failed to write to cache file: %v", cacheFile) 72 | } 73 | } 74 | 75 | w.Header().Add("Content-Type", "application/json") 76 | json.NewEncoder(w).Encode(cachedProduct.Product) 77 | } 78 | 79 | func SetProductStock(w http.ResponseWriter, r *http.Request) { 80 | params := mux.Vars(r) 81 | id,_ := strconv.Atoi(params["id"]) 82 | 83 | var product models.Product 84 | err := json.NewDecoder(r.Body).Decode(&product) 85 | if err != nil { 86 | http.Error(w, err.Error(), http.StatusBadRequest) 87 | return 88 | } 89 | 90 | log.Printf("Setting stock to : %v, for product ID: %v", product.Stock, id) 91 | setProductStock(int64(id), product.Stock) 92 | log.Printf("Updated stock for product ID: %v", id) 93 | 94 | res := response{ 95 | ID: int64(id), 96 | Message: "Stock updated", 97 | } 98 | 99 | w.Header().Add("Content-Type", "application/json") 100 | json.NewEncoder(w).Encode(res) 101 | } 102 | 103 | func getProductStock(id int64) (models.Product, error) { 104 | db := createConnection() 105 | defer db.Close() 106 | 107 | sql := `SELECT id, stock FROM "public"."products" WHERE id=$1` 108 | row := db.QueryRow(sql, id) 109 | 110 | var product models.Product 111 | err := row.Scan(&product.ID, &product.Stock) 112 | 113 | if err != nil { 114 | log.Fatalf("Error fetching product. %v", err) 115 | } 116 | 117 | return product, err 118 | } 119 | 120 | func setProductStock(id int64, stock int64) { 121 | db := createConnection() 122 | defer db.Close() 123 | 124 | sql := `UPDATE "public"."products" SET stock = $2 WHERE id=$1` 125 | _, err := db.Exec(sql, id, stock) 126 | 127 | if err != nil { 128 | log.Fatalf("Error updating product. %v", err) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /hackathon/src/stock-api/golang/src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "stock-api/router" 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/prometheus/client_golang/prometheus/promauto" 10 | ) 11 | 12 | var ( 13 | appInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{ 14 | Name: "app_info", 15 | Help: "Application info", 16 | }, []string{"version", "goversion"}) 17 | ) 18 | 19 | func main() { 20 | appInfo.WithLabelValues(os.Getenv("APP_VERSION"), os.Getenv("GOLANG_VERSION")).Set(1) 21 | 22 | r := router.Router() 23 | log.Println("Starting server on port 8080...") 24 | log.Fatal(http.ListenAndServe(":8080", r)) 25 | } 26 | -------------------------------------------------------------------------------- /hackathon/src/stock-api/golang/src/middleware/prometheus.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/prometheus/client_golang/prometheus/promauto" 9 | ) 10 | 11 | var ( 12 | activeRequests = promauto.NewGauge(prometheus.GaugeOpts{ 13 | Name: "http_requests_in_progress", 14 | Help: "Active HTTP requests", 15 | }) 16 | ) 17 | 18 | var ( 19 | httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ 20 | Name: "http_request_duration_seconds", 21 | Help: "Duration of HTTP requests", 22 | }, []string{"path"}) 23 | ) 24 | 25 | func Prometheus(next http.Handler) http.Handler { 26 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | activeRequests.Inc() 28 | route := mux.CurrentRoute(r) 29 | path, _ := route.GetPathTemplate() 30 | timer := prometheus.NewTimer(httpDuration.WithLabelValues(path)) 31 | next.ServeHTTP(w, r) 32 | timer.ObserveDuration() 33 | activeRequests.Dec() 34 | }) 35 | } -------------------------------------------------------------------------------- /hackathon/src/stock-api/golang/src/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Product struct { 4 | ID int64 `json:"id"` 5 | Stock int64 `json:"stock"` 6 | } 7 | 8 | type CachedProduct struct { 9 | Product Product `json:"product"` 10 | ExpiresAt int64 `json:"expiresAt"` 11 | } -------------------------------------------------------------------------------- /hackathon/src/stock-api/golang/src/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | "github.com/prometheus/client_golang/prometheus/promhttp" 6 | "stock-api/handlers" 7 | "stock-api/middleware" 8 | ) 9 | 10 | func Router() *mux.Router { 11 | router := mux.NewRouter() 12 | router.Use(middleware.Prometheus) 13 | 14 | router.Path("/metrics").Handler(promhttp.Handler()) 15 | router.HandleFunc("/healthz", handlers.GetHealth).Methods("GET") 16 | router.HandleFunc("/stock/{id}", handlers.GetProductStock).Methods("GET", "OPTIONS") 17 | router.HandleFunc("/stock/{id}", handlers.SetProductStock).Methods("PUT", "OPTIONS") 18 | 19 | return router 20 | } 21 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Dockerfile: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | # We use .NET Core SDK version 3.1 for the build. 4 | 5 | # Run restore.sh and then build.sh - you'll need to make 6 | # the files executable first with chmod +x . 7 | 8 | # Build output is in the folder: 9 | # /out 10 | 11 | # The app should run using a .NET Core ASP.NET 3.1 image, with a minimal OS. 12 | 13 | # We need to set four environment variables - 14 | # DOTNET_VERSION and APP_VERSION 15 | # Widgetario__ProductsApi__Url set to "http://products-api/products" 16 | # Widgetario__StockApi__Url set to "http://stock-api:8080/stock" 17 | 18 | # The startup command needs to run the DLL from the build: 19 | # dotnet Widgetario.Web.dll 20 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Logging; 4 | using OpenTracing; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Threading.Tasks; 9 | using Widgetario.Web.Models; 10 | using Widgetario.Web.Services; 11 | 12 | namespace Widgetario.Web.Controllers 13 | { 14 | public class HomeController : Controller 15 | { 16 | private readonly IConfiguration _config; 17 | private readonly ITracer _tracer; 18 | private readonly ILogger _logger; 19 | private readonly ProductService _productsService; 20 | private readonly StockService _stockService; 21 | 22 | public HomeController(ProductService productsService, StockService stockService, ITracer tracer, IConfiguration config, ILogger logger) 23 | { 24 | _productsService = productsService; 25 | _stockService = stockService; 26 | _tracer = tracer; 27 | _config = config; 28 | _logger = logger; 29 | } 30 | 31 | public async Task Index() 32 | { 33 | var stopwatch = Stopwatch.StartNew(); 34 | _logger.LogDebug($"Loading products & stock"); 35 | var model = new ProductViewModel(); 36 | using (var loadScope = _tracer.BuildSpan("api-load").StartActive()) 37 | { 38 | using (var productLoadScope = _tracer.BuildSpan("product-api-load").StartActive()) 39 | { 40 | model.Products = await _productsService.GetProducts(); 41 | _logger.LogTrace($"Loaded: {model.Products.Count()} products from API"); 42 | } 43 | foreach (var product in model.Products) 44 | { 45 | using (var stockLoadScope = _tracer.BuildSpan("stock-api-load").StartActive()) 46 | { 47 | var productStock = await _stockService.GetStock(product.Id); 48 | product.Stock = productStock.Stock; 49 | _logger.LogTrace($"Fetched stock count: {product.Stock} for product ID: {product.Id} from API"); 50 | } 51 | } 52 | if (model.Products.Sum(x=>x.Stock) == 0) 53 | { 54 | _logger.LogWarning("No stock for any products!"); 55 | } 56 | _logger.LogDebug($"Products & stock load took: {stopwatch.Elapsed.TotalMilliseconds}ms"); 57 | } 58 | 59 | if (_config.GetValue("Widgetario:Debug")) 60 | { 61 | ViewData["Environment"] = $"{_config["Widgetario:Environment"]} @ {Dns.GetHostName()}"; 62 | } 63 | else 64 | { 65 | ViewData["Environment"] = $"{_config["Widgetario:Environment"]}"; 66 | } 67 | 68 | ViewData["Theme"] = _config.GetValue("Widgetario:Theme") ?? "light"; 69 | 70 | _logger.LogInformation($"Returning: {model.Products.Count()} products"); 71 | return View(model); 72 | } 73 | 74 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 75 | public IActionResult Error() 76 | { 77 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Controllers/UpController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Widgetario.Web.Controllers 5 | { 6 | public class UpController : Controller 7 | { 8 | private readonly ILogger _logger; 9 | 10 | public UpController(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | public IActionResult Index() 16 | { 17 | _logger.LogTrace($"/up called"); 18 | return Ok(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Models/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Widgetario.Web.Models 4 | { 5 | public class ErrorViewModel 6 | { 7 | public string RequestId { get; set; } 8 | 9 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Models/Product.cs: -------------------------------------------------------------------------------- 1 | namespace Widgetario.Web.Models 2 | { 3 | public class Product 4 | { 5 | public long Id { get; set; } 6 | 7 | public string Name { get; set; } 8 | 9 | public double Price { get; set; } 10 | 11 | public int Stock { get; set; } 12 | 13 | public string StockMessage 14 | { 15 | get 16 | { 17 | var message = "Plenty"; 18 | if (Stock == 0) 19 | { 20 | message = "SOLD OUT!"; 21 | } 22 | else if (Stock < 50) 23 | { 24 | message = "Last few..."; 25 | } 26 | return message; 27 | } 28 | } 29 | 30 | 31 | public string DisplayPrice 32 | { 33 | get 34 | { 35 | return $"${Price.ToString("#.00")}"; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Models/ProductStock.cs: -------------------------------------------------------------------------------- 1 | namespace Widgetario.Web.Models 2 | { 3 | public class ProductStock 4 | { 5 | public long Id { get; set; } 6 | 7 | public int Stock { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Models/ProductViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Widgetario.Web.Models 4 | { 5 | public class ProductViewModel 6 | { 7 | public IEnumerable Products { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | using Prometheus; 10 | using Serilog; 11 | 12 | namespace Widgetario.Web 13 | { 14 | public class Program 15 | { 16 | private static readonly Gauge _InfoGauge = 17 | Metrics.CreateGauge("app_info", "Application info", "dotnet_version", "assembly_name", "app_version"); 18 | 19 | public static void Main(string[] args) 20 | { 21 | var appVersion = Environment.GetEnvironmentVariable("APP_VERSION"); 22 | var dotnetVersion = Environment.GetEnvironmentVariable("DOTNET_VERSION"); 23 | _InfoGauge.Labels(dotnetVersion, "Widgetario.Web", appVersion).Set(1); 24 | CreateHostBuilder(args).Build().Run(); 25 | } 26 | 27 | public static IHostBuilder CreateHostBuilder(string[] args) => 28 | Host.CreateDefaultBuilder(args) 29 | .UseSerilog((builderContext, config) => 30 | { 31 | config.ReadFrom.Configuration(builderContext.Configuration); 32 | }) 33 | .ConfigureAppConfiguration((builderContext, config) => 34 | { 35 | config.AddJsonFile("appsettings.json") 36 | .AddEnvironmentVariables() 37 | .AddJsonFile("config/serilog.json", optional: true, reloadOnChange: true) 38 | .AddJsonFile("config/logging.json", optional: true, reloadOnChange: true) 39 | .AddJsonFile("secrets/api.json", optional: true, reloadOnChange: true); 40 | }) 41 | .ConfigureWebHostDefaults(webBuilder => 42 | { 43 | webBuilder.UseStartup(); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54605", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Widgetario.Web": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Services/ProductService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using RestSharp; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using Widgetario.Web.Models; 7 | 8 | namespace Widgetario.Web.Services 9 | { 10 | public class ProductService 11 | { 12 | private readonly IConfiguration _config; 13 | 14 | public string ApiUrl { get; private set; } 15 | 16 | public ProductService(IConfiguration config) 17 | { 18 | _config = config; 19 | ApiUrl = _config["Widgetario:ProductsApi:Url"]; 20 | } 21 | 22 | public async Task> GetProducts() 23 | { 24 | var client = new RestClient(ApiUrl); 25 | var request = new RestRequest(); 26 | var response = await client.ExecuteGetAsync>(request); 27 | if (!response.IsSuccessful) 28 | { 29 | throw new Exception($"Service call failed, status: {response.StatusCode}, message: {response.ErrorMessage}"); 30 | } 31 | return response.Data; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Services/StockService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using RestSharp; 3 | using System; 4 | using System.Threading.Tasks; 5 | using Widgetario.Web.Models; 6 | 7 | namespace Widgetario.Web.Services 8 | { 9 | public class StockService 10 | { 11 | private readonly IConfiguration _config; 12 | 13 | public string ApiUrl { get; private set; } 14 | 15 | public StockService(IConfiguration config) 16 | { 17 | _config = config; 18 | ApiUrl = _config["Widgetario:StockApi:Url"]; 19 | } 20 | 21 | public async Task GetStock(long productId) 22 | { 23 | var client = new RestClient(ApiUrl); 24 | var request = new RestRequest($"{productId}"); 25 | var response = await client.ExecuteGetAsync(request); 26 | if (!response.IsSuccessful) 27 | { 28 | throw new Exception($"Service call failed, status: {response.StatusCode}, message: {response.ErrorMessage}"); 29 | } 30 | return response.Data; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Startup.cs: -------------------------------------------------------------------------------- 1 | using Jaeger; 2 | using Jaeger.Reporters; 3 | using Jaeger.Samplers; 4 | using Jaeger.Senders.Thrift; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | using OpenTracing; 12 | using OpenTracing.Util; 13 | using Prometheus; 14 | using Widgetario.Web.Services; 15 | 16 | namespace Widgetario.Web 17 | { 18 | public class Startup 19 | { 20 | public Startup(IConfiguration configuration) 21 | { 22 | Configuration = configuration; 23 | } 24 | 25 | public IConfiguration Configuration { get; } 26 | 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddControllersWithViews(); 30 | services.AddScoped(); 31 | services.AddScoped(); 32 | 33 | if (Configuration.GetValue("Widgetario:Tracing:Enabled")) 34 | { 35 | services.AddSingleton(serviceProvider => 36 | { 37 | var loggerFactory = serviceProvider.GetRequiredService(); 38 | var sampler = new ConstSampler(sample: true); 39 | 40 | var reporter = new RemoteReporter.Builder() 41 | .WithLoggerFactory(loggerFactory) 42 | .WithSender(new UdpSender(Configuration["Widgetario:Tracing:Target"], 6831, 0)) 43 | .Build(); 44 | 45 | var tracer = new Tracer.Builder("Widgetario.Web") 46 | .WithLoggerFactory(loggerFactory) 47 | .WithSampler(sampler) 48 | .WithReporter(reporter) 49 | .Build(); 50 | 51 | GlobalTracer.Register(tracer); 52 | return tracer; 53 | }); 54 | services.AddOpenTracing(); 55 | } 56 | else 57 | { 58 | services.AddSingleton(GlobalTracer.Instance); 59 | } 60 | } 61 | 62 | 63 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 64 | { 65 | if (env.IsDevelopment()) 66 | { 67 | app.UseDeveloperExceptionPage(); 68 | } 69 | else 70 | { 71 | app.UseExceptionHandler("/Error"); 72 | } 73 | 74 | app.UseStaticFiles(); 75 | app.UseRouting(); 76 | 77 | app.UseMetricServer(); 78 | app.UseHttpMetrics(); 79 | 80 | app.UseAuthorization(); 81 | app.UseEndpoints(endpoints => 82 | { 83 | endpoints.MapControllerRoute( 84 | name: "default", 85 | pattern: "{controller=Home}/{action=Index}/{id?}"); 86 | }); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model ProductViewModel 2 | @{ 3 | ViewData["Title"] = "Products"; 4 | } 5 | 6 | @if (ViewData["Theme"] != null) 7 | { 8 | var css = "/css/themes/" + ViewData["Theme"] + ".css"; 9 | 10 | } 11 | 12 | @if (ViewData["Environment"] != null) 13 | { 14 |
15 |
16 |

@ViewData["Environment"]

17 |
18 |
19 | } 20 | 21 | 22 | 23 | @if (@Model.Products == null) 24 | { 25 |

Loading...

26 | } 27 | else 28 | { 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | @foreach (var product in @Model.Products) 39 | { 40 | 41 | 42 | 43 | 44 | 45 | } 46 | 47 |
Your widgetJustAvailability
@product.Name@product.DisplayPrice@product.StockMessage
48 | } -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | @{ 3 | ViewData["Title"] = "Error"; 4 | } 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (Model.ShowRequestId) 10 | { 11 |

12 | Request ID: @Model.RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - Widgetario.Web 7 | 8 | 9 | 10 | 11 |
12 |
13 | @RenderBody() 14 |
15 |
16 | 17 | 18 | 19 | @RenderSection("Scripts", required: false) 20 | 21 | 22 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Widgetario.Web 2 | @using Widgetario.Web.Models 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/Widgetario.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.File" ], 4 | "MinimumLevel": "Information", 5 | "WriteTo": [ 6 | { 7 | "Name": "File", 8 | "Args": { "path": "/logs/app.log" } 9 | } 10 | ], 11 | "Enrich": [ "FromLogContext", "WithMachineName" ], 12 | "Properties": { 13 | "Application": "Widgetario.Web" 14 | } 15 | }, 16 | "AllowedHosts": "*", 17 | "Widgetario" :{ 18 | "ProductsApi": { 19 | "Url": "http://localhost:8080/products" 20 | }, 21 | "StockApi": { 22 | "Url": "http://localhost:8088/stock" 23 | }, 24 | "Tracing": { 25 | "Enabled": false, 26 | "Target": "jaeger" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/config/serilog.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.File" ], 4 | "MinimumLevel": "Information", 5 | "WriteTo": [ 6 | { 7 | "Name": "File", 8 | "Args": { "path": "/logs/app.log" } 9 | } 10 | ], 11 | "Enrich": [ "FromLogContext", "WithMachineName" ], 12 | "Properties": { 13 | "Application": "Widgetario.Web" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | 4 | a.navbar-brand { 5 | white-space: normal; 6 | text-align: center; 7 | word-break: break-all; 8 | } 9 | 10 | /* Provide sufficient contrast against white background */ 11 | a { 12 | color: #0366d6; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .nav-pills .nav-link.active, .nav-pills .show > .nav-link { 22 | color: #fff; 23 | background-color: #1b6ec2; 24 | border-color: #1861ac; 25 | } 26 | 27 | /* Sticky footer styles 28 | -------------------------------------------------- */ 29 | html { 30 | font-size: 14px; 31 | } 32 | @media (min-width: 768px) { 33 | html { 34 | font-size: 16px; 35 | } 36 | } 37 | 38 | .border-top { 39 | border-top: 1px solid #e5e5e5; 40 | } 41 | .border-bottom { 42 | border-bottom: 1px solid #e5e5e5; 43 | } 44 | 45 | .box-shadow { 46 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 47 | } 48 | 49 | button.accept-policy { 50 | font-size: 1rem; 51 | line-height: inherit; 52 | } 53 | 54 | /* Sticky footer styles 55 | -------------------------------------------------- */ 56 | html { 57 | position: relative; 58 | min-height: 100%; 59 | } 60 | 61 | body { 62 | /* Margin bottom by footer height */ 63 | margin-bottom: 60px; 64 | } 65 | 66 | .footer { 67 | position: absolute; 68 | bottom: 0; 69 | width: 100%; 70 | white-space: nowrap; 71 | line-height: 60px; /* Vertically center the text there */ 72 | } 73 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/css/themes/dark.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 22px; 3 | font-family: Georgia; 4 | background-color: darkslategray; 5 | color: #f9fffd; 6 | } 7 | 8 | .table { 9 | color: #f9fffd; 10 | } -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/css/themes/light.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 22px; 3 | font-family: Georgia; 4 | background-color: #f9fffd; 5 | color: darkslategray; 6 | } 7 | 8 | .table { 9 | color: darkslategray; 10 | } -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/hackathon/src/web/dotnet/Widgetario.Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/hackathon/src/web/dotnet/Widgetario.Web/wwwroot/img/logo.png -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/img/logo2-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/hackathon/src/web/dotnet/Widgetario.Web/wwwroot/img/logo2-small.png -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/img/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/hackathon/src/web/dotnet/Widgetario.Web/wwwroot/img/logo2.png -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your Javascript code. 5 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2018 Twitter, Inc. 4 | Copyright (c) 2011-2018 The Bootstrap Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | // Unobtrusive validation support library for jQuery and jQuery Validate 2 | // Copyright (c) .NET Foundation. All rights reserved. 3 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 4 | // @version v3.2.11 5 | !function(a){"function"==typeof define&&define.amd?define("jquery.validate.unobtrusive",["jquery-validation"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery-validation")):jQuery.validator.unobtrusive=a(jQuery)}(function(a){function e(a,e,n){a.rules[e]=n,a.message&&(a.messages[e]=a.message)}function n(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function t(a){return a.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function r(a){return a.substr(0,a.lastIndexOf(".")+1)}function i(a,e){return 0===a.indexOf("*.")&&(a=a.replace("*.",e)),a}function o(e,n){var r=a(this).find("[data-valmsg-for='"+t(n[0].name)+"']"),i=r.attr("data-valmsg-replace"),o=i?a.parseJSON(i)!==!1:null;r.removeClass("field-validation-valid").addClass("field-validation-error"),e.data("unobtrusiveContainer",r),o?(r.empty(),e.removeClass("input-validation-error").appendTo(r)):e.hide()}function d(e,n){var t=a(this).find("[data-valmsg-summary=true]"),r=t.find("ul");r&&r.length&&n.errorList.length&&(r.empty(),t.addClass("validation-summary-errors").removeClass("validation-summary-valid"),a.each(n.errorList,function(){a("
  • ").html(this.message).appendTo(r)}))}function s(e){var n=e.data("unobtrusiveContainer");if(n){var t=n.attr("data-valmsg-replace"),r=t?a.parseJSON(t):null;n.addClass("field-validation-valid").removeClass("field-validation-error"),e.removeData("unobtrusiveContainer"),r&&n.empty()}}function l(e){var n=a(this),t="__jquery_unobtrusive_validation_form_reset";if(!n.data(t)){n.data(t,!0);try{n.data("validator").resetForm()}finally{n.removeData(t)}n.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors"),n.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}}function u(e){var n=a(e),t=n.data(v),r=a.proxy(l,e),i=f.unobtrusive.options||{},u=function(n,t){var r=i[n];r&&a.isFunction(r)&&r.apply(e,t)};return t||(t={options:{errorClass:i.errorClass||"input-validation-error",errorElement:i.errorElement||"span",errorPlacement:function(){o.apply(e,arguments),u("errorPlacement",arguments)},invalidHandler:function(){d.apply(e,arguments),u("invalidHandler",arguments)},messages:{},rules:{},success:function(){s.apply(e,arguments),u("success",arguments)}},attachValidation:function(){n.off("reset."+v,r).on("reset."+v,r).validate(this.options)},validate:function(){return n.validate(),n.valid()}},n.data(v,t)),t}var m,f=a.validator,v="unobtrusiveValidation";return f.unobtrusive={adapters:[],parseElement:function(e,n){var t,r,i,o=a(e),d=o.parents("form")[0];d&&(t=u(d),t.options.rules[e.name]=r={},t.options.messages[e.name]=i={},a.each(this.adapters,function(){var n="data-val-"+this.name,t=o.attr(n),s={};void 0!==t&&(n+="-",a.each(this.params,function(){s[this]=o.attr(n+this)}),this.adapt({element:e,form:d,message:t,params:s,rules:r,messages:i}))}),a.extend(r,{__dummy__:!0}),n||t.attachValidation())},parse:function(e){var n=a(e),t=n.parents().addBack().filter("form").add(n.find("form")).has("[data-val=true]");n.find("[data-val=true]").each(function(){f.unobtrusive.parseElement(this,!0)}),t.each(function(){var a=u(this);a&&a.attachValidation()})}},m=f.unobtrusive.adapters,m.add=function(a,e,n){return n||(n=e,e=[]),this.push({name:a,params:e,adapt:n}),this},m.addBool=function(a,n){return this.add(a,function(t){e(t,n||a,!0)})},m.addMinMax=function(a,n,t,r,i,o){return this.add(a,[i||"min",o||"max"],function(a){var i=a.params.min,o=a.params.max;i&&o?e(a,r,[i,o]):i?e(a,n,i):o&&e(a,t,o)})},m.addSingleVal=function(a,n,t){return this.add(a,[n||"val"],function(r){e(r,t||a,r.params[n])})},f.addMethod("__dummy__",function(a,e,n){return!0}),f.addMethod("regex",function(a,e,n){var t;return!!this.optional(e)||(t=new RegExp(n).exec(a),t&&0===t.index&&t[0].length===a.length)}),f.addMethod("nonalphamin",function(a,e,n){var t;return n&&(t=a.match(/\W/g),t=t&&t.length>=n),t}),f.methods.extension?(m.addSingleVal("accept","mimtype"),m.addSingleVal("extension","extension")):m.addSingleVal("extension","extension","accept"),m.addSingleVal("regex","pattern"),m.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"),m.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range"),m.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength"),m.add("equalto",["other"],function(n){var o=r(n.element.name),d=n.params.other,s=i(d,o),l=a(n.form).find(":input").filter("[name='"+t(s)+"']")[0];e(n,"equalTo",l)}),m.add("required",function(a){"INPUT"===a.element.tagName.toUpperCase()&&"CHECKBOX"===a.element.type.toUpperCase()||e(a,"required",!0)}),m.add("remote",["url","type","additionalfields"],function(o){var d={url:o.params.url,type:o.params.type||"GET",data:{}},s=r(o.element.name);a.each(n(o.params.additionalfields||o.element.name),function(e,n){var r=i(n,s);d.data[r]=function(){var e=a(o.form).find(":input").filter("[name='"+t(r)+"']");return e.is(":checkbox")?e.filter(":checked").val()||e.filter(":hidden").val()||"":e.is(":radio")?e.filter(":checked").val()||"":e.val()}}),e(o,"remote",d)}),m.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&e(a,"minlength",a.params.min),a.params.nonalphamin&&e(a,"nonalphamin",a.params.nonalphamin),a.params.regex&&e(a,"regex",a.params.regex)}),m.add("fileextensions",["extensions"],function(a){e(a,"extension",a.params.extensions)}),a(function(){f.unobtrusive.parse(document)}),f.unobtrusive}); -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/Widgetario.Web/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, https://js.foundation/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd Widgetario.Web 4 | dotnet publish -c Release -o /out Widgetario.Web.csproj --no-restore -------------------------------------------------------------------------------- /hackathon/src/web/dotnet/restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd Widgetario.Web 4 | dotnet restore -------------------------------------------------------------------------------- /img/docker-desktop-kubernetes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/img/docker-desktop-kubernetes.png -------------------------------------------------------------------------------- /img/widgetario-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/img/widgetario-architecture.png -------------------------------------------------------------------------------- /img/widgetario-solution-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/img/widgetario-solution-1.png -------------------------------------------------------------------------------- /img/widgetario-solution-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/img/widgetario-solution-2.png -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | Welcome to the Docker labs. 2 | 3 | These are hands-on resources to help you learn Docker. 4 | 5 | ## Pre-reqs 6 | 7 | - [Set up Docker and a Git client](./setup/README.md) 8 | - Create a [Docker Hub](https://hub.docker.com/signup) account (free) 9 | - Download the lab content 10 | - Open a terminal (PowerShell, Bash, ZSH or whatever you use) 11 | - Run: `git clone https://github.com/courselabs/docker` 12 | - Open the folder: `cd docker` 13 | - Log in to Docker Hub: 14 | - `docker login` - using your Docker Hub ID 15 | - _Optional_ 16 | - Install [Visual Studio Code](https://code.visualstudio.com) (free - Windows, macOS and Linux) to browse the repo and documentation 17 | 18 | 19 | ## Part 1 - Containers and Images 20 | 21 | - [Running containers](labs/containers/README.md) 22 | - [Constructing the container environment](labs/env/README.md) 23 | - [Building images](labs/images/README.md) 24 | - [Using image registries](labs/registries/README.md) 25 | 26 | ## Part 2 - Multi-Container Applications 27 | 28 | - [Docker Compose](labs/compose/README.md) 29 | - [Modelling apps with Compose](labs/compose-model/README.md) 30 | - [Building apps with Compose](labs/compose-build/README.md) 31 | - [Limitations of Compose](labs/compose-limits/README.md) 32 | 33 | ## Part 3 - Advanced Docker 34 | 35 | - [Multi-stage builds](labs/multi-stage/README.md) 36 | - [Container networking](labs/networking/README.md) 37 | - [Understanding orchestration](labs/orchestration/README.md) 38 | - [Kubernetes 101](labs/kubernetes/README.md) 39 | 40 | ## Part 4 - Real-World Docker 41 | 42 | - [Troubleshooting](labs/troubleshooting/README.md) 43 | - [Hackathon!](hackathon/README.md) 44 | 45 | ### Credits 46 | 47 | Created by [@EltonStoneman](https://twitter.com/EltonStoneman) ([sixeyed](https://github.com/sixeyed)): Freelance Consultant and Trainer. Author of [Learn Docker in a Month of Lunches](https://www.manning.com/books/learn-docker-in-a-month-of-lunches), [Learn Kubernetes in a Month of Lunches](https://www.manning.com/books/learn-kubernetes-in-a-month-of-lunches) and [many Pluralsight courses](https://pluralsight.pxf.io/c/1197078/424552/7490?u=https%3A%2F%2Fwww.pluralsight.com%2Fauthors%2Felton-stoneman). -------------------------------------------------------------------------------- /labs/compose-build/hints.md: -------------------------------------------------------------------------------- 1 | # Lab Hints 2 | 3 | Image tags are often used for application versions, and a [semantic versioning](https://semver.org) approach is very common. 4 | 5 | You might have multiple tags for the same image: 6 | 7 | - `1` - major version 8 | - `1.0` - major + minor version 9 | - `1.0.100` - major + minor version + build number 10 | 11 | That lets users choose to pin to a specific version - `1.0.100` will never change. 12 | 13 | Minor versions get updated with each build - `1.0` is `1.0.100` now but could be an alias for `1.0.126` next month. 14 | 15 | Major versions get updated with each build **and** each minor version update - `1` is `1.0.100` now, but it could be `1.2.407` next year. 16 | 17 | The RNG app uses a similar approach. 18 | 19 | > Need more? Here's the [solution](solution.md). -------------------------------------------------------------------------------- /labs/compose-build/rng/amd64.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05-linux-amd64 4 | 5 | rng-web: 6 | image: courselabs/rng-web:21.05-linux-amd64 7 | -------------------------------------------------------------------------------- /labs/compose-build/rng/args.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: ${REPOSITORY:-courselabs}/rng-api:${RELEASE:-21.05}-${BUILD_NUMBER:-0} 4 | build: 5 | args: 6 | BUILD_VERSION: ${RELEASE:-21.05}.${BUILD_NUMBER:-0} 7 | BUILD_TAG: ${GITHUB_WORKFLOW:-local}-${BUILD_NUMBER:-0}-${GITHUB_REF:-local} 8 | COMMIT_SHA: ${GITHUB_SHA:-local} 9 | 10 | rng-web: 11 | image: ${REPOSITORY:-courselabs}/rng-web:${RELEASE:-21.05}-${BUILD_NUMBER:-0} 12 | build: 13 | args: 14 | BUILD_VERSION: ${RELEASE:-21.05}.${BUILD_NUMBER:-0} 15 | BUILD_TAG: ${GITHUB_WORKFLOW:-local}-${BUILD_NUMBER:-0}-${GITHUB_REF:-local} 16 | COMMIT_SHA: ${GITHUB_SHA:-local} -------------------------------------------------------------------------------- /labs/compose-build/rng/arm64.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:${RELEASE:-21.05}-linux-arm64 4 | 5 | rng-web: 6 | image: courselabs/rng-web:${RELEASE:-21.05}-linux-arm64 7 | -------------------------------------------------------------------------------- /labs/compose-build/rng/build.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | build: 4 | context: . 5 | dockerfile: docker/api/Dockerfile 6 | 7 | rng-web: 8 | build: 9 | context: . 10 | dockerfile: docker/web/Dockerfile -------------------------------------------------------------------------------- /labs/compose-build/rng/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | rng-api: 4 | image: courselabs/rng-api:21.05 5 | 6 | rng-web: 7 | image: courselabs/rng-web:21.05 8 | -------------------------------------------------------------------------------- /labs/compose-build/rng/config/prod/api/override.json: -------------------------------------------------------------------------------- 1 | { 2 | "Rng" : { 3 | "Range": { 4 | "Min": 0, 5 | "Max": 5000 6 | }, 7 | "FailAfter": { 8 | "Enabled": false 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /labs/compose-build/rng/config/prod/logging.env: -------------------------------------------------------------------------------- 1 | Logging__LogLevel__Default=Information -------------------------------------------------------------------------------- /labs/compose-build/rng/core.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: rng-api:21.05 4 | networks: 5 | - app-net 6 | 7 | rng-web: 8 | image: rng-web:21.05 9 | environment: 10 | - RngApi__Url=http://rng-api/rng 11 | networks: 12 | - app-net 13 | 14 | networks: 15 | app-net: 16 | -------------------------------------------------------------------------------- /labs/compose-build/rng/dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | environment: 4 | - Logging__LogLevel__Default=Debug 5 | ports: 6 | - "8089:80" 7 | 8 | rng-web: 9 | environment: 10 | - Logging__LogLevel__Default=Debug 11 | - RngApi__Url=http://rng-api/rng 12 | ports: 13 | - "8090:80" 14 | -------------------------------------------------------------------------------- /labs/compose-build/rng/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: rng-api:21.05-local 4 | networks: 5 | - app-net 6 | build: 7 | context: . 8 | dockerfile: docker/api/Dockerfile 9 | 10 | rng-web: 11 | image: rng-web:21.05-local 12 | environment: 13 | - RngApi__Url=http://rng-api/rng 14 | ports: 15 | - 8090:80 16 | networks: 17 | - app-net 18 | build: 19 | context: . 20 | dockerfile: docker/web/Dockerfile 21 | 22 | networks: 23 | app-net: 24 | -------------------------------------------------------------------------------- /labs/compose-build/rng/docker/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS builder 2 | ARG BUILD_VERSION=1.0.0 3 | 4 | WORKDIR /src 5 | COPY src/Numbers.Api/Numbers.Api.csproj . 6 | RUN dotnet restore 7 | 8 | COPY src/Numbers.Api/ . 9 | RUN dotnet publish -c Release /p:Version=$BUILD_VERSION -o /out Numbers.Api.csproj 10 | 11 | # app image 12 | FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine 13 | 14 | ENTRYPOINT ["dotnet", "/app/Numbers.Api.dll"] 15 | 16 | WORKDIR /app 17 | COPY --from=builder /out/ . 18 | 19 | ARG BUILD_TAG=local 20 | ARG COMMIT_SHA=local 21 | LABEL build_tag=${BUILD_TAG} 22 | LABEL commit_sha=${COMMIT_SHA} -------------------------------------------------------------------------------- /labs/compose-build/rng/docker/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS builder 2 | ARG BUILD_VERSION=1.0.0 3 | 4 | WORKDIR /src 5 | COPY src/Numbers.Web/Numbers.Web.csproj . 6 | RUN dotnet restore 7 | 8 | COPY src/Numbers.Web/ . 9 | RUN dotnet publish -c Release /p:Version=$BUILD_VERSION -o /out Numbers.Web.csproj 10 | 11 | # app image 12 | FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine 13 | 14 | ENV RngApi__Url=http://numbers-api/rng 15 | 16 | ENTRYPOINT ["dotnet", "/app/Numbers.Web.dll"] 17 | 18 | WORKDIR /app 19 | COPY --from=builder /out/ . 20 | 21 | ARG BUILD_TAG=local 22 | ARG COMMIT_SHA=local 23 | LABEL build_tag=${BUILD_TAG} 24 | LABEL commit_sha=${COMMIT_SHA} -------------------------------------------------------------------------------- /labs/compose-build/rng/push-manifests.ps1: -------------------------------------------------------------------------------- 1 | $images=$(yq e '.services.[].image' compose.yml) 2 | 3 | foreach ($image in $images) 4 | { 5 | docker manifest create --amend $image ` 6 | "$($image)-linux-arm64" ` 7 | "$($image)-linux-amd64" 8 | 9 | docker manifest push $image 10 | } 11 | -------------------------------------------------------------------------------- /labs/compose-build/rng/release.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:${RELEASE:-21.05} 4 | 5 | rng-web: 6 | image: courselabs/rng-web:${RELEASE:-21.05} -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Api/Controllers/HealthController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Numbers.Api.Controllers 4 | { 5 | [ApiController] 6 | [Route("[controller]")] 7 | public class HealthController : ControllerBase 8 | { 9 | [HttpGet] 10 | public IActionResult Get() 11 | { 12 | if (Status.Healthy) 13 | { 14 | return Ok("Ok"); 15 | } 16 | else 17 | { 18 | return StatusCode(500); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Api/Controllers/RngController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Numbers.Api.Controllers 7 | { 8 | [ApiController] 9 | [Route("[controller]")] 10 | public class RngController : ControllerBase 11 | { 12 | private static Random _Random = new Random(); 13 | private static int _CallCount; 14 | 15 | private readonly IConfiguration _config; 16 | private readonly ILogger _logger; 17 | 18 | public RngController(IConfiguration config, ILogger logger) 19 | { 20 | _config = config; 21 | _logger = logger; 22 | if (_CallCount == 0) 23 | { 24 | _logger.LogInformation("Random number generator initialized"); 25 | } 26 | } 27 | 28 | [HttpGet] 29 | public IActionResult Get() 30 | { 31 | _CallCount++; 32 | if (_config.GetValue("Rng:FailAfter:Enabled") && _CallCount > _config.GetValue("Rng:FailAfter:CallCount")) 33 | { 34 | if (_config["Rng:FailAfter:Action"] == "Exit") 35 | { 36 | _logger.LogError($"FailAfter enabled. Call: {_CallCount}. Exiting."); 37 | Environment.Exit(100); 38 | } 39 | _logger.LogWarning($"FailAfter enabled. Call: {_CallCount}. Going unhealthy."); 40 | Status.Healthy = false; 41 | } 42 | 43 | if (Status.Healthy) 44 | { 45 | var min = _config.GetValue("Rng:Range:Min"); 46 | var max = _config.GetValue("Rng:Range:Max"); 47 | var n = _Random.Next(min, max); 48 | _logger.LogDebug($"Call: {_CallCount}. Returning random number: {n}, from min: {min}, max: {max}"); 49 | return Ok(n); 50 | } 51 | else 52 | { 53 | _logger.LogWarning("Unhealthy!"); 54 | return StatusCode(500); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Api/Numbers.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace Numbers.Api 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | CreateHostBuilder(args).Build().Run(); 12 | } 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureAppConfiguration((builderContext, config) => 17 | { 18 | config.AddJsonFile("config/logging.json", optional: true) 19 | .AddEnvironmentVariables() 20 | .AddJsonFile("config/override.json", optional: true); 21 | }) 22 | .ConfigureWebHostDefaults(webBuilder => 23 | { 24 | webBuilder.UseStartup(); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace Numbers.Api 8 | { 9 | public class Startup 10 | { 11 | public Startup(IConfiguration configuration) 12 | { 13 | Configuration = configuration; 14 | } 15 | 16 | public IConfiguration Configuration { get; } 17 | 18 | public void ConfigureServices(IServiceCollection services) 19 | { 20 | services.AddControllers(); 21 | } 22 | 23 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 24 | { 25 | if (env.IsDevelopment()) 26 | { 27 | app.UseDeveloperExceptionPage(); 28 | } 29 | 30 | app.UseRouting(); 31 | 32 | app.UseEndpoints(endpoints => 33 | { 34 | endpoints.MapControllers(); 35 | }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Api/Status.cs: -------------------------------------------------------------------------------- 1 | namespace Numbers.Api 2 | { 3 | public static class Status 4 | { 5 | public static bool Healthy { get; set; } 6 | 7 | static Status() 8 | { 9 | Healthy = true; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Warning" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Rng" : { 11 | "Range": { 12 | "Min": 0, 13 | "Max": 100 14 | }, 15 | "FailAfter": { 16 | "Enabled": false, 17 | "CallCount" : 3, 18 | "Action" : "Exit" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 |

    Sorry, there's nothing at this address.

    8 |
    9 |
    10 |
    11 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/Numbers.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/error" 2 | 3 | 4 |

    Error.

    5 |

    An error occurred while processing your request.

    6 | 7 |

    Development Mode

    8 |

    9 | Swapping to Development environment will display more detailed information about the error that occurred. 10 |

    11 |

    12 | The Development environment shouldn't be enabled for deployed applications. 13 | It can result in displaying sensitive information from exceptions to end users. 14 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 15 | and restarting the app. 16 |

    -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | @using Numbers.Web.Services 4 | @inject RandomNumberService RngService 5 | 6 |

    Docker Course Labs Random Number Generator

    7 | 8 | @if (callFailed){ 9 |

    RNG service unavailable!

    10 | } 11 | 12 | @if (callFailed == false && randomNumber != -1){ 13 |

    Here it is: @randomNumber

    14 | } 15 | 16 | 17 | 18 | @code { 19 | bool callFailed = false; 20 | int randomNumber = -1; 21 | 22 | void GetRandomNumber() 23 | { 24 | callFailed = false; 25 | try 26 | { 27 | randomNumber = RngService.GetNumber(); 28 | } 29 | catch 30 | { 31 | callFailed = true; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace Numbers.Web.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | 5 | 6 | 7 | 8 | 9 | 10 | Numbers.Web 11 | 12 | 13 | 14 | 15 | 16 | 17 | @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered)) 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace Numbers.Web 13 | { 14 | public class Program 15 | { 16 | public static void Main(string[] args) 17 | { 18 | CreateHostBuilder(args).Build().Run(); 19 | } 20 | 21 | public static IHostBuilder CreateHostBuilder(string[] args) => 22 | Host.CreateDefaultBuilder(args) 23 | .ConfigureAppConfiguration((builderContext, config) => 24 | { 25 | config.AddJsonFile("config/logging.json", optional: true) 26 | .AddEnvironmentVariables() 27 | .AddJsonFile("config/override.json", optional: true); 28 | }) 29 | .ConfigureWebHostDefaults(webBuilder => 30 | { 31 | webBuilder.UseStartup(); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/Services/RandomNumberService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Logging; 5 | using RestSharp; 6 | 7 | namespace Numbers.Web.Services 8 | { 9 | public class RandomNumberService 10 | { 11 | private readonly IConfiguration _config; 12 | private readonly ILogger _logger; 13 | 14 | public RandomNumberService(IConfiguration config, ILogger logger) 15 | { 16 | _config = config; 17 | _logger = logger; 18 | _logger.LogInformation($"Using API at: {_config["RngApi:Url"]}"); 19 | } 20 | 21 | public int GetNumber() 22 | { 23 | var client = new RestClient(_config["RngApi:Url"]); 24 | var request = new RestRequest(); 25 | var response = client.Execute(request); 26 | if (!response.IsSuccessful) 27 | { 28 | throw new Exception("Service call failed"); 29 | } 30 | return int.Parse(response.Content); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
    4 |
    5 | Source Code 6 |
    7 | 8 |
    9 | @Body 10 |
    11 |
    12 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Components; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.HttpsPolicy; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using Numbers.Web.Services; 13 | 14 | namespace Numbers.Web 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public IConfiguration Configuration { get; } 24 | 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | services.AddRazorPages(); 28 | services.AddServerSideBlazor(); 29 | services.AddSingleton(); 30 | } 31 | 32 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 33 | { 34 | if (env.IsDevelopment()) 35 | { 36 | app.UseDeveloperExceptionPage(); 37 | } 38 | else 39 | { 40 | app.UseExceptionHandler("/Error"); 41 | } 42 | 43 | app.UseStaticFiles(); 44 | app.UseRouting(); 45 | 46 | app.UseEndpoints(endpoints => 47 | { 48 | endpoints.MapBlazorHub(); 49 | endpoints.MapFallbackToPage("/_Host"); 50 | }); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.JSInterop 8 | @using Numbers.Web 9 | @using Numbers.Web.Shared 10 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Warning" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "RngApi": { 11 | "Url": "http://localhost:5000/rng" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/labs/compose-build/rng/src/Numbers.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/labs/compose-build/rng/src/Numbers.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/labs/compose-build/rng/src/Numbers.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/labs/compose-build/rng/src/Numbers.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #0366d6; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | app { 18 | position: relative; 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | .top-row { 24 | height: 3.5rem; 25 | display: flex; 26 | align-items: center; 27 | } 28 | 29 | .main { 30 | flex: 1; 31 | } 32 | 33 | .main .top-row { 34 | background-color: #f7f7f7; 35 | border-bottom: 1px solid #d6d5d5; 36 | justify-content: flex-end; 37 | } 38 | 39 | .main .top-row > a { 40 | margin-left: 1.5rem; 41 | } 42 | 43 | .sidebar { 44 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 45 | } 46 | 47 | .sidebar .top-row { 48 | background-color: rgba(0,0,0,0.4); 49 | } 50 | 51 | .sidebar .navbar-brand { 52 | font-size: 1.1rem; 53 | } 54 | 55 | .sidebar .oi { 56 | width: 2rem; 57 | font-size: 1.1rem; 58 | vertical-align: text-top; 59 | top: -2px; 60 | } 61 | 62 | .nav-item { 63 | font-size: 0.9rem; 64 | padding-bottom: 0.5rem; 65 | } 66 | 67 | .nav-item:first-of-type { 68 | padding-top: 1rem; 69 | } 70 | 71 | .nav-item:last-of-type { 72 | padding-bottom: 1rem; 73 | } 74 | 75 | .nav-item a { 76 | color: #d7d7d7; 77 | border-radius: 4px; 78 | height: 3rem; 79 | display: flex; 80 | align-items: center; 81 | line-height: 3rem; 82 | } 83 | 84 | .nav-item a.active { 85 | background-color: rgba(255,255,255,0.25); 86 | color: white; 87 | } 88 | 89 | .nav-item a:hover { 90 | background-color: rgba(255,255,255,0.1); 91 | color: white; 92 | } 93 | 94 | .content { 95 | padding-top: 1.1rem; 96 | } 97 | 98 | .navbar-toggler { 99 | background-color: rgba(255, 255, 255, 0.1); 100 | } 101 | 102 | .valid.modified:not([type=checkbox]) { 103 | outline: 1px solid #26b050; 104 | } 105 | 106 | .invalid { 107 | outline: 1px solid red; 108 | } 109 | 110 | .validation-message { 111 | color: red; 112 | } 113 | 114 | @media (max-width: 767.98px) { 115 | .main .top-row { 116 | display: none; 117 | } 118 | } 119 | 120 | @media (min-width: 768px) { 121 | app { 122 | flex-direction: row; 123 | } 124 | 125 | .sidebar { 126 | width: 250px; 127 | height: 100vh; 128 | position: sticky; 129 | top: 0; 130 | } 131 | 132 | .main .top-row { 133 | position: sticky; 134 | top: 0; 135 | } 136 | 137 | .main > div { 138 | padding-left: 2rem !important; 139 | padding-right: 1.5rem !important; 140 | } 141 | 142 | .navbar-toggler { 143 | display: none; 144 | } 145 | 146 | .sidebar .collapse { 147 | /* Never collapse the sidebar for wide screens */ 148 | display: block; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /labs/compose-build/rng/src/Numbers.Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/labs/compose-build/rng/src/Numbers.Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /labs/compose-build/solution.md: -------------------------------------------------------------------------------- 1 | # Lab Solution 2 | 3 | The second part of the GitHub build uses the `latest` Docker Compose file: 4 | 5 | - [release.yml](./rng/release.yml) uses different image tags, with the `RELEASE` environment variable but not the `BUILD_NUMBER` variable the main Compose file uses 6 | 7 | When you merge in the latest file it will build images with the tag `21.05`, which is the version for this release of the app. 8 | 9 | Consumers can use `21.05` to get the current build for this release, or e.g. `21.05-57` to get a specific build: 10 | 11 | ``` 12 | docker pull courselabs/rng-api:21.05 13 | 14 | docker image ls courselabs/rng-api 15 | ``` 16 | 17 | Those two tags are aliases of the same image now, but with the next release the `21.05` tag will advance and will be an alias of the a later build. 18 | 19 | > Back to the [exercises](README.md). -------------------------------------------------------------------------------- /labs/compose-limits/hints.md: -------------------------------------------------------------------------------- 1 | # Lab Hints 2 | 3 | Think about the ongoing lifecycle of your apps: 4 | 5 | - servers need to be taken offline for maintenance 6 | 7 | - operating system updates need to be deployed for your container images 8 | 9 | - application config changes over time 10 | 11 | - and hopefully your app gets more popular and you need to support more users. 12 | 13 | > Need more? Here's the [solution](solution.md). -------------------------------------------------------------------------------- /labs/compose-limits/rng/v1.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | environment: 5 | - Rng__FailAfter__Enabled=true 6 | - Rng__FailAfter__CallCount=3 7 | - Rng__FailAfter__Action=Exit 8 | networks: 9 | - app-net 10 | 11 | rng-web: 12 | image: courselabs/rng-web:21.05 13 | environment: 14 | - Logging__LogLevel__Default=Debug 15 | - RngApi__Url=http://rng-api/rng 16 | ports: 17 | - "8088:80" 18 | networks: 19 | - app-net 20 | 21 | networks: 22 | app-net: 23 | -------------------------------------------------------------------------------- /labs/compose-limits/rng/v2.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | environment: 5 | - Rng__FailAfter__Enabled=true 6 | - Rng__FailAfter__CallCount=3 7 | - Rng__FailAfter__Action=Exit 8 | restart: always 9 | networks: 10 | - app-net 11 | 12 | rng-web: 13 | image: courselabs/rng-web:21.05 14 | environment: 15 | - Logging__LogLevel__Default=Debug 16 | - RngApi__Url=http://rng-api/rng 17 | restart: always 18 | ports: 19 | - "8088:80" 20 | networks: 21 | - app-net 22 | 23 | networks: 24 | app-net: 25 | -------------------------------------------------------------------------------- /labs/compose-limits/rng/v3.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | environment: 5 | - Rng__FailAfter__Enabled=true 6 | - Rng__FailAfter__CallCount=10 7 | - Rng__FailAfter__Action=Exit 8 | restart: always 9 | scale: 3 10 | networks: 11 | - app-net 12 | 13 | rng-web: 14 | image: courselabs/rng-web:21.05 15 | environment: 16 | - Logging__LogLevel__Default=Debug 17 | - RngApi__Url=http://rng-api/rng 18 | restart: always 19 | scale: 2 20 | ports: 21 | - "8088:80" 22 | networks: 23 | - app-net 24 | 25 | networks: 26 | app-net: 27 | -------------------------------------------------------------------------------- /labs/compose-limits/rng/v4.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | environment: 5 | - Rng__FailAfter__Enabled=true 6 | - Rng__FailAfter__CallCount=10 7 | - Rng__FailAfter__Action=Exit 8 | restart: always 9 | scale: 3 10 | cpus: 4 11 | mem_limit: 8g 12 | networks: 13 | - app-net 14 | 15 | rng-web: 16 | image: courselabs/rng-web:21.05 17 | environment: 18 | - Logging__LogLevel__Default=Debug 19 | - RngApi__Url=http://rng-api/rng 20 | restart: always 21 | scale: 1 22 | cpus: 32 23 | mem_limit: 64g 24 | ports: 25 | - "8088:80" 26 | networks: 27 | - app-net 28 | 29 | networks: 30 | app-net: 31 | -------------------------------------------------------------------------------- /labs/compose-limits/solution.md: -------------------------------------------------------------------------------- 1 | # Lab Solution 2 | 3 | It's all about having to run everything on a single server: 4 | 5 | - when the server goes offline - planned or unplanned - you lose all your apps 6 | 7 | - you'll update your container images regularly (at least monthly) to get OS and library updates, as well as new features. Updating your app means replacing containers, so there will be downtime while that happens because there are no other servers running additional containers 8 | 9 | - same with config, any changes get deployed by replacing containers which means your app can be offline - and may be broken when it comes back if there's a config mistake 10 | 11 | - your scaling options are limited to the CPU, memory and network ports on the machine. Most important is the port - only one container can listen on a port, so you can't run multiple copies of a public-facing component. 12 | 13 | 14 | > Back to the [exercises](README.md). -------------------------------------------------------------------------------- /labs/compose-model/hints.md: -------------------------------------------------------------------------------- 1 | # Lab Hints 2 | 3 | The Compose CLI can be configured with environment variables, and you can set defaults for them in an [environment file](https://docs.docker.com/compose/env-file/). 4 | 5 | You'll need a new override file for the lab setup, then setting the default project name and the file locations in the environment file should get you there. 6 | 7 | > Need more? Here's the [solution](solution.md). -------------------------------------------------------------------------------- /labs/compose-model/lab/.env: -------------------------------------------------------------------------------- 1 | # compose configuration - default to dev: 2 | COMPOSE_PROJECT_NAME=rng-lab 3 | COMPOSE_PATH_SEPARATOR=; 4 | COMPOSE_FILE=./rng/core.yml;./rng/lab.yml 5 | -------------------------------------------------------------------------------- /labs/compose-model/lab/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | ports: 4 | - "8389:80" 5 | 6 | rng-web: 7 | ports: 8 | - "8390:80" 9 | 10 | secrets: 11 | dotnet-logging: 12 | file: ./config/dev/logging.json 13 | rng-api: 14 | file: ./config/dev/api/override.json 15 | rng-web: 16 | file: ./config/dev/web/override.json 17 | -------------------------------------------------------------------------------- /labs/compose-model/rng/config/dev/api/override.json: -------------------------------------------------------------------------------- 1 | { 2 | "Rng" : { 3 | "Range": { 4 | "Min": 0, 5 | "Max": 50 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /labs/compose-model/rng/config/dev/logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging" : { 3 | "LogLevel" : { 4 | "Default" : "Debug" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /labs/compose-model/rng/config/dev/web/override.json: -------------------------------------------------------------------------------- 1 | { 2 | "RngApi" : { 3 | "Url" : "http://rng-api/rng" 4 | } 5 | } -------------------------------------------------------------------------------- /labs/compose-model/rng/config/logging.env: -------------------------------------------------------------------------------- 1 | Logging__LogLevel__Default=Debug -------------------------------------------------------------------------------- /labs/compose-model/rng/config/test/api/override.json: -------------------------------------------------------------------------------- 1 | { 2 | "Rng" : { 3 | "Range": { 4 | "Min": 0, 5 | "Max": 5000 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /labs/compose-model/rng/config/test/logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging" : { 3 | "LogLevel" : { 4 | "Default" : "Information" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /labs/compose-model/rng/config/test/web/override.json: -------------------------------------------------------------------------------- 1 | { 2 | "RngApi" : { 3 | "Url" : "http://rng-api/rng" 4 | } 5 | } -------------------------------------------------------------------------------- /labs/compose-model/rng/core.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | secrets: 5 | - source: dotnet-logging 6 | target: /app/config/logging.json 7 | - source: rng-api 8 | target: /app/config/override.json 9 | networks: 10 | - app-net 11 | 12 | rng-web: 13 | image: courselabs/rng-web:21.05 14 | secrets: 15 | - source: dotnet-logging 16 | target: /app/config/logging.json 17 | - source: rng-web 18 | target: /app/config/override.json 19 | networks: 20 | - app-net 21 | 22 | networks: 23 | app-net: 24 | -------------------------------------------------------------------------------- /labs/compose-model/rng/dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | ports: 4 | - "8189:80" 5 | 6 | rng-web: 7 | ports: 8 | - "8190:80" 9 | 10 | secrets: 11 | dotnet-logging: 12 | file: ./config/dev/logging.json 13 | rng-api: 14 | file: ./config/dev/api/override.json 15 | rng-web: 16 | file: ./config/dev/web/override.json 17 | -------------------------------------------------------------------------------- /labs/compose-model/rng/test.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | ports: 4 | - "8289:80" 5 | 6 | rng-web: 7 | ports: 8 | - "8290:80" 9 | 10 | secrets: 11 | dotnet-logging: 12 | file: ./config/test/logging.json 13 | rng-api: 14 | file: ./config/test/api/override.json 15 | rng-web: 16 | file: ./config/test/web/override.json 17 | -------------------------------------------------------------------------------- /labs/compose-model/rng/v1.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | environment: 5 | - Logging__LogLevel__Default=Debug 6 | ports: 7 | - "8089:80" 8 | networks: 9 | - app-net 10 | 11 | rng-web: 12 | image: courselabs/rng-web:21.05 13 | environment: 14 | - Logging__LogLevel__Default=Debug 15 | - RngApi__Url=http://rng-api/rng 16 | ports: 17 | - "8090:80" 18 | networks: 19 | - app-net 20 | 21 | networks: 22 | app-net: 23 | -------------------------------------------------------------------------------- /labs/compose-model/rng/v2.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | env_file: ./config/logging.env 5 | ports: 6 | - "8089:80" 7 | networks: 8 | - app-net 9 | 10 | rng-web: 11 | image: courselabs/rng-web:21.05 12 | env_file: ./config/logging.env 13 | environment: 14 | - RngApi__Url=http://rng-api/rng 15 | ports: 16 | - "8090:80" 17 | networks: 18 | - app-net 19 | 20 | networks: 21 | app-net: -------------------------------------------------------------------------------- /labs/compose-model/rng/v3.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | volumes: 5 | - ./config/dev/logging.json:/app/config/logging.json 6 | secrets: 7 | - source: rng-api 8 | target: /app/config/override.json 9 | ports: 10 | - "8089:80" 11 | networks: 12 | - app-net 13 | 14 | rng-web: 15 | image: courselabs/rng-web:21.05 16 | volumes: 17 | - ./config/dev/logging.json:/app/config/logging.json 18 | secrets: 19 | - source: rng-web 20 | target: /app/config/override.json 21 | ports: 22 | - "8090:80" 23 | networks: 24 | - app-net 25 | 26 | networks: 27 | app-net: 28 | 29 | secrets: 30 | rng-api: 31 | file: ./config/dev/api/override.json 32 | rng-web: 33 | file: ./config/dev/web/override.json 34 | -------------------------------------------------------------------------------- /labs/compose-model/solution.md: -------------------------------------------------------------------------------- 1 | # Lab Solution 2 | 3 | The lab model for the app just sets new ports and uses the existing dev config files: 4 | 5 | - [lab/compose.yml](./lab/compose.yml) 6 | 7 | The Compose env file sets a project name and default files - using the core and the lab override: 8 | 9 | - [lab/.env](./lab/.env) 10 | 11 | Switch to the lab folder: 12 | 13 | ``` 14 | cd labs/compose-model 15 | ``` 16 | 17 | Copy in the sample solution: 18 | 19 | ``` 20 | cp ./lab/.env . 21 | cp ./lab/compose.yml ./rng/lab.yml 22 | ``` 23 | 24 | Start the app: 25 | 26 | ``` 27 | docker-compose up -d 28 | ``` 29 | 30 | > Try it out at http://localhost:8390 31 | 32 | Check API logs: 33 | 34 | ``` 35 | docker logs rng-lab_rng-api_1 36 | ``` 37 | 38 | > Shows the info and debug level logs 39 | 40 | > Back to the [exercises](README.md). -------------------------------------------------------------------------------- /labs/compose/hints.md: -------------------------------------------------------------------------------- 1 | # Lab Hints 2 | 3 | Adding components to a Compose definition just means adding a new service block - and you can add another network block too. 4 | 5 | A service definition can have multiple network entries, but the network name used in the service needs to match a network defined in the Compose file. 6 | 7 | And you can look at the details of any Docker object by inspecting it. 8 | 9 | > Need more? Here's the [solution](solution.md). -------------------------------------------------------------------------------- /labs/compose/lab/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | environment: 5 | - Logging__LogLevel__Default=Debug 6 | ports: 7 | - "8089:80" 8 | networks: 9 | - app-net 10 | 11 | rng-web: 12 | image: courselabs/rng-web:21.05 13 | environment: 14 | - Logging__LogLevel__Default=Debug 15 | - RngApi__Url=http://rng-api/rng 16 | ports: 17 | - "8090:80" 18 | networks: 19 | - app-net 20 | 21 | nginx: 22 | image: nginx:alpine 23 | networks: 24 | - app-net 25 | - front-end 26 | 27 | networks: 28 | app-net: 29 | front-end: 30 | -------------------------------------------------------------------------------- /labs/compose/nginx/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | nginx: 3 | image: nginx:1.21-alpine 4 | ports: 5 | - "8082:80" -------------------------------------------------------------------------------- /labs/compose/rng/v1.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | environment: 5 | - Logging__LogLevel__Default=Debug 6 | ports: 7 | - "8089:80" 8 | networks: 9 | - app-net 10 | 11 | rng-web: 12 | image: courselabs/rng-web:21.05 13 | environment: 14 | - Logging__LogLevel__Default=Debug 15 | ports: 16 | - "8090:80" 17 | networks: 18 | - app-net 19 | 20 | networks: 21 | app-net: 22 | -------------------------------------------------------------------------------- /labs/compose/rng/v2.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | environment: 5 | - Logging__LogLevel__Default=Debug 6 | ports: 7 | - "8089:80" 8 | networks: 9 | - app-net 10 | 11 | rng-web: 12 | image: courselabs/rng-web:21.05 13 | environment: 14 | - Logging__LogLevel__Default=Debug 15 | - RngApi__Url=http://rng-api/rng 16 | ports: 17 | - "8090:80" 18 | networks: 19 | - app-net 20 | 21 | networks: 22 | app-net: 23 | -------------------------------------------------------------------------------- /labs/compose/solution.md: -------------------------------------------------------------------------------- 1 | # Lab Solution 2 | 3 | The sample solution [lab/compose.yaml](./lab/compose.yml) adds: 4 | 5 | - a network called `front-end` with no options, so it will be created with the Docker defaults 6 | - a service called `nginx` which uses the Nginx image and connects to the `front-end` and `app-net` networks. 7 | 8 | Compose uses the directory name of the YAML file to identify the application - so to update the app, the Compose file needs to be copied to the `rng` folder. 9 | 10 | Copy the Compose file and deploy the update: 11 | 12 | ``` 13 | cp ./labs/compose/lab/compose.yml ./labs/compose/rng/lab.yml 14 | 15 | docker-compose -f ./labs/compose/rng/lab.yml up -d 16 | ``` 17 | 18 | > You'll see the new network and container created, but the RNG web and API containers will be left unchanged. The spec hasn't changed for those services, so the containers match the desired state. 19 | 20 | Inspect the new container to show the network details: 21 | 22 | ``` 23 | docker inspect rng_nginx_1 24 | ``` 25 | 26 | > You'll see it has two IP addresses in the network section at the end of the output. This is one IP from each network - like a machine with two network cards. 27 | 28 | Test connectivity from the Nginx container to the web container: 29 | 30 | ``` 31 | docker exec rng_nginx_1 nslookup rng-web 32 | ``` 33 | 34 | > The new container can resolve IP addresses for the original container. 35 | 36 | 37 | And from the web container to Nginx: 38 | 39 | ``` 40 | docker exec rng_rng-web_1 ping -c3 nginx 41 | ``` 42 | 43 | > The old containers can reach the new one. 44 | 45 | > Back to the [exercises](README.md). -------------------------------------------------------------------------------- /labs/containers/hints.md: -------------------------------------------------------------------------------- 1 | # Lab Hints 2 | 3 | [Official images](https://hub.docker.com/search?q=java&type=image&image_filter=official) on Docker Hub are the place to start looking - they're packages which are maintained by the project team and quality-controlled by Docker. 4 | 5 | The Hub pages tell you the Docker command to run to download the container package. 6 | 7 | *Tags* are different variants you can use. You can filter by the version number and the runtime, but it will take some investigation. The *Alpine* Linux distro is usually the smallest you can find. 8 | 9 | > Need more? Here's the [solution](solution.md). -------------------------------------------------------------------------------- /labs/containers/solution.md: -------------------------------------------------------------------------------- 1 | # Lab Solution 2 | 3 | Search for `java` on Docker Hub and the official image is the top hit, but open the page at https://hub.docker.com/_/java and you will see it's been deprecated. 4 | 5 | That means the `java` image (and the `openjdk` image which replaced it) are no longer being maintained. The images are old, so you should avoid them. 6 | 7 | There are a few alternatives with active maintainers (e.g. Amazon and IBM). My preference is for [eclipse-temurin](https://hub.docker.com/_/eclipse-temurin) which is an open-source build of OpenJDK. 8 | 9 | You might start by copying the command from the Docker Hub page, which will download the default image: 10 | 11 | ``` 12 | docker pull eclipse-temurin 13 | ``` 14 | 15 | Run a container to check the Java version: 16 | 17 | ``` 18 | docker run eclipse-temurin java -version 19 | ``` 20 | 21 | > The output will show the OpenJDK and JRE versions 22 | 23 | Check the [tags page on Docker Hub](https://hub.docker.com/_/eclipse-temurin/tags) and search for `alpine` and you'll see there are JRE and IDK versions. 24 | 25 | The JRE build for Alpine is the smallest - the tag contains the version number, so this pulls a small JRE image for OpenJDK 23: 26 | 27 | ``` 28 | docker pull eclipse-temurin:23-jre-alpine 29 | ``` 30 | 31 | Check the sizes: 32 | 33 | ``` 34 | docker image ls eclipse-temurin 35 | ``` 36 | 37 | > Your output may be different, but mine shows the default ("latest") image is 478MB; Alpin JRE is 206MB... 38 | 39 | > Back to the [exercises](README.md). -------------------------------------------------------------------------------- /labs/env/exercises.env: -------------------------------------------------------------------------------- 1 | COURSELABS=env 2 | LOG_LEVEL=debug 3 | FEATURES_CACHE_ENABLED=true -------------------------------------------------------------------------------- /labs/env/hints.md: -------------------------------------------------------------------------------- 1 | # Lab Hints 2 | 3 | The Docker CLI often uses the same names as Linux commands - in Linux you use `cp` to copy. 4 | 5 | Remember the docs are [here](https://docs.docker.com/engine/reference/commandline/cli/) - that should be enough :) 6 | 7 | > Need more? Here's the [solution](solution.md). -------------------------------------------------------------------------------- /labs/env/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Docker Course Labs 5 | 6 | 7 |

    Docker Course Labs

    8 |

    Container Environment Lab

    9 |

    The real content is at docker.courselabs.co

    10 | 11 | -------------------------------------------------------------------------------- /labs/env/scripts/print-network.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo '' 4 | echo '** Hostname **' 5 | hostname 6 | echo '' 7 | 8 | echo '** IP **' 9 | ip a s eth0 | grep 'inet ' | cut -d' ' -f6| cut -d/ -f1 10 | echo '' 11 | 12 | echo '** DNS **' 13 | cat /etc/resolv.conf | grep nameserver | cut -c 12- 14 | echo '' -------------------------------------------------------------------------------- /labs/env/solution.md: -------------------------------------------------------------------------------- 1 | # Lab Solution 2 | 3 | You can run a command inside the container to list files: 4 | 5 | ``` 6 | docker exec tls ls /certs 7 | ``` 8 | 9 | The [docker cp](https://docs.docker.com/engine/reference/commandline/cp/) command copies files between containers and the local filesystem. 10 | 11 | To copy from the container called `tls`, use: 12 | 13 | ``` 14 | docker cp tls:/certs/server-cert.pem . 15 | 16 | docker cp tls:/certs/server-key.pem . 17 | ``` 18 | 19 | You can also copy from the local machine into the container, but the target path needs to exist: 20 | 21 | ``` 22 | docker exec tls mkdir /certs-backup 23 | 24 | docker cp server-cert.pem tls:/certs-backup/ 25 | 26 | docker cp server-key.pem tls:/certs-backup/ 27 | 28 | docker exec tls ls /certs-backup 29 | ``` 30 | 31 | > Back to the [exercises](README.md). -------------------------------------------------------------------------------- /labs/images/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu -------------------------------------------------------------------------------- /labs/images/curl/Dockerfile: -------------------------------------------------------------------------------- 1 | # Alpine is a tiny Linux distro 2 | FROM alpine 3 | 4 | # apk is the package manager for Alpine 5 | # this installs curl into the container image 6 | RUN apk add curl 7 | 8 | # tells Docker to run curl when it starts a container 9 | CMD curl -------------------------------------------------------------------------------- /labs/images/curl/Dockerfile.v2: -------------------------------------------------------------------------------- 1 | # specifying an exact version makes the build repeatable 2 | FROM alpine:3.16 3 | 4 | # using the no-cache option creates a smaller image 5 | RUN apk add --no-cache curl=7.83.1-r3 6 | 7 | # entrypoint lets users add options when the container starts 8 | ENTRYPOINT ["curl"] -------------------------------------------------------------------------------- /labs/images/hints.md: -------------------------------------------------------------------------------- 1 | # Lab Hints 2 | 3 | Don't worry about installing Java, remember the official [OpenJDK](https://hub.docker.com/_/openjdk) image does that for you. The app is built for Java 8, but it runs fine on newer versions. 4 | 5 | The Dockerfile should be straightforward, but you need to think about the paths. The Java class file may be in a different directory from your Dockerfile, and Docker needs to access both from the context. 6 | 7 | Also the Java command inside the container needs to use the correct path to find the class file you copy into the image. 8 | 9 | > Need more? Here's the [solution](solution.md). -------------------------------------------------------------------------------- /labs/images/java/HelloWorld.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/courselabs/docker/bb2fed68581264e0fc2e344d92ff68e5938da183/labs/images/java/HelloWorld.class -------------------------------------------------------------------------------- /labs/images/java/HelloWorld.java: -------------------------------------------------------------------------------- 1 | public class HelloWorld { 2 | public static void main(String[] args) { 3 | System.out.println("Hello, World"); 4 | } 5 | } -------------------------------------------------------------------------------- /labs/images/lab/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jre-alpine 2 | # or :11-jre-slim 3 | 4 | WORKDIR /app 5 | COPY java/HelloWorld.class . 6 | 7 | CMD java HelloWorld -------------------------------------------------------------------------------- /labs/images/solution.md: -------------------------------------------------------------------------------- 1 | # Lab Solution 2 | 3 | Here's a sample solution: 4 | 5 | - [lab/Dockerfile](./lab/Dockerfile) 6 | 7 | It just creates a working directory called `/app` in the container filesystem, and copies in the Java class file. 8 | 9 | The class file and the Dockerfile are in different directories, so you need to use a context where Docker can access both files: 10 | 11 | ``` 12 | - labs 13 | |- images <- this is the context 14 | |-- java <- so Docker can get the class file from here 15 | |-- lab <- and the Dockerfile from here 16 | ``` 17 | Build the image using that context and specifying the path to the Dockerfile: 18 | 19 | ``` 20 | docker build -t java-hello-world -f labs/images/lab/Dockerfile labs/images 21 | ``` 22 | 23 | Run a container from the image: 24 | 25 | ``` 26 | docker run java-hello-world 27 | ``` 28 | 29 | > The output should say `Hello, World` 30 | 31 | > Back to the [exercises](README.md). -------------------------------------------------------------------------------- /labs/images/web/Dockerfile: -------------------------------------------------------------------------------- 1 | # use a specific version of Nginx: 2 | FROM nginx:1.23.0-alpine 3 | 4 | # overwrite the default HTML doc: 5 | COPY index.html /usr/share/nginx/html/ -------------------------------------------------------------------------------- /labs/images/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Docker Course Labs 5 | 6 | 7 |

    Docker Course Labs

    8 |

    Image Building Lab

    9 |

    The real content is at docker.courselabs.co

    10 | 11 | -------------------------------------------------------------------------------- /labs/kubernetes/pods/sleep.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: sleep 5 | spec: 6 | containers: 7 | - name: sleep 8 | image: kiamol/ch03-sleep -------------------------------------------------------------------------------- /labs/kubernetes/pods/whoami.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: whoami 5 | labels: 6 | app: whoami 7 | spec: 8 | containers: 9 | - name: app 10 | image: sixeyed/whoami:21.04 -------------------------------------------------------------------------------- /labs/kubernetes/services/whoami-nodeport.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: whoami-np 5 | spec: 6 | selector: 7 | app: whoami 8 | ports: 9 | - name: http 10 | port: 8080 11 | nodePort: 30000 12 | targetPort: 80 13 | type: NodePort -------------------------------------------------------------------------------- /labs/multi-stage/hints.md: -------------------------------------------------------------------------------- 1 | # Lab Hints 2 | 3 | The [Dockerfile](./whoami/Dockerfile) for the app uses the `ENTRYPOINT` instruction to start the app, so any arguments you pass to the `docker run` command get passed onto the container. 4 | 5 | In the Dockerfile the `EXPOSE` command tells Docker which ports the app expects to listen to. This is built into the image as metadata, it's not linked to the ports the app actually listens on. 6 | 7 | Publishing all ports won't do what you want if the app listens on a different port than the container image expects. 8 | 9 | > Need more? Here's the [solution](solution.md). -------------------------------------------------------------------------------- /labs/multi-stage/simple/Dockerfile: -------------------------------------------------------------------------------- 1 | # pretend to install some packages 2 | FROM alpine:3.13 AS base 3 | RUN echo 'Adding deps...' > /deps.txt 4 | 5 | # pretend build 6 | FROM base AS build 7 | RUN echo 'Building...' > /build.txt 8 | 9 | # pretend test suite 10 | FROM build AS test 11 | COPY --from=build /build.txt /build.txt 12 | RUN echo 'Testing...' >> /build.txt 13 | 14 | # final app image 15 | FROM base 16 | COPY --from=build /build.txt /build.txt 17 | CMD cat /deps.txt && cat /build.txt -------------------------------------------------------------------------------- /labs/multi-stage/solution.md: -------------------------------------------------------------------------------- 1 | # Lab Solution 2 | 3 | Running the app with a custom port just needs you to pass the arguments to the application: 4 | 5 | ``` 6 | # run the app listening on port 5000 inside the container: 7 | docker run -d -P --name whoami2 whoami -port 5000 8 | ``` 9 | 10 | ## Troubleshooting 11 | 12 | You can check the port the app uses - but if you try to call it you won't get a response: 13 | 14 | ``` 15 | docker port whoami2 16 | 17 | curl localhost: 18 | ``` 19 | 20 | Check the logs and you'll see why - Docker is directing traffic to port 80, which is the port the image exposes. But the app is not listening on that port: 21 | 22 | ``` 23 | docker logs whoami2 24 | ``` 25 | 26 | ## The fix 27 | 28 | You need to explicitly map the port if you're configuring the app to use a port which is not exposed in the image: 29 | 30 | ``` 31 | docker run -d -p 8050:5000 --name whoami3 whoami -port 5000 32 | 33 | curl localhost:8050 34 | ``` 35 | 36 | Or if you want Docker to set a random host port, specify only the target port: 37 | 38 | ``` 39 | docker run -d -p :5000 --name whoami4 whoami -port 5000 40 | 41 | docker port whoami4 42 | 43 | curl localhost: 44 | ``` 45 | 46 | > Back to the [exercises](README.md). -------------------------------------------------------------------------------- /labs/multi-stage/whoami/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16.4-alpine AS builder 2 | ENV CGO_ENABLED=0 3 | 4 | RUN apk --no-cache --no-progress add \ 5 | ca-certificates \ 6 | git \ 7 | tzdata \ 8 | && update-ca-certificates 9 | 10 | WORKDIR /go/whoami 11 | COPY go.mod . 12 | RUN go mod download 13 | 14 | COPY app.go . 15 | RUN go build -o /out/whoami 16 | 17 | # app 18 | FROM scratch 19 | 20 | ENTRYPOINT ["/app/whoami"] 21 | EXPOSE 80 22 | 23 | COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo 24 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 25 | COPY --from=builder /out/whoami /app/ 26 | -------------------------------------------------------------------------------- /labs/multi-stage/whoami/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/traefik/whoami 2 | 3 | go 1.16 4 | 5 | require github.com/gorilla/websocket v1.4.2 6 | -------------------------------------------------------------------------------- /labs/networking/compose-network.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | environment: 5 | - Logging__LogLevel__Default=Debug 6 | volumes: 7 | - ./scripts:/scripts 8 | networks: 9 | app-net: 10 | ipv4_address: 10.218.0.10 11 | 12 | rng-web: 13 | image: courselabs/rng-web:21.05 14 | environment: 15 | - Logging__LogLevel__Default=Debug 16 | - RngApi__Url=http://10.218.0.10/rng 17 | ports: 18 | - "8090:80" 19 | networks: 20 | - app-net 21 | 22 | networks: 23 | app-net: 24 | name: courselabs-rng 25 | ipam: 26 | driver: default 27 | config: 28 | - subnet: 10.218.0.0/16 29 | 30 | -------------------------------------------------------------------------------- /labs/networking/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | environment: 5 | - Logging__LogLevel__Default=Debug 6 | networks: 7 | - app-net 8 | 9 | rng-web: 10 | image: courselabs/rng-web:21.05 11 | environment: 12 | - Logging__LogLevel__Default=Debug 13 | - RngApi__Url=http://rng-api/rng 14 | ports: 15 | - "8090:80" 16 | networks: 17 | - app-net 18 | 19 | networks: 20 | app-net: 21 | -------------------------------------------------------------------------------- /labs/networking/hints.md: -------------------------------------------------------------------------------- 1 | # Lab Hints 2 | 3 | You can't scale up the most recent deployment, because the Compose spec uses a specific IP address for the API container. Roll back to the [compose.yml](./compose.yml) spec and you can scale up now. 4 | 5 | Follow the logs of the API containers and you should see them all being used when you get lots of random numbers from the website. 6 | 7 | Check the details of all the API containers to see the networking setup Compose applies to get that load-balancing behaviour. 8 | 9 | > Need more? Here's the [solution](solution.md). -------------------------------------------------------------------------------- /labs/networking/scripts/network-info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo '' 4 | 5 | echo "** Hostname **" 6 | echo $(hostname) 7 | echo '' 8 | 9 | echo "** IP address **" 10 | echo $(hostname -i) 11 | echo '' 12 | 13 | echo "** DNS server: **" 14 | cat /etc/resolv.conf | grep nameserver | cut -c 12- 15 | echo '' -------------------------------------------------------------------------------- /labs/networking/solution.md: -------------------------------------------------------------------------------- 1 | # Lab Solution 2 | 3 | Roll back to the earlier deployment: 4 | 5 | ``` 6 | docker-compose -f labs/networking/compose.yml up -d 7 | ``` 8 | 9 | And you can scale up with the Compose CLI: 10 | 11 | ``` 12 | docker-compose -f labs/networking/compose.yml up -d --scale rng-api=3 13 | ``` 14 | 15 | > You'll see two new API containers created. 16 | 17 | Follow the logs from all API containers: 18 | 19 | ``` 20 | docker-compose -f labs/networking/compose.yml logs -f rng-api 21 | ``` 22 | 23 | > Use the app at http://localhost:8090 and you'll see different API containers generating numbers. 24 | 25 | Inspect the API containers and you'll see compose sets the same network alias for each of them: 26 | 27 | ``` 28 | docker inspect networking_rng-api_1 29 | ``` 30 | 31 | - includes the container's hostname and the Compose service name in the .NetworkSettings.Networks section, e.g. 32 | 33 | ``` 34 | "Aliases": [ 35 | "rng-api", 36 | "b382ce0e8ffc" 37 | ] 38 | ``` 39 | 40 | Any containers with the same alias will get returned in the DNS response for that domain name. 41 | 42 | 43 | > Back to the [exercises](README.md). -------------------------------------------------------------------------------- /labs/orchestration/config/api.json: -------------------------------------------------------------------------------- 1 | { 2 | "Rng" : { 3 | "Range": { 4 | "Min": 0, 5 | "Max": 50000 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /labs/orchestration/config/logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging" : { 3 | "LogLevel" : { 4 | "Default" : "Information" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /labs/orchestration/config/web.json: -------------------------------------------------------------------------------- 1 | { 2 | "RngApi" : { 3 | "Url" : "http://rng-api/rng" 4 | } 5 | } -------------------------------------------------------------------------------- /labs/orchestration/rng-v1.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | rng-api: 5 | image: courselabs/rng-api:21.05 6 | configs: 7 | - source: rng-logging 8 | target: /app/config/logging.json 9 | secrets: 10 | - source: rng-api 11 | target: /app/config/override.json 12 | ports: 13 | - "8089:80" 14 | networks: 15 | - app-net 16 | 17 | rng-web: 18 | image: courselabs/rng-web:21.05 19 | configs: 20 | - source: rng-logging 21 | target: /app/config/logging.json 22 | secrets: 23 | - source: rng-web 24 | target: /app/config/override.json 25 | ports: 26 | - "8090:80" 27 | networks: 28 | - app-net 29 | 30 | networks: 31 | app-net: 32 | 33 | configs: 34 | rng-logging: 35 | external: true 36 | 37 | secrets: 38 | rng-api: 39 | external: true 40 | 41 | rng-web: 42 | external: true 43 | -------------------------------------------------------------------------------- /labs/orchestration/rng-v2.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | rng-api: 5 | image: courselabs/rng-api:21.05 6 | environment: 7 | - Logging__LogLevel__Default=Debug 8 | - Rng__FailAfter__Enabled=true 9 | - Rng__FailAfter__CallCount=10 10 | - Rng__FailAfter__Action=Exit 11 | configs: 12 | - source: rng-logging 13 | target: /app/config/logging.json 14 | secrets: 15 | - source: rng-api 16 | target: /app/config/override.json 17 | ports: 18 | - "8089:80" 19 | networks: 20 | - app-net 21 | deploy: 22 | replicas: 4 23 | 24 | rng-web: 25 | image: courselabs/rng-web:21.05 26 | configs: 27 | - source: rng-logging 28 | target: /app/config/logging.json 29 | secrets: 30 | - source: rng-web 31 | target: /app/config/override.json 32 | ports: 33 | - "8090:80" 34 | networks: 35 | - app-net 36 | deploy: 37 | replicas: 2 38 | 39 | networks: 40 | app-net: 41 | 42 | configs: 43 | rng-logging: 44 | external: true 45 | 46 | secrets: 47 | rng-api: 48 | external: true 49 | 50 | rng-web: 51 | external: true 52 | -------------------------------------------------------------------------------- /labs/registries/hints.md: -------------------------------------------------------------------------------- 1 | # Lab Hints 2 | 3 | The default tag is shown when you list images, but the default registry isn't. 4 | 5 | You can print the `info` about your Docker engine to get a hint on the default registry domain. 6 | 7 | > Need more? Here's the [solution](solution.md). -------------------------------------------------------------------------------- /labs/registries/solution.md: -------------------------------------------------------------------------------- 1 | # Lab Solution 2 | 3 | The default registry is Docker Hub - using the domain `docker.io`, and the default image tag is `latest`. 4 | 5 | So `kiamol/ch05-pi` is the short form of `docker.io/kiamol/ch05-pi:latest`: 6 | 7 | ``` 8 | docker pull kiamol/ch05-pi 9 | 10 | docker pull docker.io/kiamol/ch05-pi:latest 11 | ``` 12 | 13 | Check the image list and you'll only see one - these aren't aliases, they're just different froms of the same name: 14 | 15 | ``` 16 | docker image ls kiamol/ch05-pi 17 | ``` 18 | 19 | > Be wary of using `latest` images - it's a confusing name because it might not be the latest version. 20 | 21 | For other registries you need to include the domain in the reference - so images on MCR need to be prefixed with `mcr.microsoft.com/`: 22 | 23 | ``` 24 | docker pull mcr.microsoft.com/dotnet/runtime:6.0 25 | ``` 26 | 27 | > Back to the [exercises](README.md). -------------------------------------------------------------------------------- /labs/troubleshooting/.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME=troubleshooting 2 | -------------------------------------------------------------------------------- /labs/troubleshooting/README.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting Containerized Apps 2 | 3 | You'll spend a lot of your time with the Docker CLI and your Compose YAML files troubleshooting problems. 4 | 5 | The CLIs validate commands and specs for correctness when you deploy them, but they don't check that your app will actually work. 6 | 7 | Images can be misconfigured, the container environment could be incorrect, and components might be unable to reach each other. 8 | 9 | ## Lab 10 | 11 | This one is all lab :) Try running this app - and make whatever changes you need to get the app running, so the containers run and the app works. 12 | 13 | ``` 14 | docker-compose -f labs/troubleshooting/compose.yml up -d 15 | ``` 16 | 17 | > Your goal is to browse to http://localhost:8090 and have a working random number generator. 18 | 19 | Don't go straight to the solution! These are the sort of issues you will get all the time, so it's good to start working through the steps to diagnose problems. 20 | 21 | > Stuck? Try [hints](hints.md) or check the [solution](solution.md). 22 | 23 | ___ 24 | ## Cleanup 25 | 26 | When you're done you can remove all the objects: 27 | 28 | ``` 29 | docker-compose -f labs/troubleshooting/compose.yml down 30 | ``` -------------------------------------------------------------------------------- /labs/troubleshooting/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | entrypoint: ["dontet", "/app/Numbers.Api.dll"] 5 | env_file: ./config/logging.env 6 | ports: 7 | - "8089:80" 8 | volumes: 9 | - ./config/api:/app 10 | networks: 11 | - app-net 12 | 13 | rng-web: 14 | image: courselabs/rng-website:21.05 15 | env_file: ./config/logging.env 16 | environment: 17 | - RngApi__Url=http://rng-api/rng 18 | ports: 19 | - "8089:80" 20 | cpus: 25 21 | networks: 22 | - front-end 23 | 24 | networks: 25 | app-net: 26 | front-end: 27 | -------------------------------------------------------------------------------- /labs/troubleshooting/config/api/override.json: -------------------------------------------------------------------------------- 1 | { 2 | "Rng" : { 3 | "Range": { 4 | "Min": 0, 5 | "Max": 50 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /labs/troubleshooting/config/logging.env: -------------------------------------------------------------------------------- 1 | Logging__LogLevel__Default=Debug -------------------------------------------------------------------------------- /labs/troubleshooting/hints.md: -------------------------------------------------------------------------------- 1 | # Lab Hints 2 | 3 | Fixing apps is a process of checking the status of running objects and seeing what's wrong, then fixing up the YAML and redeploying. 4 | 5 | You'll find different classes of problem with this app: 6 | 7 | - validation failures which stop containers being created 8 | - startup failures which mean containers can't run 9 | - runtime failures where the container is running but the app doesn't work 10 | - networking failures where containers can't reach each other 11 | 12 | Use the normal command lines to deploy and check the output carefully. 13 | 14 | Once the containers are running you can print the logs and run commands inside the containers to check the filesystem. 15 | 16 | > Need more? Here's the [solution](solution.md). -------------------------------------------------------------------------------- /labs/troubleshooting/lab/solution.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rng-api: 3 | image: courselabs/rng-api:21.05 4 | entrypoint: ["dotnet", "/app/Numbers.Api.dll"] 5 | env_file: ./config/logging.env 6 | ports: 7 | - "8089:80" 8 | volumes: 9 | - ./config/api:/app/config 10 | networks: 11 | - app-net 12 | 13 | rng-web: 14 | image: courselabs/rng-web:21.05 15 | env_file: ./config/logging.env 16 | environment: 17 | - RngApi__Url=http://rng-api/rng 18 | ports: 19 | - "8090:80" 20 | cpus: 2.5 21 | networks: 22 | - app-net 23 | - front-end 24 | 25 | networks: 26 | app-net: 27 | front-end: 28 | -------------------------------------------------------------------------------- /labs/troubleshooting/solution.md: -------------------------------------------------------------------------------- 1 | # Lab Solution 2 | 3 | My solution is here: 4 | 5 | - [solution.yml](./lab/solution.yml) 6 | 7 | Copy it to the main folder so the config paths are correct, and the app will work: 8 | 9 | ``` 10 | cp labs/troubleshooting/lab/solution.yml labs/troubleshooting/ 11 | 12 | docker-compose -f labs/troubleshooting/solution.yml up -d 13 | ``` 14 | 15 | > Use the app at http://localhost:8090 16 | 17 | ## Validation failures 18 | 19 | 1. The image name is incorrect for the web component. You'll see a `manifest unknown` error. 20 | 21 | 2. Not enough CPUs - the web application request `25` CPUs, which is a typo - it should be `2.5`. Unless you have a mega powerful machine, Docker can't allocate enough CPUs to create the container. 22 | 23 | ## Startup failures 24 | 25 | 1. Incorrect entrypoint for the API. It's specifying the command to run but `dontet` is a typo. You'll see `executable file not found in $PATH` - it should be `dotnet`. 26 | 27 | 2. Duplicate port mapping - both the web app and API are trying to publish port `8089`. You'll see `port is already allocated` - the web app should be publishing to port `8090`. 28 | 29 | ## Runtime failures 30 | 31 | 1. The API container exits after starting. Check the logs and you'll see `the application was not found`. The volume mount is incorrect - it's loading the config folder into the `/app` folder, which overwrites the contents from the image so there is no application binary. The volume target should be `/app/config`. 32 | 33 | ## Networking failures 34 | 35 | 1. Try using the website and you'll get the `RNG service unavailable!` error. The web container logs show you the app is using the correct URL, but if you run `nslookup` the API container can't be found. The spec uses two different networks, so the containers aren't connected - the web container needs to attach to the `app-net` network. 36 | 37 | 38 | > Back to the [exercises](README.md). -------------------------------------------------------------------------------- /setup/README.md: -------------------------------------------------------------------------------- 1 | # Set Up Docker and Git 2 | 3 | Docker runs as a background service on server operating systems, but in a local environment the easiest option is Docker Desktop. 4 | 5 | We'll also use [Git](https://git-scm.com) for source control, so you'll need a client on your machine to talk to GitHub. 6 | 7 | ## Git Client - Mac, Windows or Linux 8 | 9 | Git is a free, open source tool for source control: 10 | 11 | - [Install Git](https://git-scm.com/downloads) 12 | 13 | ## Docker Desktop - Mac or Windows 14 | 15 | If you're on macOS or Windows 10, Docker Desktop is for you: 16 | 17 | - [Install Docker Desktop](https://www.docker.com/products/docker-desktop) 18 | 19 | The download and install takes a few minutes. When it's done, run the _Docker_ app and you'll see the Docker whale logo in your taskbar (Windows) or menu bar (macOS). 20 | 21 | > On Windows 10 the install may need a restart before you get here. 22 | 23 | ## **OR** Docker Engine - Linux 24 | 25 |
    26 | Running Docker on Linux 27 | 28 | Docker Engine is the background service which runs containers. You can install it - along with the Docker command line - for lots of different Linux distros: 29 | 30 | - [Install Docker Engine](https://docs.docker.com/engine/install/) 31 | - [Install Docker Compose](https://docs.docker.com/compose/install/) 32 | 33 | > If you're using WSL on Windows 10, it's much easier to use Docker Desktop which integrates with your WSL distro. 34 | 35 |

    36 | 37 | ## Check your setup 38 | 39 | When you have Git and Docker installed you should be able to run these commands and get some output: 40 | 41 | ``` 42 | git --version 43 | ``` 44 | 45 | I'm using Git for Windows and my output is: 46 | 47 | ``` 48 | git version 2.31.1.windows.1 49 | ``` 50 | 51 | Then run: 52 | 53 | ``` 54 | docker version 55 | ``` 56 | 57 | I'm using Docker Desktop on Windows and mine says: 58 | 59 | ``` 60 | Client: 61 | Cloud integration: 1.0.14 62 | Version: 20.10.6 63 | API version: 1.41 64 | Go version: go1.16.3 65 | Git commit: 370c289 66 | Built: Fri Apr 9 22:49:36 2021 67 | OS/Arch: windows/amd64 68 | Context: default 69 | Experimental: true 70 | 71 | Server: Docker Engine - Community 72 | Engine: 73 | Version: 20.10.6 74 | API version: 1.41 (minimum version 1.12) 75 | Go version: go1.13.15 76 | Git commit: 8728dd2 77 | Built: Fri Apr 9 22:44:56 2021 78 | OS/Arch: linux/amd64 79 | ... 80 | ``` 81 | 82 | And then: 83 | 84 | ``` 85 | docker-compose version 86 | ``` 87 | 88 | My output is: 89 | 90 | ``` 91 | docker-compose version 1.29.1, build c34c88b2 92 | docker-py version: 5.0.0 93 | CPython version: 3.9.0 94 | OpenSSL version: OpenSSL 1.1.1g 21 Apr 2020 95 | ``` 96 | 97 | > Your details and version numbers may be different - that's fine. If you get errors then we need to look into it, because you'll need to have Docker running for all of the exercises. 98 | 99 | > ❗ If you're running Docker Desktop on Windows, make sure you're in _Linux container mode_. This is the default mode, but if you've changed to using Windows containers (from the whale toolbar menu), then you'll need to switch back. --------------------------------------------------------------------------------