diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..abe6ae79f3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Set default behavior to automatically normalize line endings. +* text=auto + +# Force batch scripts to always use CRLF line endings so that if a repo is accessed +# in Windows via a file share from Linux, the scripts will work. +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf +*.{ics,[iI][cC][sS]} text eol=crlf + +# Force bash scripts to always use LF line endings so that if a repo is accessed +# in Unix via a file share from Windows, the scripts will work. +*.sh text eol=lf diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE new file mode 100644 index 0000000000..2ad58e518a --- /dev/null +++ b/.github/ISSUE_TEMPLATE @@ -0,0 +1,40 @@ +** PLEASE ONLY USE THIS ISSUE TRACKER TO SUBMIT ISSUES WITH THE EXAMPLE VOTING APP ** + +* 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) * +* 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. * +* Or go to the [Docker Forums](https://forums.docker.com/) * + +Please provide the following information so we can assess the issue you're having + +**Description** + + + +**Steps to reproduce the issue, if relevant:** +1. +2. +3. + +**Describe the results you received:** + + +**Describe the results you expected:** + + +**Additional information you deem important (e.g. issue happens only occasionally):** + +**Output of `docker version`:** + +``` +(paste your output here) +``` + +**Output of `docker info`:** + +``` +(paste your output here) +``` + +**Additional environment details (AWS, Docker for Mac, Docker for Windows, VirtualBox, physical, etc.):** diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..f9ecf576e1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml new file mode 100644 index 0000000000..a946a87b03 --- /dev/null +++ b/.github/workflows/call-docker-build-result.yaml @@ -0,0 +1,82 @@ +name: Build Result +# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml + +on: + # we want pull requests so we can build(test) but not push to image registry + push: + branches: + - 'main' + # only build when important files change + paths: + - 'result/**' + - '.github/workflows/call-docker-build-result.yaml' + pull_request: + branches: + - 'main' + # only build when important files change + paths: + - 'result/**' + - '.github/workflows/call-docker-build-result.yaml' + +jobs: + call-docker-build: + + name: Result Call Docker Build + + uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main + + permissions: + contents: read + packages: write # needed to push docker image to ghcr.io + pull-requests: write # needed to create and update comments in PRs + + secrets: + + # Only needed if with:dockerhub-enable is true below + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + + # Only needed if with:dockerhub-enable is true below + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} + + with: + + ### REQUIRED + ### ENABLE ONE OR BOTH REGISTRIES + ### tell docker where to push. + ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below + dockerhub-enable: true + ghcr-enable: true + + ### REQUIRED + ### A list of the account/repo names for docker build. List should match what's enabled above + ### defaults to: + image-names: | + ghcr.io/dockersamples/example-voting-app-result + dockersamples/examplevotingapp_result + + ### REQUIRED set rules for tagging images, based on special action syntax: + ### https://github.com/docker/metadata-action#tags-input + ### defaults to: + tag-rules: | + type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=raw,value=before,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=raw,value=after,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=ref,event=pr + + ### path to where docker should copy files into image + ### defaults to root of repository (.) + context: result + + ### Dockerfile alternate name. Default is Dockerfile (relative to context path) + # file: Containerfile + + ### build stage to target, defaults to empty, which builds to last stage in Dockerfile + # target: + + ### platforms to build for, defaults to linux/amd64 + ### other options: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64,linux/arm/v7 + + ### Create a PR comment with image tags and labels + ### defaults to false + # comment-enable: false diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml new file mode 100644 index 0000000000..cb4a484a2a --- /dev/null +++ b/.github/workflows/call-docker-build-vote.yaml @@ -0,0 +1,82 @@ +name: Build Vote +# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml + +on: + # we want pull requests so we can build(test) but not push to image registry + push: + branches: + - 'main' + # only build when important files change + paths: + - 'vote/**' + - '.github/workflows/call-docker-build-vote.yaml' + pull_request: + branches: + - 'main' + # only build when important files change + paths: + - 'vote/**' + - '.github/workflows/call-docker-build-vote.yaml' + +jobs: + call-docker-build: + + name: Vote Call Docker Build + + uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main + + permissions: + contents: read + packages: write # needed to push docker image to ghcr.io + pull-requests: write # needed to create and update comments in PRs + + secrets: + + # Only needed if with:dockerhub-enable is true below + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + + # Only needed if with:dockerhub-enable is true below + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} + + with: + + ### REQUIRED + ### ENABLE ONE OR BOTH REGISTRIES + ### tell docker where to push. + ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below + dockerhub-enable: true + ghcr-enable: true + + ### REQUIRED + ### A list of the account/repo names for docker build. List should match what's enabled above + ### defaults to: + image-names: | + ghcr.io/dockersamples/example-voting-app-vote + dockersamples/examplevotingapp_vote + + ### REQUIRED set rules for tagging images, based on special action syntax: + ### https://github.com/docker/metadata-action#tags-input + ### defaults to: + tag-rules: | + type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=raw,value=before,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=raw,value=after,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=ref,event=pr + + ### path to where docker should copy files into image + ### defaults to root of repository (.) + context: vote + + ### Dockerfile alternate name. Default is Dockerfile (relative to context path) + # file: Containerfile + + ### build stage to target, defaults to empty, which builds to last stage in Dockerfile + # target: + + ### platforms to build for, defaults to linux/amd64 + ### other options: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64,linux/arm/v7 + + ### Create a PR comment with image tags and labels + ### defaults to false + # comment-enable: false diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml new file mode 100644 index 0000000000..5abfb6bc9c --- /dev/null +++ b/.github/workflows/call-docker-build-worker.yaml @@ -0,0 +1,82 @@ +name: Build Worker +# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml + +on: + # we want pull requests so we can build(test) but not push to image registry + push: + branches: + - 'main' + # only build when important files change + paths: + - 'worker/**' + - '.github/workflows/call-docker-build-worker.yaml' + pull_request: + branches: + - 'main' + # only build when important files change + paths: + - 'worker/**' + - '.github/workflows/call-docker-build-worker.yaml' + +jobs: + call-docker-build: + + name: Worker Call Docker Build + + uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main + + permissions: + contents: read + packages: write # needed to push docker image to ghcr.io + pull-requests: write # needed to create and update comments in PRs + + secrets: + + # Only needed if with:dockerhub-enable is true below + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + + # Only needed if with:dockerhub-enable is true below + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} + + with: + + ### REQUIRED + ### ENABLE ONE OR BOTH REGISTRIES + ### tell docker where to push. + ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below + dockerhub-enable: true + ghcr-enable: true + + ### REQUIRED + ### A list of the account/repo names for docker build. List should match what's enabled above + ### defaults to: + image-names: | + ghcr.io/dockersamples/example-voting-app-worker + dockersamples/examplevotingapp_worker + + ### REQUIRED set rules for tagging images, based on special action syntax: + ### https://github.com/docker/metadata-action#tags-input + ### defaults to: + tag-rules: | + type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=ref,event=pr + + ### path to where docker should copy files into image + ### defaults to root of repository (.) + context: worker + + ### Dockerfile alternate name. Default is Dockerfile (relative to context path) + # file: Containerfile + + ### build stage to target, defaults to empty, which builds to last stage in Dockerfile + # target: + + ### platforms to build for, defaults to linux/amd64 + ### other options: linux/amd64,linux/arm64,linux/arm/v7 + # FIXME worker arm/v7 support doesn't build in .net core 3.1 with QEMU + # a fix would likely run the .net build on amd64 but with a target of arm/v7 + platforms: linux/amd64,linux/arm64,linux/arm/v7 + + ### Create a PR comment with image tags and labels + ### defaults to false + # comment-enable: false diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..5d344d5df6 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,132 @@ +name: Voting App CI/CD + +on: + push: + branches: + - ruchira-devops-development + +env: + PROJECT_ID: loyal-burner-493907-u6 + REGION: us-central1 + REPOSITORY: my-repo + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Authenticate to GCP + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Configure Docker + run: gcloud auth configure-docker us-central1-docker.pkg.dev + + - name: Build and push vote image + run: | + docker build -t us-central1-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/vote:latest ./vote + docker push us-central1-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/vote:latest + + - name: Build and push result image + run: | + docker build -t us-central1-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/result:latest ./result + docker push us-central1-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/result:latest + + - name: Build and push worker image + run: | + docker build -t us-central1-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/worker:latest ./worker + docker push us-central1-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/worker:latest + + deploy: + runs-on: ubuntu-latest + needs: build + steps: + - name: Authenticate to GCP + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Deploy vote to Cloud Run + run: | + gcloud run deploy vote-dev \ + --image=us-central1-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/vote:latest \ + --region=${{ env.REGION }} \ + --platform=managed \ + --port=80 \ + --allow-unauthenticated + + - name: Make vote and result public + run: | + gcloud run services add-iam-policy-binding vote-dev \ + --region=${{ env.REGION }} \ + --member="allUsers" \ + --role="roles/run.invoker" + + gcloud run services add-iam-policy-binding result-dev \ + --region=${{ env.REGION }} \ + --member="allUsers" \ + --role="roles/run.invoker" + + - name: Deploy result to Cloud Run + run: | + gcloud run deploy result-dev \ + --image=us-central1-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/result:latest \ + --region=${{ env.REGION }} \ + --platform=managed \ + --port=80 \ + --allow-unauthenticated + + - name: Deploy worker as Cloud Run Job + run: | + gcloud run jobs deploy worker-dev \ + --image=us-central1-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/worker:latest \ + --region=${{ env.REGION }} + + smoke-test: + runs-on: ubuntu-latest + needs: deploy + steps: + - name: Test vote service + run: | + STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://vote-dev-768488519065.us-central1.run.app) + if [ "$STATUS" = "200" ]; then + echo "Vote service is up!" + else + echo "Vote service returned $STATUS" + exit 1 + fi + + - name: Test result service + run: | + STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://result-dev-768488519065.us-central1.run.app) + if [ "$STATUS" = "200" ]; then + echo "Result service is up!" + else + echo "Result service returned $STATUS" + exit 1 + fi + + notify: + runs-on: ubuntu-latest + needs: smoke-test + if: always() + steps: + - name: Send Google Chat Notification + env: + WEBHOOK_URL: ${{ secrets.GCHAT_WEBHOOK_URL }} + JOB_STATUS: ${{ needs.smoke-test.result }} + ACTOR: ${{ github.actor }} + BRANCH: ${{ github.ref_name }} + run: | + if [ "$JOB_STATUS" = "success" ]; then + MSG="Deployment successful! Voting App deployed to Cloud Run. Branch: $BRANCH | Triggered by: $ACTOR" + else + MSG="Deployment failed! Voting App. Branch: $BRANCH | Triggered by: $ACTOR" + fi + curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"text":"'"$MSG"'"}' \ + "$WEBHOOK_URL" diff --git a/.gitignore b/.gitignore index 0d20b6487c..2f69028132 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,12 @@ *.pyc +project.lock.json +bin/ +obj/ +.vs/ +node_modules/ +# Terraform +.terraform/ +*.tfstate +*.tfstate.backup +.terraform.lock.hcl +terraform/.terraform/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..3e9f0bd7ac --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Node: Results debugger", + "type": "node", + "request": "attach", + "port": 9229, + "address": "localhost", + "skipFiles": [ + "/**" + ], + "remoteRoot": "/app", + "localRoot": "${workspaceFolder}/result" + } + ] +} \ No newline at end of file diff --git a/MAINTAINERS b/MAINTAINERS index 8a4fe962a0..a7f9d7c983 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1,4 +1,9 @@ +Bret Fisher +Michael Irwin + +# Alumni, thanks for your work! Aanand Prasad Ben Firshman Fernando Mayo +Mano Marks Maxime Heckel diff --git a/README.md b/README.md index c0345285b8..8516424ba1 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,65 @@ -Example Voting App -================== +# Example Voting App -This is an example Docker app with multiple services. It is run with Docker Compose and uses Docker Networking to connect containers together. You will need Docker Compose 1.6 or later. +A simple distributed application running across multiple Docker containers. -More info at https://blog.docker.com/2015/11/docker-toolbox-compose/ +## Getting started -Architecture ------ +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/). -* A Python webapp which lets you vote between two options -* A Redis queue which collects new votes -* A Java worker which consumes votes and stores them in… -* A Postgres database backed by a Docker volume -* A Node.js webapp which shows the results of the voting in real time +This solution uses Python, Node.js, .NET, with Redis for messaging and Postgres for storage. -Running -------- +Run in this directory to build and run the app: -Run in this directory: +```shell +docker compose up +``` - $ docker-compose up +The `vote` app will be running at [http://localhost:8080](http://localhost:8080), and the `results` will be at [http://localhost:8081](http://localhost:8081). -The app will be running on port 5000 on your Docker host, and the results will be on port 5001. +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: -Docker Hub images ------------------ +```shell +docker swarm init +``` -Docker Hub images for services in this app are built automatically from master: +Once you have your swarm, in this directory run: - - [docker/example-voting-app-voting-app](https://hub.docker.com/r/docker/example-voting-app-voting-app/) - - [docker/example-voting-app-result-app](https://hub.docker.com/r/docker/example-voting-app-result-app/) - - [docker/example-voting-app-worker](https://hub.docker.com/r/docker/example-voting-app-worker/) +```shell +docker stack deploy --compose-file docker-stack.yml vote +``` + +## Run the app in Kubernetes + +The folder k8s-specifications contains the YAML specifications of the Voting App's services. + +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.) + +```shell +kubectl create -f k8s-specifications/ +``` + +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. + +To remove them, run: + +```shell +kubectl delete -f k8s-specifications/ +``` + +## Architecture + +![Architecture diagram](architecture.excalidraw.png) + +* A front-end web app in [Python](/vote) which lets you vote between two options +* A [Redis](https://hub.docker.com/_/redis/) which collects new votes +* A [.NET](/worker/) worker which consumes votes and stores them in… +* A [Postgres](https://hub.docker.com/_/postgres/) database backed by a Docker volume +* A [Node.js](/result) web app which shows the results of the voting in real time + +## Notes + +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. + +This isn't an example of a properly architected perfectly designed distributed app... it's just a simple +example of the various types of pieces and languages you might see (queues, persistent data, etc), and how to +deal with them in Docker at a basic level. diff --git a/architecture.excalidraw.png b/architecture.excalidraw.png new file mode 100644 index 0000000000..643bacdbe9 Binary files /dev/null and b/architecture.excalidraw.png differ diff --git a/docker-compose.images.yml b/docker-compose.images.yml new file mode 100644 index 0000000000..8909aae794 --- /dev/null +++ b/docker-compose.images.yml @@ -0,0 +1,69 @@ +# for running in docker compose with prebuilt images + +# version is now using "compose spec" +# v2 and v3 are now combined! +# docker-compose v1.27+ required + +services: + vote: + image: dockersamples/examplevotingapp_vote + depends_on: + redis: + condition: service_healthy + ports: + - "8080:80" + networks: + - front-tier + - back-tier + + result: + image: dockersamples/examplevotingapp_result + depends_on: + db: + condition: service_healthy + ports: + - "8081:80" + networks: + - front-tier + - back-tier + + worker: + image: dockersamples/examplevotingapp_worker + depends_on: + redis: + condition: service_healthy + db: + condition: service_healthy + networks: + - back-tier + + redis: + image: redis:alpine + volumes: + - "./healthchecks:/healthchecks" + healthcheck: + test: /healthchecks/redis.sh + interval: "5s" + networks: + - back-tier + + db: + image: postgres:15-alpine + environment: + POSTGRES_USER: "postgres" + POSTGRES_PASSWORD: "postgres" + volumes: + - "db-data:/var/lib/postgresql/data" + - "./healthchecks:/healthchecks" + healthcheck: + test: /healthchecks/postgres.sh + interval: "5s" + networks: + - back-tier + +volumes: + db-data: + +networks: + front-tier: + back-tier: diff --git a/docker-compose.yml b/docker-compose.yml index 440ee47868..5915ffd741 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,51 +1,93 @@ -version: "2" +# version is now using "compose spec" +# v2 and v3 are now combined! +# docker-compose v1.27+ required services: - voting-app: - build: ./voting-app/. + vote: + build: + context: ./vote + target: dev + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 10s volumes: - - ./voting-app:/app + - ./vote:/usr/local/app ports: - - "5000:80" - links: - - redis + - "8080:80" networks: - front-tier - back-tier - result-app: - build: ./result-app/. + result: + build: ./result + # use nodemon rather than node for local dev + entrypoint: nodemon --inspect=0.0.0.0 server.js + depends_on: + db: + condition: service_healthy volumes: - - ./result-app:/app + - ./result:/usr/local/app ports: - - "5001:80" - links: - - db + - "8081:80" + - "127.0.0.1:9229:9229" networks: - front-tier - back-tier worker: - build: ./worker - links: - - db - - redis + build: + context: ./worker + depends_on: + redis: + condition: service_healthy + db: + condition: service_healthy networks: - back-tier redis: image: redis:alpine - ports: ["6379"] + volumes: + - "./healthchecks:/healthchecks" + healthcheck: + test: /healthchecks/redis.sh + interval: "5s" networks: - back-tier db: - image: postgres:9.4 + image: postgres:15-alpine + environment: + POSTGRES_USER: "postgres" + POSTGRES_PASSWORD: "postgres" volumes: - "db-data:/var/lib/postgresql/data" + - "./healthchecks:/healthchecks" + healthcheck: + test: /healthchecks/postgres.sh + interval: "5s" networks: - back-tier + # this service runs once to seed the database with votes + # it won't run unless you specify the "seed" profile + # docker compose --profile seed up -d + seed: + build: ./seed-data + profiles: ["seed"] + depends_on: + vote: + condition: service_healthy + networks: + - front-tier + restart: "no" + volumes: db-data: diff --git a/docker-stack.yml b/docker-stack.yml new file mode 100644 index 0000000000..356b944caf --- /dev/null +++ b/docker-stack.yml @@ -0,0 +1,53 @@ +# this file is meant for Docker Swarm stacks only +# trying it in compose will fail because of multiple replicas trying to bind to the same port +# Swarm currently does not support Compose Spec, so we'll pin to the older version 3.9 + +version: "3.9" + +services: + + redis: + image: redis:alpine + networks: + - frontend + + db: + image: postgres:15-alpine + environment: + POSTGRES_USER: "postgres" + POSTGRES_PASSWORD: "postgres" + volumes: + - db-data:/var/lib/postgresql/data + networks: + - backend + + vote: + image: dockersamples/examplevotingapp_vote + ports: + - 8080:80 + networks: + - frontend + deploy: + replicas: 2 + + result: + image: dockersamples/examplevotingapp_result + ports: + - 8081:80 + networks: + - backend + + worker: + image: dockersamples/examplevotingapp_worker + networks: + - frontend + - backend + deploy: + replicas: 2 + +networks: + frontend: + backend: + +volumes: + db-data: diff --git a/healthchecks/postgres.sh b/healthchecks/postgres.sh new file mode 100755 index 0000000000..299416740e --- /dev/null +++ b/healthchecks/postgres.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -eo pipefail + +host="$(hostname -i || echo '127.0.0.1')" +user="${POSTGRES_USER:-postgres}" +db="${POSTGRES_DB:-$POSTGRES_USER}" +export PGPASSWORD="${POSTGRES_PASSWORD:-}" + +args=( + # force postgres to not use the local unix socket (test "external" connectibility) + --host "$host" + --username "$user" + --dbname "$db" + --quiet --no-align --tuples-only +) + +if select="$(echo 'SELECT 1' | psql "${args[@]}")" && [ "$select" = '1' ]; then + exit 0 +fi + +exit 1 diff --git a/healthchecks/redis.sh b/healthchecks/redis.sh new file mode 100755 index 0000000000..3953758f94 --- /dev/null +++ b/healthchecks/redis.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -eo pipefail + +host="$(hostname -i || echo '127.0.0.1')" + +if ping="$(redis-cli -h "$host" ping)" && [ "$ping" = 'PONG' ]; then + exit 0 +fi + +exit 1 diff --git a/k8s-specifications/db-deployment.yaml b/k8s-specifications/db-deployment.yaml new file mode 100644 index 0000000000..bc94ca7368 --- /dev/null +++ b/k8s-specifications/db-deployment.yaml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: db + name: db +spec: + replicas: 1 + selector: + matchLabels: + app: db + template: + metadata: + labels: + app: db + spec: + containers: + - image: postgres:15-alpine + name: postgres + env: + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: postgres + ports: + - containerPort: 5432 + name: postgres + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: db-data + volumes: + - name: db-data + emptyDir: {} diff --git a/k8s-specifications/db-service.yaml b/k8s-specifications/db-service.yaml new file mode 100644 index 0000000000..104f1e8268 --- /dev/null +++ b/k8s-specifications/db-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: db + name: db +spec: + type: ClusterIP + ports: + - name: "db-service" + port: 5432 + targetPort: 5432 + selector: + app: db + diff --git a/k8s-specifications/redis-deployment.yaml b/k8s-specifications/redis-deployment.yaml new file mode 100644 index 0000000000..24aa52135f --- /dev/null +++ b/k8s-specifications/redis-deployment.yaml @@ -0,0 +1,28 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: redis + name: redis +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - image: redis:alpine + name: redis + ports: + - containerPort: 6379 + name: redis + volumeMounts: + - mountPath: /data + name: redis-data + volumes: + - name: redis-data + emptyDir: {} diff --git a/k8s-specifications/redis-service.yaml b/k8s-specifications/redis-service.yaml new file mode 100644 index 0000000000..809d31d875 --- /dev/null +++ b/k8s-specifications/redis-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: redis + name: redis +spec: + type: ClusterIP + ports: + - name: "redis-service" + port: 6379 + targetPort: 6379 + selector: + app: redis + diff --git a/k8s-specifications/result-deployment.yaml b/k8s-specifications/result-deployment.yaml new file mode 100644 index 0000000000..b85488a667 --- /dev/null +++ b/k8s-specifications/result-deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: result + name: result +spec: + replicas: 1 + selector: + matchLabels: + app: result + template: + metadata: + labels: + app: result + spec: + containers: + - image: dockersamples/examplevotingapp_result + name: result + ports: + - containerPort: 80 + name: result diff --git a/k8s-specifications/result-service.yaml b/k8s-specifications/result-service.yaml new file mode 100644 index 0000000000..0fed5e0cc5 --- /dev/null +++ b/k8s-specifications/result-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: result + name: result +spec: + type: NodePort + ports: + - name: "result-service" + port: 8081 + targetPort: 80 + nodePort: 31001 + selector: + app: result diff --git a/k8s-specifications/vote-deployment.yaml b/k8s-specifications/vote-deployment.yaml new file mode 100644 index 0000000000..165a9478f8 --- /dev/null +++ b/k8s-specifications/vote-deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: vote + name: vote +spec: + replicas: 1 + selector: + matchLabels: + app: vote + template: + metadata: + labels: + app: vote + spec: + containers: + - image: dockersamples/examplevotingapp_vote + name: vote + ports: + - containerPort: 80 + name: vote diff --git a/k8s-specifications/vote-service.yaml b/k8s-specifications/vote-service.yaml new file mode 100644 index 0000000000..d7a05b5513 --- /dev/null +++ b/k8s-specifications/vote-service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: vote + name: vote +spec: + type: NodePort + ports: + - name: "vote-service" + port: 8080 + targetPort: 80 + nodePort: 31000 + selector: + app: vote + diff --git a/k8s-specifications/worker-deployment.yaml b/k8s-specifications/worker-deployment.yaml new file mode 100644 index 0000000000..9e35450aec --- /dev/null +++ b/k8s-specifications/worker-deployment.yaml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: worker + name: worker +spec: + replicas: 1 + selector: + matchLabels: + app: worker + template: + metadata: + labels: + app: worker + spec: + containers: + - image: dockersamples/examplevotingapp_worker + name: worker diff --git a/result-app/Dockerfile b/result-app/Dockerfile deleted file mode 100644 index 3045ede8a8..0000000000 --- a/result-app/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM node:5.11.0-slim - -WORKDIR /app - -ADD package.json /app/package.json -RUN npm config set registry http://registry.npmjs.org -RUN npm install && npm ls -RUN mv /app/node_modules /node_modules - -ADD . /app - -ENV PORT 80 -EXPOSE 80 - -CMD ["node", "server.js"] diff --git a/result-app/package.json b/result-app/package.json deleted file mode 100644 index 97b53e01d8..0000000000 --- a/result-app/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "result-app", - "version": "1.0.0", - "description": "", - "main": "server.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "MIT", - "dependencies": { - "body-parser": "^1.14.1", - "cookie-parser": "^1.4.0", - "express": "^4.13.3", - "method-override": "^2.3.5", - "async": "^1.5.0", - "pg": "^4.4.3", - "socket.io": "^1.3.7" - } -} diff --git a/result-app/views/socket.io.js b/result-app/views/socket.io.js deleted file mode 100644 index 67f5484fb1..0000000000 --- a/result-app/views/socket.io.js +++ /dev/null @@ -1,6988 +0,0 @@ -!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.io=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && !this.encoding) { - var pack = this.packetBuffer.shift(); - this.packet(pack); - } -}; - -/** - * Clean up transport subscriptions and packet buffer. - * - * @api private - */ - -Manager.prototype.cleanup = function(){ - var sub; - while (sub = this.subs.shift()) sub.destroy(); - - this.packetBuffer = []; - this.encoding = false; - - this.decoder.destroy(); -}; - -/** - * Close the current socket. - * - * @api private - */ - -Manager.prototype.close = -Manager.prototype.disconnect = function(){ - this.skipReconnect = true; - this.backoff.reset(); - this.readyState = 'closed'; - this.engine && this.engine.close(); -}; - -/** - * Called upon engine close. - * - * @api private - */ - -Manager.prototype.onclose = function(reason){ - debug('close'); - this.cleanup(); - this.backoff.reset(); - this.readyState = 'closed'; - this.emit('close', reason); - if (this._reconnection && !this.skipReconnect) { - this.reconnect(); - } -}; - -/** - * Attempt a reconnection. - * - * @api private - */ - -Manager.prototype.reconnect = function(){ - if (this.reconnecting || this.skipReconnect) return this; - - var self = this; - - if (this.backoff.attempts >= this._reconnectionAttempts) { - debug('reconnect failed'); - this.backoff.reset(); - this.emitAll('reconnect_failed'); - this.reconnecting = false; - } else { - var delay = this.backoff.duration(); - debug('will wait %dms before reconnect attempt', delay); - - this.reconnecting = true; - var timer = setTimeout(function(){ - if (self.skipReconnect) return; - - debug('attempting reconnect'); - self.emitAll('reconnect_attempt', self.backoff.attempts); - self.emitAll('reconnecting', self.backoff.attempts); - - // check again for the case socket closed in above events - if (self.skipReconnect) return; - - self.open(function(err){ - if (err) { - debug('reconnect attempt error'); - self.reconnecting = false; - self.reconnect(); - self.emitAll('reconnect_error', err.data); - } else { - debug('reconnect success'); - self.onreconnect(); - } - }); - }, delay); - - this.subs.push({ - destroy: function(){ - clearTimeout(timer); - } - }); - } -}; - -/** - * Called upon successful reconnect. - * - * @api private - */ - -Manager.prototype.onreconnect = function(){ - var attempt = this.backoff.attempts; - this.reconnecting = false; - this.backoff.reset(); - this.updateSocketIds(); - this.emitAll('reconnect', attempt); -}; - -},{"./on":4,"./socket":5,"./url":6,"backo2":7,"component-bind":8,"component-emitter":9,"debug":10,"engine.io-client":11,"indexof":40,"object-component":41,"socket.io-parser":44}],4:[function(_dereq_,module,exports){ - -/** - * Module exports. - */ - -module.exports = on; - -/** - * Helper for subscriptions. - * - * @param {Object|EventEmitter} obj with `Emitter` mixin or `EventEmitter` - * @param {String} event name - * @param {Function} callback - * @api public - */ - -function on(obj, ev, fn) { - obj.on(ev, fn); - return { - destroy: function(){ - obj.removeListener(ev, fn); - } - }; -} - -},{}],5:[function(_dereq_,module,exports){ - -/** - * Module dependencies. - */ - -var parser = _dereq_('socket.io-parser'); -var Emitter = _dereq_('component-emitter'); -var toArray = _dereq_('to-array'); -var on = _dereq_('./on'); -var bind = _dereq_('component-bind'); -var debug = _dereq_('debug')('socket.io-client:socket'); -var hasBin = _dereq_('has-binary'); - -/** - * Module exports. - */ - -module.exports = exports = Socket; - -/** - * Internal events (blacklisted). - * These events can't be emitted by the user. - * - * @api private - */ - -var events = { - connect: 1, - connect_error: 1, - connect_timeout: 1, - disconnect: 1, - error: 1, - reconnect: 1, - reconnect_attempt: 1, - reconnect_failed: 1, - reconnect_error: 1, - reconnecting: 1 -}; - -/** - * Shortcut to `Emitter#emit`. - */ - -var emit = Emitter.prototype.emit; - -/** - * `Socket` constructor. - * - * @api public - */ - -function Socket(io, nsp){ - this.io = io; - this.nsp = nsp; - this.json = this; // compat - this.ids = 0; - this.acks = {}; - if (this.io.autoConnect) this.open(); - this.receiveBuffer = []; - this.sendBuffer = []; - this.connected = false; - this.disconnected = true; -} - -/** - * Mix in `Emitter`. - */ - -Emitter(Socket.prototype); - -/** - * Subscribe to open, close and packet events - * - * @api private - */ - -Socket.prototype.subEvents = function() { - if (this.subs) return; - - var io = this.io; - this.subs = [ - on(io, 'open', bind(this, 'onopen')), - on(io, 'packet', bind(this, 'onpacket')), - on(io, 'close', bind(this, 'onclose')) - ]; -}; - -/** - * "Opens" the socket. - * - * @api public - */ - -Socket.prototype.open = -Socket.prototype.connect = function(){ - if (this.connected) return this; - - this.subEvents(); - this.io.open(); // ensure open - if ('open' == this.io.readyState) this.onopen(); - return this; -}; - -/** - * Sends a `message` event. - * - * @return {Socket} self - * @api public - */ - -Socket.prototype.send = function(){ - var args = toArray(arguments); - args.unshift('message'); - this.emit.apply(this, args); - return this; -}; - -/** - * Override `emit`. - * If the event is in `events`, it's emitted normally. - * - * @param {String} event name - * @return {Socket} self - * @api public - */ - -Socket.prototype.emit = function(ev){ - if (events.hasOwnProperty(ev)) { - emit.apply(this, arguments); - return this; - } - - var args = toArray(arguments); - var parserType = parser.EVENT; // default - if (hasBin(args)) { parserType = parser.BINARY_EVENT; } // binary - var packet = { type: parserType, data: args }; - - // event ack callback - if ('function' == typeof args[args.length - 1]) { - debug('emitting packet with ack id %d', this.ids); - this.acks[this.ids] = args.pop(); - packet.id = this.ids++; - } - - if (this.connected) { - this.packet(packet); - } else { - this.sendBuffer.push(packet); - } - - return this; -}; - -/** - * Sends a packet. - * - * @param {Object} packet - * @api private - */ - -Socket.prototype.packet = function(packet){ - packet.nsp = this.nsp; - this.io.packet(packet); -}; - -/** - * Called upon engine `open`. - * - * @api private - */ - -Socket.prototype.onopen = function(){ - debug('transport is open - connecting'); - - // write connect packet if necessary - if ('/' != this.nsp) { - this.packet({ type: parser.CONNECT }); - } -}; - -/** - * Called upon engine `close`. - * - * @param {String} reason - * @api private - */ - -Socket.prototype.onclose = function(reason){ - debug('close (%s)', reason); - this.connected = false; - this.disconnected = true; - delete this.id; - this.emit('disconnect', reason); -}; - -/** - * Called with socket packet. - * - * @param {Object} packet - * @api private - */ - -Socket.prototype.onpacket = function(packet){ - if (packet.nsp != this.nsp) return; - - switch (packet.type) { - case parser.CONNECT: - this.onconnect(); - break; - - case parser.EVENT: - this.onevent(packet); - break; - - case parser.BINARY_EVENT: - this.onevent(packet); - break; - - case parser.ACK: - this.onack(packet); - break; - - case parser.BINARY_ACK: - this.onack(packet); - break; - - case parser.DISCONNECT: - this.ondisconnect(); - break; - - case parser.ERROR: - this.emit('error', packet.data); - break; - } -}; - -/** - * Called upon a server event. - * - * @param {Object} packet - * @api private - */ - -Socket.prototype.onevent = function(packet){ - var args = packet.data || []; - debug('emitting event %j', args); - - if (null != packet.id) { - debug('attaching ack callback to event'); - args.push(this.ack(packet.id)); - } - - if (this.connected) { - emit.apply(this, args); - } else { - this.receiveBuffer.push(args); - } -}; - -/** - * Produces an ack callback to emit with an event. - * - * @api private - */ - -Socket.prototype.ack = function(id){ - var self = this; - var sent = false; - return function(){ - // prevent double callbacks - if (sent) return; - sent = true; - var args = toArray(arguments); - debug('sending ack %j', args); - - var type = hasBin(args) ? parser.BINARY_ACK : parser.ACK; - self.packet({ - type: type, - id: id, - data: args - }); - }; -}; - -/** - * Called upon a server acknowlegement. - * - * @param {Object} packet - * @api private - */ - -Socket.prototype.onack = function(packet){ - debug('calling ack %s with %j', packet.id, packet.data); - var fn = this.acks[packet.id]; - fn.apply(this, packet.data); - delete this.acks[packet.id]; -}; - -/** - * Called upon server connect. - * - * @api private - */ - -Socket.prototype.onconnect = function(){ - this.connected = true; - this.disconnected = false; - this.emit('connect'); - this.emitBuffered(); -}; - -/** - * Emit buffered events (received and emitted). - * - * @api private - */ - -Socket.prototype.emitBuffered = function(){ - var i; - for (i = 0; i < this.receiveBuffer.length; i++) { - emit.apply(this, this.receiveBuffer[i]); - } - this.receiveBuffer = []; - - for (i = 0; i < this.sendBuffer.length; i++) { - this.packet(this.sendBuffer[i]); - } - this.sendBuffer = []; -}; - -/** - * Called upon server disconnect. - * - * @api private - */ - -Socket.prototype.ondisconnect = function(){ - debug('server disconnect (%s)', this.nsp); - this.destroy(); - this.onclose('io server disconnect'); -}; - -/** - * Called upon forced client/server side disconnections, - * this method ensures the manager stops tracking us and - * that reconnections don't get triggered for this. - * - * @api private. - */ - -Socket.prototype.destroy = function(){ - if (this.subs) { - // clean subscriptions to avoid reconnections - for (var i = 0; i < this.subs.length; i++) { - this.subs[i].destroy(); - } - this.subs = null; - } - - this.io.destroy(this); -}; - -/** - * Disconnects the socket manually. - * - * @return {Socket} self - * @api public - */ - -Socket.prototype.close = -Socket.prototype.disconnect = function(){ - if (this.connected) { - debug('performing disconnect (%s)', this.nsp); - this.packet({ type: parser.DISCONNECT }); - } - - // remove socket from pool - this.destroy(); - - if (this.connected) { - // fire events - this.onclose('io client disconnect'); - } - return this; -}; - -},{"./on":4,"component-bind":8,"component-emitter":9,"debug":10,"has-binary":36,"socket.io-parser":44,"to-array":48}],6:[function(_dereq_,module,exports){ -(function (global){ - -/** - * Module dependencies. - */ - -var parseuri = _dereq_('parseuri'); -var debug = _dereq_('debug')('socket.io-client:url'); - -/** - * Module exports. - */ - -module.exports = url; - -/** - * URL parser. - * - * @param {String} url - * @param {Object} An object meant to mimic window.location. - * Defaults to window.location. - * @api public - */ - -function url(uri, loc){ - var obj = uri; - - // default to window.location - var loc = loc || global.location; - if (null == uri) uri = loc.protocol + '//' + loc.host; - - // relative path support - if ('string' == typeof uri) { - if ('/' == uri.charAt(0)) { - if ('/' == uri.charAt(1)) { - uri = loc.protocol + uri; - } else { - uri = loc.hostname + uri; - } - } - - if (!/^(https?|wss?):\/\//.test(uri)) { - debug('protocol-less url %s', uri); - if ('undefined' != typeof loc) { - uri = loc.protocol + '//' + uri; - } else { - uri = 'https://' + uri; - } - } - - // parse - debug('parse %s', uri); - obj = parseuri(uri); - } - - // make sure we treat `localhost:80` and `localhost` equally - if (!obj.port) { - if (/^(http|ws)$/.test(obj.protocol)) { - obj.port = '80'; - } - else if (/^(http|ws)s$/.test(obj.protocol)) { - obj.port = '443'; - } - } - - obj.path = obj.path || '/'; - - // define unique id - obj.id = obj.protocol + '://' + obj.host + ':' + obj.port; - // define href - obj.href = obj.protocol + '://' + obj.host + (loc && loc.port == obj.port ? '' : (':' + obj.port)); - - return obj; -} - -}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"debug":10,"parseuri":42}],7:[function(_dereq_,module,exports){ - -/** - * Expose `Backoff`. - */ - -module.exports = Backoff; - -/** - * Initialize backoff timer with `opts`. - * - * - `min` initial timeout in milliseconds [100] - * - `max` max timeout [10000] - * - `jitter` [0] - * - `factor` [2] - * - * @param {Object} opts - * @api public - */ - -function Backoff(opts) { - opts = opts || {}; - this.ms = opts.min || 100; - this.max = opts.max || 10000; - this.factor = opts.factor || 2; - this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0; - this.attempts = 0; -} - -/** - * Return the backoff duration. - * - * @return {Number} - * @api public - */ - -Backoff.prototype.duration = function(){ - var ms = this.ms * Math.pow(this.factor, this.attempts++); - if (this.jitter) { - var rand = Math.random(); - var deviation = Math.floor(rand * this.jitter * ms); - ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation; - } - return Math.min(ms, this.max) | 0; -}; - -/** - * Reset the number of attempts. - * - * @api public - */ - -Backoff.prototype.reset = function(){ - this.attempts = 0; -}; - -/** - * Set the minimum duration - * - * @api public - */ - -Backoff.prototype.setMin = function(min){ - this.ms = min; -}; - -/** - * Set the maximum duration - * - * @api public - */ - -Backoff.prototype.setMax = function(max){ - this.max = max; -}; - -/** - * Set the jitter - * - * @api public - */ - -Backoff.prototype.setJitter = function(jitter){ - this.jitter = jitter; -}; - - -},{}],8:[function(_dereq_,module,exports){ -/** - * Slice reference. - */ - -var slice = [].slice; - -/** - * Bind `obj` to `fn`. - * - * @param {Object} obj - * @param {Function|String} fn or string - * @return {Function} - * @api public - */ - -module.exports = function(obj, fn){ - if ('string' == typeof fn) fn = obj[fn]; - if ('function' != typeof fn) throw new Error('bind() requires a function'); - var args = slice.call(arguments, 2); - return function(){ - return fn.apply(obj, args.concat(slice.call(arguments))); - } -}; - -},{}],9:[function(_dereq_,module,exports){ - -/** - * Expose `Emitter`. - */ - -module.exports = Emitter; - -/** - * Initialize a new `Emitter`. - * - * @api public - */ - -function Emitter(obj) { - if (obj) return mixin(obj); -}; - -/** - * Mixin the emitter properties. - * - * @param {Object} obj - * @return {Object} - * @api private - */ - -function mixin(obj) { - for (var key in Emitter.prototype) { - obj[key] = Emitter.prototype[key]; - } - return obj; -} - -/** - * Listen on the given `event` with `fn`. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.on = -Emitter.prototype.addEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - (this._callbacks[event] = this._callbacks[event] || []) - .push(fn); - return this; -}; - -/** - * Adds an `event` listener that will be invoked a single - * time then automatically removed. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.once = function(event, fn){ - var self = this; - this._callbacks = this._callbacks || {}; - - function on() { - self.off(event, on); - fn.apply(this, arguments); - } - - on.fn = fn; - this.on(event, on); - return this; -}; - -/** - * Remove the given callback for `event` or all - * registered callbacks. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.off = -Emitter.prototype.removeListener = -Emitter.prototype.removeAllListeners = -Emitter.prototype.removeEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - - // all - if (0 == arguments.length) { - this._callbacks = {}; - return this; - } - - // specific event - var callbacks = this._callbacks[event]; - if (!callbacks) return this; - - // remove all handlers - if (1 == arguments.length) { - delete this._callbacks[event]; - return this; - } - - // remove specific handler - var cb; - for (var i = 0; i < callbacks.length; i++) { - cb = callbacks[i]; - if (cb === fn || cb.fn === fn) { - callbacks.splice(i, 1); - break; - } - } - return this; -}; - -/** - * Emit `event` with the given args. - * - * @param {String} event - * @param {Mixed} ... - * @return {Emitter} - */ - -Emitter.prototype.emit = function(event){ - this._callbacks = this._callbacks || {}; - var args = [].slice.call(arguments, 1) - , callbacks = this._callbacks[event]; - - if (callbacks) { - callbacks = callbacks.slice(0); - for (var i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(this, args); - } - } - - return this; -}; - -/** - * Return array of callbacks for `event`. - * - * @param {String} event - * @return {Array} - * @api public - */ - -Emitter.prototype.listeners = function(event){ - this._callbacks = this._callbacks || {}; - return this._callbacks[event] || []; -}; - -/** - * Check if this emitter has `event` handlers. - * - * @param {String} event - * @return {Boolean} - * @api public - */ - -Emitter.prototype.hasListeners = function(event){ - return !! this.listeners(event).length; -}; - -},{}],10:[function(_dereq_,module,exports){ - -/** - * Expose `debug()` as the module. - */ - -module.exports = debug; - -/** - * Create a debugger with the given `name`. - * - * @param {String} name - * @return {Type} - * @api public - */ - -function debug(name) { - if (!debug.enabled(name)) return function(){}; - - return function(fmt){ - fmt = coerce(fmt); - - var curr = new Date; - var ms = curr - (debug[name] || curr); - debug[name] = curr; - - fmt = name - + ' ' - + fmt - + ' +' + debug.humanize(ms); - - // This hackery is required for IE8 - // where `console.log` doesn't have 'apply' - window.console - && console.log - && Function.prototype.apply.call(console.log, console, arguments); - } -} - -/** - * The currently active debug mode names. - */ - -debug.names = []; -debug.skips = []; - -/** - * Enables a debug mode by name. This can include modes - * separated by a colon and wildcards. - * - * @param {String} name - * @api public - */ - -debug.enable = function(name) { - try { - localStorage.debug = name; - } catch(e){} - - var split = (name || '').split(/[\s,]+/) - , len = split.length; - - for (var i = 0; i < len; i++) { - name = split[i].replace('*', '.*?'); - if (name[0] === '-') { - debug.skips.push(new RegExp('^' + name.substr(1) + '$')); - } - else { - debug.names.push(new RegExp('^' + name + '$')); - } - } -}; - -/** - * Disable debug output. - * - * @api public - */ - -debug.disable = function(){ - debug.enable(''); -}; - -/** - * Humanize the given `ms`. - * - * @param {Number} m - * @return {String} - * @api private - */ - -debug.humanize = function(ms) { - var sec = 1000 - , min = 60 * 1000 - , hour = 60 * min; - - if (ms >= hour) return (ms / hour).toFixed(1) + 'h'; - if (ms >= min) return (ms / min).toFixed(1) + 'm'; - if (ms >= sec) return (ms / sec | 0) + 's'; - return ms + 'ms'; -}; - -/** - * Returns true if the given mode name is enabled, false otherwise. - * - * @param {String} name - * @return {Boolean} - * @api public - */ - -debug.enabled = function(name) { - for (var i = 0, len = debug.skips.length; i < len; i++) { - if (debug.skips[i].test(name)) { - return false; - } - } - for (var i = 0, len = debug.names.length; i < len; i++) { - if (debug.names[i].test(name)) { - return true; - } - } - return false; -}; - -/** - * Coerce `val`. - */ - -function coerce(val) { - if (val instanceof Error) return val.stack || val.message; - return val; -} - -// persist - -try { - if (window.localStorage) debug.enable(localStorage.debug); -} catch(e){} - -},{}],11:[function(_dereq_,module,exports){ - -module.exports = _dereq_('./lib/'); - -},{"./lib/":12}],12:[function(_dereq_,module,exports){ - -module.exports = _dereq_('./socket'); - -/** - * Exports parser - * - * @api public - * - */ -module.exports.parser = _dereq_('engine.io-parser'); - -},{"./socket":13,"engine.io-parser":25}],13:[function(_dereq_,module,exports){ -(function (global){ -/** - * Module dependencies. - */ - -var transports = _dereq_('./transports'); -var Emitter = _dereq_('component-emitter'); -var debug = _dereq_('debug')('engine.io-client:socket'); -var index = _dereq_('indexof'); -var parser = _dereq_('engine.io-parser'); -var parseuri = _dereq_('parseuri'); -var parsejson = _dereq_('parsejson'); -var parseqs = _dereq_('parseqs'); - -/** - * Module exports. - */ - -module.exports = Socket; - -/** - * Noop function. - * - * @api private - */ - -function noop(){} - -/** - * Socket constructor. - * - * @param {String|Object} uri or options - * @param {Object} options - * @api public - */ - -function Socket(uri, opts){ - if (!(this instanceof Socket)) return new Socket(uri, opts); - - opts = opts || {}; - - if (uri && 'object' == typeof uri) { - opts = uri; - uri = null; - } - - if (uri) { - uri = parseuri(uri); - opts.host = uri.host; - opts.secure = uri.protocol == 'https' || uri.protocol == 'wss'; - opts.port = uri.port; - if (uri.query) opts.query = uri.query; - } - - this.secure = null != opts.secure ? opts.secure : - (global.location && 'https:' == location.protocol); - - if (opts.host) { - var pieces = opts.host.split(':'); - opts.hostname = pieces.shift(); - if (pieces.length) { - opts.port = pieces.pop(); - } else if (!opts.port) { - // if no port is specified manually, use the protocol default - opts.port = this.secure ? '443' : '80'; - } - } - - this.agent = opts.agent || false; - this.hostname = opts.hostname || - (global.location ? location.hostname : 'localhost'); - this.port = opts.port || (global.location && location.port ? - location.port : - (this.secure ? 443 : 80)); - this.query = opts.query || {}; - if ('string' == typeof this.query) this.query = parseqs.decode(this.query); - this.upgrade = false !== opts.upgrade; - this.path = (opts.path || '/engine.io').replace(/\/$/, '') + '/'; - this.forceJSONP = !!opts.forceJSONP; - this.jsonp = false !== opts.jsonp; - this.forceBase64 = !!opts.forceBase64; - this.enablesXDR = !!opts.enablesXDR; - this.timestampParam = opts.timestampParam || 't'; - this.timestampRequests = opts.timestampRequests; - this.transports = opts.transports || ['polling', 'websocket']; - this.readyState = ''; - this.writeBuffer = []; - this.callbackBuffer = []; - this.policyPort = opts.policyPort || 843; - this.rememberUpgrade = opts.rememberUpgrade || false; - this.binaryType = null; - this.onlyBinaryUpgrades = opts.onlyBinaryUpgrades; - - // SSL options for Node.js client - this.pfx = opts.pfx || null; - this.key = opts.key || null; - this.passphrase = opts.passphrase || null; - this.cert = opts.cert || null; - this.ca = opts.ca || null; - this.ciphers = opts.ciphers || null; - this.rejectUnauthorized = opts.rejectUnauthorized || null; - - this.open(); -} - -Socket.priorWebsocketSuccess = false; - -/** - * Mix in `Emitter`. - */ - -Emitter(Socket.prototype); - -/** - * Protocol version. - * - * @api public - */ - -Socket.protocol = parser.protocol; // this is an int - -/** - * Expose deps for legacy compatibility - * and standalone browser access. - */ - -Socket.Socket = Socket; -Socket.Transport = _dereq_('./transport'); -Socket.transports = _dereq_('./transports'); -Socket.parser = _dereq_('engine.io-parser'); - -/** - * Creates transport of the given type. - * - * @param {String} transport name - * @return {Transport} - * @api private - */ - -Socket.prototype.createTransport = function (name) { - debug('creating transport "%s"', name); - var query = clone(this.query); - - // append engine.io protocol identifier - query.EIO = parser.protocol; - - // transport name - query.transport = name; - - // session id if we already have one - if (this.id) query.sid = this.id; - - var transport = new transports[name]({ - agent: this.agent, - hostname: this.hostname, - port: this.port, - secure: this.secure, - path: this.path, - query: query, - forceJSONP: this.forceJSONP, - jsonp: this.jsonp, - forceBase64: this.forceBase64, - enablesXDR: this.enablesXDR, - timestampRequests: this.timestampRequests, - timestampParam: this.timestampParam, - policyPort: this.policyPort, - socket: this, - pfx: this.pfx, - key: this.key, - passphrase: this.passphrase, - cert: this.cert, - ca: this.ca, - ciphers: this.ciphers, - rejectUnauthorized: this.rejectUnauthorized - }); - - return transport; -}; - -function clone (obj) { - var o = {}; - for (var i in obj) { - if (obj.hasOwnProperty(i)) { - o[i] = obj[i]; - } - } - return o; -} - -/** - * Initializes transport to use and starts probe. - * - * @api private - */ -Socket.prototype.open = function () { - var transport; - if (this.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf('websocket') != -1) { - transport = 'websocket'; - } else if (0 == this.transports.length) { - // Emit error on next tick so it can be listened to - var self = this; - setTimeout(function() { - self.emit('error', 'No transports available'); - }, 0); - return; - } else { - transport = this.transports[0]; - } - this.readyState = 'opening'; - - // Retry with the next transport if the transport is disabled (jsonp: false) - var transport; - try { - transport = this.createTransport(transport); - } catch (e) { - this.transports.shift(); - this.open(); - return; - } - - transport.open(); - this.setTransport(transport); -}; - -/** - * Sets the current transport. Disables the existing one (if any). - * - * @api private - */ - -Socket.prototype.setTransport = function(transport){ - debug('setting transport %s', transport.name); - var self = this; - - if (this.transport) { - debug('clearing existing transport %s', this.transport.name); - this.transport.removeAllListeners(); - } - - // set up transport - this.transport = transport; - - // set up transport listeners - transport - .on('drain', function(){ - self.onDrain(); - }) - .on('packet', function(packet){ - self.onPacket(packet); - }) - .on('error', function(e){ - self.onError(e); - }) - .on('close', function(){ - self.onClose('transport close'); - }); -}; - -/** - * Probes a transport. - * - * @param {String} transport name - * @api private - */ - -Socket.prototype.probe = function (name) { - debug('probing transport "%s"', name); - var transport = this.createTransport(name, { probe: 1 }) - , failed = false - , self = this; - - Socket.priorWebsocketSuccess = false; - - function onTransportOpen(){ - if (self.onlyBinaryUpgrades) { - var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary; - failed = failed || upgradeLosesBinary; - } - if (failed) return; - - debug('probe transport "%s" opened', name); - transport.send([{ type: 'ping', data: 'probe' }]); - transport.once('packet', function (msg) { - if (failed) return; - if ('pong' == msg.type && 'probe' == msg.data) { - debug('probe transport "%s" pong', name); - self.upgrading = true; - self.emit('upgrading', transport); - if (!transport) return; - Socket.priorWebsocketSuccess = 'websocket' == transport.name; - - debug('pausing current transport "%s"', self.transport.name); - self.transport.pause(function () { - if (failed) return; - if ('closed' == self.readyState) return; - debug('changing transport and sending upgrade packet'); - - cleanup(); - - self.setTransport(transport); - transport.send([{ type: 'upgrade' }]); - self.emit('upgrade', transport); - transport = null; - self.upgrading = false; - self.flush(); - }); - } else { - debug('probe transport "%s" failed', name); - var err = new Error('probe error'); - err.transport = transport.name; - self.emit('upgradeError', err); - } - }); - } - - function freezeTransport() { - if (failed) return; - - // Any callback called by transport should be ignored since now - failed = true; - - cleanup(); - - transport.close(); - transport = null; - } - - //Handle any error that happens while probing - function onerror(err) { - var error = new Error('probe error: ' + err); - error.transport = transport.name; - - freezeTransport(); - - debug('probe transport "%s" failed because of error: %s', name, err); - - self.emit('upgradeError', error); - } - - function onTransportClose(){ - onerror("transport closed"); - } - - //When the socket is closed while we're probing - function onclose(){ - onerror("socket closed"); - } - - //When the socket is upgraded while we're probing - function onupgrade(to){ - if (transport && to.name != transport.name) { - debug('"%s" works - aborting "%s"', to.name, transport.name); - freezeTransport(); - } - } - - //Remove all listeners on the transport and on self - function cleanup(){ - transport.removeListener('open', onTransportOpen); - transport.removeListener('error', onerror); - transport.removeListener('close', onTransportClose); - self.removeListener('close', onclose); - self.removeListener('upgrading', onupgrade); - } - - transport.once('open', onTransportOpen); - transport.once('error', onerror); - transport.once('close', onTransportClose); - - this.once('close', onclose); - this.once('upgrading', onupgrade); - - transport.open(); - -}; - -/** - * Called when connection is deemed open. - * - * @api public - */ - -Socket.prototype.onOpen = function () { - debug('socket open'); - this.readyState = 'open'; - Socket.priorWebsocketSuccess = 'websocket' == this.transport.name; - this.emit('open'); - this.flush(); - - // we check for `readyState` in case an `open` - // listener already closed the socket - if ('open' == this.readyState && this.upgrade && this.transport.pause) { - debug('starting upgrade probes'); - for (var i = 0, l = this.upgrades.length; i < l; i++) { - this.probe(this.upgrades[i]); - } - } -}; - -/** - * Handles a packet. - * - * @api private - */ - -Socket.prototype.onPacket = function (packet) { - if ('opening' == this.readyState || 'open' == this.readyState) { - debug('socket receive: type "%s", data "%s"', packet.type, packet.data); - - this.emit('packet', packet); - - // Socket is live - any packet counts - this.emit('heartbeat'); - - switch (packet.type) { - case 'open': - this.onHandshake(parsejson(packet.data)); - break; - - case 'pong': - this.setPing(); - break; - - case 'error': - var err = new Error('server error'); - err.code = packet.data; - this.emit('error', err); - break; - - case 'message': - this.emit('data', packet.data); - this.emit('message', packet.data); - break; - } - } else { - debug('packet received with socket readyState "%s"', this.readyState); - } -}; - -/** - * Called upon handshake completion. - * - * @param {Object} handshake obj - * @api private - */ - -Socket.prototype.onHandshake = function (data) { - this.emit('handshake', data); - this.id = data.sid; - this.transport.query.sid = data.sid; - this.upgrades = this.filterUpgrades(data.upgrades); - this.pingInterval = data.pingInterval; - this.pingTimeout = data.pingTimeout; - this.onOpen(); - // In case open handler closes socket - if ('closed' == this.readyState) return; - this.setPing(); - - // Prolong liveness of socket on heartbeat - this.removeListener('heartbeat', this.onHeartbeat); - this.on('heartbeat', this.onHeartbeat); -}; - -/** - * Resets ping timeout. - * - * @api private - */ - -Socket.prototype.onHeartbeat = function (timeout) { - clearTimeout(this.pingTimeoutTimer); - var self = this; - self.pingTimeoutTimer = setTimeout(function () { - if ('closed' == self.readyState) return; - self.onClose('ping timeout'); - }, timeout || (self.pingInterval + self.pingTimeout)); -}; - -/** - * Pings server every `this.pingInterval` and expects response - * within `this.pingTimeout` or closes connection. - * - * @api private - */ - -Socket.prototype.setPing = function () { - var self = this; - clearTimeout(self.pingIntervalTimer); - self.pingIntervalTimer = setTimeout(function () { - debug('writing ping packet - expecting pong within %sms', self.pingTimeout); - self.ping(); - self.onHeartbeat(self.pingTimeout); - }, self.pingInterval); -}; - -/** -* Sends a ping packet. -* -* @api public -*/ - -Socket.prototype.ping = function () { - this.sendPacket('ping'); -}; - -/** - * Called on `drain` event - * - * @api private - */ - -Socket.prototype.onDrain = function() { - for (var i = 0; i < this.prevBufferLen; i++) { - if (this.callbackBuffer[i]) { - this.callbackBuffer[i](); - } - } - - this.writeBuffer.splice(0, this.prevBufferLen); - this.callbackBuffer.splice(0, this.prevBufferLen); - - // setting prevBufferLen = 0 is very important - // for example, when upgrading, upgrade packet is sent over, - // and a nonzero prevBufferLen could cause problems on `drain` - this.prevBufferLen = 0; - - if (this.writeBuffer.length == 0) { - this.emit('drain'); - } else { - this.flush(); - } -}; - -/** - * Flush write buffers. - * - * @api private - */ - -Socket.prototype.flush = function () { - if ('closed' != this.readyState && this.transport.writable && - !this.upgrading && this.writeBuffer.length) { - debug('flushing %d packets in socket', this.writeBuffer.length); - this.transport.send(this.writeBuffer); - // keep track of current length of writeBuffer - // splice writeBuffer and callbackBuffer on `drain` - this.prevBufferLen = this.writeBuffer.length; - this.emit('flush'); - } -}; - -/** - * Sends a message. - * - * @param {String} message. - * @param {Function} callback function. - * @return {Socket} for chaining. - * @api public - */ - -Socket.prototype.write = -Socket.prototype.send = function (msg, fn) { - this.sendPacket('message', msg, fn); - return this; -}; - -/** - * Sends a packet. - * - * @param {String} packet type. - * @param {String} data. - * @param {Function} callback function. - * @api private - */ - -Socket.prototype.sendPacket = function (type, data, fn) { - if ('closing' == this.readyState || 'closed' == this.readyState) { - return; - } - - var packet = { type: type, data: data }; - this.emit('packetCreate', packet); - this.writeBuffer.push(packet); - this.callbackBuffer.push(fn); - this.flush(); -}; - -/** - * Closes the connection. - * - * @api private - */ - -Socket.prototype.close = function () { - if ('opening' == this.readyState || 'open' == this.readyState) { - this.readyState = 'closing'; - - var self = this; - - function close() { - self.onClose('forced close'); - debug('socket closing - telling transport to close'); - self.transport.close(); - } - - function cleanupAndClose() { - self.removeListener('upgrade', cleanupAndClose); - self.removeListener('upgradeError', cleanupAndClose); - close(); - } - - function waitForUpgrade() { - // wait for upgrade to finish since we can't send packets while pausing a transport - self.once('upgrade', cleanupAndClose); - self.once('upgradeError', cleanupAndClose); - } - - if (this.writeBuffer.length) { - this.once('drain', function() { - if (this.upgrading) { - waitForUpgrade(); - } else { - close(); - } - }); - } else if (this.upgrading) { - waitForUpgrade(); - } else { - close(); - } - } - - return this; -}; - -/** - * Called upon transport error - * - * @api private - */ - -Socket.prototype.onError = function (err) { - debug('socket error %j', err); - Socket.priorWebsocketSuccess = false; - this.emit('error', err); - this.onClose('transport error', err); -}; - -/** - * Called upon transport close. - * - * @api private - */ - -Socket.prototype.onClose = function (reason, desc) { - if ('opening' == this.readyState || 'open' == this.readyState || 'closing' == this.readyState) { - debug('socket close with reason: "%s"', reason); - var self = this; - - // clear timers - clearTimeout(this.pingIntervalTimer); - clearTimeout(this.pingTimeoutTimer); - - // clean buffers in next tick, so developers can still - // grab the buffers on `close` event - setTimeout(function() { - self.writeBuffer = []; - self.callbackBuffer = []; - self.prevBufferLen = 0; - }, 0); - - // stop event from firing again for transport - this.transport.removeAllListeners('close'); - - // ensure transport won't stay open - this.transport.close(); - - // ignore further transport communication - this.transport.removeAllListeners(); - - // set ready state - this.readyState = 'closed'; - - // clear session id - this.id = null; - - // emit close event - this.emit('close', reason, desc); - } -}; - -/** - * Filters upgrades, returning only those matching client transports. - * - * @param {Array} server upgrades - * @api private - * - */ - -Socket.prototype.filterUpgrades = function (upgrades) { - var filteredUpgrades = []; - for (var i = 0, j = upgrades.length; i