diff --git a/README.md b/README.md index f15a08ccffde160e639d94068a3981833e1a8c84..43c44a4f42f36318af9c20bd82ac0a9147c7e52a 100644 --- a/README.md +++ b/README.md @@ -4,347 +4,24 @@ Gitlab-workhorse is a smart reverse proxy for GitLab. It handles "large" HTTP requests such as file downloads, file uploads, Git push/pull and Git archive downloads. -## Features that rely on Workhorse +Workhorse itself is not a feature, but there are [several features in +GitLab](doc/gitlab_features.md) that would not work efficiently without Workhorse. -Workhorse itself is not a feature, but there are several features in -GitLab that would not work efficiently without Workhorse. +## Documentation -To put the efficiency benefit in context, consider that in 2020Q3 on GitLab.com [we see][thanos] Rails application threads using on average about 200MB of RSS vs about 200KB for Workhorse goroutines. +Workhorse documentation is available in the [`doc` folder of this repostitory](doc/). -[thanos]: https://thanos-query.ops.gitlab.net/graph?g0.range_input=1h&g0.max_source_resolution=0s&g0.expr=sum(ruby_process_resident_memory_bytes%7Bapp%3D%22webservice%22%2Cenv%3D%22gprd%22%2Crelease%3D%22gitlab%22%7D)%20%2F%20sum(puma_max_threads%7Bapp%3D%22webservice%22%2Cenv%3D%22gprd%22%2Crelease%3D%22gitlab%22%7D)&g0.tab=1&g1.range_input=1h&g1.max_source_resolution=0s&g1.expr=sum(go_memstats_sys_bytes%7Bapp%3D%22webservice%22%2Cenv%3D%22gprd%22%2Crelease%3D%22gitlab%22%7D)%2Fsum(go_goroutines%7Bapp%3D%22webservice%22%2Cenv%3D%22gprd%22%2Crelease%3D%22gitlab%22%7D)&g1.tab=1 +* Architectural overview + * [GitLab features that rely on Workhorse](doc/architecture/gitlab_features.md) + * [Websocket channel support](doc/architecture/channel.md) +* Operating workhorse + * [Source installation](doc/operations/install.md) + * [Workhorse configuration](doc/operations/configuration.md) +* [Contributing](CONTRIBUTING.md) + * [Testing your code](doc/development/tests.md) -Examples of features that rely on Workhorse: - -### 1. `git clone` and `git push` over HTTP - -Git clone, pull and push are slow because they transfer large amounts -of data and because each is CPU intensive on the GitLab side. Without -workhorse, HTTP access to Git repositories would compete with regular -web access to the application, requiring us to run way more Rails -application servers. - -### 2. CI runner long polling - -GitLab CI runners fetch new CI jobs by polling the GitLab server. -Workhorse acts as a kind of "waiting room" where CI runners can sit -and wait for new CI jobs. Because of Go's efficiency we can fit a lot -of runners in the waiting room at little cost. Without this waiting -room mechanism we would have to add a lot more Rails server capacity. - -### 3. File uploads and downloads - -File uploads and downloads may be slow either because the file is -large or because the user's connection is slow. Workhorse can handle -the slow part for Rails. This improves the efficiency of features such -as CI artifacts, package repositories, LFS objects, etc. - -### 4. Websocket proxying - -Features such as the web terminal require a long lived connection -between the user's web browser and a container inside GitLab that is -not directly accessible from the internet. Dedicating a Rails -application thread to proxying such a connection would cost much more -memory than it costs to have Workhorse look after it. - -## Quick facts (how does Workhorse work) - -- Workhorse can handle some requests without involving Rails at all: - for example, JavaScript files and CSS files are served straight - from disk. -- Workhorse can modify responses sent by Rails: for example if you use - `send_file` in Rails then gitlab-workhorse will open the file on - disk and send its contents as the response body to the client. -- Workhorse can take over requests after asking permission from Rails. - Example: handling `git clone`. -- Workhorse can modify requests before passing them to Rails. Example: - when handling a Git LFS upload Workhorse first asks permission from - Rails, then it stores the request body in a tempfile, then it sends - a modified request containing the tempfile path to Rails. -- Workhorse can manage long-lived WebSocket connections for Rails. - Example: handling the terminal websocket for environments. -- Workhorse does not connect to Postgres, only to Rails and (optionally) Redis. -- We assume that all requests that reach Workhorse pass through an - upstream proxy such as NGINX or Apache first. -- Workhorse does not accept HTTPS connections. -- Workhorse does not clean up idle client connections. -- We assume that all requests to Rails pass through Workhorse. - -For more information see ['A brief history of -gitlab-workhorse'][brief-history-blog]. - -## Usage - -``` - gitlab-workhorse [OPTIONS] - -Options: - -apiCiLongPollingDuration duration - Long polling duration for job requesting for runners (default 50s - enabled) (default 50ns) - -apiLimit uint - Number of API requests allowed at single time - -apiQueueDuration duration - Maximum queueing duration of requests (default 30s) - -apiQueueLimit uint - Number of API requests allowed to be queued - -authBackend string - Authentication/authorization backend (default "http://localhost:8080") - -authSocket string - Optional: Unix domain socket to dial authBackend at - -cableBackend string - Optional: ActionCable backend (default authBackend) - -cableSocket string - Optional: Unix domain socket to dial cableBackend at (default authSocket) - -config string - TOML file to load config from - -developmentMode - Allow the assets to be served from Rails app - -documentRoot string - Path to static files content (default "public") - -listenAddr string - Listen address for HTTP server (default "localhost:8181") - -listenNetwork string - Listen 'network' (tcp, tcp4, tcp6, unix) (default "tcp") - -listenUmask int - Umask for Unix socket - -logFile string - Log file location - -logFormat string - Log format to use defaults to text (text, json, structured, none) (default "text") - -pprofListenAddr string - pprof listening address, e.g. 'localhost:6060' - -prometheusListenAddr string - Prometheus listening address, e.g. 'localhost:9229' - -proxyHeadersTimeout duration - How long to wait for response headers when proxying the request (default 5m0s) - -secretPath string - File with secret key to authenticate with authBackend (default "./.gitlab_workhorse_secret") - -version - Print version and exit -``` - -The 'auth backend' refers to the GitLab Rails application. The name is -a holdover from when gitlab-workhorse only handled Git push/pull over -HTTP. - -Gitlab-workhorse can listen on either a TCP or a Unix domain socket. It -can also open a second listening TCP listening socket with the Go -[net/http/pprof profiler server](http://golang.org/pkg/net/http/pprof/). - -Gitlab-workhorse can listen on redis events (currently only builds/register -for runners). This requires you to pass a valid TOML config file via -`-config` flag. -For regular setups it only requires the following (replacing the string -with the actual socket) - -### Redis - -Gitlab-workhorse integrates with Redis to do long polling for CI build -requests. This is configured via two things: - -- Redis settings in the TOML config file -- The `-apiCiLongPollingDuration` command line flag to control polling - behavior for CI build requests - -It is OK to enable Redis in the config file but to leave CI polling -disabled; this just results in an idle Redis pubsub connection. The -opposite is not possible: CI long polling requires a correct Redis -configuration. - -Below we discuss the options for the `[redis]` section in the config -file. - -``` -[redis] -URL = "unix:///var/run/gitlab/redis.sock" -Password = "my_awesome_password" -Sentinel = [ "tcp://sentinel1:23456", "tcp://sentinel2:23456" ] -SentinelMaster = "mymaster" -``` - -- `URL` takes a string in the format `unix://path/to/redis.sock` or -`tcp://host:port`. -- `Password` is only required if your redis instance is password-protected -- `Sentinel` is used if you are using Sentinel. - *NOTE* that if both `Sentinel` and `URL` are given, only `Sentinel` will be used - -Optional fields are as follows: -``` -[redis] -DB = 0 -ReadTimeout = "1s" -KeepAlivePeriod = "5m" -MaxIdle = 1 -MaxActive = 1 -``` - -- `DB` is the Database to connect to. Defaults to `0` -- `ReadTimeout` is how long a redis read-command can take. Defaults to `1s` -- `KeepAlivePeriod` is how long the redis connection is to be kept alive without anything flowing through it. Defaults to `5m` -- `MaxIdle` is how many idle connections can be in the redis-pool at once. Defaults to 1 -- `MaxActive` is how many connections the pool can keep. Defaults to 1 - -### Relative URL support - -If you are mounting GitLab at a relative URL, e.g. -`example.com/gitlab`, then you should also use this relative URL in -the `authBackend` setting: - -``` -gitlab-workhorse -authBackend http://localhost:8080/gitlab -``` - -### Interaction of authBackend and authSocket - -The interaction between `authBackend` and `authSocket` can be a bit -confusing. It comes down to: if `authSocket` is set it overrides the -_host_ part of `authBackend` but not the relative path. - -In table form: - -|authBackend|authSocket|Workhorse connects to?|Rails relative URL| -|---|---|---|---| -|unset|unset|`localhost:8080`|`/`| -|`http://localhost:3000`|unset|`localhost:3000`|`/`| -|`http://localhost:3000/gitlab`|unset|`localhost:3000`|`/gitlab`| -|unset|`/path/to/socket`|`/path/to/socket`|`/`| -|`http://localhost:3000`|`/path/to/socket`|`/path/to/socket`|`/`| -|`http://localhost:3000/gitlab`|`/path/to/socket`|`/path/to/socket`|`/gitlab`| - -The same applies to `cableBackend` and `cableSocket`. - -## Installation - -To install gitlab-workhorse you need [Go 1.13 or -newer](https://golang.org/dl) and [GNU -Make](https://www.gnu.org/software/make/). - -To install into `/usr/local/bin` run `make install`. - -``` -make install -``` - -To install into `/foo/bin` set the PREFIX variable. - -``` -make install PREFIX=/foo -``` - -On some operating systems, such as FreeBSD, you may have to use -`gmake` instead of `make`. - -## Dependencies - -### Exiftool - -Workhorse uses [exiftool](https://www.sno.phy.queensu.ca/~phil/exiftool/) for -removing EXIF data (which may contain sensitive information) from uploaded -images. If you installed GitLab: - -- Using the Omnibus package, you're all set. - *NOTE* that if you are using CentOS Minimal, you may need to install `perl` - package: `yum install perl` -- From source, make sure `exiftool` is installed: - - ```sh - # Debian/Ubuntu - sudo apt-get install libimage-exiftool-perl - - # RHEL/CentOS - sudo yum install perl-Image-ExifTool - ``` - -## Error tracking - -GitLab-Workhorse supports remote error tracking with -[Sentry](https://sentry.io). To enable this feature set the -`GITLAB_WORKHORSE_SENTRY_DSN` environment variable. -You can also set the `GITLAB_WORKHORSE_SENTRY_ENVIRONMENT` environment variable to -use the Sentry environment functionality to separate staging, production and -development. - -Omnibus (`/etc/gitlab/gitlab.rb`): - -``` -gitlab_workhorse['env'] = { - 'GITLAB_WORKHORSE_SENTRY_DSN' => 'https://foobar' - 'GITLAB_WORKHORSE_SENTRY_ENVIRONMENT' => 'production' -} -``` - -Source installations (`/etc/default/gitlab`): - -``` -export GITLAB_WORKHORSE_SENTRY_DSN='https://foobar' -export GITLAB_WORKHORSE_SENTRY_ENVIRONMENT='production' -``` - -## Tests - -Run the tests with: - -``` -make clean test -``` - -### Coverage / what to test - -Each feature in gitlab-workhorse should have an integration test that -verifies that the feature 'kicks in' on the right requests and leaves -other requests unaffected. It is better to also have package-level tests -for specific behavior but the high-level integration tests should have -the first priority during development. - -It is OK if a feature is only covered by integration tests. - -## Distributed Tracing - -Workhorse supports distributed tracing through [LabKit][] using [OpenTracing APIs](https://opentracing.io). - -By default, no tracing implementation is linked into the binary, but different OpenTracing providers can be linked in using [build tags][build-tags]/[build constraints][build-tags]. This can be done by setting the `BUILD_TAGS` make variable. - -For more details of the supported providers, see LabKit, but as an example, for Jaeger tracing support, include the tags: `BUILD_TAGS="tracer_static tracer_static_jaeger"`. - -```shell -make BUILD_TAGS="tracer_static tracer_static_jaeger" -``` - -Once Workhorse is compiled with an opentracing provider, the tracing configuration is configured via the `GITLAB_TRACING` environment variable. - -For example: - -```shell -GITLAB_TRACING=opentracing://jaeger ./gitlab-workhorse -``` - -## Continuous Profiling - -Workhorse supports continuous profiling through [LabKit][] using [Stackdriver Profiler](https://cloud.google.com/profiler). - -By default, the Stackdriver Profiler implementation is linked in the binary using [build tags][build-tags], though it's not -required and can be skipped. - -For example: - -```shell -make BUILD_TAGS="" -``` - -Once Workhorse is compiled with Continuous Profiling, the profiler configuration can be set via `GITLAB_CONTINUOUS_PROFILING` -environment variable. - -For example: - -```shell -GITLAB_CONTINUOUS_PROFILING="stackdriver?service=workhorse&service_version=1.0.1&project_id=test-123 ./gitlab-workhorse" -``` - -More information about see the [LabKit monitoring docs](https://gitlab.com/gitlab-org/labkit/-/blob/master/monitoring/doc.go). ## License This code is distributed under the MIT license, see the [LICENSE](LICENSE) file. -[brief-history-blog]: https://about.gitlab.com/2016/04/12/a-brief-history-of-gitlab-workhorse/ -[LabKit]: https://gitlab.com/gitlab-org/labkit/ -[build-tags]: https://golang.org/pkg/go/build/#hdr-Build_Constraints diff --git a/doc/architecture/channel.md b/doc/architecture/channel.md new file mode 100644 index 0000000000000000000000000000000000000000..8423405a9cff1802e88e461555e77041cd7db7d7 --- /dev/null +++ b/doc/architecture/channel.md @@ -0,0 +1,194 @@ +# Websocket channel support + +In some cases, GitLab can provide in-browser terminal access to an +environment (which is a running server or container, onto which a +project has been deployed), or even access to services running in CI +through a WebSocket. Workhorse manages the WebSocket upgrade and +long-lived connection to the websocket connection, which frees +up GitLab to process other requests. + +This document outlines the architecture of these connections. + +## Introduction to WebSockets + +A websocket is an "upgraded" HTTP/1.1 request. Their purpose is to +permit bidirectional communication between a client and a server. +**Websockets are not HTTP**. Clients can send messages (known as +frames) to the server at any time, and vice-versa. Client messages +are not necessarily requests, and server messages are not necessarily +responses. WebSocket URLs have schemes like `ws://` (unencrypted) or +`wss://` (TLS-secured). + +When requesting an upgrade to WebSocket, the browser sends a HTTP/1.1 +request that looks like this: + +``` +GET /path.ws HTTP/1.1 +Connection: upgrade +Upgrade: websocket +Sec-WebSocket-Protocol: terminal.gitlab.com +# More headers, including security measures +``` + +At this point, the connection is still HTTP, so this is a request and +the server can send a normal HTTP response, including `404 Not Found`, +`500 Internal Server Error`, etc. + +If the server decides to permit the upgrade, it will send a HTTP +`101 Switching Protocols` response. From this point, the connection +is no longer HTTP. It is a WebSocket and frames, not HTTP requests, +will flow over it. The connection will persist until the client or +server closes the connection. + +In addition to the subprotocol, individual websocket frames may +also specify a message type - examples include `BinaryMessage`, +`TextMessage`, `Ping`, `Pong` or `Close`. Only binary frames can +contain arbitrary data - other frames are expected to be valid +UTF-8 strings, in addition to any subprotocol expectations. + +## Browser to Workhorse + +Using the terminal as an example, GitLab serves a JavaScript terminal +emulator to the browser on a URL like +`https://gitlab.com/group/project/-/environments/1/terminal`. +This opens a websocket connection to, e.g., +`wss://gitlab.com/group/project/-/environments/1/terminal.ws`, +This endpoint doesn't exist in GitLab - only in Workhorse. + +When receiving the connection, Workhorse first checks that the +client is authorized to access the requested terminal. It does +this by performing a "preauthentication" request to GitLab. + +If the client has the appropriate permissions and the terminal +exists, GitLab responds with a successful response that includes +details of the terminal that the client should be connected to. +Otherwise, it returns an appropriate HTTP error response. + +Errors are passed back to the client as HTTP responses, but if +GitLab returns valid terminal details to Workhorse, it will +connect to the specified terminal, upgrade the browser to a +WebSocket, and proxy between the two connections for as long +as the browser's credentials are valid. Workhorse will also +send regular `PingMessage` control frames to the browser, to +keep intervening proxies from terminating the connection +while the browser is present. + +The browser must request an upgrade with a specific subprotocol: + +### `terminal.gitlab.com` + +This subprotocol considers `TextMessage` frames to be invalid. +Control frames, such as `PingMessage` or `CloseMessage`, have +their usual meanings. + +`BinaryMessage` frames sent from the browser to the server are +arbitrary text input. + +`BinaryMessage` frames sent from the server to the browser are +arbitrary text output. + +These frames are expected to contain ANSI text control codes +and may be in any encoding. + +### `base64.terminal.gitlab.com` + +This subprotocol considers `BinaryMessage` frames to be invalid. +Control frames, such as `PingMessage` or `CloseMessage`, have +their usual meanings. + +`TextMessage` frames sent from the browser to the server are +base64-encoded arbitrary text input (so the server must +base64-decode them before inputting them). + +`TextMessage` frames sent from the server to the browser are +base64-encoded arbitrary text output (so the browser must +base64-decode them before outputting them). + +In their base64-encoded form, these frames are expected to +contain ANSI terminal control codes, and may be in any encoding. + +## Workhorse to GitLab + +Using again the terminal as an example, before upgrading the browser, +Workhorse sends a normal HTTP request to GitLab on a URL like +`https://gitlab.com/group/project/environments/1/terminal.ws/authorize`. +This returns a JSON response containing details of where the +terminal can be found, and how to connect it. In particular, +the following details are returned in case of success: + +* WebSocket URL to **connect** to, e.g.: `wss://example.com/terminals/1.ws?tty=1` +* WebSocket subprotocols to support, e.g.: `["channel.k8s.io"]` +* Headers to send, e.g.: `Authorization: Token xxyyz..` +* Certificate authority to verify `wss` connections with (optional) + +Workhorse periodically re-checks this endpoint, and if it gets an +error response, or the details of the terminal change, it will +terminate the websocket session. + +## Workhorse to the WebSocket server + +In GitLab, environments or CI jobs may have a deployment service (e.g., +`KubernetesService`) associated with them. This service knows +where the terminals or the service for an environment may be found, and these +details are returned to Workhorse by GitLab. + +These URLs are *also* WebSocket URLs, and GitLab tells Workhorse +which subprotocols to speak over the connection, along with any +authentication details required by the remote end. + +Before upgrading the browser's connection to a websocket, +Workhorse opens a HTTP client connection, according to the +details given to it by Workhorse, and attempts to upgrade +that connection to a websocket. If it fails, an error +response is sent to the browser; otherwise, the browser is +also upgraded. + +Workhorse now has two websocket connections, albeit with +differing subprotocols. It decodes incoming frames from the +browser, re-encodes them to the the channel's subprotocol, and +sends them to the channel. Similarly, it decodes incoming +frames from the channel, re-encodes them to the browser's +subprotocol, and sends them to the browser. + +When either connection closes or enters an error state, +Workhorse detects the error and closes the other connection, +terminating the channel session. If the browser is the +connection that has disconnected, Workhorse will send an ANSI +`End of Transmission` control code (the `0x04` byte) to the +channel, encoded according to the appropriate subprotocol. +Workhorse will automatically reply to any websocket ping frame +sent by the channel, to avoid being disconnected. + +Currently, Workhorse only supports the following subprotocols. +Supporting new deployment services will require new subprotocols +to be supported: + +### `channel.k8s.io` + +Used by Kubernetes, this subprotocol defines a simple multiplexed +channel. + +Control frames have their usual meanings. `TextMessage` frames are +invalid. `BinaryMessage` frames represent I/O to a specific file +descriptor. + +The first byte of each `BinaryMessage` frame represents the file +descriptor (fd) number, as a `uint8` (so the value `0x00` corresponds +to fd 0, `STDIN`, while `0x01` corresponds to fd 1, `STDOUT`). + +The remaining bytes represent arbitrary data. For frames received +from the server, they are bytes that have been received from that +fd. For frames sent to the server, they are bytes that should be +written to that fd. + +### `base64.channel.k8s.io` + +Also used by Kubernetes, this subprotocol defines a similar multiplexed +channel to `channel.k8s.io`. The main differences are: + +* `TextMessage` frames are valid, rather than `BinaryMessage` frames. +* The first byte of each `TextMessage` frame represents the file + descriptor as a numeric UTF-8 character, so the character `U+0030`, + or "0", is fd 0, STDIN). +* The remaining bytes represent base64-encoded arbitrary data. + diff --git a/doc/architecture/gitlab_features.md b/doc/architecture/gitlab_features.md new file mode 100644 index 0000000000000000000000000000000000000000..051d00a50618ebd9eedb58087d2d381167476d59 --- /dev/null +++ b/doc/architecture/gitlab_features.md @@ -0,0 +1,69 @@ +# Features that rely on Workhorse + +Workhorse itself is not a feature, but there are several features in +GitLab that would not work efficiently without Workhorse. + +To put the efficiency benefit in context, consider that in 2020Q3 on +GitLab.com [we see][thanos] Rails application threads using on average +about 200MB of RSS vs about 200KB for Workhorse goroutines. + +Examples of features that rely on Workhorse: + +## 1. `git clone` and `git push` over HTTP + +Git clone, pull and push are slow because they transfer large amounts +of data and because each is CPU intensive on the GitLab side. Without +workhorse, HTTP access to Git repositories would compete with regular +web access to the application, requiring us to run way more Rails +application servers. + +## 2. CI runner long polling + +GitLab CI runners fetch new CI jobs by polling the GitLab server. +Workhorse acts as a kind of "waiting room" where CI runners can sit +and wait for new CI jobs. Because of Go's efficiency we can fit a lot +of runners in the waiting room at little cost. Without this waiting +room mechanism we would have to add a lot more Rails server capacity. + +## 3. File uploads and downloads + +File uploads and downloads may be slow either because the file is +large or because the user's connection is slow. Workhorse can handle +the slow part for Rails. This improves the efficiency of features such +as CI artifacts, package repositories, LFS objects, etc. + +## 4. Websocket proxying + +Features such as the web terminal require a long lived connection +between the user's web browser and a container inside GitLab that is +not directly accessible from the internet. Dedicating a Rails +application thread to proxying such a connection would cost much more +memory than it costs to have Workhorse look after it. + +## Quick facts (how does Workhorse work) + +- Workhorse can handle some requests without involving Rails at all: + for example, JavaScript files and CSS files are served straight + from disk. +- Workhorse can modify responses sent by Rails: for example if you use + `send_file` in Rails then gitlab-workhorse will open the file on + disk and send its contents as the response body to the client. +- Workhorse can take over requests after asking permission from Rails. + Example: handling `git clone`. +- Workhorse can modify requests before passing them to Rails. Example: + when handling a Git LFS upload Workhorse first asks permission from + Rails, then it stores the request body in a tempfile, then it sends + a modified request containing the tempfile path to Rails. +- Workhorse can manage long-lived WebSocket connections for Rails. + Example: handling the terminal websocket for environments. +- Workhorse does not connect to Postgres, only to Rails and (optionally) Redis. +- We assume that all requests that reach Workhorse pass through an + upstream proxy such as NGINX or Apache first. +- Workhorse does not accept HTTPS connections. +- Workhorse does not clean up idle client connections. +- We assume that all requests to Rails pass through Workhorse. + +For more information see ['A brief history of gitlab-workhorse'][brief-history-blog]. + +[thanos]: https://thanos-query.ops.gitlab.net/graph?g0.range_input=1h&g0.max_source_resolution=0s&g0.expr=sum(ruby_process_resident_memory_bytes%7Bapp%3D%22webservice%22%2Cenv%3D%22gprd%22%2Crelease%3D%22gitlab%22%7D)%20%2F%20sum(puma_max_threads%7Bapp%3D%22webservice%22%2Cenv%3D%22gprd%22%2Crelease%3D%22gitlab%22%7D)&g0.tab=1&g1.range_input=1h&g1.max_source_resolution=0s&g1.expr=sum(go_memstats_sys_bytes%7Bapp%3D%22webservice%22%2Cenv%3D%22gprd%22%2Crelease%3D%22gitlab%22%7D)%2Fsum(go_goroutines%7Bapp%3D%22webservice%22%2Cenv%3D%22gprd%22%2Crelease%3D%22gitlab%22%7D)&g1.tab=1 +[brief-history-blog]: https://about.gitlab.com/2016/04/12/a-brief-history-of-gitlab-workhorse/ diff --git a/doc/channel.md b/doc/channel.md index 31b57e9c4155d14eb210265c1a7a7482da1456cc..68553170775b97e4652e12ef87d3a3c440e5e214 100644 --- a/doc/channel.md +++ b/doc/channel.md @@ -1,193 +1 @@ -# Websocket channel support - -In some cases, GitLab can provide in-browser terminal access to an -environment (which is a running server or container, onto which a -project has been deployed), or even access to services running in CI -through a WebSocket. Workhorse manages the WebSocket upgrade and -long-lived connection to the websocket connection, which frees -up GitLab to process other requests. - -This document outlines the architecture of these connections. - -## Introduction to WebSockets - -A websocket is an "upgraded" HTTP/1.1 request. Their purpose is to -permit bidirectional communication between a client and a server. -**Websockets are not HTTP**. Clients can send messages (known as -frames) to the server at any time, and vice-versa. Client messages -are not necessarily requests, and server messages are not necessarily -responses. WebSocket URLs have schemes like `ws://` (unencrypted) or -`wss://` (TLS-secured). - -When requesting an upgrade to WebSocket, the browser sends a HTTP/1.1 -request that looks like this: - -``` -GET /path.ws HTTP/1.1 -Connection: upgrade -Upgrade: websocket -Sec-WebSocket-Protocol: terminal.gitlab.com -# More headers, including security measures -``` - -At this point, the connection is still HTTP, so this is a request and -the server can send a normal HTTP response, including `404 Not Found`, -`500 Internal Server Error`, etc. - -If the server decides to permit the upgrade, it will send a HTTP -`101 Switching Protocols` response. From this point, the connection -is no longer HTTP. It is a WebSocket and frames, not HTTP requests, -will flow over it. The connection will persist until the client or -server closes the connection. - -In addition to the subprotocol, individual websocket frames may -also specify a message type - examples include `BinaryMessage`, -`TextMessage`, `Ping`, `Pong` or `Close`. Only binary frames can -contain arbitrary data - other frames are expected to be valid -UTF-8 strings, in addition to any subprotocol expectations. - -## Browser to Workhorse - -Using the terminal as an example, GitLab serves a JavaScript terminal -emulator to the browser on a URL like -`https://gitlab.com/group/project/-/environments/1/terminal`. -This opens a websocket connection to, e.g., -`wss://gitlab.com/group/project/-/environments/1/terminal.ws`, -This endpoint doesn't exist in GitLab - only in Workhorse. - -When receiving the connection, Workhorse first checks that the -client is authorized to access the requested terminal. It does -this by performing a "preauthentication" request to GitLab. - -If the client has the appropriate permissions and the terminal -exists, GitLab responds with a successful response that includes -details of the terminal that the client should be connected to. -Otherwise, it returns an appropriate HTTP error response. - -Errors are passed back to the client as HTTP responses, but if -GitLab returns valid terminal details to Workhorse, it will -connect to the specified terminal, upgrade the browser to a -WebSocket, and proxy between the two connections for as long -as the browser's credentials are valid. Workhorse will also -send regular `PingMessage` control frames to the browser, to -keep intervening proxies from terminating the connection -while the browser is present. - -The browser must request an upgrade with a specific subprotocol: - -### `terminal.gitlab.com` - -This subprotocol considers `TextMessage` frames to be invalid. -Control frames, such as `PingMessage` or `CloseMessage`, have -their usual meanings. - -`BinaryMessage` frames sent from the browser to the server are -arbitrary text input. - -`BinaryMessage` frames sent from the server to the browser are -arbitrary text output. - -These frames are expected to contain ANSI text control codes -and may be in any encoding. - -### `base64.terminal.gitlab.com` - -This subprotocol considers `BinaryMessage` frames to be invalid. -Control frames, such as `PingMessage` or `CloseMessage`, have -their usual meanings. - -`TextMessage` frames sent from the browser to the server are -base64-encoded arbitrary text input (so the server must -base64-decode them before inputting them). - -`TextMessage` frames sent from the server to the browser are -base64-encoded arbitrary text output (so the browser must -base64-decode them before outputting them). - -In their base64-encoded form, these frames are expected to -contain ANSI terminal control codes, and may be in any encoding. - -## Workhorse to GitLab - -Using again the terminal as an example, before upgrading the browser, -Workhorse sends a normal HTTP request to GitLab on a URL like -`https://gitlab.com/group/project/environments/1/terminal.ws/authorize`. -This returns a JSON response containing details of where the -terminal can be found, and how to connect it. In particular, -the following details are returned in case of success: - -* WebSocket URL to **connect** to, e.g.: `wss://example.com/terminals/1.ws?tty=1` -* WebSocket subprotocols to support, e.g.: `["channel.k8s.io"]` -* Headers to send, e.g.: `Authorization: Token xxyyz..` -* Certificate authority to verify `wss` connections with (optional) - -Workhorse periodically re-checks this endpoint, and if it gets an -error response, or the details of the terminal change, it will -terminate the websocket session. - -## Workhorse to the WebSocket server - -In GitLab, environments or CI jobs may have a deployment service (e.g., -`KubernetesService`) associated with them. This service knows -where the terminals or the service for an environment may be found, and these -details are returned to Workhorse by GitLab. - -These URLs are *also* WebSocket URLs, and GitLab tells Workhorse -which subprotocols to speak over the connection, along with any -authentication details required by the remote end. - -Before upgrading the browser's connection to a websocket, -Workhorse opens a HTTP client connection, according to the -details given to it by Workhorse, and attempts to upgrade -that connection to a websocket. If it fails, an error -response is sent to the browser; otherwise, the browser is -also upgraded. - -Workhorse now has two websocket connections, albeit with -differing subprotocols. It decodes incoming frames from the -browser, re-encodes them to the the channel's subprotocol, and -sends them to the channel. Similarly, it decodes incoming -frames from the channel, re-encodes them to the browser's -subprotocol, and sends them to the browser. - -When either connection closes or enters an error state, -Workhorse detects the error and closes the other connection, -terminating the channel session. If the browser is the -connection that has disconnected, Workhorse will send an ANSI -`End of Transmission` control code (the `0x04` byte) to the -channel, encoded according to the appropriate subprotocol. -Workhorse will automatically reply to any websocket ping frame -sent by the channel, to avoid being disconnected. - -Currently, Workhorse only supports the following subprotocols. -Supporting new deployment services will require new subprotocols -to be supported: - -### `channel.k8s.io` - -Used by Kubernetes, this subprotocol defines a simple multiplexed -channel. - -Control frames have their usual meanings. `TextMessage` frames are -invalid. `BinaryMessage` frames represent I/O to a specific file -descriptor. - -The first byte of each `BinaryMessage` frame represents the file -descriptor (fd) number, as a `uint8` (so the value `0x00` corresponds -to fd 0, `STDIN`, while `0x01` corresponds to fd 1, `STDOUT`). - -The remaining bytes represent arbitrary data. For frames received -from the server, they are bytes that have been received from that -fd. For frames sent to the server, they are bytes that should be -written to that fd. - -### `base64.channel.k8s.io` - -Also used by Kubernetes, this subprotocol defines a similar multiplexed -channel to `channel.k8s.io`. The main differences are: - -* `TextMessage` frames are valid, rather than `BinaryMessage` frames. -* The first byte of each `TextMessage` frame represents the file - descriptor as a numeric UTF-8 character, so the character `U+0030`, - or "0", is fd 0, STDIN). -* The remaining bytes represent base64-encoded arbitrary data. +This file was moved to [`architecture/channel.md`](doc/architecture/channel.md). diff --git a/doc/development/tests.md b/doc/development/tests.md new file mode 100644 index 0000000000000000000000000000000000000000..33d69954b5afa9e81fb5125df5870ce905886aa0 --- /dev/null +++ b/doc/development/tests.md @@ -0,0 +1,17 @@ +# Testing your code + +Run the tests with: + +``` +make clean test +``` + +## Coverage / what to test + +Each feature in gitlab-workhorse should have an integration test that +verifies that the feature 'kicks in' on the right requests and leaves +other requests unaffected. It is better to also have package-level tests +for specific behavior but the high-level integration tests should have +the first priority during development. + +It is OK if a feature is only covered by integration tests. diff --git a/doc/operations/configuration.md b/doc/operations/configuration.md new file mode 100644 index 0000000000000000000000000000000000000000..70c594dbc09931e64f335f3e9897ecadd853f3bf --- /dev/null +++ b/doc/operations/configuration.md @@ -0,0 +1,217 @@ +# Workhorse configuration + +For historical reasons Workhorse uses both command line flags, a configuration file and environment variables. + +All new configuration options that get added to Workhorse should go into the configuration file. + +## CLI options + +``` + gitlab-workhorse [OPTIONS] + +Options: + -apiCiLongPollingDuration duration + Long polling duration for job requesting for runners (default 50s - enabled) (default 50ns) + -apiLimit uint + Number of API requests allowed at single time + -apiQueueDuration duration + Maximum queueing duration of requests (default 30s) + -apiQueueLimit uint + Number of API requests allowed to be queued + -authBackend string + Authentication/authorization backend (default "http://localhost:8080") + -authSocket string + Optional: Unix domain socket to dial authBackend at + -cableBackend string + Optional: ActionCable backend (default authBackend) + -cableSocket string + Optional: Unix domain socket to dial cableBackend at (default authSocket) + -config string + TOML file to load config from + -developmentMode + Allow the assets to be served from Rails app + -documentRoot string + Path to static files content (default "public") + -listenAddr string + Listen address for HTTP server (default "localhost:8181") + -listenNetwork string + Listen 'network' (tcp, tcp4, tcp6, unix) (default "tcp") + -listenUmask int + Umask for Unix socket + -logFile string + Log file location + -logFormat string + Log format to use defaults to text (text, json, structured, none) (default "text") + -pprofListenAddr string + pprof listening address, e.g. 'localhost:6060' + -prometheusListenAddr string + Prometheus listening address, e.g. 'localhost:9229' + -proxyHeadersTimeout duration + How long to wait for response headers when proxying the request (default 5m0s) + -secretPath string + File with secret key to authenticate with authBackend (default "./.gitlab_workhorse_secret") + -version + Print version and exit +``` + +The 'auth backend' refers to the GitLab Rails application. The name is +a holdover from when gitlab-workhorse only handled Git push/pull over +HTTP. + +Gitlab-workhorse can listen on either a TCP or a Unix domain socket. It +can also open a second listening TCP listening socket with the Go +[net/http/pprof profiler server](http://golang.org/pkg/net/http/pprof/). + +Gitlab-workhorse can listen on redis events (currently only builds/register +for runners). This requires you to pass a valid TOML config file via +`-config` flag. +For regular setups it only requires the following (replacing the string +with the actual socket) + +## Redis + +Gitlab-workhorse integrates with Redis to do long polling for CI build +requests. This is configured via two things: + +- Redis settings in the TOML config file +- The `-apiCiLongPollingDuration` command line flag to control polling + behavior for CI build requests + +It is OK to enable Redis in the config file but to leave CI polling +disabled; this just results in an idle Redis pubsub connection. The +opposite is not possible: CI long polling requires a correct Redis +configuration. + +Below we discuss the options for the `[redis]` section in the config +file. + +``` +[redis] +URL = "unix:///var/run/gitlab/redis.sock" +Password = "my_awesome_password" +Sentinel = [ "tcp://sentinel1:23456", "tcp://sentinel2:23456" ] +SentinelMaster = "mymaster" +``` + +- `URL` takes a string in the format `unix://path/to/redis.sock` or +`tcp://host:port`. +- `Password` is only required if your redis instance is password-protected +- `Sentinel` is used if you are using Sentinel. + *NOTE* that if both `Sentinel` and `URL` are given, only `Sentinel` will be used + +Optional fields are as follows: +``` +[redis] +DB = 0 +ReadTimeout = "1s" +KeepAlivePeriod = "5m" +MaxIdle = 1 +MaxActive = 1 +``` + +- `DB` is the Database to connect to. Defaults to `0` +- `ReadTimeout` is how long a redis read-command can take. Defaults to `1s` +- `KeepAlivePeriod` is how long the redis connection is to be kept alive without anything flowing through it. Defaults to `5m` +- `MaxIdle` is how many idle connections can be in the redis-pool at once. Defaults to 1 +- `MaxActive` is how many connections the pool can keep. Defaults to 1 + +## Relative URL support + +If you are mounting GitLab at a relative URL, e.g. +`example.com/gitlab`, then you should also use this relative URL in +the `authBackend` setting: + +``` +gitlab-workhorse -authBackend http://localhost:8080/gitlab +``` + +## Interaction of authBackend and authSocket + +The interaction between `authBackend` and `authSocket` can be a bit +confusing. It comes down to: if `authSocket` is set it overrides the +_host_ part of `authBackend` but not the relative path. + +In table form: + +|authBackend|authSocket|Workhorse connects to?|Rails relative URL| +|---|---|---|---| +|unset|unset|`localhost:8080`|`/`| +|`http://localhost:3000`|unset|`localhost:3000`|`/`| +|`http://localhost:3000/gitlab`|unset|`localhost:3000`|`/gitlab`| +|unset|`/path/to/socket`|`/path/to/socket`|`/`| +|`http://localhost:3000`|`/path/to/socket`|`/path/to/socket`|`/`| +|`http://localhost:3000/gitlab`|`/path/to/socket`|`/path/to/socket`|`/gitlab`| + +The same applies to `cableBackend` and `cableSocket`. + +## Error tracking + +GitLab-Workhorse supports remote error tracking with +[Sentry](https://sentry.io). To enable this feature set the +`GITLAB_WORKHORSE_SENTRY_DSN` environment variable. +You can also set the `GITLAB_WORKHORSE_SENTRY_ENVIRONMENT` environment variable to +use the Sentry environment functionality to separate staging, production and +development. + +Omnibus (`/etc/gitlab/gitlab.rb`): + +``` +gitlab_workhorse['env'] = { + 'GITLAB_WORKHORSE_SENTRY_DSN' => 'https://foobar' + 'GITLAB_WORKHORSE_SENTRY_ENVIRONMENT' => 'production' +} +``` + +Source installations (`/etc/default/gitlab`): + +``` +export GITLAB_WORKHORSE_SENTRY_DSN='https://foobar' +export GITLAB_WORKHORSE_SENTRY_ENVIRONMENT='production' +``` + +## Distributed Tracing + +Workhorse supports distributed tracing through [LabKit][] using [OpenTracing APIs](https://opentracing.io). + +By default, no tracing implementation is linked into the binary, but different OpenTracing providers can be linked in using [build tags][build-tags]/[build constraints][build-tags]. This can be done by setting the `BUILD_TAGS` make variable. + +For more details of the supported providers, see LabKit, but as an example, for Jaeger tracing support, include the tags: `BUILD_TAGS="tracer_static tracer_static_jaeger"`. + +```shell +make BUILD_TAGS="tracer_static tracer_static_jaeger" +``` + +Once Workhorse is compiled with an opentracing provider, the tracing configuration is configured via the `GITLAB_TRACING` environment variable. + +For example: + +```shell +GITLAB_TRACING=opentracing://jaeger ./gitlab-workhorse +``` + +## Continuous Profiling + +Workhorse supports continuous profiling through [LabKit][] using [Stackdriver Profiler](https://cloud.google.com/profiler). + +By default, the Stackdriver Profiler implementation is linked in the binary using [build tags][build-tags], though it's not +required and can be skipped. + +For example: + +```shell +make BUILD_TAGS="" +``` + +Once Workhorse is compiled with Continuous Profiling, the profiler configuration can be set via `GITLAB_CONTINUOUS_PROFILING` +environment variable. + +For example: + +```shell +GITLAB_CONTINUOUS_PROFILING="stackdriver?service=workhorse&service_version=1.0.1&project_id=test-123 ./gitlab-workhorse" +``` + +More information about see the [LabKit monitoring docs](https://gitlab.com/gitlab-org/labkit/-/blob/master/monitoring/doc.go). + +[LabKit]: https://gitlab.com/gitlab-org/labkit/ +[build-tags]: https://golang.org/pkg/go/build/#hdr-Build_Constraints diff --git a/doc/operations/install.md b/doc/operations/install.md new file mode 100644 index 0000000000000000000000000000000000000000..a16124cf397a3e7aee4d16892d0bf01286ed48c0 --- /dev/null +++ b/doc/operations/install.md @@ -0,0 +1,44 @@ +# Installation + +To install gitlab-workhorse you need [Go 1.13 or +newer](https://golang.org/dl) and [GNU +Make](https://www.gnu.org/software/make/). + +To install into `/usr/local/bin` run `make install`. + +``` +make install +``` + +To install into `/foo/bin` set the PREFIX variable. + +``` +make install PREFIX=/foo +``` + +On some operating systems, such as FreeBSD, you may have to use +`gmake` instead of `make`. + +*NOTE*: Some features depends on build tags, make sure to check +[Workhorse configuration](doc/operations/configuration.md) to enable them. + +## Run time dependencies + +### Exiftool + +Workhorse uses [exiftool](https://www.sno.phy.queensu.ca/~phil/exiftool/) for +removing EXIF data (which may contain sensitive information) from uploaded +images. If you installed GitLab: + +- Using the Omnibus package, you're all set. + *NOTE* that if you are using CentOS Minimal, you may need to install `perl` + package: `yum install perl` +- From source, make sure `exiftool` is installed: + + ```sh + # Debian/Ubuntu + sudo apt-get install libimage-exiftool-perl + + # RHEL/CentOS + sudo yum install perl-Image-ExifTool + ```