├── .github ├── ISSUE_TEMPLATE ├── dependabot.yml └── workflows │ ├── call-docker-build-result.yaml │ ├── call-docker-build-vote.yaml │ ├── call-docker-build-worker.yaml │ ├── commit-stage.yml │ ├── publish-dockerfile.yml │ └── publish-java.yml ├── .gitignore ├── LICENSE ├── MAINTAINERS ├── README.md ├── architecture.excalidraw.png ├── dagger.json ├── dagger ├── .gitattributes ├── dagger │ └── dagger.gen.go ├── go.mod ├── go.sum └── main.go ├── docker-compose.images.yml ├── docker-compose.yml ├── docker-stack.yml ├── dotnet └── worker │ ├── Dockerfile │ ├── Program.cs │ ├── Worker.csproj │ ├── components │ ├── results-statestore.yaml │ └── votes-statestore.yaml │ ├── dagger.json │ └── dagger │ ├── .gitattributes │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json ├── example-voting-app.sln ├── go ├── result │ ├── .gitattributes │ ├── Dockerfile │ ├── components │ │ └── results-statestore.yaml │ ├── dagger.json │ ├── dagger │ │ ├── .gitattributes │ │ ├── dagger │ │ │ └── dagger.gen.go │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── main_test.go │ └── static │ │ ├── angular.min.js │ │ ├── app.js │ │ ├── css │ │ └── style.css │ │ ├── index.html │ │ └── socket.io.js ├── vote │ ├── Dockerfile │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── static │ │ └── css │ │ │ └── style.css │ └── templates │ │ └── index.html └── worker │ ├── Dockerfile │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── query.json ├── healthchecks ├── postgres.sh └── redis.sh ├── java ├── echo │ ├── .mvn │ │ └── wrapper │ │ │ └── maven-wrapper.properties │ ├── Dockerfile │ ├── ce.json │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── com │ │ │ └── salaboy │ │ │ └── echo │ │ │ ├── EchoApplication.java │ │ │ ├── config │ │ │ └── WebSocketConfig.java │ │ │ └── workflow │ │ │ ├── PickAWinnerActivity.java │ │ │ ├── PrizeWorkflow.java │ │ │ ├── StoreWinnerActivity.java │ │ │ └── WorkflowPayload.java │ │ └── resources │ │ └── static │ │ ├── app.js │ │ ├── css │ │ └── style.css │ │ └── index.html ├── model │ ├── .mvn │ │ └── wrapper │ │ │ └── maven-wrapper.properties │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── salaboy │ │ └── model │ │ ├── Results.java │ │ ├── Score.java │ │ └── Vote.java ├── result │ ├── .mvn │ │ └── wrapper │ │ │ └── maven-wrapper.properties │ ├── Dockerfile │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── salaboy │ │ │ │ └── result │ │ │ │ ├── FetchResultsJob.java │ │ │ │ ├── ResultApplication.java │ │ │ │ ├── ResultController.java │ │ │ │ └── WebSocketsConfig.java │ │ └── resources │ │ │ ├── application.yml │ │ │ └── static │ │ │ ├── angular.min.js │ │ │ ├── app.js │ │ │ ├── css │ │ │ └── style.css │ │ │ └── index.html │ │ └── test │ │ └── java │ │ └── com │ │ └── salaboy │ │ └── result │ │ ├── BaseIntegrationTest.java │ │ └── ResultTest.java ├── vote │ ├── .mvn │ │ └── wrapper │ │ │ ├── maven-wrapper.jar │ │ │ └── maven-wrapper.properties │ ├── Dockerfile │ ├── components │ │ └── votes-statestore.yaml │ ├── dagger.json │ ├── dagger │ │ ├── .gitattributes │ │ ├── dagger │ │ │ └── dagger.gen.go │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── salaboy │ │ │ │ └── vote │ │ │ │ └── VoteApplication.java │ │ └── resources │ │ │ ├── application.yml │ │ │ ├── static │ │ │ └── css │ │ │ │ └── style.css │ │ │ └── templates │ │ │ └── index.html │ │ └── test │ │ └── java │ │ └── com │ │ └── salaboy │ │ └── vote │ │ ├── BaseIntegrationTest.java │ │ ├── TestRestController.java │ │ └── VoteApplicationTests.java └── worker │ ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties │ ├── Dockerfile │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ ├── query.json │ └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── salaboy │ │ │ └── worker │ │ │ ├── WorkerApplication.java │ │ │ ├── WorkerConfig.java │ │ │ ├── WorkerController.java │ │ │ └── WorkerJob.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── com │ └── salaboy │ └── worker │ ├── BaseIntegrationTest.java │ └── WorkerTest.java ├── k8s-dapr-shared-and-knative ├── README.md ├── db-deployment.yaml ├── db-service.yaml ├── echo-kservice.yaml ├── echo-subscription.yaml ├── pubsub-rabbitmq.yml ├── rabbitmq │ ├── deploy.sh │ ├── resources │ │ └── cluster.yml │ └── test.sh ├── redis-deployment.yaml ├── redis-service.yaml ├── result-kservice.yaml ├── results-statestore.yaml ├── vote-kservice.yaml ├── votes-statestore.yaml └── worker-deployment.yaml ├── k8s-dapr ├── README.md ├── db-deployment.yaml ├── db-service.yaml ├── redis-deployment.yaml ├── redis-service.yaml ├── result-deployment.yaml ├── result-service.yaml ├── results-statestore.yaml ├── vote-deployment.yaml ├── vote-service.yaml ├── votes-statestore.yaml └── worker-deployment.yaml ├── k8s-specifications ├── db-deployment.yaml ├── db-service.yaml ├── redis-deployment.yaml ├── redis-service.yaml ├── result-deployment.yaml ├── result-service.yaml ├── vote-deployment.yaml ├── vote-service.yaml └── worker-deployment.yaml ├── platform ├── apps │ ├── app-echo.yml │ ├── app-result.yml │ ├── app-vote.yml │ ├── app-worker.yml │ ├── pubsub-rabbitmq.yml │ ├── statestore-results.yml │ ├── statestore-votes.yml │ └── subscription-echo.yml ├── gitops │ ├── apps │ │ ├── base │ │ │ └── kustomization.yaml │ │ ├── cloud │ │ │ ├── kustomization.yaml │ │ │ └── patch-echo.yml │ │ └── local │ │ │ └── kustomization.yaml │ ├── clusters │ │ ├── cloud │ │ │ ├── apps.yml │ │ │ └── infrastructure.yml │ │ └── local │ │ │ ├── apps.yml │ │ │ └── infrastructure.yml │ └── infrastructure │ │ ├── dapr │ │ └── dapr-shared.yml │ │ ├── data │ │ ├── postgresql.yml │ │ ├── rabbitmq.yml │ │ └── redis.yml │ │ └── observability │ │ ├── dashboards-java.yml │ │ ├── dashboards-knative.yml │ │ ├── dashboards-prometheus.yml │ │ ├── dashboards-rabbitmq.yml │ │ └── grafana.yml └── installation │ ├── README.md │ ├── values-cloud.yml │ └── values-local.yml ├── result ├── .dockerignore ├── Dockerfile ├── docker-compose.test.yml ├── package-lock.json ├── package.json ├── server.js ├── tests │ ├── Dockerfile │ ├── render.js │ └── tests.sh └── views │ ├── angular.min.js │ ├── app.js │ ├── index.html │ ├── socket.io.js │ └── stylesheets │ └── style.css ├── seed-data ├── Dockerfile ├── generate-votes.sh └── make-data.py ├── vote ├── Dockerfile ├── app.py ├── requirements.txt ├── static │ └── stylesheets │ │ └── style.css └── templates │ └── index.html └── worker ├── Dockerfile ├── Program.cs └── Worker.csproj /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | ** PLEASE ONLY USE THIS ISSUE TRACKER TO SUBMIT ISSUES WITH THE EXAMPLE VOTING APP ** 2 | 3 | * If you have a bug working with Docker itself, not related to these labs, please file the bug on the [Docker repo](https://github.com/docker/docker) * 4 | * If you would like general support figuring out how to do something with Docker, please use the Docker Slack channel. If you're not on that channel, sign up for the [Docker Community](http://dockr.ly/MeetUp) and you'll get an invite. * 5 | * Or go to the [Docker Forums](https://forums.docker.com/) * 6 | 7 | Please provide the following information so we can assess the issue you're having 8 | 9 | **Description** 10 | 11 | 14 | 15 | **Steps to reproduce the issue, if relevant:** 16 | 1. 17 | 2. 18 | 3. 19 | 20 | **Describe the results you received:** 21 | 22 | 23 | **Describe the results you expected:** 24 | 25 | 26 | **Additional information you deem important (e.g. issue happens only occasionally):** 27 | 28 | **Output of `docker version`:** 29 | 30 | ``` 31 | (paste your output here) 32 | ``` 33 | 34 | **Output of `docker info`:** 35 | 36 | ``` 37 | (paste your output here) 38 | ``` 39 | 40 | **Additional environment details (AWS, Docker for Mac, Docker for Windows, VirtualBox, physical, etc.):** 41 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | -------------------------------------------------------------------------------- /.github/workflows/call-docker-build-result.yaml: -------------------------------------------------------------------------------- 1 | name: Build Result 2 | # template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml 3 | 4 | on: 5 | # we want pull requests so we can build(test) but not push to image registry 6 | push: 7 | branches: 8 | - 'main' 9 | # only build when important files change 10 | paths: 11 | - 'result/**' 12 | - '.github/workflows/call-docker-build-result.yaml' 13 | pull_request: 14 | branches: 15 | - 'main' 16 | # only build when important files change 17 | paths: 18 | - 'result/**' 19 | - '.github/workflows/call-docker-build-result.yaml' 20 | 21 | jobs: 22 | call-docker-build: 23 | 24 | name: Result Call Docker Build 25 | 26 | uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main 27 | 28 | permissions: 29 | contents: read 30 | packages: write # needed to push docker image to ghcr.io 31 | pull-requests: write # needed to create and update comments in PRs 32 | 33 | secrets: 34 | 35 | # Only needed if with:dockerhub-enable is true below 36 | dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} 37 | 38 | # Only needed if with:dockerhub-enable is true below 39 | dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} 40 | 41 | with: 42 | 43 | ### REQUIRED 44 | ### ENABLE ONE OR BOTH REGISTRIES 45 | ### tell docker where to push. 46 | ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below 47 | dockerhub-enable: true 48 | ghcr-enable: true 49 | 50 | ### REQUIRED 51 | ### A list of the account/repo names for docker build. List should match what's enabled above 52 | ### defaults to: 53 | image-names: | 54 | ghcr.io/dockersamples/example-voting-app-result 55 | dockersamples/examplevotingapp_result 56 | 57 | ### REQUIRED set rules for tagging images, based on special action syntax: 58 | ### https://github.com/docker/metadata-action#tags-input 59 | ### defaults to: 60 | tag-rules: | 61 | type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} 62 | type=raw,value=before,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} 63 | type=raw,value=after,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} 64 | type=ref,event=pr 65 | 66 | ### path to where docker should copy files into image 67 | ### defaults to root of repository (.) 68 | context: result 69 | 70 | ### Dockerfile alternate name. Default is Dockerfile (relative to context path) 71 | # file: Containerfile 72 | 73 | ### build stage to target, defaults to empty, which builds to last stage in Dockerfile 74 | # target: 75 | 76 | ### platforms to build for, defaults to linux/amd64 77 | ### other options: linux/amd64,linux/arm64,linux/arm/v7 78 | platforms: linux/amd64,linux/arm64,linux/arm/v7 79 | 80 | ### Create a PR comment with image tags and labels 81 | ### defaults to false 82 | # comment-enable: false 83 | -------------------------------------------------------------------------------- /.github/workflows/call-docker-build-vote.yaml: -------------------------------------------------------------------------------- 1 | name: Build Vote 2 | # template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml 3 | 4 | on: 5 | # we want pull requests so we can build(test) but not push to image registry 6 | push: 7 | branches: 8 | - 'main' 9 | # only build when important files change 10 | paths: 11 | - 'vote/**' 12 | - '.github/workflows/call-docker-build-vote.yaml' 13 | pull_request: 14 | branches: 15 | - 'main' 16 | # only build when important files change 17 | paths: 18 | - 'vote/**' 19 | - '.github/workflows/call-docker-build-vote.yaml' 20 | 21 | jobs: 22 | call-docker-build: 23 | 24 | name: Vote Call Docker Build 25 | 26 | uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main 27 | 28 | permissions: 29 | contents: read 30 | packages: write # needed to push docker image to ghcr.io 31 | pull-requests: write # needed to create and update comments in PRs 32 | 33 | secrets: 34 | 35 | # Only needed if with:dockerhub-enable is true below 36 | dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} 37 | 38 | # Only needed if with:dockerhub-enable is true below 39 | dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} 40 | 41 | with: 42 | 43 | ### REQUIRED 44 | ### ENABLE ONE OR BOTH REGISTRIES 45 | ### tell docker where to push. 46 | ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below 47 | dockerhub-enable: true 48 | ghcr-enable: true 49 | 50 | ### REQUIRED 51 | ### A list of the account/repo names for docker build. List should match what's enabled above 52 | ### defaults to: 53 | image-names: | 54 | ghcr.io/dockersamples/example-voting-app-vote 55 | dockersamples/examplevotingapp_vote 56 | 57 | ### REQUIRED set rules for tagging images, based on special action syntax: 58 | ### https://github.com/docker/metadata-action#tags-input 59 | ### defaults to: 60 | tag-rules: | 61 | type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} 62 | type=raw,value=before,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} 63 | type=raw,value=after,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} 64 | type=ref,event=pr 65 | 66 | ### path to where docker should copy files into image 67 | ### defaults to root of repository (.) 68 | context: vote 69 | 70 | ### Dockerfile alternate name. Default is Dockerfile (relative to context path) 71 | # file: Containerfile 72 | 73 | ### build stage to target, defaults to empty, which builds to last stage in Dockerfile 74 | # target: 75 | 76 | ### platforms to build for, defaults to linux/amd64 77 | ### other options: linux/amd64,linux/arm64,linux/arm/v7 78 | platforms: linux/amd64,linux/arm64,linux/arm/v7 79 | 80 | ### Create a PR comment with image tags and labels 81 | ### defaults to false 82 | # comment-enable: false 83 | -------------------------------------------------------------------------------- /.github/workflows/call-docker-build-worker.yaml: -------------------------------------------------------------------------------- 1 | name: Build Worker 2 | # template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml 3 | 4 | on: 5 | # we want pull requests so we can build(test) but not push to image registry 6 | push: 7 | branches: 8 | - 'main' 9 | # only build when important files change 10 | paths: 11 | - 'worker/**' 12 | - '.github/workflows/call-docker-build-worker.yaml' 13 | pull_request: 14 | branches: 15 | - 'main' 16 | # only build when important files change 17 | paths: 18 | - 'worker/**' 19 | - '.github/workflows/call-docker-build-worker.yaml' 20 | 21 | jobs: 22 | call-docker-build: 23 | 24 | name: Worker Call Docker Build 25 | 26 | uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main 27 | 28 | permissions: 29 | contents: read 30 | packages: write # needed to push docker image to ghcr.io 31 | pull-requests: write # needed to create and update comments in PRs 32 | 33 | secrets: 34 | 35 | # Only needed if with:dockerhub-enable is true below 36 | dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} 37 | 38 | # Only needed if with:dockerhub-enable is true below 39 | dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} 40 | 41 | with: 42 | 43 | ### REQUIRED 44 | ### ENABLE ONE OR BOTH REGISTRIES 45 | ### tell docker where to push. 46 | ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below 47 | dockerhub-enable: true 48 | ghcr-enable: true 49 | 50 | ### REQUIRED 51 | ### A list of the account/repo names for docker build. List should match what's enabled above 52 | ### defaults to: 53 | image-names: | 54 | ghcr.io/dockersamples/example-voting-app-worker 55 | dockersamples/examplevotingapp_worker 56 | 57 | ### REQUIRED set rules for tagging images, based on special action syntax: 58 | ### https://github.com/docker/metadata-action#tags-input 59 | ### defaults to: 60 | tag-rules: | 61 | type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} 62 | type=ref,event=pr 63 | 64 | ### path to where docker should copy files into image 65 | ### defaults to root of repository (.) 66 | context: worker 67 | 68 | ### Dockerfile alternate name. Default is Dockerfile (relative to context path) 69 | # file: Containerfile 70 | 71 | ### build stage to target, defaults to empty, which builds to last stage in Dockerfile 72 | # target: 73 | 74 | ### platforms to build for, defaults to linux/amd64 75 | ### other options: linux/amd64,linux/arm64,linux/arm/v7 76 | # FIXME worker arm/v7 support doesn't build in .net core 3.1 with QEMU 77 | # a fix would likely run the .net build on amd64 but with a target of arm/v7 78 | platforms: linux/amd64,linux/arm64,linux/arm/v7 79 | 80 | ### Create a PR comment with image tags and labels 81 | ### defaults to false 82 | # comment-enable: false 83 | -------------------------------------------------------------------------------- /.github/workflows/commit-stage.yml: -------------------------------------------------------------------------------- 1 | name: Commit Stage 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | publish-dockerfile: 8 | name: Publish (Dockerfile) 9 | permissions: 10 | contents: read 11 | packages: write 12 | id-token: write 13 | uses: ./.github/workflows/publish-dockerfile.yml 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | project: [ 18 | {context: go/result, image: result-go}, 19 | {context: go/vote, image: vote-go}, 20 | {context: go/worker, image: worker-go}, 21 | {context: dotnet/worker, image: worker-dotnet}, 22 | ] 23 | with: 24 | context: ${{ matrix.project.context }} 25 | image: ${{ matrix.project.image }} 26 | secrets: 27 | push-token: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | publish-java: 30 | name: Publish (Java) 31 | permissions: 32 | contents: read 33 | packages: write 34 | id-token: write 35 | uses: ./.github/workflows/publish-java.yml 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | project: [ 40 | {context: java/echo, image: echo-java}, 41 | {context: java/vote, image: vote-java}, 42 | {context: java/result, image: result-java}, 43 | {context: java/worker, image: worker-java}, 44 | ] 45 | with: 46 | context: ${{ matrix.project.context }} 47 | image: ${{ matrix.project.image }} 48 | secrets: 49 | push-token: ${{ secrets.GITHUB_TOKEN }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | project.lock.json 3 | bin/ 4 | obj/ 5 | .vs/ 6 | node_modules/ 7 | 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | # Compiled class file 25 | *.class 26 | 27 | # Log file 28 | *.log 29 | 30 | # BlueJ files 31 | *.ctxt 32 | 33 | # Mobile Tools for Java (J2ME) 34 | .mtj.tmp/ 35 | 36 | # Package Files # 37 | *.jar 38 | *.war 39 | *.nar 40 | *.ear 41 | *.zip 42 | *.tar.gz 43 | *.rar 44 | 45 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 46 | hs_err_pid* 47 | 48 | HELP.md 49 | .gradle 50 | build/ 51 | !gradle/wrapper/gradle-wrapper.jar 52 | !**/src/main/**/build/ 53 | !**/src/test/**/build/ 54 | 55 | target/ 56 | !.mvn/wrapper/maven-wrapper.jar 57 | !**/src/main/**/target/ 58 | !**/src/test/**/target/ 59 | 60 | ### STS ### 61 | .apt_generated 62 | .classpath 63 | .factorypath 64 | .project 65 | .settings 66 | .springBeans 67 | .sts4-cache 68 | !**/src/main/**/bin/ 69 | !**/src/test/**/bin/ 70 | 71 | ### IntelliJ IDEA ### 72 | .idea 73 | *.iws 74 | *.iml 75 | *.ipr 76 | out/ 77 | !**/src/main/**/out/ 78 | !**/src/test/**/out/ 79 | 80 | ### NetBeans ### 81 | /nbproject/private/ 82 | /nbbuild/ 83 | /dist/ 84 | /nbdist/ 85 | /.nb-gradle/ 86 | 87 | ### VS Code ### 88 | .vscode/ 89 | 90 | ############# 91 | ### macOS ### 92 | ############# 93 | 94 | # General 95 | .DS_Store 96 | *.DS_Store 97 | **/.DS_Store 98 | .AppleDouble 99 | .LSOverride 100 | 101 | # Icon must end with two \r 102 | Icon 103 | 104 | # Thumbnails 105 | ._* 106 | 107 | # Files that might appear in the root of a volume 108 | .DocumentRevisions-V100 109 | .fseventsd 110 | .Spotlight-V100 111 | .TemporaryItems 112 | .Trashes 113 | .VolumeIcon.icns 114 | .com.apple.timemachine.donotpresent 115 | 116 | # Directories potentially created on remote AFP share 117 | .AppleDB 118 | .AppleDesktop 119 | Network Trash Folder 120 | Temporary Items 121 | .apdisk 122 | 123 | 124 | **/dagger.gen.go 125 | **/dagger/internal 126 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Bret Fisher 2 | Michael Irwin 3 | 4 | # Alumni, thanks for your work! 5 | Aanand Prasad 6 | Ben Firshman 7 | Fernando Mayo 8 | Mano Marks 9 | Maxime Heckel 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example Voting App 2 | 3 | A simple distributed application running across multiple Docker containers. 4 | 5 | ## Getting started 6 | 7 | Download [Docker Desktop](https://www.docker.com/products/docker-desktop) for Mac or Windows. [Docker Compose](https://docs.docker.com/compose) will be automatically installed. On Linux, make sure you have the latest version of [Compose](https://docs.docker.com/compose/install/). 8 | 9 | This solution uses Python, Node.js, .NET, with Redis for messaging and Postgres for storage. 10 | 11 | Run in this directory to build and run the app: 12 | 13 | ```shell 14 | docker compose up 15 | ``` 16 | 17 | The `vote` app will be running at [http://localhost:5000](http://localhost:5000), and the `results` will be at [http://localhost:5001](http://localhost:5001). 18 | 19 | Alternately, if you want to run it on a [Docker Swarm](https://docs.docker.com/engine/swarm/), first make sure you have a swarm. If you don't, run: 20 | 21 | ```shell 22 | docker swarm init 23 | ``` 24 | 25 | Once you have your swarm, in this directory run: 26 | 27 | ```shell 28 | docker stack deploy --compose-file docker-stack.yml vote 29 | ``` 30 | 31 | ## Run the app in Kubernetes 32 | 33 | The folder k8s-specifications contains the YAML specifications of the Voting App's services. 34 | 35 | Run the following command to create the deployments and services. Note it will create these resources in your current namespace (`default` if you haven't changed it.) 36 | 37 | ```shell 38 | kubectl create -f k8s-specifications/ 39 | ``` 40 | 41 | The `vote` web app is then available on port 31000 on each host of the cluster, the `result` web app is available on port 31001. 42 | 43 | To remove them, run: 44 | 45 | ```shell 46 | kubectl delete -f k8s-specifications/ 47 | ``` 48 | 49 | ## Architecture 50 | 51 | ![Architecture diagram](architecture.excalidraw.png) 52 | 53 | * A front-end web app in [Python](/vote) which lets you vote between two options 54 | * A [Redis](https://hub.docker.com/_/redis/) which collects new votes 55 | * A [.NET](/worker/) worker which consumes votes and stores them in… 56 | * A [Postgres](https://hub.docker.com/_/postgres/) database backed by a Docker volume 57 | * A [Node.js](/result) web app which shows the results of the voting in real time 58 | 59 | ## Notes 60 | 61 | The voting application only accepts one vote per client browser. It does not register additional votes if a vote has already been submitted from a client. 62 | 63 | This isn't an example of a properly architected perfectly designed distributed app... it's just a simple 64 | example of the various types of pieces and languages you might see (queues, persistent data, etc), and how to 65 | deal with them in Docker at a basic level. 66 | -------------------------------------------------------------------------------- /architecture.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salaboy/example-voting-app/3b7c3057283f18b6512ecad7d3a4fde32ea66ba1/architecture.excalidraw.png -------------------------------------------------------------------------------- /dagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voteapp", 3 | "sdk": "go", 4 | "dependencies": [ 5 | { 6 | "name": "proxy", 7 | "source": "github.com/kpenfound/dagger-modules/proxy@d5dfa6e20d26fd53246c20af721364c8f57e4b50" 8 | }, 9 | { 10 | "name": "result", 11 | "source": "go/result" 12 | }, 13 | { 14 | "name": "vote", 15 | "source": "java/vote" 16 | }, 17 | { 18 | "name": "worker", 19 | "source": "dotnet/worker" 20 | } 21 | ], 22 | "source": "dagger", 23 | "engineVersion": "v0.10.1" 24 | } 25 | -------------------------------------------------------------------------------- /dagger/.gitattributes: -------------------------------------------------------------------------------- 1 | /dagger.gen.go linguist-generated 2 | /querybuilder/** linguist-generated 3 | /internal/dagger/** linguist-generated 4 | /internal/querybuilder/** linguist-generated 5 | -------------------------------------------------------------------------------- /dagger/go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.21.3 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.17.31 7 | github.com/Khan/genqlient v0.6.0 8 | github.com/vektah/gqlparser/v2 v2.5.6 9 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa 10 | golang.org/x/sync v0.6.0 11 | ) 12 | 13 | require github.com/stretchr/testify v1.9.0 // indirect 14 | -------------------------------------------------------------------------------- /dagger/go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/gqlgen v0.17.31 h1:VncSQ82VxieHkea8tz11p7h/zSbvHSxSDZfywqWt158= 2 | github.com/99designs/gqlgen v0.17.31/go.mod h1:i4rEatMrzzu6RXaHydq1nmEPZkb3bKQsnxNRHS4DQB4= 3 | github.com/Khan/genqlient v0.6.0 h1:Bwb1170ekuNIVIwTJEqvO8y7RxBxXu639VJOkKSrwAk= 4 | github.com/Khan/genqlient v0.6.0/go.mod h1:rvChwWVTqXhiapdhLDV4bp9tz/Xvtewwkon4DpWWCRM= 5 | github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= 6 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 7 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 8 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 19 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 22 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 23 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 24 | github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU= 25 | github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= 26 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 27 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 28 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 29 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 33 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /dagger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "main/internal/dagger" 4 | 5 | type Voteapp struct{} 6 | 7 | // example usage: "dagger call vote-app --dir . up" 8 | func (m *Voteapp) VoteApp(dir *Directory) *Service { 9 | redisSvc := dag.Container(). 10 | From("redis/redis-stack"). 11 | WithExposedPort(6379).AsService() 12 | 13 | postgresSvc := dag.Container(). 14 | From("postgres:15-alpine"). 15 | WithEnvVariable("POSTGRES_PASSWORD", "postgres"). 16 | WithExposedPort(5432).AsService() 17 | 18 | voteSvc := dag.Vote(). 19 | Serve(dir.Directory("java/vote"), dagger.VoteServeOpts{ 20 | Redis: redisSvc, 21 | ComponentsDir: dir.Directory("java/vote/components"), 22 | }) 23 | 24 | resultSvc := dag.Result(). 25 | Serve(dir.Directory("go/result"), dagger.ResultServeOpts{ 26 | ComponentsPath: dag.Directory().WithFile("results-statestore.yaml", dir.Directory("k8s-dapr").File("results-statestore.yaml")), 27 | PostgresSvc: postgresSvc, 28 | }) 29 | 30 | workerSvc := dag.Worker(). 31 | Serve(dir.Directory("dotnet/worker"), 32 | dagger.WorkerServeOpts{ 33 | RedisSvc: redisSvc, 34 | PostgresSvc: postgresSvc, 35 | ComponentsPath: dir.Directory("k8s-dapr"), 36 | }, 37 | ) 38 | 39 | return dag.Proxy(). 40 | WithService(voteSvc, "vote", 8080, 8080). 41 | WithService(resultSvc, "result", 3000, 3000). 42 | WithService(workerSvc, "worker", 3001, 3001). 43 | Service() 44 | } 45 | -------------------------------------------------------------------------------- /docker-compose.images.yml: -------------------------------------------------------------------------------- 1 | # for running in docker compose with prebuilt images 2 | 3 | # version is now using "compose spec" 4 | # v2 and v3 are now combined! 5 | # docker-compose v1.27+ required 6 | 7 | services: 8 | vote: 9 | image: dockersamples/examplevotingapp_vote 10 | depends_on: 11 | redis: 12 | condition: service_healthy 13 | ports: 14 | - "8000:80" 15 | networks: 16 | - front-tier 17 | - back-tier 18 | 19 | result: 20 | image: dockersamples/examplevotingapp_result 21 | depends_on: 22 | db: 23 | condition: service_healthy 24 | ports: 25 | - "6001:80" 26 | networks: 27 | - front-tier 28 | - back-tier 29 | 30 | worker: 31 | image: dockersamples/examplevotingapp_worker 32 | depends_on: 33 | redis: 34 | condition: service_healthy 35 | db: 36 | condition: service_healthy 37 | networks: 38 | - back-tier 39 | 40 | redis: 41 | image: redis:alpine 42 | volumes: 43 | - "./healthchecks:/healthchecks" 44 | healthcheck: 45 | test: /healthchecks/redis.sh 46 | interval: "5s" 47 | networks: 48 | - back-tier 49 | 50 | db: 51 | image: postgres:15-alpine 52 | environment: 53 | POSTGRES_USER: "postgres" 54 | POSTGRES_PASSWORD: "postgres" 55 | volumes: 56 | - "db-data:/var/lib/postgresql/data" 57 | - "./healthchecks:/healthchecks" 58 | healthcheck: 59 | test: /healthchecks/postgres.sh 60 | interval: "5s" 61 | networks: 62 | - back-tier 63 | 64 | volumes: 65 | db-data: 66 | 67 | networks: 68 | front-tier: 69 | back-tier: 70 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # version is now using "compose spec" 2 | # v2 and v3 are now combined! 3 | # docker-compose v1.27+ required 4 | 5 | services: 6 | vote: 7 | build: 8 | context: ./vote 9 | target: dev 10 | depends_on: 11 | redis: 12 | condition: service_healthy 13 | healthcheck: 14 | test: ["CMD", "curl", "-f", "http://localhost"] 15 | interval: 15s 16 | timeout: 5s 17 | retries: 3 18 | start_period: 10s 19 | volumes: 20 | - ./vote:/usr/local/app 21 | ports: 22 | - "8000:80" 23 | networks: 24 | - front-tier 25 | - back-tier 26 | 27 | result: 28 | build: ./result 29 | # use nodemon rather than node for local dev 30 | entrypoint: nodemon --inspect=0.0.0.0 server.js 31 | depends_on: 32 | db: 33 | condition: service_healthy 34 | volumes: 35 | - ./result:/usr/local/app 36 | ports: 37 | - "6001:80" 38 | - "127.0.0.1:9229:9229" 39 | networks: 40 | - front-tier 41 | - back-tier 42 | 43 | worker: 44 | build: 45 | context: ./worker 46 | depends_on: 47 | redis: 48 | condition: service_healthy 49 | db: 50 | condition: service_healthy 51 | networks: 52 | - back-tier 53 | 54 | redis: 55 | image: redis:alpine 56 | volumes: 57 | - "./healthchecks:/healthchecks" 58 | healthcheck: 59 | test: /healthchecks/redis.sh 60 | interval: "5s" 61 | networks: 62 | - back-tier 63 | 64 | db: 65 | image: postgres:15-alpine 66 | environment: 67 | POSTGRES_USER: "postgres" 68 | POSTGRES_PASSWORD: "postgres" 69 | volumes: 70 | - "db-data:/var/lib/postgresql/data" 71 | - "./healthchecks:/healthchecks" 72 | healthcheck: 73 | test: /healthchecks/postgres.sh 74 | interval: "5s" 75 | networks: 76 | - back-tier 77 | 78 | # this service runs once to seed the database with votes 79 | # it won't run unless you specify the "seed" profile 80 | # docker compose --profile seed up -d 81 | seed: 82 | build: ./seed-data 83 | profiles: ["seed"] 84 | depends_on: 85 | vote: 86 | condition: service_healthy 87 | networks: 88 | - front-tier 89 | restart: "no" 90 | 91 | volumes: 92 | db-data: 93 | 94 | networks: 95 | front-tier: 96 | back-tier: 97 | -------------------------------------------------------------------------------- /docker-stack.yml: -------------------------------------------------------------------------------- 1 | # this file is meant for Docker Swarm stacks only 2 | # trying it in compose will fail because of multiple replicas trying to bind to the same port 3 | # Swarm currently does not support Compose Spec, so we'll pin to the older version 3.9 4 | 5 | version: "3.9" 6 | 7 | services: 8 | 9 | redis: 10 | image: redis:alpine 11 | networks: 12 | - frontend 13 | 14 | db: 15 | image: postgres:15-alpine 16 | environment: 17 | POSTGRES_USER: "postgres" 18 | POSTGRES_PASSWORD: "postgres" 19 | volumes: 20 | - db-data:/var/lib/postgresql/data 21 | networks: 22 | - backend 23 | 24 | vote: 25 | image: dockersamples/examplevotingapp_vote 26 | ports: 27 | - 8000:80 28 | networks: 29 | - frontend 30 | deploy: 31 | replicas: 2 32 | 33 | result: 34 | image: dockersamples/examplevotingapp_result 35 | ports: 36 | - 6001:80 37 | networks: 38 | - backend 39 | 40 | worker: 41 | image: dockersamples/examplevotingapp_worker 42 | networks: 43 | - frontend 44 | - backend 45 | deploy: 46 | replicas: 2 47 | 48 | networks: 49 | frontend: 50 | backend: 51 | 52 | volumes: 53 | db-data: 54 | -------------------------------------------------------------------------------- /dotnet/worker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Image 2 | FROM mcr.microsoft.com/dotnet/sdk:7.0 as build 3 | ARG TARGETPLATFORM 4 | ARG TARGETARCH 5 | RUN echo "I am building for $TARGETPLATFORM" 6 | 7 | WORKDIR /source 8 | COPY *.csproj . 9 | RUN dotnet restore -a $TARGETARCH 10 | 11 | COPY . . 12 | RUN dotnet publish -c release -o /app -a $TARGETARCH --self-contained false --no-restore 13 | 14 | # app image 15 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 16 | WORKDIR /app 17 | COPY --from=build /app . 18 | ENTRYPOINT ["dotnet", "Worker.dll"] -------------------------------------------------------------------------------- /dotnet/worker/Worker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /dotnet/worker/components/results-statestore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: results-statestore 5 | spec: 6 | type: state.in-memory 7 | version: v1 8 | -------------------------------------------------------------------------------- /dotnet/worker/components/votes-statestore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: votes-statestore 5 | spec: 6 | type: state.redis 7 | version: v1 8 | metadata: 9 | - name: keyPrefix 10 | value: name 11 | - name: redisHost 12 | value: redis:6379 13 | - name: redisPassword 14 | value: "" 15 | - name: actorStateStore 16 | value: "true" 17 | - name: queryIndexes 18 | value: | 19 | [ 20 | { 21 | "name": "voteIndex", 22 | "indexes": [ 23 | { 24 | "key": "type", 25 | "type": "TEXT" 26 | } 27 | ] 28 | } 29 | ] 30 | 31 | -------------------------------------------------------------------------------- /dotnet/worker/dagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worker", 3 | "sdk": "typescript", 4 | "dependencies": [ 5 | { 6 | "name": "dapr", 7 | "source": "github.com/marcosnils/daggerverse/dapr@15660afb386b32b955d96ee42a7354338b56c570" 8 | }, 9 | { 10 | "name": "proxy", 11 | "source": "github.com/kpenfound/dagger-modules/proxy@d5dfa6e20d26fd53246c20af721364c8f57e4b50" 12 | } 13 | ], 14 | "source": "dagger", 15 | "engineVersion": "v0.10.1" 16 | } 17 | -------------------------------------------------------------------------------- /dotnet/worker/dagger/.gitattributes: -------------------------------------------------------------------------------- 1 | /sdk/** linguist-generated 2 | -------------------------------------------------------------------------------- /dotnet/worker/dagger/.gitignore: -------------------------------------------------------------------------------- 1 | /sdk 2 | /node_modules/** 3 | -------------------------------------------------------------------------------- /dotnet/worker/dagger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "typescript": "^5.3.2" 4 | }, 5 | "devDependencies": { 6 | "@dagger.io/dagger": "file:sdk" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /dotnet/worker/dagger/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | dag, 3 | Directory, 4 | object, 5 | func, 6 | Service, 7 | Container, 8 | } from "@dagger.io/dagger"; 9 | 10 | @object() 11 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 12 | class Worker { 13 | @func() 14 | serve( 15 | dir: Directory, 16 | redisSvc?: Service, 17 | postgresSvc?: Service, 18 | componentsPath?: Directory, 19 | ): Service { 20 | return ( 21 | this.build(dir, redisSvc, postgresSvc, componentsPath) 22 | //we don't need a healtcheck since this service doesn't actually listen 23 | //to any ports 24 | .withExposedPort(3000, { experimentalSkipHealthcheck: true }) 25 | .asService() 26 | ); 27 | } 28 | 29 | @func() 30 | build( 31 | dir: Directory, 32 | redisSvc?: Service, 33 | postgresSvc?: Service, 34 | componentsPath?: Directory, 35 | ): Container { 36 | if (!componentsPath) { 37 | componentsPath = dir.directory("components"); 38 | } 39 | 40 | if (!redisSvc) { 41 | redisSvc = dag 42 | .container() 43 | .from("redis/redis-stack") 44 | .withExposedPort(6379) 45 | .asService(); 46 | } 47 | 48 | if (!postgresSvc) { 49 | postgresSvc = dag 50 | .container() 51 | .from("postgres:15-alpine") 52 | .withEnvVariable("POSTGRES_PASSWORD", "postgres") 53 | .withExposedPort(5432) 54 | .asService(); 55 | } 56 | const dapr = dag 57 | .dapr() 58 | .dapr("worker", { componentsPath: componentsPath }) 59 | .withServiceBinding("redis", redisSvc) 60 | .withServiceBinding("db", postgresSvc) 61 | .withExposedPort(50001) 62 | .asService(); 63 | 64 | const build = dag 65 | .container() 66 | .from("mcr.microsoft.com/dotnet/sdk:7.0") 67 | .withMountedDirectory("/source", dir.withoutDirectory("dagger")) 68 | .withWorkdir("/source") 69 | .withExec(["dotnet", "restore"]) 70 | .withExec([ 71 | "dotnet", 72 | "publish", 73 | "-c", 74 | "release", 75 | "-o", 76 | "/app", 77 | "--self-contained", 78 | "false", 79 | "--no-restore", 80 | ]); 81 | 82 | return dag 83 | .container() 84 | .from("mcr.microsoft.com/dotnet/aspnet:7.0") 85 | .withMountedDirectory("/app", build.directory("/app")) 86 | .withServiceBinding("dapr", dapr) 87 | .withEnvVariable("DAPR_GRPC_ENDPOINT", "http://dapr:50001") 88 | .withEnvVariable("BASE_URL", "http://dapr") 89 | .withWorkdir("/app") 90 | .withEntrypoint(["dotnet", "Worker.dll"]); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /dotnet/worker/dagger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "moduleResolution": "Node", 5 | "experimentalDecorators": true, 6 | "paths": { 7 | "@dagger.io/dagger": ["./sdk"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example-voting-app.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker", "worker\Worker.csproj", "{B3BB5AC6-48AB-49AB-9AF3-B8CAB176FDB9}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet", "dotnet", "{4E82DDAB-75CE-46F9-9795-3C80487CDF61}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker", "dotnet\worker\Worker.csproj", "{5F2E99FB-3101-4005-849B-21C584CCF4C0}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {B3BB5AC6-48AB-49AB-9AF3-B8CAB176FDB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {B3BB5AC6-48AB-49AB-9AF3-B8CAB176FDB9}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {B3BB5AC6-48AB-49AB-9AF3-B8CAB176FDB9}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {B3BB5AC6-48AB-49AB-9AF3-B8CAB176FDB9}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {5F2E99FB-3101-4005-849B-21C584CCF4C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {5F2E99FB-3101-4005-849B-21C584CCF4C0}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {5F2E99FB-3101-4005-849B-21C584CCF4C0}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {5F2E99FB-3101-4005-849B-21C584CCF4C0}.Release|Any CPU.Build.0 = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(NestedProjects) = preSolution 31 | {5F2E99FB-3101-4005-849B-21C584CCF4C0} = {4E82DDAB-75CE-46F9-9795-3C80487CDF61} 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {C6D40AE7-B905-4807-AE7F-33A1B08AE295} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /go/result/.gitattributes: -------------------------------------------------------------------------------- 1 | /dagger.gen.go linguist-generated 2 | /querybuilder/** linguist-generated 3 | -------------------------------------------------------------------------------- /go/result/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.21 4 | 5 | # Set destination for COPY 6 | WORKDIR / 7 | 8 | # Download Go modules 9 | COPY go.mod go.sum ./ 10 | RUN go mod download 11 | 12 | # Copy the source code. Note the slash at the end, as explained in 13 | # https://docs.docker.com/engine/reference/builder/#copy 14 | COPY *.go ./ 15 | COPY static/ ./static/ 16 | 17 | # Build 18 | RUN CGO_ENABLED=0 GOOS=linux go build -o /main 19 | 20 | # Optional: 21 | # To bind to a TCP port, runtime parameters must be supplied to the docker command. 22 | # But we can document in the Dockerfile what ports 23 | # the application is going to listen on by default. 24 | # https://docs.docker.com/engine/reference/builder/#expose 25 | EXPOSE 8080 26 | 27 | # Run 28 | CMD ["/main"] -------------------------------------------------------------------------------- /go/result/components/results-statestore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: results-statestore 5 | spec: 6 | type: state.in-memory 7 | version: v1 8 | -------------------------------------------------------------------------------- /go/result/dagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "result", 3 | "sdk": "go", 4 | "dependencies": [ 5 | { 6 | "name": "dapr", 7 | "source": "github.com/marcosnils/daggerverse/dapr@15660afb386b32b955d96ee42a7354338b56c570" 8 | }, 9 | { 10 | "name": "go", 11 | "source": "github.com/sagikazarmark/daggerverse/go@bc2ccb71307797d3066a56e1e57d75205523cfad" 12 | } 13 | ], 14 | "source": "dagger", 15 | "engineVersion": "v0.10.1" 16 | } 17 | -------------------------------------------------------------------------------- /go/result/dagger/.gitattributes: -------------------------------------------------------------------------------- 1 | /dagger.gen.go linguist-generated 2 | /querybuilder/** linguist-generated 3 | /internal/dagger/** linguist-generated 4 | /internal/querybuilder/** linguist-generated 5 | -------------------------------------------------------------------------------- /go/result/dagger/go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.21.3 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.17.31 7 | github.com/Khan/genqlient v0.6.0 8 | github.com/vektah/gqlparser/v2 v2.5.6 9 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa 10 | golang.org/x/sync v0.6.0 11 | ) 12 | 13 | require github.com/stretchr/testify v1.9.0 // indirect 14 | -------------------------------------------------------------------------------- /go/result/dagger/go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/gqlgen v0.17.31 h1:VncSQ82VxieHkea8tz11p7h/zSbvHSxSDZfywqWt158= 2 | github.com/99designs/gqlgen v0.17.31/go.mod h1:i4rEatMrzzu6RXaHydq1nmEPZkb3bKQsnxNRHS4DQB4= 3 | github.com/Khan/genqlient v0.6.0 h1:Bwb1170ekuNIVIwTJEqvO8y7RxBxXu639VJOkKSrwAk= 4 | github.com/Khan/genqlient v0.6.0/go.mod h1:rvChwWVTqXhiapdhLDV4bp9tz/Xvtewwkon4DpWWCRM= 5 | github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= 6 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 7 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 8 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 19 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 22 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 23 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 24 | github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU= 25 | github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= 26 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 27 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 28 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 29 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 33 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /go/result/dagger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "context" 4 | 5 | type Result struct{} 6 | 7 | func (m *Result) Serve( 8 | dir *Directory, 9 | // +optional 10 | componentsPath *Directory, 11 | // +optional 12 | postgresSvc *Service, 13 | ) *Service { 14 | return m.Build(dir, componentsPath, postgresSvc). 15 | WithExposedPort(3000). 16 | AsService() 17 | } 18 | 19 | func (m *Result) Build( 20 | dir *Directory, 21 | // +optional 22 | componentsPath *Directory, 23 | // +optional 24 | postgresSvc *Service, 25 | ) *Container { 26 | if componentsPath == nil { 27 | componentsPath = dir.Directory("components") 28 | } 29 | 30 | dapr := dag.Dapr().Dapr("result", DaprDaprOpts{ComponentsPath: componentsPath}) 31 | 32 | if postgresSvc != nil { 33 | dapr = dapr.WithServiceBinding("db", postgresSvc) 34 | } 35 | 36 | return dag.Go().WithSource(dir).Container(). 37 | WithServiceBinding("dapr", dapr.AsService()). 38 | WithEnvVariable("DAPR_GRPC_ENDPOINT", "dapr"). 39 | WithEntrypoint([]string{"go", "run", "main.go"}) 40 | } 41 | 42 | func (m *Result) Test( 43 | ctx context.Context, 44 | dir *Directory, 45 | ) (string, error) { 46 | return dag.Go().WithSource(dir).Container(). 47 | WithExec([]string{"go", "test", "-v", "./..."}).Stdout(ctx) 48 | } 49 | -------------------------------------------------------------------------------- /go/result/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/salaboy/example-voting-app/go/result 2 | 3 | go 1.21.6 4 | 5 | require ( 6 | github.com/dapr/go-sdk v1.9.1 7 | github.com/gin-gonic/gin v1.9.1 8 | github.com/googollee/go-socket.io v1.7.0 9 | ) 10 | 11 | require ( 12 | github.com/bytedance/sonic v1.9.1 // indirect 13 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 14 | github.com/dapr/dapr v1.12.0-rc.4 // indirect 15 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 16 | github.com/gin-contrib/sse v0.1.0 // indirect 17 | github.com/go-playground/locales v0.14.1 // indirect 18 | github.com/go-playground/universal-translator v0.18.1 // indirect 19 | github.com/go-playground/validator/v10 v10.14.0 // indirect 20 | github.com/goccy/go-json v0.10.2 // indirect 21 | github.com/gofrs/uuid v4.0.0+incompatible // indirect 22 | github.com/golang/protobuf v1.5.3 // indirect 23 | github.com/gomodule/redigo v1.8.4 // indirect 24 | github.com/google/uuid v1.5.0 // indirect 25 | github.com/gorilla/websocket v1.5.0 // indirect 26 | github.com/json-iterator/go v1.1.12 // indirect 27 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 28 | github.com/kr/pretty v0.3.1 // indirect 29 | github.com/leodido/go-urn v1.2.4 // indirect 30 | github.com/mattn/go-isatty v0.0.20 // indirect 31 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 32 | github.com/modern-go/reflect2 v1.0.2 // indirect 33 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 34 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 35 | github.com/ugorji/go/codec v1.2.11 // indirect 36 | golang.org/x/arch v0.3.0 // indirect 37 | golang.org/x/crypto v0.14.0 // indirect 38 | golang.org/x/net v0.17.0 // indirect 39 | golang.org/x/sys v0.15.0 // indirect 40 | golang.org/x/text v0.13.0 // indirect 41 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 // indirect 42 | google.golang.org/grpc v1.57.0 // indirect 43 | google.golang.org/protobuf v1.31.0 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /go/result/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | 9 | "time" 10 | 11 | dapr "github.com/dapr/go-sdk/client" 12 | "github.com/gin-gonic/gin" 13 | socketio "github.com/googollee/go-socket.io" 14 | ) 15 | 16 | var ( 17 | STATESTORE_RESULTS_NAME = "results-statestore" 18 | ) 19 | var conns = []socketio.Conn{} 20 | 21 | type Data struct { 22 | OptionA int `json:"optionA"` 23 | OptionB int `json:"optionB"` 24 | } 25 | 26 | type Result struct { 27 | Data Data `json:"data"` 28 | } 29 | 30 | type Score struct { 31 | A string `json:"a"` 32 | B string `json:"b"` 33 | } 34 | 35 | func main() { 36 | 37 | router := gin.New() 38 | 39 | server := socketio.NewServer(nil) 40 | 41 | server.OnConnect("/", func(s socketio.Conn) error { 42 | s.SetContext("") 43 | log.Println("connected:", s.ID()) 44 | 45 | conns = append(conns, s) 46 | 47 | return nil 48 | }) 49 | 50 | server.OnError("/", func(s socketio.Conn, e error) { 51 | log.Println("on error:", e) 52 | }) 53 | 54 | server.OnDisconnect("/", func(s socketio.Conn, msg string) { 55 | log.Println("closed", msg) 56 | }) 57 | 58 | go func() { 59 | if err := server.Serve(); err != nil { 60 | log.Fatalf("socketio listen error: %s\n", err) 61 | } 62 | }() 63 | defer server.Close() 64 | 65 | router.StaticFile("/css/style.css", "static/css/style.css") 66 | router.StaticFile("/app.js", "static/app.js") 67 | router.StaticFile("/socket.io.js", "static/socket.io.js") 68 | router.StaticFile("/angular.min.js", "static/angular.min.js") 69 | router.StaticFile("/", "static/index.html") 70 | 71 | router.GET("/socket.io/*any", gin.WrapH(server)) 72 | router.POST("/socket.io/*any", gin.WrapH(server)) 73 | 74 | go getVotes() 75 | 76 | if err := router.Run(":3000"); err != nil { 77 | log.Fatal("failed run app: ", err) 78 | } 79 | 80 | } 81 | 82 | func getVotes() { 83 | daprClient, err := dapr.NewClient() 84 | ctx := context.Background() 85 | if err != nil { 86 | panic(err) 87 | } 88 | for { 89 | time.Sleep(1 * time.Second) 90 | log.Printf("Getting votes... ") 91 | resultsState, err := daprClient.GetState(ctx, STATESTORE_RESULTS_NAME, "results", nil) 92 | if err != nil { 93 | log.Printf("An error occured while getting the results: %v", err) 94 | continue 95 | } 96 | results := &Data{} 97 | 98 | log.Printf("Result: %s", resultsState.Value) 99 | 100 | err = json.Unmarshal(resultsState.Value, results) 101 | if err != nil { 102 | log.Printf("There was an error decoding the results into the struct: %v", err) 103 | } 104 | 105 | var score = &Score{ 106 | A: fmt.Sprintf("%d", results.OptionA), 107 | B: fmt.Sprintf("%d", results.OptionB), 108 | } 109 | 110 | var jsonScore, _ = json.Marshal(score) 111 | 112 | log.Printf("JSON Score %s", jsonScore) 113 | 114 | for _, s := range conns { 115 | s.Emit("scores", string(jsonScore)) 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /go/result/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestResult(t *testing.T) { 6 | } 7 | -------------------------------------------------------------------------------- /go/result/static/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("catsvsdogs", []); 2 | console.log("Location Host: " + location.host); 3 | 4 | var socket = io.connect("//" + location.host, { 5 | transports: ["polling", "websocket"], 6 | path: "/socket.io/", 7 | }); 8 | 9 | var bg1 = document.getElementById("background-stats-1"); 10 | var bg2 = document.getElementById("background-stats-2"); 11 | 12 | app.controller("statsCtrl", function ($scope) { 13 | $scope.aPercent = 50; 14 | $scope.bPercent = 50; 15 | var updateScores = function () { 16 | socket.on("scores", function (json) { 17 | console.log("JSON update scores: " + json); 18 | data = JSON.parse(json); 19 | console.log("Getting scores" + data); 20 | var a = parseInt(data.a || 0); 21 | var b = parseInt(data.b || 0); 22 | 23 | var percentages = getPercentages(a, b); 24 | 25 | bg1.style.width = percentages.a + "%"; 26 | bg2.style.width = percentages.b + "%"; 27 | 28 | $scope.$apply(function () { 29 | $scope.aPercent = percentages.a; 30 | $scope.bPercent = percentages.b; 31 | $scope.total = a + b; 32 | }); 33 | }); 34 | }; 35 | 36 | var init = function () { 37 | document.body.style.opacity = 1; 38 | updateScores(); 39 | }; 40 | socket.on("connect", function (data) { 41 | init(); 42 | }); 43 | }); 44 | 45 | function getPercentages(a, b) { 46 | var result = {}; 47 | 48 | if (a + b > 0) { 49 | result.a = Math.round((a / (a + b)) * 100); 50 | result.b = 100 - result.a; 51 | } else { 52 | result.a = result.b = 50; 53 | } 54 | 55 | return result; 56 | } 57 | -------------------------------------------------------------------------------- /go/result/static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | *{ 4 | box-sizing:border-box; 5 | } 6 | html,body{ 7 | margin:0; 8 | padding:0; 9 | height:100%; 10 | font-family: 'Open Sans'; 11 | } 12 | body{ 13 | opacity:0; 14 | transition: all 1s linear; 15 | } 16 | 17 | .divider{ 18 | height: 150px; 19 | width:2px; 20 | background-color: #C0C9CE; 21 | position: relative; 22 | top: 50%; 23 | float: left; 24 | transform: translateY(-50%); 25 | } 26 | 27 | #background-stats-1{ 28 | background-color: #2196f3; 29 | } 30 | 31 | #background-stats-2{ 32 | background-color: #00cbca; 33 | } 34 | 35 | #content-container{ 36 | z-index:2; 37 | position:relative; 38 | margin:0 auto; 39 | display:table; 40 | padding:10px; 41 | max-width:940px; 42 | height:100%; 43 | } 44 | #content-container-center{ 45 | display:table-cell; 46 | text-align:center; 47 | vertical-align:middle; 48 | } 49 | #result{ 50 | z-index: 3; 51 | position: absolute; 52 | bottom: 40px; 53 | right: 20px; 54 | color: #fff; 55 | opacity: 0.5; 56 | font-size: 45px; 57 | font-weight: 600; 58 | } 59 | #choice{ 60 | transition: all 300ms linear; 61 | line-height:1.3em; 62 | background:#fff; 63 | box-shadow: 10px 0 0 #fff, -10px 0 0 #fff; 64 | vertical-align:middle; 65 | font-size:40px; 66 | font-weight: 600; 67 | width: 450px; 68 | height: 200px; 69 | } 70 | #choice a{ 71 | text-decoration:none; 72 | } 73 | #choice a:hover, #choice a:focus{ 74 | outline:0; 75 | text-decoration:underline; 76 | } 77 | 78 | #choice .choice{ 79 | width: 49%; 80 | position: relative; 81 | top: 50%; 82 | transform: translateY(-50%); 83 | text-align: left; 84 | padding-left: 50px; 85 | } 86 | 87 | #choice .choice .label{ 88 | text-transform: uppercase; 89 | } 90 | 91 | #choice .choice.dogs{ 92 | color: #00cbca; 93 | float: right; 94 | } 95 | 96 | #choice .choice.cats{ 97 | color: #2196f3; 98 | float: left; 99 | } 100 | #background-stats{ 101 | z-index:1; 102 | height:100%; 103 | width:100%; 104 | position:absolute; 105 | } 106 | #background-stats div{ 107 | transition: width 400ms ease-in-out; 108 | display:inline-block; 109 | margin-bottom:-4px; 110 | width:50%; 111 | height:100%; 112 | } 113 | -------------------------------------------------------------------------------- /go/result/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cats vs Dogs -- Result 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
Cats
24 |
{{aPercent | number:1}}%
25 |
26 |
27 |
28 |
Dogs
29 |
{{bPercent | number:1}}%
30 |
31 |
32 |
33 |
34 |
35 | No votes yet 36 | {{total}} vote 37 | {{total}} votes 38 |
39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /go/vote/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.21 4 | 5 | # Set destination for COPY 6 | WORKDIR / 7 | 8 | # Download Go modules 9 | COPY go.mod go.sum ./ 10 | RUN go mod download 11 | 12 | # Copy the source code. Note the slash at the end, as explained in 13 | # https://docs.docker.com/engine/reference/builder/#copy 14 | COPY *.go ./ 15 | COPY static/ ./static/ 16 | COPY templates/ ./templates/ 17 | 18 | # Build 19 | RUN CGO_ENABLED=0 GOOS=linux go build -o /main 20 | 21 | # Optional: 22 | # To bind to a TCP port, runtime parameters must be supplied to the docker command. 23 | # But we can document in the Dockerfile what ports 24 | # the application is going to listen on by default. 25 | # https://docs.docker.com/engine/reference/builder/#expose 26 | EXPOSE 8080 27 | 28 | # Run 29 | CMD ["/main"] -------------------------------------------------------------------------------- /go/vote/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/salaboy/example-voting-app/go/vote 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/dapr/go-sdk v1.9.1 7 | github.com/gofiber/fiber/v2 v2.52.0 8 | github.com/gofiber/template/html/v2 v2.1.0 9 | github.com/google/uuid v1.5.0 10 | ) 11 | 12 | require ( 13 | github.com/andybalholm/brotli v1.0.5 // indirect 14 | github.com/dapr/dapr v1.12.0-rc.4 // indirect 15 | github.com/gofiber/template v1.8.2 // indirect 16 | github.com/gofiber/utils v1.1.0 // indirect 17 | github.com/golang/protobuf v1.5.3 // indirect 18 | github.com/klauspost/compress v1.17.0 // indirect 19 | github.com/kr/pretty v0.3.1 // indirect 20 | github.com/mattn/go-colorable v0.1.13 // indirect 21 | github.com/mattn/go-isatty v0.0.20 // indirect 22 | github.com/mattn/go-runewidth v0.0.15 // indirect 23 | github.com/rivo/uniseg v0.2.0 // indirect 24 | github.com/valyala/bytebufferpool v1.0.0 // indirect 25 | github.com/valyala/fasthttp v1.51.0 // indirect 26 | github.com/valyala/tcplisten v1.0.0 // indirect 27 | golang.org/x/net v0.17.0 // indirect 28 | golang.org/x/sys v0.15.0 // indirect 29 | golang.org/x/text v0.13.0 // indirect 30 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 // indirect 31 | google.golang.org/grpc v1.57.0 // indirect 32 | google.golang.org/protobuf v1.31.0 // indirect 33 | gopkg.in/yaml.v3 v3.0.1 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /go/vote/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "math/rand" 8 | "os" 9 | "time" 10 | 11 | dapr "github.com/dapr/go-sdk/client" 12 | fiber "github.com/gofiber/fiber/v2" 13 | html "github.com/gofiber/template/html/v2" 14 | "github.com/google/uuid" 15 | ) 16 | 17 | var ( 18 | STATESTORE_NAME = "statestore" 19 | hostname = getHostname() 20 | option_a = getEnv("OPTION_A", "Cats") 21 | option_b = getEnv("OPTION_B", "Dogs") 22 | ) 23 | 24 | type Vote struct { 25 | Type string `json:"type"` 26 | VoterId string `json:"voterId"` 27 | Option string `json:"option"` 28 | } 29 | 30 | func main() { 31 | 32 | engine := html.New("./templates", ".html") 33 | 34 | app := fiber.New(fiber.Config{ 35 | Views: engine, 36 | }) 37 | 38 | app.Static("/", "./static/") 39 | 40 | app.Post("/", serve) 41 | app.Get("/", serve) 42 | 43 | app.Listen(":3000") 44 | } 45 | 46 | func serve(c *fiber.Ctx) error { 47 | rand.Seed(time.Now().UnixMicro()) 48 | voterId := c.Cookies("voter_id") 49 | if voterId == "" { 50 | voterId = uuid.New().String()[0:10] 51 | } 52 | 53 | vote := "None" 54 | 55 | if c.Method() == "POST" { 56 | vote = c.FormValue("vote") 57 | 58 | log.Printf("Got a vote from %s, %s", voterId, vote) 59 | daprClient, err := dapr.NewClient() 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | ctx := context.Background() 65 | vote := &Vote{ 66 | Type: "vote", 67 | VoterId: voterId, 68 | Option: vote, 69 | } 70 | 71 | jsonData, err := json.Marshal(vote) 72 | if err != nil { 73 | log.Printf("An error occured while marshalling vote to json: %v", err) 74 | } 75 | 76 | err = daprClient.SaveState(ctx, STATESTORE_NAME, "voter-"+voterId, jsonData, map[string]string{ 77 | "contentType": "application/json", 78 | }) 79 | if err != nil { 80 | log.Printf("An error occured while storing the vote: %v", err) 81 | } 82 | } 83 | 84 | return c.Render("index", fiber.Map{ 85 | "option_a": option_a, 86 | "option_b": option_b, 87 | "hostname": hostname, 88 | "vote": vote, 89 | }) 90 | } 91 | 92 | // getEnv returns the value of an environment variable, or a fallback value if it is not set. 93 | func getEnv(key, fallback string) string { 94 | value, exists := os.LookupEnv(key) 95 | if !exists { 96 | value = fallback 97 | } 98 | return value 99 | } 100 | 101 | func getHostname() string { 102 | hostname, err := os.Hostname() 103 | if err != nil { 104 | hostname = "N/A" 105 | } 106 | return hostname 107 | } 108 | -------------------------------------------------------------------------------- /go/vote/static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | *{ 4 | box-sizing:border-box; 5 | } 6 | html,body{ 7 | margin: 0; 8 | padding: 0; 9 | background-color: #F7F8F9; 10 | height: 100vh; 11 | font-family: 'Open Sans'; 12 | } 13 | 14 | button{ 15 | border-radius: 0; 16 | width: 100%; 17 | height: 50%; 18 | } 19 | 20 | button[type="submit"] { 21 | -webkit-appearance:none; -webkit-border-radius:0; 22 | } 23 | 24 | button i{ 25 | float: right; 26 | padding-right: 30px; 27 | margin-top: 3px; 28 | } 29 | 30 | button.a{ 31 | background-color: #1aaaf8; 32 | } 33 | 34 | button.b{ 35 | background-color: #00cbca; 36 | } 37 | 38 | #tip{ 39 | text-align: left; 40 | color: #c0c9ce; 41 | font-size: 14px; 42 | } 43 | 44 | #hostname{ 45 | position: absolute; 46 | bottom: 100px; 47 | right: 0; 48 | left: 0; 49 | color: #8f9ea8; 50 | font-size: 24px; 51 | } 52 | 53 | #content-container{ 54 | z-index: 2; 55 | position: relative; 56 | margin: 0 auto; 57 | display: table; 58 | padding: 10px; 59 | max-width: 940px; 60 | height: 100%; 61 | } 62 | #content-container-center{ 63 | display: table-cell; 64 | text-align: center; 65 | } 66 | 67 | #content-container-center h3{ 68 | color: #254356; 69 | } 70 | 71 | #choice{ 72 | transition: all 300ms linear; 73 | line-height: 1.3em; 74 | display: inline; 75 | vertical-align: middle; 76 | font-size: 3em; 77 | } 78 | #choice a{ 79 | text-decoration:none; 80 | } 81 | #choice a:hover, #choice a:focus{ 82 | outline:0; 83 | text-decoration:underline; 84 | } 85 | 86 | #choice button{ 87 | display: block; 88 | height: 80px; 89 | width: 330px; 90 | border: none; 91 | color: white; 92 | text-transform: uppercase; 93 | font-size:18px; 94 | font-weight: 700; 95 | margin-top: 10px; 96 | margin-bottom: 10px; 97 | text-align: left; 98 | padding-left: 50px; 99 | } 100 | 101 | #choice button.a:hover{ 102 | background-color: #1488c6; 103 | } 104 | 105 | #choice button.b:hover{ 106 | background-color: #00a2a1; 107 | } 108 | 109 | #choice button.a:focus{ 110 | background-color: #1488c6; 111 | } 112 | 113 | #choice button.b:focus{ 114 | background-color: #00a2a1; 115 | } 116 | 117 | #background-stats{ 118 | z-index:1; 119 | height:100%; 120 | width:100%; 121 | position:absolute; 122 | } 123 | #background-stats div{ 124 | transition: width 400ms ease-in-out; 125 | display:inline-block; 126 | margin-bottom:-4px; 127 | width:50%; 128 | height:100%; 129 | } 130 | -------------------------------------------------------------------------------- /go/vote/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.option_a}} vs {{.option_b}}! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

{{.option_a}} vs {{.option_b}}!

17 |
18 | 19 | 20 |
21 |
22 | (Tip: you can change your vote) 23 |
24 |
25 | Processed by container ID {{.hostname}} 26 |
27 |
28 |
29 | 30 | 31 | 32 | {% if vote %} 33 | 47 | {% endif %} 48 | 49 | 50 | -------------------------------------------------------------------------------- /go/worker/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.21 4 | 5 | # Set destination for COPY 6 | WORKDIR / 7 | 8 | # Download Go modules 9 | COPY go.mod go.sum ./ 10 | RUN go mod download 11 | 12 | # Copy the source code. Note the slash at the end, as explained in 13 | # https://docs.docker.com/engine/reference/builder/#copy 14 | COPY *.go ./ 15 | 16 | # Build 17 | RUN CGO_ENABLED=0 GOOS=linux go build -o /main 18 | 19 | # Optional: 20 | # To bind to a TCP port, runtime parameters must be supplied to the docker command. 21 | # But we can document in the Dockerfile what ports 22 | # the application is going to listen on by default. 23 | # https://docs.docker.com/engine/reference/builder/#expose 24 | EXPOSE 8080 25 | 26 | # Run 27 | CMD ["/main"] -------------------------------------------------------------------------------- /go/worker/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/salaboy/example-voting-app/go/worker 2 | 3 | go 1.21.6 4 | 5 | require ( 6 | github.com/dapr/go-sdk v1.9.1 7 | github.com/gofiber/fiber/v2 v2.52.0 8 | ) 9 | 10 | require ( 11 | github.com/andybalholm/brotli v1.0.5 // indirect 12 | github.com/dapr/dapr v1.12.0-rc.4 // indirect 13 | github.com/golang/protobuf v1.5.3 // indirect 14 | github.com/google/uuid v1.5.0 // indirect 15 | github.com/klauspost/compress v1.17.0 // indirect 16 | github.com/kr/pretty v0.3.1 // indirect 17 | github.com/mattn/go-colorable v0.1.13 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/mattn/go-runewidth v0.0.15 // indirect 20 | github.com/rivo/uniseg v0.2.0 // indirect 21 | github.com/valyala/bytebufferpool v1.0.0 // indirect 22 | github.com/valyala/fasthttp v1.51.0 // indirect 23 | github.com/valyala/tcplisten v1.0.0 // indirect 24 | golang.org/x/net v0.17.0 // indirect 25 | golang.org/x/sys v0.15.0 // indirect 26 | golang.org/x/text v0.13.0 // indirect 27 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 // indirect 28 | google.golang.org/grpc v1.57.0 // indirect 29 | google.golang.org/protobuf v1.31.0 // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /go/worker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "time" 11 | 12 | dapr "github.com/dapr/go-sdk/client" 13 | "github.com/gofiber/fiber/v2" 14 | ) 15 | 16 | var ( 17 | STATESTORE_VOTES_NAME = "statestore" 18 | STATESTORE_RESULTS_NAME = "results" 19 | ) 20 | 21 | type Post struct { 22 | Results []Data `json:"results"` 23 | } 24 | 25 | type Data struct { 26 | Vote Vote `json:"data"` 27 | } 28 | 29 | type Vote struct { 30 | Option string `json:"option"` 31 | Type string `json:"type"` 32 | VoterId string `json:"voterId"` 33 | } 34 | 35 | type Results struct { 36 | OptionA int `json:"optionA"` 37 | OptionB int `json:"optionB"` 38 | } 39 | 40 | func query() { 41 | for { 42 | time.Sleep(2 * time.Second) 43 | // HTTP endpoint 44 | queryurl := "http://localhost:3500/v1.0-alpha1/state/" + STATESTORE_VOTES_NAME + "/query?metadata.contentType=application/json&metadata.queryIndexName=voteIndex" 45 | 46 | // JSON body 47 | body := []byte(`{ 48 | "filter": { 49 | "EQ": { "type": "vote" } 50 | }, 51 | "sort": [ 52 | { 53 | "key": "type", 54 | "order": "DESC" 55 | } 56 | ] 57 | }`) 58 | 59 | // Create a HTTP post request 60 | r, err := http.NewRequest("POST", queryurl, bytes.NewBuffer(body)) 61 | r.Header.Add("Content-Type", "application/json") 62 | r.Header.Add("dapr-app-id", "worker ") 63 | if err != nil { 64 | log.Printf("An error occured creating the request: %v", err) 65 | } 66 | 67 | client := &http.Client{} 68 | res, err := client.Do(r) 69 | if err != nil { 70 | log.Printf("An error sending request: %v", err) 71 | } 72 | 73 | post := &Post{} 74 | derr := json.NewDecoder(res.Body).Decode(post) 75 | if derr != nil { 76 | log.Printf("An error decoding the body: %v", err) 77 | } 78 | 79 | defer res.Body.Close() 80 | 81 | fmt.Println("Code", res.StatusCode) 82 | fmt.Println("Body:", post) 83 | 84 | var optionACount = 0 85 | var optionBCount = 0 86 | for _, r := range post.Results { 87 | fmt.Println("Option:", r.Vote.Option) 88 | if r.Vote.Option == "a" { 89 | optionACount = optionACount + 1 90 | } else if r.Vote.Option == "b" { 91 | optionBCount = optionBCount + 1 92 | } 93 | 94 | } 95 | 96 | results := &Results{ 97 | OptionA: optionACount, 98 | OptionB: optionBCount, 99 | } 100 | 101 | jsonDataResult, err := json.Marshal(results) 102 | if err != nil { 103 | log.Printf("An error occured while marshalling results to json: %v", err) 104 | } 105 | 106 | ctx := context.Background() 107 | 108 | daprClient, err := dapr.NewClient() 109 | if err != nil { 110 | panic(err) 111 | } 112 | 113 | // metadata, err := daprClient.GetMetadata(ctx); 114 | // for _, c := range metadata.RegisteredComponents { 115 | // c.Capabilities 116 | // c.Name, 117 | // c.Type, 118 | 119 | // } 120 | 121 | err = daprClient.SaveState(ctx, STATESTORE_VOTES_NAME, "results", jsonDataResult, map[string]string{ 122 | "contentType": "application/json", 123 | }) 124 | if err != nil { 125 | log.Printf("An error occured while storing the vote: %v", err) 126 | } 127 | 128 | } 129 | 130 | } 131 | 132 | func main() { 133 | 134 | app := fiber.New() 135 | 136 | app.Get("/", func(c *fiber.Ctx) error { 137 | return c.SendString("Hello, World!") 138 | }) 139 | 140 | go query() 141 | 142 | log.Fatal(app.Listen(":3000")) 143 | 144 | } 145 | -------------------------------------------------------------------------------- /go/worker/query.json: -------------------------------------------------------------------------------- 1 | { 2 | "filter": { 3 | "EQ": { "type": "vote" } 4 | }, 5 | "sort": [ 6 | { 7 | "key": "type", 8 | "order": "DESC" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /healthchecks/postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | host="$(hostname -i || echo '127.0.0.1')" 5 | user="${POSTGRES_USER:-postgres}" 6 | db="${POSTGRES_DB:-$POSTGRES_USER}" 7 | export PGPASSWORD="${POSTGRES_PASSWORD:-}" 8 | 9 | args=( 10 | # force postgres to not use the local unix socket (test "external" connectibility) 11 | --host "$host" 12 | --username "$user" 13 | --dbname "$db" 14 | --quiet --no-align --tuples-only 15 | ) 16 | 17 | if select="$(echo 'SELECT 1' | psql "${args[@]}")" && [ "$select" = '1' ]; then 18 | exit 0 19 | fi 20 | 21 | exit 1 22 | -------------------------------------------------------------------------------- /healthchecks/redis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eo pipefail 3 | 4 | host="$(hostname -i || echo '127.0.0.1')" 5 | 6 | if ping="$(redis-cli -h "$host" ping)" && [ "$ping" = 'PONG' ]; then 7 | exit 0 8 | fi 9 | 10 | exit 1 11 | -------------------------------------------------------------------------------- /java/echo/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /java/echo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/eclipse-temurin:21-jdk-jammy AS builder 2 | WORKDIR workspace 3 | ARG JAR_FILE=target/*.jar 4 | COPY ${JAR_FILE} app.jar 5 | RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted 6 | 7 | FROM docker.io/library/eclipse-temurin:21-jre-jammy 8 | RUN useradd spring 9 | USER spring 10 | WORKDIR workspace 11 | COPY --from=builder workspace/extracted/dependencies/ ./ 12 | COPY --from=builder workspace/extracted/spring-boot-loader/ ./ 13 | COPY --from=builder workspace/extracted/snapshot-dependencies/ ./ 14 | COPY --from=builder workspace/extracted/application/ ./ 15 | ENTRYPOINT ["java", "-jar", "app.jar"] -------------------------------------------------------------------------------- /java/echo/ce.json: -------------------------------------------------------------------------------- 1 | { 2 | "cloudEventsVersion": "0.1", 3 | "eventID": "6480da1a-5028-4301-acc3-fbae628207b3", 4 | "source": "http://example.com/repomanager", 5 | "eventType": "com.example.repro.create", 6 | "eventTypeVersion": "v1.5", 7 | "eventTime": "2018-04-01T23:12:34Z", 8 | "schemaURL": "https://product.example.com/schema/repo-create", 9 | "contentType": "application/json", 10 | "data": { 11 | "option": "a" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /java/echo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.0 9 | 10 | 11 | com.salaboy 12 | echo 13 | 0.0.1-SNAPSHOT 14 | echo 15 | Echo Service 16 | 17 | 21 18 | 0.11.0-SNAPSHOT 19 | 0.11.0 20 | 21 | 22 | 23 | io.diagrid.dapr 24 | dapr-client-spring-boot-starter 25 | ${daprSpringBoot.version} 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-actuator 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-websocket 38 | 39 | 40 | io.dapr 41 | dapr-sdk-workflows 42 | ${daprWorkflows.version} 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-test 47 | test 48 | 49 | 50 | 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-maven-plugin 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /java/echo/src/main/java/com/salaboy/echo/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.echo.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration(proxyBeanMethods = false) 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void configureMessageBroker(MessageBrokerRegistry config) { 15 | config.enableSimpleBroker("/topic"); 16 | config.setApplicationDestinationPrefixes("/app"); 17 | } 18 | 19 | @Override 20 | public void registerStompEndpoints(StompEndpointRegistry registry) { 21 | registry.addEndpoint("/websocket"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /java/echo/src/main/java/com/salaboy/echo/workflow/PrizeWorkflow.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.echo.workflow; 2 | 3 | import io.dapr.workflows.Workflow; 4 | import io.dapr.workflows.WorkflowStub; 5 | 6 | import java.time.Duration; 7 | 8 | import org.slf4j.Logger; 9 | 10 | public class PrizeWorkflow extends Workflow { 11 | @Override 12 | public WorkflowStub create() { 13 | return ctx -> { 14 | Logger logger = ctx.getLogger(); 15 | String instanceId = ctx.getInstanceId(); 16 | logger.info("Starting Workflow: " + ctx.getName()); 17 | logger.info("Instance ID: " + instanceId); 18 | logger.info("Current Orchestration Time: " + ctx.getCurrentInstant()); 19 | 20 | WorkflowPayload workflowPayload = ctx.getInput(WorkflowPayload.class); 21 | workflowPayload.setWorkflowId(instanceId); 22 | 23 | workflowPayload = ctx.callActivity(PickAWinnerActivity.class.getName(), workflowPayload, WorkflowPayload.class).await(); 24 | 25 | logger.info("Current Orchestration Time: " + ctx.getCurrentInstant()); 26 | 27 | logger.info("A Winner was selected: " + workflowPayload.getWinner()); 28 | logger.info("Let's see if the winner is in the audience!"); 29 | 30 | boolean winnerIsInTheAudience = ctx.waitForExternalEvent("WinnerInTheAudience", Duration.ofMinutes(5), boolean.class).await(); 31 | logger.info("Is the Winner in the Audience? " + winnerIsInTheAudience); 32 | if(!winnerIsInTheAudience){ 33 | ctx.complete(workflowPayload); 34 | return; 35 | }else{ 36 | workflowPayload.setWinnerInTheAudience(winnerIsInTheAudience); 37 | } 38 | logger.info("The winner is in the audience! Let's see if we can deliver the book to the winner!"); 39 | boolean winnerGotTheBook = ctx.waitForExternalEvent("WinnerGotTheBook", Duration.ofMinutes(5), boolean.class).await(); 40 | logger.info("Did the winner got the book? " + winnerIsInTheAudience); 41 | if(!winnerGotTheBook){ 42 | ctx.complete(workflowPayload); 43 | return; 44 | }else{ 45 | workflowPayload.setWinnerGotTheBook(winnerGotTheBook); 46 | } 47 | logger.info("The winner got the book copy!"); 48 | logger.info("Let's proceed to store the winner in the hall of fame!"); 49 | workflowPayload = ctx.callActivity(StoreWinnerActivity.class.getName(), workflowPayload, WorkflowPayload.class).await(); 50 | 51 | logger.info("The winner is now and forever in the hall of fame!"); 52 | 53 | ctx.complete(workflowPayload); 54 | 55 | }; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /java/echo/src/main/java/com/salaboy/echo/workflow/StoreWinnerActivity.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.echo.workflow; 2 | 3 | import io.dapr.workflows.runtime.WorkflowActivity; 4 | import io.dapr.workflows.runtime.WorkflowActivityContext; 5 | 6 | public class StoreWinnerActivity implements WorkflowActivity{ 7 | 8 | @Override 9 | public Object run(WorkflowActivityContext ctx) { 10 | WorkflowPayload workflowPayload = ctx.getInput(WorkflowPayload.class); 11 | //Store winner using the Dapr SDKs 12 | 13 | return ""; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /java/echo/src/main/java/com/salaboy/echo/workflow/WorkflowPayload.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.echo.workflow; 2 | 3 | public class WorkflowPayload { 4 | private String workflowId; 5 | private String winner; 6 | private String option; 7 | private boolean winnerInTheAudience = false; 8 | private boolean winnerGotTheBook = false; 9 | 10 | public WorkflowPayload(String workflowId, String winner, String option, boolean winnerInTheAudience, 11 | boolean winnerGotTheBook) { 12 | this.workflowId = workflowId; 13 | this.winner = winner; 14 | this.option = option; 15 | this.winnerInTheAudience = winnerInTheAudience; 16 | this.winnerGotTheBook = winnerGotTheBook; 17 | } 18 | 19 | public WorkflowPayload() { 20 | } 21 | 22 | public WorkflowPayload(String option) { 23 | this.option = option; 24 | } 25 | 26 | public void setWorkflowId(String workflowId) { 27 | this.workflowId = workflowId; 28 | } 29 | 30 | public String getWorkflowId(){ 31 | return this.workflowId; 32 | } 33 | 34 | public String getWinner() { 35 | return winner; 36 | } 37 | 38 | public void setWinner(String winner) { 39 | this.winner = winner; 40 | } 41 | 42 | public String getOption() { 43 | return option; 44 | } 45 | 46 | public void setOption(String option) { 47 | this.option = option; 48 | } 49 | 50 | public boolean isWinnerInTheAudience() { 51 | return winnerInTheAudience; 52 | } 53 | 54 | public void setWinnerInTheAudience(boolean winnerInTheAudience) { 55 | this.winnerInTheAudience = winnerInTheAudience; 56 | } 57 | 58 | public boolean isWinnerGotTheBook() { 59 | return winnerGotTheBook; 60 | } 61 | 62 | public void setWinnerGotTheBook(boolean winnerGotTheBook) { 63 | this.winnerGotTheBook = winnerGotTheBook; 64 | } 65 | 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /java/echo/src/main/resources/static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | html, 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | background-color: #f7f8f9; 11 | height: 100vh; 12 | font-family: "Open Sans"; 13 | background-image: linear-gradient( 14 | to right top, 15 | #d16ba5, 16 | #c777b9, 17 | #ba83ca, 18 | #aa8fd8, 19 | #9a9ae1, 20 | #8aa7ec, 21 | #79b3f4, 22 | #69bff8, 23 | #52cffe, 24 | #41dfff, 25 | #46eefa, 26 | #5ffbf1 27 | ); 28 | } 29 | 30 | button { 31 | position: relative; 32 | padding: 10px 20px; 33 | border-radius: 400px; 34 | width: 150px; 35 | height: 150px; 36 | font-size: 80px; 37 | text-transform: uppercase; 38 | font-weight: 600; 39 | letter-spacing: 2px; 40 | background: #fff; 41 | box-shadow: 0px 10px 15px -3px rgba(0, 0, 0, 0.25); 42 | border: 10px solid #46eefa; 43 | -webkit-transition: all 0.2s ease-in; 44 | -moz-transition: all 0.2s ease-in; 45 | transition: all 0.2s ease-in; 46 | } 47 | 48 | button:hover { 49 | transform: scale(1.2); 50 | box-shadow: 0px 20px 45px -3px rgba(0, 0, 0, 0.25); 51 | cursor: pointer; 52 | -webkit-transition: all 0.2s ease-out; 53 | -moz-transition: all 0.2s ease-out; 54 | transition: all 0.2s ease-out; 55 | } 56 | 57 | button.smaller { 58 | font-size: 42px; 59 | } 60 | 61 | #tip { 62 | text-align: left; 63 | color: #c0c9ce; 64 | font-size: 14px; 65 | } 66 | 67 | #hostname { 68 | position: absolute; 69 | bottom: 100px; 70 | right: 0; 71 | left: 0; 72 | color: #8f9ea8; 73 | font-size: 24px; 74 | } 75 | 76 | #content-container { 77 | z-index: 2; 78 | position: relative; 79 | margin: 0 auto; 80 | display: table; 81 | padding: 10px; 82 | width: 100%; 83 | max-width: 940px; 84 | height: 100%; 85 | position: relative; 86 | } 87 | #content-container-center { 88 | display: table-cell; 89 | text-align: center; 90 | } 91 | 92 | #content-container-center h3 { 93 | color: #254356; 94 | } 95 | 96 | #choice { 97 | transition: all 300ms linear; 98 | line-height: 1.3em; 99 | display: inline; 100 | vertical-align: middle; 101 | font-size: 3em; 102 | } 103 | #choice a { 104 | text-decoration: none; 105 | } 106 | #choice a:hover, 107 | #choice a:focus { 108 | outline: 0; 109 | text-decoration: underline; 110 | } 111 | 112 | #choice button { 113 | display: block; 114 | border: none; 115 | color: white; 116 | text-transform: uppercase; 117 | font-size: 18px; 118 | font-weight: 700; 119 | margin-top: 10px; 120 | margin-bottom: 10px; 121 | text-align: left; 122 | padding-left: 50px; 123 | } 124 | 125 | #background-stats { 126 | z-index: 1; 127 | height: 100%; 128 | width: 100%; 129 | position: absolute; 130 | } 131 | #background-stats div { 132 | transition: width 400ms ease-in-out; 133 | display: inline-block; 134 | margin-bottom: -4px; 135 | width: 50%; 136 | height: 100%; 137 | } 138 | 139 | button#cats { 140 | position: absolute; 141 | top: 20px; 142 | left: 20px; 143 | } 144 | 145 | button#dogs { 146 | position: absolute; 147 | top: 20px; 148 | right: 20px; 149 | } 150 | 151 | .div-1 { 152 | position: relative; 153 | text-align: center; 154 | margin: auto; 155 | width: 100%; 156 | } 157 | 158 | .bottomBar { 159 | position: absolute; 160 | bottom: 20px; 161 | left: 0; 162 | width: 100%; 163 | padding: 20px; 164 | display: flex; 165 | align-items: center; 166 | justify-content: space-between; 167 | } 168 | -------------------------------------------------------------------------------- /java/echo/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Votes Live! 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 34 | 42 | 49 | 56 |
57 |
58 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /java/model/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /java/model/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | com.salaboy 8 | model 9 | 0.0.1-SNAPSHOT 10 | 11 | model 12 | 13 | http://www.example.com 14 | 15 | 16 | UTF-8 17 | 21 18 | 21 19 | 20 | 21 | 22 | 23 | org.springframework.data 24 | spring-data-commons 25 | 3.3.0 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-test 30 | 3.3.0 31 | test 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | maven-clean-plugin 41 | 3.1.0 42 | 43 | 44 | 45 | maven-resources-plugin 46 | 3.0.2 47 | 48 | 49 | maven-compiler-plugin 50 | 3.8.0 51 | 52 | 53 | maven-surefire-plugin 54 | 2.22.1 55 | 56 | 57 | maven-jar-plugin 58 | 3.0.2 59 | 60 | 61 | maven-install-plugin 62 | 2.5.2 63 | 64 | 65 | maven-deploy-plugin 66 | 2.8.2 67 | 68 | 69 | 70 | maven-site-plugin 71 | 3.7.1 72 | 73 | 74 | maven-project-info-reports-plugin 75 | 3.0.0 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /java/model/src/main/java/com/salaboy/model/Results.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.model; 2 | 3 | import org.springframework.data.annotation.Id; 4 | 5 | public class Results { 6 | 7 | @Id 8 | private String id; 9 | private Integer optionA; 10 | private Integer optionB; 11 | 12 | 13 | public Results() { 14 | } 15 | 16 | 17 | public Results(String id, Integer optionA, Integer optionB) { 18 | this.id = id; 19 | this.optionA = optionA; 20 | this.optionB = optionB; 21 | } 22 | 23 | 24 | public Results(Integer optionA, Integer optionB) { 25 | this.optionA = optionA; 26 | this.optionB = optionB; 27 | } 28 | 29 | 30 | public Integer getOptionA() { 31 | return optionA; 32 | } 33 | public void setOptionA(Integer optionA) { 34 | this.optionA = optionA; 35 | } 36 | public Integer getOptionB() { 37 | return optionB; 38 | } 39 | public void setOptionB(Integer optionB) { 40 | this.optionB = optionB; 41 | } 42 | 43 | 44 | public String getId() { 45 | return id; 46 | } 47 | 48 | 49 | public void setId(String id) { 50 | this.id = id; 51 | } 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /java/model/src/main/java/com/salaboy/model/Score.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.model; 2 | 3 | public class Score { 4 | private String a; 5 | private String b; 6 | 7 | 8 | public Score(String a, String b) { 9 | this.a = a; 10 | this.b = b; 11 | } 12 | public String getA() { 13 | return a; 14 | } 15 | public void setA(String a) { 16 | this.a = a; 17 | } 18 | public String getB() { 19 | return b; 20 | } 21 | public void setB(String b) { 22 | this.b = b; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /java/model/src/main/java/com/salaboy/model/Vote.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.model; 2 | 3 | import org.springframework.data.annotation.Id; 4 | 5 | public class Vote { 6 | 7 | @Id 8 | private String voterId; 9 | private String option; 10 | private String type; 11 | private String user; 12 | 13 | 14 | public Vote() { 15 | } 16 | 17 | public Vote(String type, String voterId, String option, String user) { 18 | this.option = option; 19 | this.type = type; 20 | this.voterId = voterId; 21 | this.user = user; 22 | } 23 | public String getOption() { 24 | return option; 25 | } 26 | public void setOption(String option) { 27 | this.option = option; 28 | } 29 | public String getType() { 30 | return type; 31 | } 32 | public void setType(String type) { 33 | this.type = type; 34 | } 35 | public String getVoterId() { 36 | return voterId; 37 | } 38 | public void setVoterId(String voterId) { 39 | this.voterId = voterId; 40 | } 41 | 42 | public String getUser() { 43 | return user; 44 | } 45 | 46 | public void setUser(String user) { 47 | this.user = user; 48 | } 49 | 50 | 51 | 52 | 53 | } 54 | -------------------------------------------------------------------------------- /java/result/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /java/result/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/eclipse-temurin:21-jdk-jammy AS builder 2 | WORKDIR workspace 3 | ARG JAR_FILE=target/*.jar 4 | COPY ${JAR_FILE} app.jar 5 | RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted 6 | 7 | FROM docker.io/library/eclipse-temurin:21-jre-jammy 8 | RUN useradd spring 9 | USER spring 10 | WORKDIR workspace 11 | COPY --from=builder workspace/extracted/dependencies/ ./ 12 | COPY --from=builder workspace/extracted/spring-boot-loader/ ./ 13 | COPY --from=builder workspace/extracted/snapshot-dependencies/ ./ 14 | COPY --from=builder workspace/extracted/application/ ./ 15 | ENTRYPOINT ["java", "-jar", "app.jar"] -------------------------------------------------------------------------------- /java/result/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.0 9 | 10 | 11 | com.salaboy 12 | result 13 | 0.0.1-SNAPSHOT 14 | result 15 | Result Service 16 | 17 | 21 18 | 0.11.0-SNAPSHOT 19 | 0.10.14 20 | 21 | 22 | 23 | com.salaboy 24 | model 25 | 0.0.1-SNAPSHOT 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-websocket 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-actuator 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-web 38 | 39 | 40 | io.diagrid.dapr 41 | dapr-client-spring-boot-starter 42 | ${daprSpringBoot.version} 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-test 48 | test 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-testcontainers 53 | test 54 | 55 | 56 | org.testcontainers 57 | junit-jupiter 58 | test 59 | 60 | 61 | io.diagrid.dapr 62 | testcontainers-dapr 63 | ${testcontainers-dapr.version} 64 | 65 | 66 | com.redis.testcontainers 67 | testcontainers-redis-junit 68 | 1.6.4 69 | test 70 | 71 | 72 | 73 | 74 | 75 | 76 | org.springframework.boot 77 | spring-boot-maven-plugin 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /java/result/src/main/java/com/salaboy/result/FetchResultsJob.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.result; 2 | 3 | import com.salaboy.model.Results; 4 | import com.salaboy.model.Score; 5 | import io.diagrid.spring.core.keyvalue.DaprKeyValueTemplate; 6 | import org.springframework.messaging.simp.SimpMessagingTemplate; 7 | import org.springframework.scheduling.annotation.Scheduled; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class FetchResultsJob { 12 | 13 | private final DaprKeyValueTemplate keyValueTemplate; 14 | 15 | private final SimpMessagingTemplate simpMessagingTemplate; 16 | 17 | public FetchResultsJob(DaprKeyValueTemplate keyValueTemplate, SimpMessagingTemplate simpMessagingTemplate) { 18 | this.simpMessagingTemplate = simpMessagingTemplate; 19 | this.keyValueTemplate = keyValueTemplate; 20 | } 21 | 22 | @Scheduled(fixedDelay = 1000) 23 | public void fetchResults() { 24 | Results results = keyValueTemplate.findById("results", Results.class).get(); 25 | 26 | System.out.println("Fetching results: a: "+ results.getOptionA() + " - b: "+ results.getOptionB()); 27 | 28 | Score score = new Score(String.valueOf(results.getOptionA()), String.valueOf(results.getOptionB())); 29 | try { 30 | simpMessagingTemplate.convertAndSend("/topic/scores",score); 31 | } catch(Exception e) { 32 | e.printStackTrace(); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /java/result/src/main/java/com/salaboy/result/ResultApplication.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.result; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @SpringBootApplication 8 | @EnableScheduling 9 | public class ResultApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(ResultApplication.class, args); 13 | } 14 | 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /java/result/src/main/java/com/salaboy/result/ResultController.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.result; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class ResultController { 8 | 9 | @GetMapping("/") 10 | String renderHTML() { 11 | return "index.html"; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /java/result/src/main/java/com/salaboy/result/WebSocketsConfig.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.result; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration(proxyBeanMethods = false) 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketsConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void configureMessageBroker(MessageBrokerRegistry config) { 15 | config.enableSimpleBroker("/topic"); 16 | config.setApplicationDestinationPrefixes("/app"); 17 | } 18 | 19 | @Override 20 | public void registerStompEndpoints(StompEndpointRegistry registry) { 21 | registry.addEndpoint("/websocket"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /java/result/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: result 4 | dapr: 5 | statestore: 6 | name: kvstore 7 | query-index: MyQueryIndex 8 | -------------------------------------------------------------------------------- /java/result/src/main/resources/static/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("catsvsdogs", []); 2 | console.log("Location Host: " + location.host); 3 | 4 | 5 | var bg1 = document.getElementById("background-stats-1"); 6 | var bg2 = document.getElementById("background-stats-2"); 7 | 8 | var url = "" 9 | if (location.protocol === "http:"){ 10 | url = 'ws://'+location.host+'/websocket' 11 | }else{ 12 | url = 'wss://'+location.host+'/websocket' 13 | } 14 | 15 | app.controller("statsCtrl", function ($scope) { 16 | const stompClient = new StompJs.Client({ 17 | brokerURL: url 18 | }); 19 | 20 | connect(); 21 | 22 | stompClient.onConnect = (frame) => { 23 | console.log('Connected: ' + frame); 24 | init(); 25 | 26 | stompClient.subscribe('/topic/scores', (scores) => { 27 | 28 | //showGreeting(JSON.parse(greeting.body).content); 29 | console.log("JSON update scores: " + scores.body); 30 | data = JSON.parse(scores.body); 31 | console.log("Getting scores" + data); 32 | var a = parseInt(data.a || 0); 33 | var b = parseInt(data.b || 0); 34 | 35 | var percentages = getPercentages(a, b); 36 | 37 | bg1.style.width = percentages.a + "%"; 38 | bg2.style.width = percentages.b + "%"; 39 | 40 | $scope.$apply(function () { 41 | $scope.aPercent = percentages.a; 42 | $scope.bPercent = percentages.b; 43 | $scope.total = a + b; 44 | }); 45 | }); 46 | 47 | }; 48 | 49 | stompClient.onWebSocketError = (error) => { 50 | console.error('Error with websocket', error); 51 | }; 52 | 53 | stompClient.onStompError = (frame) => { 54 | console.error('Broker reported error: ' + frame.headers['message']); 55 | console.error('Additional details: ' + frame.body); 56 | }; 57 | 58 | 59 | 60 | function connect() { 61 | stompClient.activate(); 62 | } 63 | 64 | function disconnect() { 65 | stompClient.deactivate(); 66 | } 67 | 68 | $scope.aPercent = 50; 69 | $scope.bPercent = 50; 70 | 71 | 72 | var init = function () { 73 | document.body.style.opacity = 1; 74 | }; 75 | 76 | 77 | 78 | 79 | }); 80 | 81 | function getPercentages(a, b) { 82 | var result = {}; 83 | 84 | if (a + b > 0) { 85 | result.a = Math.round((a / (a + b)) * 100); 86 | result.b = 100 - result.a; 87 | } else { 88 | result.a = result.b = 50; 89 | } 90 | 91 | return result; 92 | } 93 | -------------------------------------------------------------------------------- /java/result/src/main/resources/static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | *{ 4 | box-sizing:border-box; 5 | } 6 | html,body{ 7 | margin:0; 8 | padding:0; 9 | height:100%; 10 | font-family: 'Open Sans'; 11 | } 12 | body{ 13 | opacity:0; 14 | transition: all 1s linear; 15 | } 16 | 17 | .divider{ 18 | height: 150px; 19 | width:2px; 20 | background-color: #C0C9CE; 21 | position: relative; 22 | top: 50%; 23 | float: left; 24 | transform: translateY(-50%); 25 | } 26 | 27 | #background-stats-1{ 28 | background-color: #2196f3; 29 | } 30 | 31 | #background-stats-2{ 32 | background-color: #00cbca; 33 | } 34 | 35 | #content-container{ 36 | z-index:2; 37 | position:relative; 38 | margin:0 auto; 39 | display:table; 40 | padding:10px; 41 | max-width:940px; 42 | height:100%; 43 | } 44 | #content-container-center{ 45 | display:table-cell; 46 | text-align:center; 47 | vertical-align:middle; 48 | } 49 | #result{ 50 | z-index: 3; 51 | position: absolute; 52 | bottom: 40px; 53 | right: 20px; 54 | color: #fff; 55 | opacity: 0.5; 56 | font-size: 45px; 57 | font-weight: 600; 58 | } 59 | #choice{ 60 | transition: all 300ms linear; 61 | line-height:1.3em; 62 | background:#fff; 63 | box-shadow: 10px 0 0 #fff, -10px 0 0 #fff; 64 | vertical-align:middle; 65 | font-size:40px; 66 | font-weight: 600; 67 | width: 450px; 68 | height: 200px; 69 | } 70 | #choice a{ 71 | text-decoration:none; 72 | } 73 | #choice a:hover, #choice a:focus{ 74 | outline:0; 75 | text-decoration:underline; 76 | } 77 | 78 | #choice .choice{ 79 | width: 49%; 80 | position: relative; 81 | top: 50%; 82 | transform: translateY(-50%); 83 | text-align: left; 84 | padding-left: 50px; 85 | } 86 | 87 | #choice .choice .label{ 88 | text-transform: uppercase; 89 | } 90 | 91 | #choice .choice.dogs{ 92 | color: #00cbca; 93 | float: right; 94 | } 95 | 96 | #choice .choice.cats{ 97 | color: #2196f3; 98 | float: left; 99 | } 100 | #background-stats{ 101 | z-index:1; 102 | height:100%; 103 | width:100%; 104 | position:absolute; 105 | } 106 | #background-stats div{ 107 | transition: width 400ms ease-in-out; 108 | display:inline-block; 109 | margin-bottom:-4px; 110 | width:50%; 111 | height:100%; 112 | } 113 | -------------------------------------------------------------------------------- /java/result/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cats vs Dogs -- Result 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
Cats
24 |
{{aPercent | number:1}}%
25 |
26 |
27 |
28 |
Dogs
29 |
{{bPercent | number:1}}%
30 |
31 |
32 |
33 |
34 |
35 | No votes yet 36 | {{total}} vote 37 | {{total}} votes 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /java/result/src/test/java/com/salaboy/result/BaseIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.result; 2 | 3 | import com.redis.testcontainers.RedisContainer; 4 | import io.diagrid.dapr.DaprContainer; 5 | import io.diagrid.dapr.QuotedBoolean; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.testcontainers.containers.Network; 8 | import org.testcontainers.junit.jupiter.Container; 9 | import org.testcontainers.junit.jupiter.Testcontainers; 10 | import org.testcontainers.utility.DockerImageName; 11 | 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | @Testcontainers 17 | public abstract class BaseIntegrationTest { 18 | 19 | public static Network daprNetwork = Network.newNetwork(); 20 | 21 | @Container 22 | public static RedisContainer redisContainer = new RedisContainer(DockerImageName.parse("redis/redis-stack")) 23 | .withNetworkAliases("redis") 24 | .withNetwork(daprNetwork); 25 | 26 | @Container 27 | public static DaprContainer daprContainer = new DaprContainer("daprio/daprd:1.13.2") 28 | .withAppName("local-dapr-app") 29 | .withNetwork(daprNetwork) 30 | .withComponent(new DaprContainer.Component("kvstore", "state.redis", getStateStoreProperties())) 31 | .withComponent(new DaprContainer.Component("pubsub", "pubsub.in-memory", Collections.emptyMap() )) 32 | .withAppPort(8080) 33 | .withDaprLogLevel(DaprContainer.DaprLogLevel.debug) 34 | .withAppChannelAddress("host.testcontainers.internal"); 35 | 36 | private static Map getStateStoreProperties() { 37 | Map stateStoreProperties = new HashMap(); 38 | stateStoreProperties.put("keyPrefix", "name"); 39 | stateStoreProperties.put("actorStateStore", new QuotedBoolean("true")); 40 | stateStoreProperties.put("redisHost", "redis:6379"); 41 | stateStoreProperties.put("redisPassword", ""); 42 | stateStoreProperties.put("queryIndexes", "[{\"name\": \"MyQueryIndex\",\"indexes\": [{\"key\": \"content\",\"type\": \"TEXT\"}]}]"); 43 | return stateStoreProperties; 44 | } 45 | 46 | @BeforeAll 47 | static void beforeAll() { 48 | org.testcontainers.Testcontainers.exposeHostPorts(8080); 49 | System.setProperty("dapr.grpc.port", Integer.toString(daprContainer.getGRPCPort())); 50 | System.setProperty("dapr.http.port", Integer.toString(daprContainer.getHTTPPort())); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /java/result/src/test/java/com/salaboy/result/ResultTest.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.result; 2 | 3 | import com.salaboy.model.Results; 4 | import io.diagrid.spring.core.keyvalue.DaprKeyValueTemplate; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 9 | import org.springframework.boot.test.mock.mockito.SpyBean; 10 | import org.springframework.test.context.TestPropertySource; 11 | 12 | import java.time.Duration; 13 | 14 | import static org.awaitility.Awaitility.await; 15 | import static org.mockito.Mockito.atLeast; 16 | import static org.mockito.Mockito.verify; 17 | 18 | @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) 19 | @TestPropertySource(properties = { 20 | "dapr.statestore.query-index=VotesQueryIndex", 21 | }) 22 | public class ResultTest extends BaseIntegrationTest { 23 | 24 | @Autowired 25 | DaprKeyValueTemplate keyValueTemplate; 26 | 27 | @SpyBean 28 | FetchResultsJob fetchResultsJob; 29 | 30 | @Test 31 | public void testResultJob() throws InterruptedException { 32 | Results results = new Results(3, 5); 33 | keyValueTemplate.insert("results", results); 34 | 35 | Thread.sleep(5000); 36 | 37 | await() 38 | .atMost(Duration.ofSeconds(5)) 39 | .untilAsserted(() -> verify(fetchResultsJob, atLeast(2)).fetchResults()); 40 | 41 | Thread.sleep(5000); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /java/vote/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salaboy/example-voting-app/3b7c3057283f18b6512ecad7d3a4fde32ea66ba1/java/vote/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /java/vote/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /java/vote/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/eclipse-temurin:21-jdk-jammy AS builder 2 | WORKDIR workspace 3 | ARG JAR_FILE=target/*.jar 4 | COPY ${JAR_FILE} app.jar 5 | RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted 6 | 7 | FROM docker.io/library/eclipse-temurin:21-jre-jammy 8 | RUN useradd spring 9 | USER spring 10 | WORKDIR workspace 11 | COPY --from=builder workspace/extracted/dependencies/ ./ 12 | COPY --from=builder workspace/extracted/spring-boot-loader/ ./ 13 | COPY --from=builder workspace/extracted/snapshot-dependencies/ ./ 14 | COPY --from=builder workspace/extracted/application/ ./ 15 | ENTRYPOINT ["java", "-jar", "app.jar"] -------------------------------------------------------------------------------- /java/vote/components/votes-statestore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: votes-statestore 5 | spec: 6 | type: state.redis 7 | version: v1 8 | metadata: 9 | - name: keyPrefix 10 | value: name 11 | - name: redisHost 12 | value: redis:6379 13 | - name: redisPassword 14 | value: "" 15 | - name: actorStateStore 16 | value: "true" 17 | - name: queryIndexes 18 | value: | 19 | [ 20 | { 21 | "name": "voteIndex", 22 | "indexes": [ 23 | { 24 | "key": "type", 25 | "type": "TEXT" 26 | } 27 | ] 28 | } 29 | ] -------------------------------------------------------------------------------- /java/vote/dagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vote", 3 | "sdk": "go", 4 | "dependencies": [ 5 | { 6 | "name": "dapr", 7 | "source": "github.com/marcosnils/daggerverse/dapr@15660afb386b32b955d96ee42a7354338b56c570" 8 | } 9 | ], 10 | "source": "dagger", 11 | "engineVersion": "v0.10.1" 12 | } 13 | -------------------------------------------------------------------------------- /java/vote/dagger/.gitattributes: -------------------------------------------------------------------------------- 1 | /dagger.gen.go linguist-generated 2 | /querybuilder/** linguist-generated 3 | /internal/dagger/** linguist-generated 4 | /internal/querybuilder/** linguist-generated 5 | -------------------------------------------------------------------------------- /java/vote/dagger/go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.21.3 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.17.31 7 | github.com/Khan/genqlient v0.6.0 8 | github.com/vektah/gqlparser/v2 v2.5.6 9 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa 10 | golang.org/x/sync v0.6.0 11 | ) 12 | 13 | require github.com/stretchr/testify v1.9.0 // indirect 14 | -------------------------------------------------------------------------------- /java/vote/dagger/go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/gqlgen v0.17.31 h1:VncSQ82VxieHkea8tz11p7h/zSbvHSxSDZfywqWt158= 2 | github.com/99designs/gqlgen v0.17.31/go.mod h1:i4rEatMrzzu6RXaHydq1nmEPZkb3bKQsnxNRHS4DQB4= 3 | github.com/Khan/genqlient v0.6.0 h1:Bwb1170ekuNIVIwTJEqvO8y7RxBxXu639VJOkKSrwAk= 4 | github.com/Khan/genqlient v0.6.0/go.mod h1:rvChwWVTqXhiapdhLDV4bp9tz/Xvtewwkon4DpWWCRM= 5 | github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= 6 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 7 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 8 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 19 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 22 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 23 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 24 | github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU= 25 | github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= 26 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 27 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 28 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 29 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 33 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /java/vote/dagger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Vote struct{} 4 | 5 | func (m *Vote) Build( 6 | dir *Directory, 7 | // +optional 8 | redis *Service, 9 | // +optional 10 | componentsDir *Directory, 11 | ) *Container { 12 | if redis == nil { 13 | redis = dag.Container(). 14 | From("redis/redis-stack"). 15 | WithExposedPort(6379).AsService() 16 | } 17 | 18 | if componentsDir == nil { 19 | componentsDir = dir.Directory("components") 20 | } 21 | 22 | dapr := dag.Dapr(). 23 | Dapr("vote", DaprDaprOpts{ComponentsPath: componentsDir}). 24 | WithServiceBinding("redis", redis). 25 | WithExposedPort(50001) 26 | 27 | return dag.Container().From("maven:3.9.6-eclipse-temurin-21"). 28 | WithServiceBinding("dapr", dapr.AsService()). 29 | WithEnvVariable("DAPR_GRPC_ENDPOINT", "http://dapr:50001"). 30 | WithMountedCache("/root/.m2", dag.CacheVolume("m2_cache")). 31 | WithMountedDirectory("/app", dir). 32 | WithWorkdir("/app"). 33 | WithExposedPort(8080). 34 | WithEntrypoint([]string{"mvn", "spring-boot:run"}) 35 | } 36 | 37 | func (m *Vote) Serve( 38 | dir *Directory, 39 | // +optional 40 | redis *Service, 41 | // +optional 42 | componentsDir *Directory, 43 | ) *Service { 44 | return m.Build(dir, redis, componentsDir). 45 | WithExposedPort(8080). 46 | AsService() 47 | } 48 | -------------------------------------------------------------------------------- /java/vote/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.0 9 | 10 | 11 | com.salaboy 12 | vote 13 | 0.0.1-SNAPSHOT 14 | vote 15 | Vote Service 16 | 17 | 21 18 | 0.11.0-SNAPSHOT 19 | 0.10.14 20 | 21 | 22 | 23 | com.salaboy 24 | model 25 | 0.0.1-SNAPSHOT 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-actuator 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | io.diagrid.dapr 37 | dapr-client-spring-boot-starter 38 | ${daprSpringBoot.version} 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-thymeleaf 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-test 48 | test 49 | 50 | 51 | io.diagrid.dapr 52 | testcontainers-dapr 53 | ${testcontainers-dapr.version} 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-testcontainers 58 | test 59 | 60 | 61 | com.redis.testcontainers 62 | testcontainers-redis-junit 63 | 1.6.4 64 | test 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-maven-plugin 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /java/vote/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: vote 4 | 5 | dapr: 6 | statestore: 7 | name: kvstore 8 | query-index: QueryIndex 9 | pubsub: 10 | name: pubsub 11 | 12 | management: 13 | endpoints: 14 | web: 15 | exposure: 16 | include: health,prometheus 17 | 18 | vote: 19 | state-store: votes-statestore 20 | pubsub: pubsub 21 | topic: votes 22 | -------------------------------------------------------------------------------- /java/vote/src/main/resources/static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | *{ 4 | box-sizing:border-box; 5 | } 6 | html,body{ 7 | margin: 0; 8 | padding: 0; 9 | background-color: #F7F8F9; 10 | height: 100vh; 11 | font-family: 'Open Sans'; 12 | } 13 | 14 | button{ 15 | border-radius: 0; 16 | width: 100%; 17 | height: 50%; 18 | } 19 | 20 | button[type="submit"] { 21 | -webkit-appearance:none; -webkit-border-radius:0; 22 | } 23 | 24 | button i{ 25 | float: right; 26 | padding-right: 30px; 27 | margin-top: 3px; 28 | } 29 | 30 | button.a{ 31 | background-color: #1aaaf8; 32 | } 33 | 34 | button.b{ 35 | background-color: #00cbca; 36 | } 37 | 38 | #tip{ 39 | text-align: left; 40 | color: #c0c9ce; 41 | font-size: 14px; 42 | } 43 | 44 | #hostname{ 45 | position: absolute; 46 | bottom: 100px; 47 | right: 0; 48 | left: 0; 49 | color: #8f9ea8; 50 | font-size: 24px; 51 | } 52 | 53 | #content-container{ 54 | z-index: 2; 55 | position: relative; 56 | margin: 0 auto; 57 | display: table; 58 | padding: 10px; 59 | max-width: 940px; 60 | height: 100%; 61 | } 62 | #content-container-center{ 63 | display: table-cell; 64 | text-align: center; 65 | } 66 | 67 | #content-container-center h3{ 68 | color: #254356; 69 | } 70 | 71 | #choice{ 72 | transition: all 300ms linear; 73 | line-height: 1.3em; 74 | display: inline; 75 | vertical-align: middle; 76 | font-size: 3em; 77 | } 78 | #choice a{ 79 | text-decoration:none; 80 | } 81 | #choice a:hover, #choice a:focus{ 82 | outline:0; 83 | text-decoration:underline; 84 | } 85 | 86 | #choice button{ 87 | display: block; 88 | height: 80px; 89 | width: 330px; 90 | border: none; 91 | color: white; 92 | text-transform: uppercase; 93 | font-size:18px; 94 | font-weight: 700; 95 | margin-top: 10px; 96 | margin-bottom: 10px; 97 | text-align: left; 98 | padding-left: 50px; 99 | } 100 | 101 | #choice button.a:hover{ 102 | background-color: #1488c6; 103 | } 104 | 105 | #choice button.b:hover{ 106 | background-color: #00a2a1; 107 | } 108 | 109 | #choice button.a:focus{ 110 | background-color: #1488c6; 111 | } 112 | 113 | #choice button.b:focus{ 114 | background-color: #00a2a1; 115 | } 116 | 117 | #background-stats{ 118 | z-index:1; 119 | height:100%; 120 | width:100%; 121 | position:absolute; 122 | } 123 | #background-stats div{ 124 | transition: width 400ms ease-in-out; 125 | display:inline-block; 126 | margin-bottom:-4px; 127 | width:50%; 128 | height:100%; 129 | } 130 | -------------------------------------------------------------------------------- /java/vote/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | [[${option_a}]] vs [[${option_b}]]! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

[[${option_a}]] vs [[${option_b}]]!

17 |
18 | Your user ID is

[[${user}]]

19 |
20 |
21 | 22 | 23 |
24 |
25 | (Tip: you can change your vote) 26 |
27 | 30 |
31 |
32 | 33 | 34 | 35 | {% if vote %} 36 | 50 | {% endif %} 51 | 52 | 53 | -------------------------------------------------------------------------------- /java/vote/src/test/java/com/salaboy/vote/BaseIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.vote; 2 | 3 | import com.redis.testcontainers.RedisContainer; 4 | import io.diagrid.dapr.DaprContainer; 5 | import io.diagrid.dapr.QuotedBoolean; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.testcontainers.containers.Network; 8 | import org.testcontainers.junit.jupiter.Container; 9 | import org.testcontainers.junit.jupiter.Testcontainers; 10 | import org.testcontainers.utility.DockerImageName; 11 | 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | @Testcontainers 17 | public abstract class BaseIntegrationTest { 18 | 19 | public static Network daprNetwork = Network.newNetwork(); 20 | 21 | @Container 22 | public static RedisContainer redisContainer = new RedisContainer(DockerImageName.parse("redis/redis-stack")) 23 | .withNetworkAliases("redis") 24 | .withNetwork(daprNetwork); 25 | 26 | @Container 27 | public static DaprContainer daprContainer = new DaprContainer("daprio/daprd:1.13.2") 28 | .withAppName("local-dapr-app") 29 | .withNetwork(daprNetwork) 30 | .withComponent(new DaprContainer.Component("kvstore", "state.redis", getStateStoreProperties())) 31 | .withComponent(new DaprContainer.Component("pubsub", "pubsub.in-memory", Collections.emptyMap() )) 32 | .withAppPort(8080) 33 | .withDaprLogLevel(DaprContainer.DaprLogLevel.debug) 34 | .withAppChannelAddress("host.testcontainers.internal"); 35 | 36 | private static Map getStateStoreProperties() { 37 | Map stateStoreProperties = new HashMap(); 38 | stateStoreProperties.put("keyPrefix", "name"); 39 | stateStoreProperties.put("actorStateStore", new QuotedBoolean("true")); 40 | stateStoreProperties.put("redisHost", "redis:6379"); 41 | stateStoreProperties.put("redisPassword", ""); 42 | stateStoreProperties.put("queryIndexes", "[{\"name\": \"VotesQueryIndex\",\"indexes\": [{\"key\": \"type\",\"type\": \"TEXT\"}]}]"); 43 | return stateStoreProperties; 44 | } 45 | 46 | @BeforeAll 47 | static void beforeAll() { 48 | org.testcontainers.Testcontainers.exposeHostPorts(8080); 49 | System.setProperty("dapr.grpc.port", Integer.toString(daprContainer.getGRPCPort())); 50 | System.setProperty("dapr.http.port", Integer.toString(daprContainer.getHTTPPort())); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /java/vote/src/test/java/com/salaboy/vote/TestRestController.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.vote; 2 | 3 | import com.salaboy.model.Vote; 4 | import io.dapr.Topic; 5 | import io.dapr.client.domain.CloudEvent; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | @RestController 16 | public class TestRestController { 17 | 18 | private static final Logger LOG = LoggerFactory.getLogger(TestRestController.class); 19 | 20 | public static final String pubSubName = "pubsub"; 21 | public static final String topicName = "newVote"; 22 | 23 | private List> events = new ArrayList<>(); 24 | 25 | @Topic(name = topicName, pubsubName = pubSubName) 26 | @PostMapping("/subscribe") 27 | public void handleMessages(@RequestBody CloudEvent event) { 28 | LOG.info("++++++CONSUME {}------", event); 29 | events.add(event); 30 | } 31 | 32 | public List> getEvents() { 33 | return events; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /java/vote/src/test/java/com/salaboy/vote/VoteApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.vote; 2 | 3 | import com.salaboy.model.Vote; 4 | import io.dapr.client.domain.CloudEvent; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 9 | import org.springframework.mock.web.MockHttpServletResponse; 10 | import org.springframework.test.context.TestPropertySource; 11 | import org.springframework.ui.ExtendedModelMap; 12 | import org.springframework.util.LinkedMultiValueMap; 13 | import org.springframework.util.MultiValueMap; 14 | 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.UUID; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | 21 | @SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT) 22 | @TestPropertySource(properties = { 23 | "vote.pubsub=pubsub", 24 | "vote.topic=newVote" 25 | }) 26 | class VoteApplicationTests extends BaseIntegrationTest { 27 | 28 | @Autowired 29 | private VoteController voteController; 30 | 31 | @Autowired 32 | private TestRestController testRestController; 33 | 34 | @Test 35 | public void testVote() throws InterruptedException { 36 | ExtendedModelMap extendedModelMap = new ExtendedModelMap(); 37 | MultiValueMap formData = new LinkedMultiValueMap<>(); 38 | formData.put("vote", Collections.singletonList("a")); 39 | MockHttpServletResponse response = new MockHttpServletResponse(); 40 | String userId = UUID.randomUUID().toString().substring(0, 10); 41 | voteController.vote(extendedModelMap, formData, userId , response); 42 | 43 | assertThat(response.getCookies().length).isEqualTo(1); 44 | 45 | Thread.sleep(3000); 46 | 47 | List> events = testRestController.getEvents(); 48 | 49 | assertThat(!events.isEmpty()).isTrue(); 50 | assertThat(events.size()).isEqualTo(1); 51 | 52 | formData.put("vote", Collections.singletonList("b")); 53 | voteController.vote(extendedModelMap, formData, userId, response); 54 | 55 | Thread.sleep(3000); 56 | 57 | events = testRestController.getEvents(); 58 | 59 | assertThat(!events.isEmpty()).isTrue(); 60 | assertThat(events.size()).isEqualTo(2); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /java/worker/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /java/worker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/eclipse-temurin:21-jdk-jammy AS builder 2 | WORKDIR workspace 3 | ARG JAR_FILE=target/*.jar 4 | COPY ${JAR_FILE} app.jar 5 | RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted 6 | 7 | FROM docker.io/library/eclipse-temurin:21-jre-jammy 8 | RUN useradd spring 9 | USER spring 10 | WORKDIR workspace 11 | COPY --from=builder workspace/extracted/dependencies/ ./ 12 | COPY --from=builder workspace/extracted/spring-boot-loader/ ./ 13 | COPY --from=builder workspace/extracted/snapshot-dependencies/ ./ 14 | COPY --from=builder workspace/extracted/application/ ./ 15 | ENTRYPOINT ["java", "-jar", "app.jar"] -------------------------------------------------------------------------------- /java/worker/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.0 9 | 10 | 11 | com.salaboy 12 | worker 13 | 0.0.1-SNAPSHOT 14 | worker 15 | Worker Service 16 | 17 | 21 18 | 2.5.0 19 | 0.11.0-SNAPSHOT 20 | 0.10.14 21 | 22 | 23 | 24 | com.salaboy 25 | model 26 | 0.0.1-SNAPSHOT 27 | 28 | 29 | io.diagrid.dapr 30 | dapr-client-spring-boot-starter 31 | ${daprSpringBoot.version} 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-actuator 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-web 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-test 45 | test 46 | 47 | 48 | io.diagrid.dapr 49 | testcontainers-dapr 50 | ${testcontainers-dapr.version} 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-testcontainers 55 | test 56 | 57 | 58 | com.redis.testcontainers 59 | testcontainers-redis-junit 60 | 1.6.4 61 | test 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-maven-plugin 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /java/worker/query.json: -------------------------------------------------------------------------------- 1 | { 2 | "filter": { 3 | "EQ": { "type": "vote" } 4 | } 5 | 6 | } -------------------------------------------------------------------------------- /java/worker/src/main/java/com/salaboy/worker/WorkerApplication.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.worker; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @SpringBootApplication 8 | @EnableScheduling 9 | public class WorkerApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(WorkerApplication.class, args); 13 | } 14 | 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /java/worker/src/main/java/com/salaboy/worker/WorkerConfig.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.worker; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import io.dapr.client.DaprClientBuilder; 7 | import io.diagrid.spring.boot.autoconfigure.statestore.DaprStateStoreProperties; 8 | import io.diagrid.spring.core.keyvalue.DaprKeyValueAdapter; 9 | import io.diagrid.spring.core.keyvalue.DaprKeyValueTemplate; 10 | 11 | @Configuration 12 | public class WorkerConfig { 13 | 14 | @Value("${worker.vote.statestore:kvstore}") 15 | private String voteStateStore; 16 | 17 | @Value("${worker.vote.queryIndex:VotesQueryIndex}") 18 | private String voteQueryIndex; 19 | 20 | @Value("${worker.results.statestore:kvstore}") 21 | private String resultsStateStore; 22 | 23 | @Bean 24 | public DaprKeyValueAdapter voteKeyValueAdapter(DaprClientBuilder daprClientBuilder, DaprStateStoreProperties daprStateStoreProperties) { 25 | return new DaprKeyValueAdapter(daprClientBuilder.build(), daprClientBuilder.buildPreviewClient(), voteStateStore, voteQueryIndex); 26 | } 27 | 28 | @Bean 29 | public DaprKeyValueTemplate voteKeyValueTemplate(DaprKeyValueAdapter voteKeyValueAdapter) { 30 | return new DaprKeyValueTemplate(voteKeyValueAdapter); 31 | } 32 | 33 | @Bean 34 | public DaprKeyValueAdapter resultsKeyValueAdapter(DaprClientBuilder daprClientBuilder, DaprStateStoreProperties daprStateStoreProperties) { 35 | return new DaprKeyValueAdapter(daprClientBuilder.build(), daprClientBuilder.buildPreviewClient(), resultsStateStore, voteQueryIndex); 36 | } 37 | 38 | @Bean 39 | public DaprKeyValueTemplate resultsKeyValueTemplate(DaprKeyValueAdapter resultsKeyValueAdapter) { 40 | return new DaprKeyValueTemplate(resultsKeyValueAdapter); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /java/worker/src/main/java/com/salaboy/worker/WorkerController.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.worker; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | @RestController 7 | public class WorkerController { 8 | 9 | @GetMapping("/") 10 | public String ok(){ 11 | return "OK"; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /java/worker/src/main/java/com/salaboy/worker/WorkerJob.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.worker; 2 | 3 | import com.salaboy.model.Results; 4 | import com.salaboy.model.Vote; 5 | 6 | import io.diagrid.spring.core.keyvalue.DaprKeyValueTemplate; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Qualifier; 10 | import org.springframework.data.keyvalue.core.query.KeyValueQuery; 11 | import org.springframework.scheduling.annotation.Scheduled; 12 | import org.springframework.stereotype.Component; 13 | 14 | @Component 15 | public class WorkerJob { 16 | 17 | @Autowired 18 | @Qualifier("voteKeyValueTemplate") 19 | private DaprKeyValueTemplate voteKeyValueTemplate; 20 | 21 | @Autowired 22 | @Qualifier("resultsKeyValueTemplate") 23 | private DaprKeyValueTemplate resultsKeyValueTemplate; 24 | 25 | 26 | @Scheduled(fixedDelay = 1000) 27 | public void work() { 28 | System.out.println("Fetching votes.."); 29 | 30 | //http ":3500/v1.0-alpha1/state/votes-statestore/query?metadata.contentType=application/json&metadata.queryIndexName=voteIndex" < query.json 31 | KeyValueQuery keyValueQuery = new KeyValueQuery("'type' == 'vote'"); 32 | keyValueQuery.setRows(1000); 33 | keyValueQuery.setOffset(0); 34 | Iterable votes = voteKeyValueTemplate.find(keyValueQuery, Vote.class); 35 | 36 | int optionA = 0; 37 | int optionB = 0; 38 | for (Vote vote : votes) { 39 | if(vote.getOption().equals("a")){ 40 | optionA++; 41 | } 42 | if(vote.getOption().equals("b")){ 43 | optionB++; 44 | } 45 | } 46 | 47 | System.out.println("Storing results: a: "+ optionA + " - b: "+ optionB); 48 | // Count results and update using KeyValueTemplate 49 | Results results = new Results("results",optionA, optionB); 50 | resultsKeyValueTemplate.update(results); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /java/worker/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: worker 4 | 5 | dapr: 6 | statestore: 7 | name: kvstore 8 | query-index: MyQueryIndex 9 | 10 | management: 11 | endpoints: 12 | web: 13 | exposure: 14 | include: health,prometheus 15 | -------------------------------------------------------------------------------- /java/worker/src/test/java/com/salaboy/worker/BaseIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.salaboy.worker; 2 | 3 | import com.redis.testcontainers.RedisContainer; 4 | import io.diagrid.dapr.DaprContainer; 5 | import io.diagrid.dapr.QuotedBoolean; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.testcontainers.containers.Network; 8 | import org.testcontainers.junit.jupiter.Container; 9 | import org.testcontainers.junit.jupiter.Testcontainers; 10 | import org.testcontainers.utility.DockerImageName; 11 | 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | @Testcontainers 17 | public abstract class BaseIntegrationTest { 18 | 19 | public static Network daprNetwork = Network.newNetwork(); 20 | 21 | @Container 22 | public static RedisContainer redisContainer = new RedisContainer(DockerImageName.parse("redis/redis-stack")) 23 | .withNetworkAliases("redis") 24 | .withNetwork(daprNetwork); 25 | 26 | @Container 27 | public static DaprContainer daprContainer = new DaprContainer("daprio/daprd:1.13.2") 28 | .withAppName("local-dapr-app") 29 | .withNetwork(daprNetwork) 30 | .withComponent(new DaprContainer.Component("kvstore", "state.redis", getStateStoreProperties())) 31 | .withComponent(new DaprContainer.Component("pubsub", "pubsub.in-memory", Collections.emptyMap() )) 32 | .withAppPort(8080) 33 | .withDaprLogLevel(DaprContainer.DaprLogLevel.debug) 34 | .withAppChannelAddress("host.testcontainers.internal"); 35 | 36 | private static Map getStateStoreProperties() { 37 | Map stateStoreProperties = new HashMap(); 38 | stateStoreProperties.put("keyPrefix", "name"); 39 | stateStoreProperties.put("actorStateStore", new QuotedBoolean("true")); 40 | stateStoreProperties.put("redisHost", "redis:6379"); 41 | stateStoreProperties.put("redisPassword", ""); 42 | stateStoreProperties.put("queryIndexes", "[{\"name\": \"VotesQueryIndex\",\"indexes\": [{\"key\": \"type\",\"type\": \"TEXT\"}]}]"); 43 | return stateStoreProperties; 44 | } 45 | 46 | @BeforeAll 47 | static void beforeAll() { 48 | org.testcontainers.Testcontainers.exposeHostPorts(8080); 49 | System.setProperty("dapr.grpc.port", Integer.toString(daprContainer.getGRPCPort())); 50 | System.setProperty("dapr.http.port", Integer.toString(daprContainer.getHTTPPort())); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/db-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: db 6 | name: db 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: db 12 | template: 13 | metadata: 14 | labels: 15 | app: db 16 | spec: 17 | containers: 18 | - image: postgres:15-alpine 19 | name: postgres 20 | env: 21 | - name: POSTGRES_USER 22 | value: postgres 23 | - name: POSTGRES_PASSWORD 24 | value: postgres 25 | ports: 26 | - containerPort: 5432 27 | name: postgres 28 | volumeMounts: 29 | - mountPath: /var/lib/postgresql/data 30 | name: db-data 31 | volumes: 32 | - name: db-data 33 | emptyDir: {} 34 | -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/db-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: db 6 | name: db 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - name: "db-service" 11 | port: 5432 12 | targetPort: 5432 13 | selector: 14 | app: db 15 | 16 | -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/echo-kservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: serving.knative.dev/v1 2 | kind: Service 3 | metadata: 4 | name: echo 5 | namespace: default 6 | annotations: 7 | networking.knative.dev/http-protocol: "enabled" 8 | spec: 9 | template: 10 | metadata: 11 | annotations: 12 | autoscaling.knative.dev/min-scale: "1" 13 | spec: 14 | containers: 15 | - image: salaboy/echo-java:1.0.0-amd 16 | imagePullPolicy: Always 17 | ports: 18 | - containerPort: 8080 19 | env: 20 | - name: DAPR_HTTP_ENDPOINT 21 | value: http://echo-dapr.default.svc.cluster.local:3500 22 | - name: DAPR_GRPC_ENDPOINT 23 | value: http://echo-dapr.default.svc.cluster.local:50001 24 | - name: ECHO_STATE_STORE 25 | value: votes-statestore 26 | -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/echo-subscription.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Subscription 3 | metadata: 4 | name: echo-subscritpion 5 | spec: 6 | topic: newVote 7 | route: /events 8 | pubsubname: pubsub-rabbitmq -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/pubsub-rabbitmq.yml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: pubsub-rabbitmq 5 | spec: 6 | type: pubsub.rabbitmq 7 | version: v1 8 | metadata: 9 | - name: protocol 10 | value: amqp 11 | - name: hostname 12 | value: rabbitmq 13 | - name: username 14 | secretKeyRef: 15 | name: rabbitmq-credentials 16 | key: username 17 | - name: password 18 | secretKeyRef: 19 | name: rabbitmq-credentials 20 | key: password 21 | - name: heartBeat 22 | value: 10s 23 | -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/rabbitmq/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -euo pipefail 4 | 5 | echo "\n🐰 RabbitMQ deployment started." 6 | 7 | echo "\n📦 Installing RabbitMQ Cluster Kubernetes Operator..." 8 | 9 | kubectl apply -f "https://github.com/rabbitmq/cluster-operator/releases/download/v2.7.0/cluster-operator-quay-io.yml" 10 | 11 | echo "\n⌛ Waiting for RabbitMQ Operator to be deployed..." 12 | 13 | while [ $(kubectl get pod -l app.kubernetes.io/name=rabbitmq-cluster-operator -n rabbitmq-system | wc -l) -eq 0 ] ; do 14 | sleep 15 15 | done 16 | 17 | echo "\n⌛ Waiting for RabbitMQ Operator to be ready..." 18 | 19 | kubectl wait \ 20 | --for=condition=ready pod \ 21 | --selector=app.kubernetes.io/name=rabbitmq-cluster-operator \ 22 | --timeout=300s \ 23 | --namespace=rabbitmq-system 24 | 25 | echo "\n ✅ The RabbitMQ Cluster Kubernetes Operator has been successfully installed." 26 | 27 | echo "\n-----------------------------------------------------" 28 | 29 | echo "\n📦 Deploying RabbitMQ cluster..." 30 | 31 | kubectl apply -f resources/cluster.yml 32 | 33 | echo "\n⌛ Waiting for RabbitMQ cluster to be deployed..." 34 | 35 | while [ $(kubectl get pod -l app.kubernetes.io/name=rabbitmq | wc -l) -eq 0 ] ; do 36 | sleep 15 37 | done 38 | 39 | echo "\n⌛ Waiting for RabbitMQ cluster to be ready..." 40 | 41 | kubectl wait \ 42 | --for=condition=ready pod \ 43 | --selector=app.kubernetes.io/name=rabbitmq \ 44 | --timeout=600s 45 | 46 | echo "\n✅ The RabbitMQ cluster has been successfully deployed." 47 | 48 | echo "\n-----------------------------------------------------" 49 | 50 | export RABBITMQ_USERNAME=$(kubectl get secret rabbitmq-default-user -o jsonpath='{.data.username}' | base64 --decode) 51 | export RABBITMQ_PASSWORD=$(kubectl get secret rabbitmq-default-user -o jsonpath='{.data.password}' | base64 --decode) 52 | 53 | echo "Username: $RABBITMQ_USERNAME" 54 | echo "Password: $RABBITMQ_PASSWORD" 55 | 56 | echo "\n🔑 Generating Secret with RabbitMQ credentials." 57 | 58 | kubectl delete secret rabbitmq-credentials || true 59 | 60 | kubectl create secret generic rabbitmq-credentials \ 61 | --from-literal=username="$RABBITMQ_USERNAME" \ 62 | --from-literal=password="$RABBITMQ_PASSWORD" 63 | 64 | unset RABBITMQ_USERNAME 65 | unset RABBITMQ_PASSWORD 66 | 67 | echo "\n🍃 Secret 'rabbitmq-credentials' has been created for Dapr to interact with RabbitMQ." 68 | 69 | echo "\n🐰 RabbitMQ deployment completed.\n" -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/rabbitmq/resources/cluster.yml: -------------------------------------------------------------------------------- 1 | apiVersion: rabbitmq.com/v1beta1 2 | kind: RabbitmqCluster 3 | metadata: 4 | name: rabbitmq 5 | namespace: default 6 | spec: 7 | replicas: 1 8 | resources: 9 | requests: 10 | cpu: 100m 11 | memory: 275Mi 12 | limits: 13 | cpu: 1000m 14 | memory: 756Mi 15 | persistence: 16 | storage: 10Gi 17 | rabbitmq: 18 | additionalConfig: | 19 | vm_memory_high_watermark_paging_ratio = 0.99 20 | vm_memory_high_watermark.relative = 0.90 21 | terminationGracePeriodSeconds: 60 22 | 23 | -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/rabbitmq/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export RABBITMQ_USERNAME=$(kubectl get secret rabbitmq-default-user -o jsonpath='{.data.username}' | base64 --decode) 4 | export RABBITMQ_PASSWORD=$(kubectl get secret rabbitmq-default-user -o jsonpath='{.data.password}' | base64 --decode) 5 | export RABBITMQ_SERVICE=$(kubectl get service rabbitmq -o jsonpath='{.spec.clusterIP}') 6 | 7 | kubectl run perf-test --image=pivotalrabbitmq/perf-test -- --uri amqp://$RABBITMQ_USERNAME:$RABBITMQ_PASSWORD@$RABBITMQ_SERVICE 8 | 9 | unset RABBITMQ_USERNAME 10 | unset RABBITMQ_PASSWORD 11 | unset RABBITMQ_SERVICE -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/redis-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: redis 6 | name: redis 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: redis 12 | template: 13 | metadata: 14 | labels: 15 | app: redis 16 | spec: 17 | containers: 18 | - image: docker.io/redis/redis-stack-server:7.2.0-v8 19 | name: redis 20 | ports: 21 | - containerPort: 6379 22 | name: redis 23 | volumeMounts: 24 | - mountPath: /data 25 | name: redis-data 26 | volumes: 27 | - name: redis-data 28 | emptyDir: {} 29 | -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/redis-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: redis 6 | name: redis 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - name: "redis-service" 11 | port: 6379 12 | targetPort: 6379 13 | selector: 14 | app: redis 15 | 16 | -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/result-kservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: serving.knative.dev/v1 2 | kind: Service 3 | metadata: 4 | name: result 5 | namespace: default 6 | spec: 7 | template: 8 | metadata: 9 | annotations: 10 | autoscaling.knative.dev/min-scale: "1" 11 | spec: 12 | containers: 13 | - image: salaboy/result-java:1.0.0-amd 14 | imagePullPolicy: Always 15 | ports: 16 | - containerPort: 8080 17 | env: 18 | - name: DAPR_HTTP_ENDPOINT 19 | value: http://result-dapr.default.svc.cluster.local:3500 20 | - name: DAPR_GRPC_ENDPOINT 21 | value: http://result-dapr.default.svc.cluster.local:50001 22 | - name: DAPR_STATESTORE_NAME 23 | value: results-statestore -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/results-statestore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: results-statestore 5 | spec: 6 | type: state.postgresql 7 | version: v1 8 | metadata: 9 | - name: keyPrefix 10 | value: name 11 | - name: connectionString 12 | value: "host=db user=postgres password=postgres port=5432 connect_timeout=10" 13 | -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/vote-kservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: serving.knative.dev/v1 2 | kind: Service 3 | metadata: 4 | name: vote 5 | namespace: default 6 | spec: 7 | template: 8 | metadata: 9 | annotations: 10 | autoscaling.knative.dev/min-scale: "1" 11 | spec: 12 | containers: 13 | - image: salaboy/vote-java:1.0.0-amd 14 | imagePullPolicy: Always 15 | ports: 16 | - containerPort: 8080 17 | env: 18 | - name: DAPR_HTTP_ENDPOINT 19 | value: http://vote-dapr.default.svc.cluster.local:3500 20 | - name: DAPR_GRPC_ENDPOINT 21 | value: http://vote-dapr.default.svc.cluster.local:50001 22 | - name: DAPR_STATESTORE_NAME 23 | value: votes-statestore 24 | - name: DAPR_PUBSUB_NAME 25 | value: pubsub-rabbitmq 26 | - name: DAPR_TOPIC_NAME 27 | value: newVote 28 | - name: VOTE_PUBSUB 29 | value: pubsub-rabbitmq 30 | - name: VOTE_TOPIC 31 | value: newVote -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/votes-statestore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: votes-statestore 5 | spec: 6 | type: state.redis 7 | version: v1 8 | metadata: 9 | - name: keyPrefix 10 | value: name 11 | - name: redisHost 12 | value: redis:6379 13 | - name: redisPassword 14 | value: "" 15 | # - name: outboxPublishPubsub # Required 16 | # value: "pubsub-rabbitmq" 17 | # - name: outboxPublishTopic # Required 18 | # value: "newVote" 19 | # - name: outboxDiscardWhenMissingState #Optional. Defaults to false 20 | # value: false 21 | - name: actorStateStore 22 | value: "true" 23 | - name: queryIndexes 24 | value: | 25 | [ 26 | { 27 | "name": "voteIndex", 28 | "indexes": [ 29 | { 30 | "key": "type", 31 | "type": "TEXT" 32 | }, 33 | { 34 | "key": "option", 35 | "type": "TEXT" 36 | } 37 | ] 38 | } 39 | ] -------------------------------------------------------------------------------- /k8s-dapr-shared-and-knative/worker-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: worker 5 | namespace: default 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: worker 11 | template: 12 | metadata: 13 | labels: 14 | app: worker 15 | spec: 16 | containers: 17 | - name: user-container 18 | image: salaboy/worker-java:1.0.0-amd 19 | imagePullPolicy: Always 20 | env: 21 | - name: DAPR_HTTP_ENDPOINT 22 | value: http://worker-dapr.default.svc.cluster.local:3500 23 | - name: DAPR_GRPC_ENDPOINT 24 | value: http://worker-dapr.default.svc.cluster.local:50001 25 | - name: BASE_URL 26 | value: http://worker-dapr.default.svc.cluster.local 27 | - name: WORKER_VOTE_STATESTORE 28 | value: votes-statestore 29 | - name: WORKER_RESULTS_STATESTORE 30 | value: results-statestore 31 | - name: WORKER_VOTE_QUERYINDEX 32 | value: voteIndex 33 | -------------------------------------------------------------------------------- /k8s-dapr/README.md: -------------------------------------------------------------------------------- 1 | # Installing the voting app with Dapr 2 | 3 | The Dapr-ized version of the application uses the following projects 4 | - Vote Service: [`java/vote`](../java/vote/) 5 | - Worker Service: [`dotnet/worker`](../dotnet/worker/) 6 | - Results Service: [`go/result`](../go/result/) 7 | 8 | It also uses two statestores one for Votes (Redis) and one for the Results (PostgreSQL) 9 | 10 | Create a new KinD Cluster 11 | 12 | ``` 13 | kind create cluster 14 | ``` 15 | 16 | Install Dapr: 17 | 18 | ``` 19 | helm repo add dapr https://dapr.github.io/helm-charts/ 20 | helm repo update 21 | helm upgrade --install dapr dapr/dapr \ 22 | --version=1.13.0 \ 23 | --namespace dapr-system \ 24 | --create-namespace \ 25 | --wait 26 | ``` 27 | 28 | Now install the application, from the `k8s-dapr` directory: 29 | ``` 30 | kubectl apply -f . 31 | ``` 32 | 33 | Once all the pods are up and running you can use `kubectl port-forward` to access the vote and result user interfaces. 34 | In a new terminal run: 35 | 36 | ``` 37 | kubectl port-forward svc/vote 4000:6000 38 | ``` 39 | 40 | In another terminal run: 41 | 42 | ``` 43 | kubectl port-forward svc/result 3000:5001 44 | ``` 45 | 46 | You can point your browser to [http://localhost:4000](http://localhost:4000) to cast your vote. 47 | 48 | To see the results in real time you can point your browser to [http://localhost:3000](http://localhost:3000). 49 | 50 | -------------------------------------------------------------------------------- /k8s-dapr/db-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: db 6 | name: db 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: db 12 | template: 13 | metadata: 14 | labels: 15 | app: db 16 | spec: 17 | containers: 18 | - image: postgres:15-alpine 19 | name: postgres 20 | env: 21 | - name: POSTGRES_USER 22 | value: postgres 23 | - name: POSTGRES_PASSWORD 24 | value: postgres 25 | ports: 26 | - containerPort: 5432 27 | name: postgres 28 | volumeMounts: 29 | - mountPath: /var/lib/postgresql/data 30 | name: db-data 31 | volumes: 32 | - name: db-data 33 | emptyDir: {} 34 | -------------------------------------------------------------------------------- /k8s-dapr/db-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: db 6 | name: db 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - name: "db-service" 11 | port: 5432 12 | targetPort: 5432 13 | selector: 14 | app: db 15 | 16 | -------------------------------------------------------------------------------- /k8s-dapr/redis-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: redis 6 | name: redis 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: redis 12 | template: 13 | metadata: 14 | labels: 15 | app: redis 16 | spec: 17 | containers: 18 | - image: salaboy/redis-with-json:1.0.0 19 | name: redis 20 | ports: 21 | - containerPort: 6379 22 | name: redis 23 | volumeMounts: 24 | - mountPath: /data 25 | name: redis-data 26 | volumes: 27 | - name: redis-data 28 | emptyDir: {} 29 | -------------------------------------------------------------------------------- /k8s-dapr/redis-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: redis 6 | name: redis 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - name: "redis-service" 11 | port: 6379 12 | targetPort: 6379 13 | selector: 14 | app: redis 15 | 16 | -------------------------------------------------------------------------------- /k8s-dapr/result-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: result 6 | name: result 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: result 12 | template: 13 | metadata: 14 | annotations: 15 | dapr.io/app-id: result 16 | dapr.io/app-port: "3000" 17 | dapr.io/enabled: "true" 18 | dapr.io/sidecar-image: "docker.io/daprio/daprd:1.13.0-rc.2" 19 | dapr.io/log-level: "debug" 20 | labels: 21 | app: result 22 | spec: 23 | containers: 24 | - image: salaboy/examplevotingapp_result:go 25 | name: result 26 | imagePullPolicy: Always 27 | ports: 28 | - containerPort: 3000 29 | name: result 30 | -------------------------------------------------------------------------------- /k8s-dapr/result-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: result 6 | name: result 7 | spec: 8 | type: NodePort 9 | ports: 10 | - name: "result-service" 11 | port: 5001 12 | targetPort: 3000 13 | nodePort: 31001 14 | selector: 15 | app: result 16 | -------------------------------------------------------------------------------- /k8s-dapr/results-statestore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: results-statestore 5 | spec: 6 | type: state.postgresql 7 | version: v1 8 | metadata: 9 | - name: keyPrefix 10 | value: name 11 | - name: connectionString 12 | value: "host=db user=postgres password=postgres port=5432 connect_timeout=10" 13 | -------------------------------------------------------------------------------- /k8s-dapr/vote-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: vote 6 | name: vote 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: vote 12 | template: 13 | metadata: 14 | annotations: 15 | dapr.io/app-id: vote 16 | dapr.io/app-port: "8080" 17 | dapr.io/enabled: "true" 18 | dapr.io/sidecar-image: "docker.io/daprio/daprd:1.13.0-rc.2" 19 | dapr.io/log-level: "debug" 20 | labels: 21 | app: vote 22 | spec: 23 | containers: 24 | - image: salaboy/examplevotingapp_vote:java 25 | name: vote 26 | imagePullPolicy: Always 27 | ports: 28 | - containerPort: 8080 29 | name: vote 30 | -------------------------------------------------------------------------------- /k8s-dapr/vote-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: vote 6 | name: vote 7 | spec: 8 | type: NodePort 9 | ports: 10 | - name: "vote-service" 11 | port: 6000 12 | targetPort: 8080 13 | nodePort: 31000 14 | selector: 15 | app: vote 16 | 17 | -------------------------------------------------------------------------------- /k8s-dapr/votes-statestore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: votes-statestore 5 | spec: 6 | type: state.redis 7 | version: v1 8 | metadata: 9 | - name: keyPrefix 10 | value: name 11 | - name: redisHost 12 | value: redis:6379 13 | - name: redisPassword 14 | value: "" 15 | - name: actorStateStore 16 | value: "true" 17 | - name: queryIndexes 18 | value: | 19 | [ 20 | { 21 | "name": "voteIndex", 22 | "indexes": [ 23 | { 24 | "key": "type", 25 | "type": "TEXT" 26 | } 27 | ] 28 | } 29 | ] -------------------------------------------------------------------------------- /k8s-dapr/worker-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: worker 6 | name: worker 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: worker 12 | template: 13 | metadata: 14 | annotations: 15 | dapr.io/app-id: worker 16 | dapr.io/enabled: "true" 17 | dapr.io/sidecar-image: "docker.io/daprio/daprd:1.13.0-rc.2" 18 | dapr.io/log-level: "debug" 19 | labels: 20 | app: worker 21 | spec: 22 | containers: 23 | - image: salaboy/examplevotingapp_worker:dotnet 24 | name: worker 25 | imagePullPolicy: Always 26 | -------------------------------------------------------------------------------- /k8s-specifications/db-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: db 6 | name: db 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: db 12 | template: 13 | metadata: 14 | labels: 15 | app: db 16 | spec: 17 | containers: 18 | - image: postgres:15-alpine 19 | name: postgres 20 | env: 21 | - name: POSTGRES_USER 22 | value: postgres 23 | - name: POSTGRES_PASSWORD 24 | value: postgres 25 | ports: 26 | - containerPort: 5432 27 | name: postgres 28 | volumeMounts: 29 | - mountPath: /var/lib/postgresql/data 30 | name: db-data 31 | volumes: 32 | - name: db-data 33 | emptyDir: {} 34 | -------------------------------------------------------------------------------- /k8s-specifications/db-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: db 6 | name: db 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - name: "db-service" 11 | port: 5432 12 | targetPort: 5432 13 | selector: 14 | app: db 15 | 16 | -------------------------------------------------------------------------------- /k8s-specifications/redis-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: redis 6 | name: redis 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: redis 12 | template: 13 | metadata: 14 | labels: 15 | app: redis 16 | spec: 17 | containers: 18 | - image: redis:alpine 19 | name: redis 20 | ports: 21 | - containerPort: 6379 22 | name: redis 23 | volumeMounts: 24 | - mountPath: /data 25 | name: redis-data 26 | volumes: 27 | - name: redis-data 28 | emptyDir: {} 29 | -------------------------------------------------------------------------------- /k8s-specifications/redis-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: redis 6 | name: redis 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - name: "redis-service" 11 | port: 6379 12 | targetPort: 6379 13 | selector: 14 | app: redis 15 | 16 | -------------------------------------------------------------------------------- /k8s-specifications/result-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: result 6 | name: result 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: result 12 | template: 13 | metadata: 14 | labels: 15 | app: result 16 | spec: 17 | containers: 18 | - image: dockersamples/examplevotingapp_result 19 | name: result 20 | ports: 21 | - containerPort: 80 22 | name: result 23 | -------------------------------------------------------------------------------- /k8s-specifications/result-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: result 6 | name: result 7 | spec: 8 | type: NodePort 9 | ports: 10 | - name: "result-service" 11 | port: 5001 12 | targetPort: 80 13 | nodePort: 31001 14 | selector: 15 | app: result 16 | -------------------------------------------------------------------------------- /k8s-specifications/vote-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: vote 6 | name: vote 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: vote 12 | template: 13 | metadata: 14 | labels: 15 | app: vote 16 | spec: 17 | containers: 18 | - image: dockersamples/examplevotingapp_vote 19 | name: vote 20 | ports: 21 | - containerPort: 80 22 | name: vote 23 | -------------------------------------------------------------------------------- /k8s-specifications/vote-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: vote 6 | name: vote 7 | spec: 8 | type: NodePort 9 | ports: 10 | - name: "vote-service" 11 | port: 6000 12 | targetPort: 80 13 | nodePort: 31000 14 | selector: 15 | app: vote 16 | 17 | -------------------------------------------------------------------------------- /k8s-specifications/worker-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: worker 6 | name: worker 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: worker 12 | template: 13 | metadata: 14 | labels: 15 | app: worker 16 | spec: 17 | containers: 18 | - image: dockersamples/examplevotingapp_worker 19 | name: worker 20 | -------------------------------------------------------------------------------- /platform/apps/app-echo.yml: -------------------------------------------------------------------------------- 1 | apiVersion: serving.knative.dev/v1 2 | kind: Service 3 | metadata: 4 | name: echo-1 5 | namespace: default 6 | annotations: 7 | networking.knative.dev/http-protocol: "enabled" 8 | spec: 9 | template: 10 | metadata: 11 | annotations: 12 | autoscaling.knative.dev/min-scale: "1" 13 | dapr.io/app-id: echo 14 | dapr.io/enabled: "true" 15 | dapr.io/log-level: "debug" 16 | dapr.io/metrics-port: "9092" 17 | dapr.io/app-port: "8080" 18 | spec: 19 | containers: 20 | - image: docker.io/salaboy/echo-java:1.0.0-amd 21 | imagePullPolicy: Always 22 | ports: 23 | - containerPort: 8080 24 | env: 25 | # - name: PUBLIC_IP 26 | # value: echo.default.127.0.0.1.sslip.io 27 | # - name: DAPR_HTTP_ENDPOINT 28 | # value: http://echo-dapr.default.svc.cluster.local:3500 29 | # - name: DAPR_GRPC_ENDPOINT 30 | # value: http://echo-dapr.default.svc.cluster.local:50001 31 | # - name: DAPR_HTTP_ENDPOINT 32 | # value: "https://http-prj11702.api.cloud.diagrid.io" 33 | # - name: DAPR_GRPC_ENDPOINT 34 | # value: "https://grpc-prj11702.api.cloud.diagrid.io" 35 | # - name: DAPR_API_TOKEN 36 | # valueFrom: 37 | # secretKeyRef: 38 | # name: echo-secret 39 | # key: token 40 | # - name: ECHO_STATE_STORE 41 | # value: kvstore 42 | - name: ECHO_STATE_STORE 43 | value: votes-statestore -------------------------------------------------------------------------------- /platform/apps/app-result.yml: -------------------------------------------------------------------------------- 1 | apiVersion: serving.knative.dev/v1 2 | kind: Service 3 | metadata: 4 | name: result-1 5 | namespace: default 6 | spec: 7 | template: 8 | metadata: 9 | annotations: 10 | autoscaling.knative.dev/min-scale: "1" 11 | dapr.io/app-id: result 12 | dapr.io/enabled: "true" 13 | dapr.io/log-level: "debug" 14 | dapr.io/metrics-port: "9092" 15 | spec: 16 | containers: 17 | - image: docker.io/salaboy/result-java:1.0.0-amd 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 8080 21 | env: 22 | # - name: DAPR_HTTP_ENDPOINT 23 | # value: http://result-dapr.default.svc.cluster.local:3500 24 | # - name: DAPR_GRPC_ENDPOINT 25 | # value: http://result-dapr.default.svc.cluster.local:50001 26 | # - name: DAPR_HTTP_ENDPOINT 27 | # value: "https://http-prj11702.api.cloud.diagrid.io" 28 | # - name: DAPR_GRPC_ENDPOINT 29 | # value: "https://grpc-prj11702.api.cloud.diagrid.io" 30 | # - name: DAPR_API_TOKEN 31 | # valueFrom: 32 | # secretKeyRef: 33 | # name: result-secret 34 | # key: token 35 | - name: DAPR_STATESTORE_NAME 36 | value: results-statestore 37 | # - name: DAPR_STATESTORE_NAME 38 | # value: kvstore 39 | -------------------------------------------------------------------------------- /platform/apps/app-vote.yml: -------------------------------------------------------------------------------- 1 | apiVersion: serving.knative.dev/v1 2 | kind: Service 3 | metadata: 4 | name: vote-1 5 | namespace: default 6 | spec: 7 | template: 8 | metadata: 9 | annotations: 10 | autoscaling.knative.dev/min-scale: "1" 11 | autoscaling.knative.dev/target: "10" 12 | dapr.io/app-id: vote 13 | dapr.io/enabled: "true" 14 | dapr.io/log-level: "debug" 15 | dapr.io/metrics-port: "9092" 16 | spec: 17 | containers: 18 | - image: docker.io/salaboy/vote-java:1.0.0-amd 19 | imagePullPolicy: Always 20 | ports: 21 | - containerPort: 8080 22 | env: 23 | # - name: DAPR_HTTP_ENDPOINT 24 | # value: http://vote-dapr.default.svc.cluster.local:3500 25 | # - name: DAPR_GRPC_ENDPOINT 26 | # value: http://vote-dapr.default.svc.cluster.local:50001 27 | # - name: DAPR_HTTP_ENDPOINT 28 | # value: "https://http-prj11702.api.cloud.diagrid.io" 29 | # - name: DAPR_GRPC_ENDPOINT 30 | # value: "https://grpc-prj11702.api.cloud.diagrid.io" 31 | # - name: DAPR_API_TOKEN 32 | # valueFrom: 33 | # secretKeyRef: 34 | # name: vote-secret 35 | # key: token 36 | # - name: DAPR_STATESTORE_NAME 37 | # value: kvstore 38 | # - name: DAPR_PUBSUB_NAME 39 | # value: pubsub 40 | - name: DAPR_TOPIC_NAME 41 | value: newVote 42 | - name: VOTE_PUBSUB 43 | value: pubsub 44 | - name: DAPR_STATESTORE_NAME 45 | value: votes-statestore 46 | - name: DAPR_PUBSUB_NAME 47 | value: pubsub-rabbitmq 48 | # - name: DAPR_TOPIC_NAME 49 | # value: newVote 50 | # - name: VOTE_PUBSUB 51 | # value: pubsub-rabbitmq 52 | - name: VOTE_TOPIC 53 | value: newVote 54 | -------------------------------------------------------------------------------- /platform/apps/app-worker.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: worker 5 | namespace: default 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: worker 11 | template: 12 | metadata: 13 | annotations: 14 | dapr.io/app-id: worker 15 | dapr.io/enabled: "true" 16 | dapr.io/log-level: "debug" 17 | labels: 18 | app: worker 19 | spec: 20 | containers: 21 | - name: user-container 22 | image: docker.io/salaboy/worker-java:1.0.0-amd 23 | imagePullPolicy: Always 24 | env: 25 | # - name: DAPR_HTTP_ENDPOINT 26 | # value: http://worker-dapr.default.svc.cluster.local:3500 27 | # - name: DAPR_GRPC_ENDPOINT 28 | # value: http://worker-dapr.default.svc.cluster.local:50001 29 | # - name: DAPR_HTTP_ENDPOINT 30 | # value: "https://http-prj11702.api.cloud.diagrid.io" 31 | # - name: DAPR_GRPC_ENDPOINT 32 | # value: "https://grpc-prj11702.api.cloud.diagrid.io" 33 | # - name: DAPR_API_TOKEN 34 | # valueFrom: 35 | # secretKeyRef: 36 | # name: worker-secret 37 | # key: token 38 | - name: BASE_URL 39 | value: http://worker-dapr.default.svc.cluster.local 40 | - name: WORKER_VOTE_STATESTORE 41 | value: votes-statestore 42 | - name: WORKER_RESULTS_STATESTORE 43 | value: results-statestore 44 | # - name: WORKER_VOTE_STATESTORE 45 | # value: kvstore 46 | # - name: WORKER_RESULTS_STATESTORE 47 | # value: kvstore 48 | - name: WORKER_VOTE_QUERYINDEX 49 | value: voteIndex 50 | -------------------------------------------------------------------------------- /platform/apps/pubsub-rabbitmq.yml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: pubsub-rabbitmq 5 | namespace: default 6 | spec: 7 | type: pubsub.rabbitmq 8 | version: v1 9 | metadata: 10 | - name: protocol 11 | value: amqp 12 | - name: hostname 13 | value: rabbitmq.default.svc.cluster.local 14 | - name: username 15 | secretKeyRef: 16 | name: rabbitmq-default-user 17 | key: username 18 | - name: password 19 | secretKeyRef: 20 | name: rabbitmq-default-user 21 | key: password 22 | - name: heartBeat 23 | value: 10s 24 | -------------------------------------------------------------------------------- /platform/apps/statestore-results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: dapr.io/v1alpha1 3 | kind: Component 4 | metadata: 5 | name: results-statestore 6 | namespace: default 7 | spec: 8 | type: state.postgresql 9 | version: v1 10 | metadata: 11 | - name: keyPrefix 12 | value: name 13 | - name: connectionString 14 | value: "host=postgres.default.svc.cluster.local user=postgres password=postgres port=5432 connect_timeout=10" 15 | 16 | -------------------------------------------------------------------------------- /platform/apps/statestore-votes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: dapr.io/v1alpha1 3 | kind: Component 4 | metadata: 5 | name: votes-statestore 6 | namespace: default 7 | spec: 8 | type: state.redis 9 | version: v1 10 | metadata: 11 | - name: keyPrefix 12 | value: name 13 | - name: redisHost 14 | value: redis.default.svc.cluster.local:6379 15 | - name: redisPassword 16 | value: "" 17 | - name: outboxPublishPubsub # Required 18 | value: "pubsub-rabbitmq" 19 | - name: outboxPublishTopic # Required 20 | value: "newVote" 21 | - name: outboxDiscardWhenMissingState #Optional. Defaults to false 22 | value: false 23 | - name: actorStateStore 24 | value: "true" 25 | - name: queryIndexes 26 | value: | 27 | [ 28 | { 29 | "name": "voteIndex", 30 | "indexes": [ 31 | { 32 | "key": "type", 33 | "type": "TEXT" 34 | }, 35 | { 36 | "key": "option", 37 | "type": "TEXT" 38 | } 39 | ] 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /platform/apps/subscription-echo.yml: -------------------------------------------------------------------------------- 1 | # apiVersion: dapr.io/v1alpha1 2 | # kind: Subscription 3 | # metadata: 4 | # name: echo-subscritpion 5 | # namespace: default 6 | # spec: 7 | # topic: newVote 8 | # route: /events 9 | # pubsubname: pubsub-rabbitmq 10 | -------------------------------------------------------------------------------- /platform/gitops/apps/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | namespace: default 6 | 7 | resources: 8 | - ../../../apps/app-echo.yml 9 | - ../../../apps/app-result.yml 10 | - ../../../apps/app-vote.yml 11 | - ../../../apps/app-worker.yml 12 | - ../../../apps/pubsub-rabbitmq.yml 13 | - ../../../apps/statestore-results.yml 14 | - ../../../apps/statestore-votes.yml 15 | - ../../../apps/subscription-echo.yml 16 | -------------------------------------------------------------------------------- /platform/gitops/apps/cloud/kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | namespace: default 6 | 7 | resources: 8 | - ../base 9 | 10 | patches: 11 | - path: patch-echo.yml 12 | target: 13 | group: serving.knative.dev 14 | version: v1 15 | kind: Service 16 | name: echo 17 | -------------------------------------------------------------------------------- /platform/gitops/apps/cloud/patch-echo.yml: -------------------------------------------------------------------------------- 1 | apiVersion: serving.knative.dev/v1 2 | kind: Service 3 | metadata: 4 | name: echo 5 | namespace: default 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - image: docker.io/salaboy/echo-java:1.0.0-amd 11 | env: 12 | - name: DAPR_HTTP_ENDPOINT 13 | value: http://echo-dapr.default.svc.cluster.local:3500 14 | - name: DAPR_GRPC_ENDPOINT 15 | value: http://echo-dapr.default.svc.cluster.local:50001 16 | - name: ECHO_STATE_STORE 17 | value: votes-statestore 18 | # - name: DAPR_HTTP_ENDPOINT 19 | # value: "https://http-prj11702.api.cloud.diagrid.io" 20 | # - name: DAPR_GRPC_ENDPOINT 21 | # value: "https://grpc-prj11702.api.cloud.diagrid.io" 22 | # - name: DAPR_API_TOKEN 23 | # valueFrom: 24 | # secretKeyRef: 25 | # name: echo-secret 26 | # key: token 27 | # - name: ECHO_STATE_STORE 28 | # value: kvstore 29 | 30 | -------------------------------------------------------------------------------- /platform/gitops/apps/local/kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | namespace: default 6 | 7 | resources: 8 | - ../base 9 | -------------------------------------------------------------------------------- /platform/gitops/clusters/cloud/apps.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.toolkit.fluxcd.io/v1 3 | kind: Kustomization 4 | metadata: 5 | name: apps 6 | namespace: kadras-system 7 | spec: 8 | # dependsOn: 9 | # - name: dapr 10 | interval: 1m 11 | retryInterval: 1m 12 | timeout: 5m 13 | sourceRef: 14 | kind: GitRepository 15 | name: gitops-configurer 16 | path: ./platform/gitops/apps/cloud 17 | prune: true 18 | wait: true 19 | -------------------------------------------------------------------------------- /platform/gitops/clusters/cloud/infrastructure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.toolkit.fluxcd.io/v1 3 | kind: Kustomization 4 | metadata: 5 | name: data 6 | namespace: kadras-system 7 | spec: 8 | interval: 1m 9 | retryInterval: 1m 10 | timeout: 5m 11 | sourceRef: 12 | kind: GitRepository 13 | name: gitops-configurer 14 | path: ./platform/gitops/infrastructure/data 15 | prune: true 16 | wait: true 17 | 18 | --- 19 | apiVersion: kustomize.toolkit.fluxcd.io/v1 20 | kind: Kustomization 21 | metadata: 22 | name: dapr 23 | namespace: kadras-system 24 | spec: 25 | interval: 1m 26 | retryInterval: 1m 27 | timeout: 5m 28 | sourceRef: 29 | kind: GitRepository 30 | name: gitops-configurer 31 | path: ./platform/gitops/infrastructure/dapr 32 | prune: true 33 | wait: true 34 | 35 | --- 36 | apiVersion: kustomize.toolkit.fluxcd.io/v1 37 | kind: Kustomization 38 | metadata: 39 | name: observability 40 | namespace: kadras-system 41 | spec: 42 | interval: 1m 43 | retryInterval: 1m 44 | timeout: 5m 45 | sourceRef: 46 | kind: GitRepository 47 | name: gitops-configurer 48 | path: ./platform/gitops/infrastructure/observability 49 | prune: true 50 | wait: true 51 | -------------------------------------------------------------------------------- /platform/gitops/clusters/local/apps.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.toolkit.fluxcd.io/v1 3 | kind: Kustomization 4 | metadata: 5 | name: apps 6 | namespace: kadras-system 7 | spec: 8 | dependsOn: 9 | - name: dapr 10 | interval: 1m 11 | retryInterval: 1m 12 | timeout: 5m 13 | sourceRef: 14 | kind: GitRepository 15 | name: gitops-configurer 16 | path: ./platform/gitops/apps/local 17 | prune: true 18 | wait: true 19 | -------------------------------------------------------------------------------- /platform/gitops/clusters/local/infrastructure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.toolkit.fluxcd.io/v1 3 | kind: Kustomization 4 | metadata: 5 | name: data 6 | namespace: kadras-system 7 | spec: 8 | interval: 1m 9 | retryInterval: 1m 10 | timeout: 5m 11 | sourceRef: 12 | kind: GitRepository 13 | name: gitops-configurer 14 | path: ./platform/gitops/infrastructure/data 15 | prune: true 16 | wait: true 17 | 18 | --- 19 | apiVersion: kustomize.toolkit.fluxcd.io/v1 20 | kind: Kustomization 21 | metadata: 22 | name: dapr 23 | namespace: kadras-system 24 | spec: 25 | interval: 1m 26 | retryInterval: 1m 27 | timeout: 5m 28 | sourceRef: 29 | kind: GitRepository 30 | name: gitops-configurer 31 | path: ./platform/gitops/infrastructure/dapr 32 | prune: true 33 | wait: true 34 | 35 | --- 36 | apiVersion: kustomize.toolkit.fluxcd.io/v1 37 | kind: Kustomization 38 | metadata: 39 | name: observability 40 | namespace: kadras-system 41 | spec: 42 | interval: 1m 43 | retryInterval: 1m 44 | timeout: 5m 45 | sourceRef: 46 | kind: GitRepository 47 | name: gitops-configurer 48 | path: ./platform/gitops/infrastructure/observability 49 | prune: true 50 | wait: true 51 | 52 | # TODO: add patch to make Grafana work locally -------------------------------------------------------------------------------- /platform/gitops/infrastructure/dapr/dapr-shared.yml: -------------------------------------------------------------------------------- 1 | # --- 2 | # apiVersion: source.toolkit.fluxcd.io/v1 3 | # kind: HelmRepository 4 | # metadata: 5 | # name: dapr 6 | # namespace: default 7 | # spec: 8 | # type: "oci" 9 | # interval: 24h 10 | # url: oci://registry-1.docker.io/daprio 11 | 12 | # --- 13 | # apiVersion: helm.toolkit.fluxcd.io/v2 14 | # kind: HelmRelease 15 | # metadata: 16 | # name: vote 17 | # namespace: default 18 | # spec: 19 | # interval: 30m 20 | # chart: 21 | # spec: 22 | # chart: dapr-shared-chart 23 | # version: "0.0.x" 24 | # sourceRef: 25 | # kind: HelmRepository 26 | # name: dapr 27 | # namespace: default 28 | # interval: 12h 29 | # values: 30 | # shared: 31 | # appId: vote 32 | # daprd: 33 | # image: 34 | # tag: 1.13.0 35 | # strategy: deployment 36 | 37 | # --- 38 | # apiVersion: helm.toolkit.fluxcd.io/v2 39 | # kind: HelmRelease 40 | # metadata: 41 | # name: result 42 | # namespace: default 43 | # spec: 44 | # interval: 30m 45 | # chart: 46 | # spec: 47 | # chart: dapr-shared-chart 48 | # version: "0.0.x" 49 | # sourceRef: 50 | # kind: HelmRepository 51 | # name: dapr 52 | # namespace: default 53 | # interval: 12h 54 | # values: 55 | # shared: 56 | # appId: result 57 | # daprd: 58 | # image: 59 | # tag: 1.13.0 60 | # strategy: deployment 61 | 62 | # --- 63 | # apiVersion: helm.toolkit.fluxcd.io/v2 64 | # kind: HelmRelease 65 | # metadata: 66 | # name: worker 67 | # namespace: default 68 | # spec: 69 | # interval: 30m 70 | # chart: 71 | # spec: 72 | # chart: dapr-shared-chart 73 | # version: "0.0.x" 74 | # sourceRef: 75 | # kind: HelmRepository 76 | # name: dapr 77 | # namespace: default 78 | # interval: 12h 79 | # values: 80 | # shared: 81 | # appId: worker 82 | # daprd: 83 | # image: 84 | # tag: 1.13.0 85 | # strategy: deployment 86 | 87 | # --- 88 | # apiVersion: helm.toolkit.fluxcd.io/v2 89 | # kind: HelmRelease 90 | # metadata: 91 | # name: echo 92 | # namespace: default 93 | # spec: 94 | # interval: 30m 95 | # chart: 96 | # spec: 97 | # chart: dapr-shared-chart 98 | # version: "0.0.x" 99 | # sourceRef: 100 | # kind: HelmRepository 101 | # name: dapr 102 | # namespace: default 103 | # interval: 12h 104 | # values: 105 | # shared: 106 | # appId: echo 107 | # daprd: 108 | # image: 109 | # tag: 1.13.0 110 | # remoteURL: echo.default.svc.cluster.local 111 | # remotePort: 80 112 | # strategy: deployment 113 | -------------------------------------------------------------------------------- /platform/gitops/infrastructure/data/postgresql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: postgresql 6 | namespace: default 7 | labels: 8 | db: postgres 9 | spec: 10 | selector: 11 | matchLabels: 12 | db: postgres 13 | template: 14 | metadata: 15 | labels: 16 | db: postgres 17 | spec: 18 | containers: 19 | - name: postgres 20 | image: postgres:16.2-alpine 21 | env: 22 | - name: POSTGRES_USER 23 | value: postgres 24 | - name: POSTGRES_PASSWORD 25 | value: postgres 26 | ports: 27 | - containerPort: 5432 28 | name: postgres 29 | resources: 30 | requests: 31 | cpu: 100m 32 | memory: 60Mi 33 | limits: 34 | cpu: 200m 35 | memory: 120Mi 36 | volumeMounts: 37 | - mountPath: /var/lib/postgresql/data 38 | name: postgresql-data 39 | volumes: 40 | - name: postgresql-data 41 | emptyDir: {} 42 | 43 | --- 44 | apiVersion: v1 45 | kind: Service 46 | metadata: 47 | name: postgres 48 | namespace: default 49 | labels: 50 | db: postgres 51 | spec: 52 | type: ClusterIP 53 | selector: 54 | db: postgres 55 | ports: 56 | - protocol: TCP 57 | port: 5432 58 | targetPort: 5432 59 | -------------------------------------------------------------------------------- /platform/gitops/infrastructure/data/rabbitmq.yml: -------------------------------------------------------------------------------- 1 | apiVersion: rabbitmq.com/v1beta1 2 | kind: RabbitmqCluster 3 | metadata: 4 | name: rabbitmq 5 | namespace: default 6 | spec: 7 | replicas: 1 8 | resources: 9 | requests: 10 | cpu: 100m 11 | memory: 275Mi 12 | limits: 13 | cpu: 1000m 14 | memory: 756Mi 15 | persistence: 16 | storage: 10Gi 17 | rabbitmq: 18 | additionalConfig: | 19 | vm_memory_high_watermark_paging_ratio = 0.99 20 | vm_memory_high_watermark.relative = 0.90 21 | terminationGracePeriodSeconds: 60 22 | -------------------------------------------------------------------------------- /platform/gitops/infrastructure/data/redis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: redis 6 | namespace: default 7 | labels: 8 | db: redis 9 | spec: 10 | selector: 11 | matchLabels: 12 | db: redis 13 | template: 14 | metadata: 15 | labels: 16 | db: redis 17 | spec: 18 | containers: 19 | - name: redis 20 | #image: docker.io/salaboy/redis-with-json:1.0.0 21 | image: docker.io/redis/redis-stack-server:7.2.0-v8 22 | ports: 23 | - containerPort: 6379 24 | name: redis 25 | resources: 26 | requests: 27 | cpu: 100m 28 | memory: 50Mi 29 | limits: 30 | cpu: 200m 31 | memory: 100Mi 32 | volumeMounts: 33 | - mountPath: /data 34 | name: redis-data 35 | volumes: 36 | - name: redis-data 37 | emptyDir: {} 38 | 39 | --- 40 | apiVersion: v1 41 | kind: Service 42 | metadata: 43 | name: redis 44 | namespace: default 45 | labels: 46 | db: redis 47 | spec: 48 | type: ClusterIP 49 | selector: 50 | db: redis 51 | ports: 52 | - protocol: TCP 53 | port: 6379 54 | targetPort: 6379 55 | -------------------------------------------------------------------------------- /platform/installation/values-cloud.yml: -------------------------------------------------------------------------------- 1 | platform: 2 | profile: run 3 | 4 | additional_packages: 5 | - dapr 6 | - gitops-configurer 7 | - rabbitmq-operator 8 | 9 | excluded_packages: 10 | - metrics-server 11 | 12 | ingress: 13 | domain: demo.diagrid.dev 14 | issuer: 15 | type: letsencrypt 16 | email: security@kadras.io 17 | 18 | contour: 19 | certificates: 20 | useCertManager: true 21 | 22 | flux: 23 | optional_components: 24 | helm_controller: true 25 | 26 | gitops_configurer: 27 | type: flux-kustomization 28 | git: 29 | url: https://github.com/salaboy/example-voting-app 30 | path: platform/gitops/clusters/cloud 31 | -------------------------------------------------------------------------------- /platform/installation/values-local.yml: -------------------------------------------------------------------------------- 1 | platform: 2 | profile: run 3 | 4 | infrastructure_provider: local 5 | 6 | additional_packages: 7 | - dapr 8 | - gitops-configurer 9 | - rabbitmq-operator 10 | 11 | ingress: 12 | domain: 127.0.0.1.sslip.io 13 | 14 | flux: 15 | optional_components: 16 | helm_controller: true 17 | 18 | gitops_configurer: 19 | type: flux-kustomization 20 | git: 21 | url: https://github.com/salaboy/example-voting-app 22 | path: platform/gitops/clusters/local 23 | -------------------------------------------------------------------------------- /result/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /result/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-slim 2 | 3 | # add curl for healthcheck 4 | RUN apt-get update && \ 5 | apt-get install -y --no-install-recommends curl tini && \ 6 | rm -rf /var/lib/apt/lists/* 7 | 8 | WORKDIR /usr/local/app 9 | 10 | # have nodemon available for local dev use (file watching) 11 | RUN npm install -g nodemon 12 | 13 | COPY package*.json ./ 14 | 15 | RUN npm ci && \ 16 | npm cache clean --force && \ 17 | mv /usr/local/app/node_modules /node_modules 18 | 19 | COPY . . 20 | 21 | ENV PORT 80 22 | EXPOSE 80 23 | 24 | ENTRYPOINT ["/usr/bin/tini", "--"] 25 | CMD ["node", "server.js"] 26 | -------------------------------------------------------------------------------- /result/docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | 5 | sut: 6 | build: ./tests/ 7 | depends_on: 8 | - vote 9 | - result 10 | - worker 11 | networks: 12 | - front-tier 13 | 14 | vote: 15 | build: ../vote/ 16 | ports: ["80"] 17 | depends_on: 18 | - redis 19 | - db 20 | networks: 21 | - front-tier 22 | - back-tier 23 | 24 | result: 25 | build: . 26 | ports: ["80"] 27 | depends_on: 28 | - redis 29 | - db 30 | networks: 31 | - front-tier 32 | - back-tier 33 | 34 | worker: 35 | build: ../worker/ 36 | depends_on: 37 | - redis 38 | - db 39 | networks: 40 | - back-tier 41 | 42 | redis: 43 | image: redis:alpine 44 | networks: 45 | - back-tier 46 | 47 | db: 48 | image: postgres:9.4 49 | environment: 50 | POSTGRES_USER: "postgres" 51 | POSTGRES_PASSWORD: "postgres" 52 | volumes: 53 | - "db-data:/var/lib/postgresql/data" 54 | networks: 55 | - back-tier 56 | 57 | volumes: 58 | db-data: 59 | 60 | networks: 61 | front-tier: 62 | back-tier: 63 | -------------------------------------------------------------------------------- /result/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "result", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "async": "^3.1.0", 13 | "cookie-parser": "^1.4.6", 14 | "express": "^4.18.2", 15 | "method-override": "^3.0.0", 16 | "pg": "^8.8.0", 17 | "socket.io": "^4.7.2", 18 | "stoppable": "^1.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /result/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | async = require('async'), 3 | { Pool } = require('pg'), 4 | cookieParser = require('cookie-parser'), 5 | app = express(), 6 | server = require('http').Server(app), 7 | io = require('socket.io')(server); 8 | 9 | var port = process.env.PORT || 4000; 10 | 11 | io.on('connection', function (socket) { 12 | 13 | socket.emit('message', { text : 'Welcome!' }); 14 | 15 | socket.on('subscribe', function (data) { 16 | socket.join(data.channel); 17 | }); 18 | }); 19 | 20 | var pool = new Pool({ 21 | connectionString: 'postgres://postgres:postgres@db/postgres' 22 | }); 23 | 24 | async.retry( 25 | {times: 1000, interval: 1000}, 26 | function(callback) { 27 | pool.connect(function(err, client, done) { 28 | if (err) { 29 | console.error("Waiting for db"); 30 | } 31 | callback(err, client); 32 | }); 33 | }, 34 | function(err, client) { 35 | if (err) { 36 | return console.error("Giving up"); 37 | } 38 | console.log("Connected to db"); 39 | getVotes(client); 40 | } 41 | ); 42 | 43 | function getVotes(client) { 44 | client.query('SELECT vote, COUNT(id) AS count FROM votes GROUP BY vote', [], function(err, result) { 45 | if (err) { 46 | console.error("Error performing query: " + err); 47 | } else { 48 | var votes = collectVotesFromResult(result); 49 | io.sockets.emit("scores", JSON.stringify(votes)); 50 | } 51 | 52 | setTimeout(function() {getVotes(client) }, 1000); 53 | }); 54 | } 55 | 56 | function collectVotesFromResult(result) { 57 | var votes = {a: 0, b: 0}; 58 | 59 | result.rows.forEach(function (row) { 60 | votes[row.vote] = parseInt(row.count); 61 | }); 62 | 63 | return votes; 64 | } 65 | 66 | app.use(cookieParser()); 67 | app.use(express.urlencoded()); 68 | app.use(express.static(__dirname + '/views')); 69 | 70 | app.get('/', function (req, res) { 71 | res.sendFile(path.resolve(__dirname + '/views/index.html')); 72 | }); 73 | 74 | server.listen(port, function () { 75 | var port = server.address().port; 76 | console.log('App running on port ' + port); 77 | }); 78 | -------------------------------------------------------------------------------- /result/tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.9-slim 2 | 3 | RUN apt-get update -qq && apt-get install -qy \ 4 | ca-certificates \ 5 | bzip2 \ 6 | curl \ 7 | libfontconfig \ 8 | --no-install-recommends 9 | RUN yarn global add phantomjs-prebuilt 10 | ADD . /app 11 | WORKDIR /app 12 | CMD ["/app/tests.sh"] 13 | -------------------------------------------------------------------------------- /result/tests/render.js: -------------------------------------------------------------------------------- 1 | var system = require('system'); 2 | var page = require('webpage').create(); 3 | var url = system.args[1]; 4 | 5 | page.onLoadFinished = function() { 6 | setTimeout(function(){ 7 | console.log(page.content); 8 | phantom.exit(); 9 | }, 1000); 10 | }; 11 | 12 | page.open(url, function() { 13 | page.evaluate(function() { 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /result/tests/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | while ! timeout 1 bash -c "echo > /dev/tcp/vote/80"; do 4 | sleep 1 5 | done 6 | 7 | curl -sS -X POST --data "vote=b" http://vote > /dev/null 8 | sleep 10 9 | 10 | if phantomjs render.js http://result | grep -q '1 vote'; then 11 | echo -e "\\e[42m------------" 12 | echo -e "\\e[92mTests passed" 13 | echo -e "\\e[42m------------" 14 | exit 0 15 | else 16 | echo -e "\\e[41m------------" 17 | echo -e "\\e[91mTests failed" 18 | echo -e "\\e[41m------------" 19 | exit 1 20 | fi 21 | -------------------------------------------------------------------------------- /result/views/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('catsvsdogs', []); 2 | var socket = io.connect(); 3 | 4 | var bg1 = document.getElementById('background-stats-1'); 5 | var bg2 = document.getElementById('background-stats-2'); 6 | 7 | app.controller('statsCtrl', function($scope){ 8 | $scope.aPercent = 50; 9 | $scope.bPercent = 50; 10 | 11 | var updateScores = function(){ 12 | socket.on('scores', function (json) { 13 | data = JSON.parse(json); 14 | var a = parseInt(data.a || 0); 15 | var b = parseInt(data.b || 0); 16 | 17 | var percentages = getPercentages(a, b); 18 | 19 | bg1.style.width = percentages.a + "%"; 20 | bg2.style.width = percentages.b + "%"; 21 | 22 | $scope.$apply(function () { 23 | $scope.aPercent = percentages.a; 24 | $scope.bPercent = percentages.b; 25 | $scope.total = a + b; 26 | }); 27 | }); 28 | }; 29 | 30 | var init = function(){ 31 | document.body.style.opacity=1; 32 | updateScores(); 33 | }; 34 | socket.on('message',function(data){ 35 | init(); 36 | }); 37 | }); 38 | 39 | function getPercentages(a, b) { 40 | var result = {}; 41 | 42 | if (a + b > 0) { 43 | result.a = Math.round(a / (a + b) * 100); 44 | result.b = 100 - result.a; 45 | } else { 46 | result.a = result.b = 50; 47 | } 48 | 49 | return result; 50 | } -------------------------------------------------------------------------------- /result/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cats vs Dogs -- Result 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
Cats
24 |
{{aPercent | number:1}}%
25 |
26 |
27 |
28 |
Dogs
29 |
{{bPercent | number:1}}%
30 |
31 |
32 |
33 |
34 |
35 | No votes yet 36 | {{total}} vote 37 | {{total}} votes 38 |
39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /result/views/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | *{ 4 | box-sizing:border-box; 5 | } 6 | html,body{ 7 | margin:0; 8 | padding:0; 9 | height:100%; 10 | font-family: 'Open Sans'; 11 | } 12 | body{ 13 | opacity:0; 14 | transition: all 1s linear; 15 | } 16 | 17 | .divider{ 18 | height: 150px; 19 | width:2px; 20 | background-color: #C0C9CE; 21 | position: relative; 22 | top: 50%; 23 | float: left; 24 | transform: translateY(-50%); 25 | } 26 | 27 | #background-stats-1{ 28 | background-color: #2196f3; 29 | } 30 | 31 | #background-stats-2{ 32 | background-color: #00cbca; 33 | } 34 | 35 | #content-container{ 36 | z-index:2; 37 | position:relative; 38 | margin:0 auto; 39 | display:table; 40 | padding:10px; 41 | max-width:940px; 42 | height:100%; 43 | } 44 | #content-container-center{ 45 | display:table-cell; 46 | text-align:center; 47 | vertical-align:middle; 48 | } 49 | #result{ 50 | z-index: 3; 51 | position: absolute; 52 | bottom: 40px; 53 | right: 20px; 54 | color: #fff; 55 | opacity: 0.5; 56 | font-size: 45px; 57 | font-weight: 600; 58 | } 59 | #choice{ 60 | transition: all 300ms linear; 61 | line-height:1.3em; 62 | background:#fff; 63 | box-shadow: 10px 0 0 #fff, -10px 0 0 #fff; 64 | vertical-align:middle; 65 | font-size:40px; 66 | font-weight: 600; 67 | width: 450px; 68 | height: 200px; 69 | } 70 | #choice a{ 71 | text-decoration:none; 72 | } 73 | #choice a:hover, #choice a:focus{ 74 | outline:0; 75 | text-decoration:underline; 76 | } 77 | 78 | #choice .choice{ 79 | width: 49%; 80 | position: relative; 81 | top: 50%; 82 | transform: translateY(-50%); 83 | text-align: left; 84 | padding-left: 50px; 85 | } 86 | 87 | #choice .choice .label{ 88 | text-transform: uppercase; 89 | } 90 | 91 | #choice .choice.dogs{ 92 | color: #00cbca; 93 | float: right; 94 | } 95 | 96 | #choice .choice.cats{ 97 | color: #2196f3; 98 | float: left; 99 | } 100 | #background-stats{ 101 | z-index:1; 102 | height:100%; 103 | width:100%; 104 | position:absolute; 105 | } 106 | #background-stats div{ 107 | transition: width 400ms ease-in-out; 108 | display:inline-block; 109 | margin-bottom:-4px; 110 | width:50%; 111 | height:100%; 112 | } 113 | -------------------------------------------------------------------------------- /seed-data/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | # add apache bench (ab) tool 4 | RUN apt-get update \ 5 | && apt-get install -y --no-install-recommends \ 6 | apache2-utils \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | WORKDIR /seed 10 | 11 | COPY . . 12 | 13 | # create POST data files with ab friendly formats 14 | RUN python make-data.py 15 | 16 | CMD /seed/generate-votes.sh -------------------------------------------------------------------------------- /seed-data/generate-votes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # create 3000 votes (2000 for option a, 1000 for option b) 4 | ab -n 1000 -c 50 -p posta -T "application/x-www-form-urlencoded" http://vote/ 5 | ab -n 1000 -c 50 -p postb -T "application/x-www-form-urlencoded" http://vote/ 6 | ab -n 1000 -c 50 -p posta -T "application/x-www-form-urlencoded" http://vote/ 7 | -------------------------------------------------------------------------------- /seed-data/make-data.py: -------------------------------------------------------------------------------- 1 | # this creates urlencode-friendly files without EOL 2 | import urllib.parse 3 | 4 | outfile = open('postb', 'w') 5 | params = ({ 'vote': 'b' }) 6 | encoded = urllib.parse.urlencode(params) 7 | outfile.write(encoded) 8 | outfile.close() 9 | outfile = open('posta', 'w') 10 | params = ({ 'vote': 'a' }) 11 | encoded = urllib.parse.urlencode(params) 12 | outfile.write(encoded) 13 | outfile.close() 14 | -------------------------------------------------------------------------------- /vote/Dockerfile: -------------------------------------------------------------------------------- 1 | # Define a base stage that uses the official python runtime base image 2 | FROM python:3.11-slim AS base 3 | 4 | # Add curl for healthcheck 5 | RUN apt-get update && \ 6 | apt-get install -y --no-install-recommends curl && \ 7 | rm -rf /var/lib/apt/lists/* 8 | 9 | # Set the application directory 10 | WORKDIR /usr/local/app 11 | 12 | # Install our requirements.txt 13 | COPY requirements.txt ./requirements.txt 14 | RUN pip install --no-cache-dir -r requirements.txt 15 | 16 | # Define a stage specifically for development, where it'll watch for 17 | # filesystem changes 18 | FROM base AS dev 19 | RUN pip install watchdog 20 | ENV FLASK_ENV=development 21 | CMD ["python", "app.py"] 22 | 23 | # Define the final stage that will bundle the application for production 24 | FROM base AS final 25 | 26 | # Copy our code from the current folder to the working directory inside the container 27 | COPY . . 28 | 29 | # Make port 80 available for links and/or publish 30 | EXPOSE 80 31 | 32 | # Define our command to be run when launching the container 33 | CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--log-file", "-", "--access-logfile", "-", "--workers", "4", "--keep-alive", "0"] 34 | -------------------------------------------------------------------------------- /vote/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, make_response, g 2 | from redis import Redis 3 | import os 4 | import socket 5 | import random 6 | import json 7 | import logging 8 | 9 | option_a = os.getenv('OPTION_A', "Cats") 10 | option_b = os.getenv('OPTION_B', "Dogs") 11 | hostname = socket.gethostname() 12 | 13 | app = Flask(__name__) 14 | 15 | gunicorn_error_logger = logging.getLogger('gunicorn.error') 16 | app.logger.handlers.extend(gunicorn_error_logger.handlers) 17 | app.logger.setLevel(logging.INFO) 18 | 19 | def get_redis(): 20 | if not hasattr(g, 'redis'): 21 | g.redis = Redis(host="redis", db=0, socket_timeout=5) 22 | return g.redis 23 | 24 | @app.route("/", methods=['POST','GET']) 25 | def hello(): 26 | voter_id = request.cookies.get('voter_id') 27 | if not voter_id: 28 | voter_id = hex(random.getrandbits(64))[2:-1] 29 | 30 | vote = None 31 | 32 | if request.method == 'POST': 33 | redis = get_redis() 34 | vote = request.form['vote'] 35 | app.logger.info('Received vote for %s', vote) 36 | data = json.dumps({'voter_id': voter_id, 'vote': vote}) 37 | redis.rpush('votes', data) 38 | 39 | resp = make_response(render_template( 40 | 'index.html', 41 | option_a=option_a, 42 | option_b=option_b, 43 | hostname=hostname, 44 | vote=vote, 45 | )) 46 | resp.set_cookie('voter_id', voter_id) 47 | return resp 48 | 49 | 50 | if __name__ == "__main__": 51 | app.run(host='0.0.0.0', port=80, debug=True, threaded=True) 52 | -------------------------------------------------------------------------------- /vote/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Redis 3 | gunicorn 4 | -------------------------------------------------------------------------------- /vote/static/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | *{ 4 | box-sizing:border-box; 5 | } 6 | html,body{ 7 | margin: 0; 8 | padding: 0; 9 | background-color: #F7F8F9; 10 | height: 100vh; 11 | font-family: 'Open Sans'; 12 | } 13 | 14 | button{ 15 | border-radius: 0; 16 | width: 100%; 17 | height: 50%; 18 | } 19 | 20 | button[type="submit"] { 21 | -webkit-appearance:none; -webkit-border-radius:0; 22 | } 23 | 24 | button i{ 25 | float: right; 26 | padding-right: 30px; 27 | margin-top: 3px; 28 | } 29 | 30 | button.a{ 31 | background-color: #1aaaf8; 32 | } 33 | 34 | button.b{ 35 | background-color: #00cbca; 36 | } 37 | 38 | #tip{ 39 | text-align: left; 40 | color: #c0c9ce; 41 | font-size: 14px; 42 | } 43 | 44 | #hostname{ 45 | position: absolute; 46 | bottom: 100px; 47 | right: 0; 48 | left: 0; 49 | color: #8f9ea8; 50 | font-size: 24px; 51 | } 52 | 53 | #content-container{ 54 | z-index: 2; 55 | position: relative; 56 | margin: 0 auto; 57 | display: table; 58 | padding: 10px; 59 | max-width: 940px; 60 | height: 100%; 61 | } 62 | #content-container-center{ 63 | display: table-cell; 64 | text-align: center; 65 | } 66 | 67 | #content-container-center h3{ 68 | color: #254356; 69 | } 70 | 71 | #choice{ 72 | transition: all 300ms linear; 73 | line-height: 1.3em; 74 | display: inline; 75 | vertical-align: middle; 76 | font-size: 3em; 77 | } 78 | #choice a{ 79 | text-decoration:none; 80 | } 81 | #choice a:hover, #choice a:focus{ 82 | outline:0; 83 | text-decoration:underline; 84 | } 85 | 86 | #choice button{ 87 | display: block; 88 | height: 80px; 89 | width: 330px; 90 | border: none; 91 | color: white; 92 | text-transform: uppercase; 93 | font-size:18px; 94 | font-weight: 700; 95 | margin-top: 10px; 96 | margin-bottom: 10px; 97 | text-align: left; 98 | padding-left: 50px; 99 | } 100 | 101 | #choice button.a:hover{ 102 | background-color: #1488c6; 103 | } 104 | 105 | #choice button.b:hover{ 106 | background-color: #00a2a1; 107 | } 108 | 109 | #choice button.a:focus{ 110 | background-color: #1488c6; 111 | } 112 | 113 | #choice button.b:focus{ 114 | background-color: #00a2a1; 115 | } 116 | 117 | #background-stats{ 118 | z-index:1; 119 | height:100%; 120 | width:100%; 121 | position:absolute; 122 | } 123 | #background-stats div{ 124 | transition: width 400ms ease-in-out; 125 | display:inline-block; 126 | margin-bottom:-4px; 127 | width:50%; 128 | height:100%; 129 | } 130 | -------------------------------------------------------------------------------- /vote/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{option_a}} vs {{option_b}}! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

{{option_a}} vs {{option_b}}!

17 |
18 | 19 | 20 |
21 |
22 | (Tip: you can change your vote) 23 |
24 |
25 | Processed by container ID {{hostname}} 26 |
27 |
28 |
29 | 30 | 31 | 32 | {% if vote %} 33 | 47 | {% endif %} 48 | 49 | 50 | -------------------------------------------------------------------------------- /worker/Dockerfile: -------------------------------------------------------------------------------- 1 | # because of dotnet, we always build on amd64, and target platforms in cli 2 | # dotnet doesn't support QEMU for building or running. 3 | # (errors common in arm/v7 32bit) https://github.com/dotnet/dotnet-docker/issues/1537 4 | # https://hub.docker.com/_/microsoft-dotnet 5 | # hadolint ignore=DL3029 6 | # to build for a different platform than your host, use --platform= 7 | # for example, if you were on Intel (amd64) and wanted to build for ARM, you would use: 8 | # docker buildx build --platform "linux/arm64/v8" . 9 | FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:7.0 as build 10 | ARG TARGETPLATFORM 11 | ARG TARGETARCH 12 | ARG BUILDPLATFORM 13 | RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" 14 | 15 | WORKDIR /source 16 | COPY *.csproj . 17 | RUN dotnet restore -a $TARGETARCH 18 | 19 | COPY . . 20 | RUN dotnet publish -c release -o /app -a $TARGETARCH --self-contained false --no-restore 21 | 22 | # app image 23 | FROM mcr.microsoft.com/dotnet/runtime:7.0 24 | WORKDIR /app 25 | COPY --from=build /app . 26 | ENTRYPOINT ["dotnet", "Worker.dll"] -------------------------------------------------------------------------------- /worker/Worker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | --------------------------------------------------------------------------------