diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 5cfbb7cfd4..e0fc26b344 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -212,7 +212,7 @@ Examples: garden deploy service-a,service-b # only deploy service-a and service-b garden deploy --force # force re-deploy of modules, even if they're already deployed garden deploy --watch # watch for changes to code - garden deploy --hot-reload=my-service # deploys all services, with hot reloading enabled for my-service + garden deploy --watch --hot-reload=my-service # deploys all services, with hot reloading enabled for my-service garden deploy --env stage # deploy your services to an environment called stage ##### Usage diff --git a/docs/reference/config.md b/docs/reference/config.md index 99af54863e..30710b27a8 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -763,3 +763,370 @@ module: - ``` +### helm + +```yaml + +# Configure a module whose sources are located in this directory. +# +# Required. +module: + # The type of this module. + # + # Example: "container" + # + # Required. + type: + + # The name of this module. + # + # Example: "my-sweet-module" + # + # Required. + name: + + description: + + # A remote repository URL. Currently only supports git servers. Must contain a hash suffix + # pointing to a specific branch or tag, with the format: # + # + # Garden will import the repository source code into this module, but read the module's + # config from the local garden.yml file. + # + # Example: "git+https://github.com/org/repo.git#v2.0" + # + # Optional. + repositoryUrl: + + # Variables that this module can reference and expose as environment variables. + # + # Example: + # my-variable: some-value + # + # Optional. + variables: + {} + + # When false, disables pushing this module to remote registries. + # + # Optional. + allowPublish: true + + # Specify how to build the module. Note that plugins may define additional keys on this object. + # + # Optional. + build: + # The command to run inside the module's directory to perform the build. + # + # Example: + # - npm + # - run + # - build + # + # Optional. + command: + - + + # A list of modules that must be built before this module is built. + # + # Example: + # - name: some-other-module-name + # + # Optional. + dependencies: + - # Module name to build ahead of this module. + # + # Required. + name: + + # Specify one or more files or directories to copy from the built dependency to this + # module. + # + # Optional. + copy: + - # POSIX-style path or filename of the directory or file(s) to copy to the target. + # + # Required. + source: + + # POSIX-style path or filename to copy the directory or file(s) to (defaults to same + # as source path). + # + # Optional. + target: + + # A valid Helm chart name or URI. Required if the module doesn't contain the Helm chart itself. + # + # Example: "stable/nginx-ingress" + # + # Optional. + chart: + + # The path, relative to the module path, to the chart sources (i.e. where the Chart.yaml file + # is, if any). + # + # Optional. + chartPath: . + + # List of names of services that should be deployed before this chart. + # + # Optional. + dependencies: + # Valid RFC1035/RFC1123 (DNS) label (may contain lowercase letters, numbers and dashes, must + # start with a letter, and cannot end with a dash) and additionally cannot contain consecutive + # dashes, or be longer than 63 characters. + # + # Optional. + - + + # The repository URL to fetch the chart from. + # + # Optional. + repo: + + # The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in + # this module (not to be confused with Kubernetes Service resources). Because a Helm chart can + # contain any number of Kubernetes resources, this needs to be specified for certain Garden + # features and commands to work, such as hot-reloading. + # We currently map a Helm chart to a single Garden service, because all the resources in a Helm + # chart are deployed at once. + # + # Optional. + serviceResource: + # The type of Kubernetes resource to sync files to. + # + # Required. + # Allowed values: "Deployment", "DaemonSet", "StatefulSet" + kind: Deployment + + # The name of the resource to sync to. If the chart contains a single resource of the + # specified Kind, this can be omitted. + # This can include a Helm template string, e.g. '{{ template "my-chart.fullname" . }}'. This + # allows you to easily match the dynamic names given by Helm. In most cases you should copy + # this directly from the template in question in order to match it. Note that you may need to + # add single quotes around the string for the YAML to be parsed correctly. + # + # Optional. + name: + + # The name of a container in the target. Specify this if the target contains more than one + # container and the main container is not the first container in the spec. + # + # Optional. + containerName: + + # The Garden module that contains the sources for the container. This needs to be specified + # under `serviceResource` in order to enable hot-reloading for the chart, but is not necessary + # for tasks and tests. + # Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` + # field on the container module. + # Note: If you specify a module here, you don't need to specify it additionally under + # `build.dependencies` + # + # Example: "my-container-module" + # + # Optional. + containerModule: + + # If specified, overrides the arguments for the main container when running in hot-reload + # mode. + # + # Example: + # - nodemon + # - my-server.js + # + # Optional. + hotReloadArgs: + - + + # The task definitions for this module. + # + # Optional. + tasks: + # Required configuration for module tests. + # + # Optional. + - # The name of the test. + # + # Required. + name: + + # The names of any services that must be running, and the names of any tasks that must be + # executed, before the test is run. + # + # Optional. + dependencies: + - + + # Maximum duration (in seconds) of the test run. + # + # Optional. + timeout: null + + # The Deployment, DaemonSet or StatefulSet that Garden should use to execute this task. If + # not specified, the `serviceResource` configured on the module will be used. If neither is + # specified, an error will be thrown. + # + # Optional. + resource: + # The type of Kubernetes resource to sync files to. + # + # Required. + # Allowed values: "Deployment", "DaemonSet", "StatefulSet" + kind: Deployment + + # The name of the resource to sync to. If the chart contains a single resource of the + # specified Kind, this can be omitted. + # This can include a Helm template string, e.g. '{{ template "my-chart.fullname" . }}'. + # This allows you to easily match the dynamic names given by Helm. In most cases you + # should copy this directly from the template in question in order to match it. Note that + # you may need to add single quotes around the string for the YAML to be parsed correctly. + # + # Optional. + name: + + # The name of a container in the target. Specify this if the target contains more than one + # container and the main container is not the first container in the spec. + # + # Optional. + containerName: + + # The Garden module that contains the sources for the container. This needs to be + # specified under `serviceResource` in order to enable hot-reloading for the chart, but is + # not necessary for tasks and tests. + # Must be a `container` module, and for hot-reloading to work you must specify the + # `hotReload` field on the container module. + # Note: If you specify a module here, you don't need to specify it additionally under + # `build.dependencies` + # + # Example: "my-container-module" + # + # Optional. + containerModule: + + # If specified, overrides the arguments for the main container when running in hot-reload + # mode. + # + # Example: + # - nodemon + # - my-server.js + # + # Optional. + hotReloadArgs: + - + + # The arguments to pass to the pod used for execution. + # + # Optional. + args: + - + + # Key/value map of environment variables. Keys must be valid POSIX environment variable + # names (must not start with `GARDEN`) and values must be primitives. + # + # Optional. + env: + {} + + # The test suite definitions for this module. + # + # Optional. + tests: + # Required configuration for module tests. + # + # Optional. + - # The name of the test. + # + # Required. + name: + + # The names of any services that must be running, and the names of any tasks that must be + # executed, before the test is run. + # + # Optional. + dependencies: + - + + # Maximum duration (in seconds) of the test run. + # + # Optional. + timeout: null + + # The Deployment, DaemonSet or StatefulSet that Garden should use to execute this test + # suite. If not specified, the `serviceResource` configured on the module will be used. If + # neither is specified, an error will be thrown. + # + # Optional. + resource: + # The type of Kubernetes resource to sync files to. + # + # Required. + # Allowed values: "Deployment", "DaemonSet", "StatefulSet" + kind: Deployment + + # The name of the resource to sync to. If the chart contains a single resource of the + # specified Kind, this can be omitted. + # This can include a Helm template string, e.g. '{{ template "my-chart.fullname" . }}'. + # This allows you to easily match the dynamic names given by Helm. In most cases you + # should copy this directly from the template in question in order to match it. Note that + # you may need to add single quotes around the string for the YAML to be parsed correctly. + # + # Optional. + name: + + # The name of a container in the target. Specify this if the target contains more than one + # container and the main container is not the first container in the spec. + # + # Optional. + containerName: + + # The Garden module that contains the sources for the container. This needs to be + # specified under `serviceResource` in order to enable hot-reloading for the chart, but is + # not necessary for tasks and tests. + # Must be a `container` module, and for hot-reloading to work you must specify the + # `hotReload` field on the container module. + # Note: If you specify a module here, you don't need to specify it additionally under + # `build.dependencies` + # + # Example: "my-container-module" + # + # Optional. + containerModule: + + # If specified, overrides the arguments for the main container when running in hot-reload + # mode. + # + # Example: + # - nodemon + # - my-server.js + # + # Optional. + hotReloadArgs: + - + + # The arguments to pass to the pod used for testing. + # + # Optional. + args: + - + + # Key/value map of environment variables. Keys must be valid POSIX environment variable + # names (must not start with `GARDEN`) and values must be primitives. + # + # Optional. + env: + {} + + # The chart version to deploy. + # + # Optional. + version: + + # Map of values to pass to Helm when rendering the templates. May include arrays and nested + # objects. + # + # Optional. + values: + {} +``` + diff --git a/docs/using-garden/README.md b/docs/using-garden/README.md index 019405e222..65de64760e 100644 --- a/docs/using-garden/README.md +++ b/docs/using-garden/README.md @@ -14,4 +14,8 @@ Most of the time we want to develop locally, with our project running in Minikub ## [Hot Reload](./hot-reload.md) -This article discusses how to use hot reloading, so that you can update running services on the fly as you make changes to their code, without losing state and without having to destroy and re-create your container. \ No newline at end of file +This article discusses how to use hot reloading, so that you can update running services on the fly as you make changes to their code, without losing state and without having to destroy and re-create your container. + +## [Using Helm charts](./using-helm-charts.md) + +The [Helm](https://helm.sh/) package manager is one of the most commonly used tools for managing Kubernetes manifests. Garden supports using your own Helm charts, alongside your container modules. This guide shows you how to use 3rd-party (or otherwise external) Helm charts, as well as your own charts in your Garden project. We also go through how to configure tests, tasks and hot-reloading for your charts. diff --git a/docs/using-garden/using-helm-charts.md b/docs/using-garden/using-helm-charts.md new file mode 100644 index 0000000000..0a5b06e31d --- /dev/null +++ b/docs/using-garden/using-helm-charts.md @@ -0,0 +1,153 @@ +# Using Helm charts + +The [Helm](https://helm.sh/) package manager is one of the most commonly used tools for managing Kubernetes manifests. Garden supports using your own Helm charts, alongside your container modules, via the `kubernetes` and `local-kubernetes` providers. This guide shows you how to configure and use 3rd-party (or otherwise external) Helm charts, as well as your own charts in your Garden project. We also go through how to set up tests, tasks and hot-reloading for your charts. + +In this guide we'll be using the [vote-helm](https://github.com/garden-io/garden/tree/hot-as-helm/examples/vote-helm) project. If you prefer to just check out a complete example, the project itself is also a good resource. + +You may also want to check out the full [helm module reference](../reference/config.md#helm). + +## Basics + +First off, a couple of things to note on how the Helm support is implemented, with respect to Garden primitives: + +1) One `helm` _module_ maps to a single Garden _service_ (not to be confused with Kubernetes Service resources), with the same name as the module. +2) Because a Helm chart only contains manifests and not actual code (i.e. your containers/images), you'll often need to make two Garden modules for a single deployed service, e.g. one `container` module for your image, and then the `helm` module that references it. + +## Referencing external charts + +Using external charts, where the chart sources are not located in your own project, can be quite straightforward. At a +minimum, you just need to point to the chart, and perhaps provide some values as inputs. Here is the `redis` module from +our example project, for example: + +```yaml +module: + description: Redis service for queueing votes before they are aggregated + type: helm + name: redis + chart: stable/redis + values: + usePassword: false +``` + +For a simple setup, this may be all you need for a chart to be deployed with the rest of your stack. You can also list `redis` as a dependency of one of your other services, and this Helm chart is automatically deployed ahead of it, in dependency order. + +You may also add a `repo` field, to reference a specific chart repository. This may be useful if you run your own chart repository for your organization, or are referencing a module that isn't contained in the default Helm repo. + +## Local charts + +Instead of fetching the chart sources from another repository, you'll often want to include your chart sources in your Garden project. To do this, you can simply add a `garden.yml` in your chart directory (next to your `Chart.yaml`) and start by giving it a name: + +```yaml +module: + description: Helm chart for my module + type: helm + name: my-module +``` + +You can also use Garden's external repository support, to reference chart sources in another repo: + +```yaml +module: + description: Helm chart for my module + type: helm + name: my-module + repositoryUrl: https://github.com/my-org/my-helm-chart#v0.1 +``` + +## Tasks and tests + +You may also want to define _tests_ and/or _tasks_ that execute in one of the containers defined in the chart. An example of this is how we define tasks in the `vote-helm/postgres` module: + +```yaml +module: + description: Postgres database for storing voting results + type: helm + name: postgres + chart: stable/postgresql + version: 3.9.2 # the chart version to fetch + serviceResource: + kind: StatefulSet + name: postgres + tasks: + - name: db-init + args: [ psql, -w, -U, postgres, ..., -c, "'CREATE TABLE IF NOT EXISTS votes ..." ] + env: + PGPASSWORD: postgres + dependencies: + - postgres + - name: db-clear + args: [ psql, -w, -U, postgres, ..., -c, "'TRUNCATE votes'" ] + env: + PGPASSWORD: postgres + dependencies: + - postgres +``` + +Note first the `serviceResource` field. This tells Garden which Kubernetes _Deployment_, _DaemonSet_ or _StatefulSet_ to regard as the primary resource of the chart. In this case, it is simply the `postgres` application itself. When running the `db-init` and `db-clear` tasks, Garden will find the appropriate container spec in the chart based on the `serviceResource` spec, and then execute that container with the task's `args` and (optionally) the specified `env` variables. + +The same applies to any _tests_ that you specify. Take for example the `vote` module: + +```yaml +module: + description: Helm chart for the voting UI + type: helm + name: vote + serviceResource: + kind: Deployment + ... + tests: + - name: integ + args: [npm, run, test:integ] + dependencies: + - api +``` + +Instead of the top-level `serviceResource` you can also add a `resource` field with the same schema to any individual task or test specification. This can be useful if you have different containers in the chart that you want to use for different scenarios. + +## Linking container modules and Helm modules + +When your project also contains one or more `container` modules that build the images used by a `helm` module, you want to make sure the `container`s are built ahead of deploying the Helm chart, and that the correct image tag is used when deploying. The `vote-helm/worker` module and the corresponding `worker-image` module provide a simple example: + +```yaml +module: + description: Helm chart for the worker container + type: helm + name: worker + ... + build: + dependencies: [worker-image] + values: + image: + tag: ${modules.worker-image.version} +``` + +```yaml +module: + description: The worker that collects votes and stores results in a postgres table + type: container + name: worker-image +``` + +Here the `worker` module specifies the image as a build dependency, and additionally injects the `worker-image` version into the Helm chart via the `values` field. Note that the shape of the chart's `values.yml` file will dictate how exactly you provide the image version/tag to the chart (this example is based on the default template generated by `helm create`), so be sure to consult the reference for the chart in question. + +Notice that this can also work if you have multiple containers in a single chart. You just add them all as build dependencies, and the appropriate reference under `values`. + +## Hot reloading + +When your project contains the `container` module referenced by a `helm` module, you can even use Garden's [hot-reloading](./hot-reload.md) feature for a Helm chart. Going back to the `vote` module example: + +```yaml +module: + description: Helm chart for the voting UI + type: helm + name: vote + serviceResource: + kind: Deployment + containerModule: vote-image # The name of your container module. + hotReloadArgs: [npm, run, serve] # Arguments to override the default arguments in the resource's container. + ... +``` + +For hot-reloading to work you must specify `serviceResource.containerModule`, so that Garden knows which module contains the sources to use for hot-reloading. You can then optionally add `serviceResource.hotReloadArgs` to, for example, start the container with automatic reloading or in development mode. + +For the above example, you could then run `garden deploy -w --hot-reload=vote` or `garden dev --hot-reload=vote` to start the `vote` service in hot-reloading mode. When you then change the sources in the _vote-image_ module, Garden syncs the changes to the running container from the Helm chart. diff --git a/examples/simple-project/services/node-service/.dockerignore b/examples/simple-project/services/node-service/.dockerignore deleted file mode 100644 index 1cd4736667..0000000000 --- a/examples/simple-project/services/node-service/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -Dockerfile -garden.yml -app.yaml diff --git a/examples/simple-project/services/node-service/Dockerfile b/examples/simple-project/services/node-service/Dockerfile deleted file mode 100644 index 4a418d7d1e..0000000000 --- a/examples/simple-project/services/node-service/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM node:9-alpine - -ENV PORT=8080 -EXPOSE ${PORT} -WORKDIR /app - -ADD package.json /app -RUN npm install - -ADD . /app - -CMD ["npm", "start"] diff --git a/examples/simple-project/services/node-service/app.js b/examples/simple-project/services/node-service/app.js deleted file mode 100644 index 6df390404c..0000000000 --- a/examples/simple-project/services/node-service/app.js +++ /dev/null @@ -1,27 +0,0 @@ -const express = require('express'); -const request = require('request-promise') -const app = express(); - -// Unless configured otherwise, the hostname is simply the service name -const goServiceEndpoint = `http://go-service/hello-go`; - -app.get('/hello-node', (req, res) => res.send('Hello from Node service!')); - -app.get('/call-go-service', (req, res) => { - // Query the go-service and return the response - request.get(goServiceEndpoint) - .then(message => { - res.json({ - message, - }) - }) - .catch((err) => { - res.statusCode = 500 - res.json({ - error: err, - message: "Unable to reach service at " + goServiceEndpoint, - }) - }); -}); - -module.exports = { app } diff --git a/examples/simple-project/services/node-service/garden.yml b/examples/simple-project/services/node-service/garden.yml deleted file mode 100644 index 64cde58d5d..0000000000 --- a/examples/simple-project/services/node-service/garden.yml +++ /dev/null @@ -1,24 +0,0 @@ -module: - name: node-service - description: Node service container - type: container - services: - - name: node-service - command: [npm, start] - ports: - - name: http - containerPort: 8080 - ingresses: - - path: /hello-node - port: http - - path: /call-go-service - port: http - dependencies: - - go-service - tests: - - name: unit - command: [npm, test] - - name: integ - command: [npm, run, integ] - dependencies: - - go-service diff --git a/examples/simple-project/services/node-service/main.js b/examples/simple-project/services/node-service/main.js deleted file mode 100644 index 06833ec64f..0000000000 --- a/examples/simple-project/services/node-service/main.js +++ /dev/null @@ -1,3 +0,0 @@ -const { app } = require('./app'); - -app.listen(process.env.PORT, '0.0.0.0', () => console.log('Node service started')); diff --git a/examples/simple-project/services/node-service/package.json b/examples/simple-project/services/node-service/package.json deleted file mode 100644 index 790546d484..0000000000 --- a/examples/simple-project/services/node-service/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "node-service", - "version": "1.0.0", - "description": "Simple Node.js docker service", - "main": "main.js", - "scripts": { - "start": "node main.js", - "test": "echo OK", - "integ": "node_modules/mocha/bin/mocha test/integ.js" - }, - "author": "garden.io ", - "license": "ISC", - "dependencies": { - "express": "^4.16.2", - "request": "^2.83.0", - "request-promise": "^4.2.2" - }, - "devDependencies": { - "mocha": "^5.1.1", - "supertest": "^3.0.0" - } -} diff --git a/examples/simple-project/services/node-service/test/integ.js b/examples/simple-project/services/node-service/test/integ.js deleted file mode 100644 index 5cf62a6a85..0000000000 --- a/examples/simple-project/services/node-service/test/integ.js +++ /dev/null @@ -1,17 +0,0 @@ -const supertest = require("supertest") -const { app } = require("../app") - -describe('GET /call-go-service', () => { - const agent = supertest.agent(app) - - it('should respond with a message from go-service', (done) => { - agent - .get("/call-go-service") - .expect(200, { message: "Hello from Go!" }) - .end((err) => { - if (err) return done(err) - done() - }) - }) -}) - diff --git a/examples/vote-helm/README.md b/examples/vote-helm/README.md new file mode 100644 index 0000000000..c37ace1b3a --- /dev/null +++ b/examples/vote-helm/README.md @@ -0,0 +1,13 @@ +# Voting example project with Helm charts + +This is a clone of the [vote example project](../vote/README.md), modified to use Helm charts to describe +Kubernetes resources, instead of the `container` module type. + +You'll notice that we still use the `container` module types for building the container images (the corresponding +`*-image` next to each service module), but they do not contain a `service` section. + +The `helm` modules only contain the charts, which reference the container images. Garden will build the images +ahead of deploying the charts. + +The usage and workflow is the same as in the [vote project](../vote/README.md), please refer to that for usage +instructions. diff --git a/examples/vote-helm/api-image/Dockerfile b/examples/vote-helm/api-image/Dockerfile new file mode 100644 index 0000000000..ad14ad92fb --- /dev/null +++ b/examples/vote-helm/api-image/Dockerfile @@ -0,0 +1,18 @@ +# Using official python runtime base image +FROM python:2.7-alpine + +# Set the application directory +WORKDIR /app + +# Install our requirements.txt +ADD requirements.txt /app/requirements.txt +RUN pip install -r requirements.txt + +# Copy our code from the current folder to /app inside the container +ADD . /app + +# Make port 80 available for links and/or publish +EXPOSE 80 + +# Define our command to be run when launching the container +CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--log-file", "-", "--access-logfile", "-", "--workers", "4", "--keep-alive", "0"] diff --git a/examples/vote-helm/api-image/app.py b/examples/vote-helm/api-image/app.py new file mode 100644 index 0000000000..6db2b765ee --- /dev/null +++ b/examples/vote-helm/api-image/app.py @@ -0,0 +1,52 @@ +from flask import Flask, render_template, request, make_response, g +from flask_cors import CORS +from redis import Redis +import os +import socket +import random +import json + +option_a = os.getenv('OPTION_A', "Cats") +option_b = os.getenv('OPTION_B', "Dogs") +hostname = socket.gethostname() + +app = Flask(__name__) +CORS(app) + +def get_redis(): + if not hasattr(g, 'redis'): + g.redis = Redis(host="redis-master", db=0, socket_timeout=5) + return g.redis + +@app.route("/vote/", methods=['POST','GET']) +def vote(): + voter_id = hex(random.getrandbits(64))[2:-1] + + app.logger.info("received request") + + vote = None + + if request.method == 'POST': + redis = get_redis() + vote = request.form['vote'] + data = json.dumps({'voter_id': voter_id, 'vote': vote}) + + redis.rpush('votes', data) + print("Registered vote") + response = app.response_class( + response=json.dumps(data), + status=200, + mimetype='application/json' + ) + return response + + response = app.response_class( + response=json.dumps({}), + status=404, + mimetype='application/json' + ) + return response + + +if __name__ == "__main__": + app.run(host='0.0.0.0', port=80, debug=True, threaded=True) diff --git a/examples/vote-helm/api-image/garden.yml b/examples/vote-helm/api-image/garden.yml new file mode 100644 index 0000000000..4abb3cf9fd --- /dev/null +++ b/examples/vote-helm/api-image/garden.yml @@ -0,0 +1,7 @@ +module: + description: Image for the API backend for the voting UI + type: container + name: api-image + tests: + - name: unit + command: [echo, ok] diff --git a/examples/vote-helm/api-image/requirements.txt b/examples/vote-helm/api-image/requirements.txt new file mode 100644 index 0000000000..dcd270a579 --- /dev/null +++ b/examples/vote-helm/api-image/requirements.txt @@ -0,0 +1,4 @@ +Flask +Redis +gunicorn +flask-cors diff --git a/examples/vote-helm/api/.helmignore b/examples/vote-helm/api/.helmignore new file mode 100644 index 0000000000..50af031725 --- /dev/null +++ b/examples/vote-helm/api/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/examples/vote-helm/api/Chart.yaml b/examples/vote-helm/api/Chart.yaml new file mode 100644 index 0000000000..6b53945ade --- /dev/null +++ b/examples/vote-helm/api/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: api +version: 0.1.0 diff --git a/examples/vote-helm/api/garden.yml b/examples/vote-helm/api/garden.yml new file mode 100644 index 0000000000..5f1945b44f --- /dev/null +++ b/examples/vote-helm/api/garden.yml @@ -0,0 +1,16 @@ +module: + description: The API backend for the voting UI + type: helm + name: api + serviceResource: + kind: Deployment + containerModule: api-image + dependencies: + - redis + values: + image: + tag: ${modules.api-image.version} + ingress: + enabled: true + paths: [/] + hosts: [api.local.app.garden] diff --git a/examples/vote-helm/api/templates/NOTES.txt b/examples/vote-helm/api/templates/NOTES.txt new file mode 100644 index 0000000000..9e565235ac --- /dev/null +++ b/examples/vote-helm/api/templates/NOTES.txt @@ -0,0 +1,21 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range $.Values.ingress.paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "api.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ include "api.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "api.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "api.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/examples/vote-helm/api/templates/_helpers.tpl b/examples/vote-helm/api/templates/_helpers.tpl new file mode 100644 index 0000000000..54ca5c0215 --- /dev/null +++ b/examples/vote-helm/api/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "api.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "api.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "api.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/examples/vote-helm/api/templates/deployment.yaml b/examples/vote-helm/api/templates/deployment.yaml new file mode 100644 index 0000000000..93a8e6ecee --- /dev/null +++ b/examples/vote-helm/api/templates/deployment.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "api.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "api.name" . }} + helm.sh/chart: {{ include "api.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "api.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "api.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: [python, app.py] + ports: + - name: http + containerPort: 80 + protocol: TCP + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/examples/vote-helm/api/templates/ingress.yaml b/examples/vote-helm/api/templates/ingress.yaml new file mode 100644 index 0000000000..291fdf7349 --- /dev/null +++ b/examples/vote-helm/api/templates/ingress.yaml @@ -0,0 +1,40 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "api.fullname" . -}} +{{- $ingressPaths := .Values.ingress.paths -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app.kubernetes.io/name: {{ include "api.name" . }} + helm.sh/chart: {{ include "api.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . | quote }} + http: + paths: + {{- range $ingressPaths }} + - path: {{ . }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} + {{- end }} +{{- end }} diff --git a/examples/vote-helm/api/templates/service.yaml b/examples/vote-helm/api/templates/service.yaml new file mode 100644 index 0000000000..726bdc400d --- /dev/null +++ b/examples/vote-helm/api/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "api.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "api.name" . }} + helm.sh/chart: {{ include "api.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: {{ include "api.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} diff --git a/examples/vote-helm/api/values.yaml b/examples/vote-helm/api/values.yaml new file mode 100644 index 0000000000..7bbb6c9188 --- /dev/null +++ b/examples/vote-helm/api/values.yaml @@ -0,0 +1,48 @@ +# Default values for api. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: api-image + tag: stable + pullPolicy: IfNotPresent + +nameOverride: "" +fullnameOverride: "" + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + paths: [] + hosts: + - chart-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/examples/vote-helm/garden.yml b/examples/vote-helm/garden.yml new file mode 100644 index 0000000000..629d204f96 --- /dev/null +++ b/examples/vote-helm/garden.yml @@ -0,0 +1,6 @@ +project: + name: vote-helm + environments: + - name: local + providers: + - name: local-kubernetes diff --git a/examples/vote-helm/postgres/garden.yml b/examples/vote-helm/postgres/garden.yml new file mode 100644 index 0000000000..b9a9140b44 --- /dev/null +++ b/examples/vote-helm/postgres/garden.yml @@ -0,0 +1,43 @@ +module: + description: Postgres database for storing voting results + type: helm + name: postgres + chart: stable/postgresql + version: 3.9.2 + serviceResource: + kind: StatefulSet + name: postgres + tasks: + - name: db-init + args: [ + psql, + -w, + -U, postgres, + --host, postgres, + --port=5432, + -d, postgres, + -c, "'CREATE TABLE IF NOT EXISTS votes (id VARCHAR(255) NOT NULL UNIQUE, vote VARCHAR(255) NOT NULL, created_at timestamp default NULL)'" + ] + env: + PGPASSWORD: postgres + dependencies: + - postgres + - name: db-clear + args: [ + psql, + -w, + -U, postgres, + --host, postgres, + --port=5432, + -d, postgres, + -c "'TRUNCATE votes'" + ] + env: + PGPASSWORD: postgres + dependencies: + - postgres + values: + # This is a more digestable name than the default one in the template + fullnameOverride: postgres + # This should of course not be used in production + postgresqlPassword: postgres \ No newline at end of file diff --git a/examples/vote-helm/redis/garden.yml b/examples/vote-helm/redis/garden.yml new file mode 100644 index 0000000000..75f2d31029 --- /dev/null +++ b/examples/vote-helm/redis/garden.yml @@ -0,0 +1,7 @@ +module: + description: Redis service for queueing votes before they are aggregated + type: helm + name: redis + chart: stable/redis + values: + usePassword: false diff --git a/examples/vote-helm/result-image/Dockerfile b/examples/vote-helm/result-image/Dockerfile new file mode 100644 index 0000000000..2bca93313a --- /dev/null +++ b/examples/vote-helm/result-image/Dockerfile @@ -0,0 +1,18 @@ +FROM node:8.9-alpine + +RUN mkdir -p /app +WORKDIR /app + +RUN npm install -g nodemon +RUN npm config set registry https://registry.npmjs.org +COPY package.json /app/package.json +RUN npm install \ + && npm ls \ + && npm cache clean --force \ + && mv /app/node_modules /node_modules +COPY . /app + +ENV PORT 80 +EXPOSE 80 + +CMD ["node", "server.js"] diff --git a/examples/vote-helm/result-image/db.js b/examples/vote-helm/result-image/db.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/vote-helm/result-image/garden.yml b/examples/vote-helm/result-image/garden.yml new file mode 100644 index 0000000000..90f28fdc29 --- /dev/null +++ b/examples/vote-helm/result-image/garden.yml @@ -0,0 +1,4 @@ +module: + description: Results UI service container + type: container + name: result-image diff --git a/examples/vote-helm/result-image/package.json b/examples/vote-helm/result-image/package.json new file mode 100644 index 0000000000..f7863e1a1d --- /dev/null +++ b/examples/vote-helm/result-image/package.json @@ -0,0 +1,20 @@ +{ + "name": "result", + "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/examples/vote-helm/result-image/server.js b/examples/vote-helm/result-image/server.js new file mode 100644 index 0000000000..b67df797af --- /dev/null +++ b/examples/vote-helm/result-image/server.js @@ -0,0 +1,91 @@ +var express = require('express'), + async = require('async'), + path = require("path") + pg = require("pg"), + cookieParser = require('cookie-parser'), + bodyParser = require('body-parser'), + methodOverride = require('method-override'), + app = express(), + server = require('http').Server(app), + io = require('socket.io')(server); + // io = require('socket.io')(server, { path: "/result/views/socket.io" }); + +io.set('transports', ['polling']); + +var port = process.env.PORT || 4000; + +io.sockets.on('connection', function (socket) { + + socket.emit('message', { text : 'Welcome!' }); + + socket.on('subscribe', function (data) { + socket.join(data.channel); + }); +}); + +async.retry( + {times: 1000, interval: 1000}, + function(callback) { + pg.connect('postgres://postgres:postgres@postgres/postgres', function (err, client, done) { + if (err) { + console.error("Waiting for db"); + } + callback(err, client); + }); + }, + function(err, client) { + if (err) { + return console.err("Giving up"); + } + console.log("Connected to db"); + getVotes(client); + } +); + +function getVotes(client) { + client.query('SELECT vote, COUNT(id) AS count FROM votes GROUP BY vote', [], function(err, result) { + if (err) { + console.error("Error performing query: " + err); + } else { + var votes = collectVotesFromResult(result); + io.sockets.emit("scores", JSON.stringify(votes)); + } + + setTimeout(function() {getVotes(client) }, 1000); + }); +} + +function collectVotesFromResult(result) { + var votes = {a: 0, b: 0}; + + result.rows.forEach(function (row) { + votes[row.vote] = parseInt(row.count); + }); + + return votes; +} + +app.use(cookieParser()); +app.use(bodyParser()); +app.use(methodOverride('X-HTTP-Method-Override')); +app.use(function(req, res, next) { + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); + res.header("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS"); + next(); +}); + +app.use(express.static('views')); +// app.use(express.static(__dirname + '/result/views')); + +// app.use('/result', express.static(__dirname + '/views')); + +// app.get('/result', function (req, res) { +app.get('/', function (req, res) { + res.sendFile(path.resolve(__dirname + '/views/index.html')); +}); + +server.listen(port, function () { + var port = server.address().port; + console.log('App running on port ' + port); +}); diff --git a/examples/vote-helm/result-image/views/app.js b/examples/vote-helm/result-image/views/app.js new file mode 100644 index 0000000000..912496b1fe --- /dev/null +++ b/examples/vote-helm/result-image/views/app.js @@ -0,0 +1,56 @@ +var app = angular.module('catsvsdogs', []); +var socket = io.connect({ + transports:['polling'], + // path: "/result/views/socket.io", +}); +// https://vote.local.app.garden/socket.io/?EIO=3&transport=polling&t=1544105126936-2 +// https://vote.local.app.garden/result/views/socket.io/?EIO=3&transport=polling&t=1544104159102-166 + +var bg1 = document.getElementById('background-stats-1'); +var bg2 = document.getElementById('background-stats-2'); + +app.controller('statsCtrl', function($scope){ + $scope.aPercent = 50; + $scope.bPercent = 50; + + var updateScores = function(){ + socket.on('scores', function (json) { + console.log("scores", json) + var data = JSON.parse(json); + var a = parseInt(data.a || 0); + var b = parseInt(data.b || 0); + + var percentages = getPercentages(a, b); + + bg1.style.width = percentages.a + "%"; + bg2.style.width = percentages.b + "%"; + + $scope.$apply(function () { + $scope.aPercent = percentages.a; + $scope.bPercent = percentages.b; + $scope.total = a + b; + }); + }); + }; + + var init = function(){ + document.body.style.opacity=1; + updateScores(); + }; + socket.on('message',function(data){ + init(); + }); +}); + +function getPercentages(a, b) { + var result = {}; + + if (a + b > 0) { + result.a = Math.round(a / (a + b) * 100); + result.b = 100 - result.a; + } else { + result.a = result.b = 50; + } + + return result; +} diff --git a/examples/vote-helm/result-image/views/index.html b/examples/vote-helm/result-image/views/index.html new file mode 100644 index 0000000000..e5ee07f903 --- /dev/null +++ b/examples/vote-helm/result-image/views/index.html @@ -0,0 +1,43 @@ + + + + + Cats vs Dogs -- Result + + + + + + + +
+
+
+
+
+
+
+
+
+
Cats
+
{{aPercent | number:1}}%
+
+
+
+
Dogs
+
{{bPercent | number:1}}%
+
+
+
+
+
+ No votes yet + {{total}} vote + {{total}} votes +
+ + + + + diff --git a/examples/vote-helm/result-image/views/socket.io.js b/examples/vote-helm/result-image/views/socket.io.js new file mode 100644 index 0000000000..67f5484fb1 --- /dev/null +++ b/examples/vote-helm/result-image/views/socket.io.js @@ -0,0 +1,6988 @@ +!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