├── .gitignore ├── .rspec ├── Gemfile ├── Rakefile ├── cache.conf ├── spec ├── spec_helper.rb └── the_spec.rb ├── launch.bash ├── Dockerfile ├── Jenkinsfile ├── Gemfile.lock ├── Makefile ├── site.conf └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'serverspec', '~>2.0' 4 | gem 'rake' 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |t| 5 | t.pattern = 'spec/*_spec.rb' 6 | end 7 | -------------------------------------------------------------------------------- /cache.conf: -------------------------------------------------------------------------------- 1 | # nginx global configuration that gets inserted under 'http' section 2 | 3 | proxy_cache_path /tmp/cache keys_zone=egg:10m max_size=256m; 4 | proxy_cache off; 5 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'serverspec' 2 | 3 | PORT=8081 4 | URL="http://localhost:#{PORT}" 5 | 6 | BACKEND="http://localhost:8082" 7 | 8 | set :backend, :exec 9 | set :disable_sudo, true -------------------------------------------------------------------------------- /launch.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # patch the config file to set the backend address 4 | CFG=/etc/nginx/sites-enabled/default 5 | cat $CFG | sed -e "s#@@BACKEND@@#${TARGET}#g" > /tmp/default 6 | mv /tmp/default $CFG 7 | 8 | exec nginx -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | MAINTAINER Jenkins Infra team 3 | 4 | # Where to proxy the request to? 5 | ENV TARGET http://backend:8080 6 | 7 | RUN apt-get update \ 8 | && apt-get install -y nginx \ 9 | && rm -rf /var/lib/apt/lists/* \ 10 | && echo "\ndaemon off;" >> /etc/nginx/nginx.conf 11 | 12 | ADD site.conf /etc/nginx/sites-enabled/default 13 | ADD cache.conf /etc/nginx/conf.d/cache.conf 14 | ADD launch.bash / 15 | 16 | WORKDIR /etc/nginx 17 | VOLUME ["/cache"] 18 | CMD ["/launch.bash"] 19 | EXPOSE 8080 -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | def imageName = 'jenkinsciinfra/confluence-cache' 4 | 5 | /* Only keep the 10 most recent builds. */ 6 | properties([[$class: 'BuildDiscarderProperty', 7 | strategy: [$class: 'LogRotator', numToKeepStr: '10']]]) 8 | 9 | node('docker') { 10 | checkout scm 11 | 12 | /* Using this hack right now to grab the appropriate abbreviated SHA1 of 13 | * our current build's commit. We must do this because right now I cannot 14 | * refer to `env.GIT_COMMIT` in Pipeline scripts 15 | */ 16 | sh 'git rev-parse HEAD > GIT_COMMIT' 17 | shortCommit = readFile('GIT_COMMIT').take(6) 18 | def imageTag = "build${shortCommit}" 19 | 20 | stage 'Build Container' 21 | def whale = docker.build("${imageName}:${imageTag}", '--no-cache --rm .') 22 | 23 | stage 'Deploy' 24 | whale.push() 25 | } 26 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | diff-lcs (1.2.5) 5 | multi_json (1.11.0) 6 | net-scp (1.2.1) 7 | net-ssh (>= 2.6.5) 8 | net-ssh (2.9.2) 9 | rake (10.4.2) 10 | rspec (3.2.0) 11 | rspec-core (~> 3.2.0) 12 | rspec-expectations (~> 3.2.0) 13 | rspec-mocks (~> 3.2.0) 14 | rspec-core (3.2.2) 15 | rspec-support (~> 3.2.0) 16 | rspec-expectations (3.2.0) 17 | diff-lcs (>= 1.2.0, < 2.0) 18 | rspec-support (~> 3.2.0) 19 | rspec-its (1.2.0) 20 | rspec-core (>= 3.0.0) 21 | rspec-expectations (>= 3.0.0) 22 | rspec-mocks (3.2.1) 23 | diff-lcs (>= 1.2.0, < 2.0) 24 | rspec-support (~> 3.2.0) 25 | rspec-support (3.2.2) 26 | serverspec (2.13.0) 27 | multi_json 28 | rspec (~> 3.0) 29 | rspec-its 30 | specinfra (~> 2.25) 31 | specinfra (2.27.0) 32 | net-scp 33 | net-ssh 34 | 35 | PLATFORMS 36 | ruby 37 | 38 | DEPENDENCIES 39 | rake 40 | serverspec (~> 2.0) 41 | -------------------------------------------------------------------------------- /spec/the_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def cache(path) 4 | command("curl --silent --fail #{URL}#{path}") 5 | end 6 | 7 | def backend(path) 8 | command("curl --silent --fail #{BACKEND}#{path}") 9 | end 10 | 11 | describe "Confluence Cache" do 12 | it "should be listening on #{PORT}" do 13 | expect(port(PORT)).to be_listening 14 | end 15 | 16 | it "should find pre-generated cache in /cache without hitting backend" do 17 | expect(cache( "/display/hello").stdout).to match /Hit confluence-cache/ 18 | expect(backend("/display/hello").exit_status).not_to eq 0 19 | end 20 | 21 | it "should pass through most of the access" do 22 | File.write("build/wwwroot/sanfrancisco.html", "Golden Gate Bridge!") 23 | expect(cache("/sanfrancisco.html").stdout).to match /Golden Gate Bridge!/ 24 | File.write("build/wwwroot/sanfrancisco.html", "Bay Bridge!") 25 | expect(cache("/sanfrancisco.html").stdout).to match /Bay Bridge!/ 26 | end 27 | 28 | it "should cache /s/... files" do 29 | File.write("build/wwwroot/s/tokyo.html", "Asakusa!") 30 | expect(cache("/s/tokyo.html").stdout).to match /Asakusa!/ 31 | File.write("build/wwwroot/s/tokyo.html", "Shinjuku!") 32 | expect(cache("/s/tokyo.html").stdout).to match /Asakusa!/ # should still be old stale one 33 | end 34 | end 35 | 36 | 37 | 38 | #describe command("curl #{URL}/sanfrancisco.html") do 39 | # its(:stdout) { should match 'Golden Gate Bridge!' } 40 | #end 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGENAME=jenkinsciinfra/confluence-cache 2 | TAG=$(shell date '+%Y%m%d_%H%M%S') 3 | 4 | image : 5 | docker build -t ${IMAGENAME} . 6 | 7 | run : 8 | docker run -P --rm -i -t ${IMAGENAME} 9 | 10 | tag : 11 | docker tag ${IMAGENAME} ${IMAGENAME}:${TAG} 12 | 13 | push : 14 | docker push ${IMAGENAME} 15 | 16 | 17 | # run two containers side by side for testing 18 | # access http://localhost:8081/ to go through nginx 19 | # "http://localhost:8081/display/hello" should be cached 20 | test-setup: image build/cache 21 | # start the fresh backend 22 | @docker kill backend > /dev/null 2>&1 || true 23 | @docker rm backend > /dev/null 2>&1 || true 24 | @docker kill cc > /dev/null 2>&1 || true 25 | @docker rm cc > /dev/null 2>&1 || true 26 | mkdir -p build/wwwroot/s || true 27 | docker run -d -p 8082:80 --name=backend -v `pwd`/build/wwwroot:/usr/share/nginx/html:ro nginx 28 | 29 | # start the cache 30 | @docker kill cc > /dev/null 2>&1 || true 31 | docker run -d -p 8081:8080 -t -i --name=cc -v `pwd`/build/cache:/cache --link backend:backend -e TARGET=http://backend jenkinsciinfra/confluence-cache 32 | 33 | test-run: 34 | bundle exec rake spec 35 | 36 | test-teardown: 37 | docker kill backend 38 | docker rm backend 39 | docker kill cc 40 | docker rm cc 41 | 42 | test: test-setup test-run test-teardown 43 | 44 | build/cache: 45 | mkdir -p build/cache/display 46 | echo "Hit confluence-cache" > build/cache/display/hello.html 47 | 48 | clean: 49 | rm -rf cache -------------------------------------------------------------------------------- /site.conf: -------------------------------------------------------------------------------- 1 | # Nginx configuration file 2 | # attempt to serve from the cache, and otherwise deliver to the backend 3 | server { 4 | listen 0.0.0.0:8080; 5 | 6 | root /cache; 7 | 8 | location /display/ { 9 | # hack inspired by http://wiki.nginx.org/IfIsEvil 10 | # the goal here is to try a static file only when there's no query parameter. 11 | # it's like making try_files contingent on the $args != "" condition, except 12 | # that's illegal. 13 | # 14 | # so the trick here is to use a bogus status code. we use the rewrite module 15 | # and if there's any query parameter, we initiate the error handling mode with 16 | # status code 418. which is forwarding it to Confluence 17 | 18 | error_page 418 = @confluence; 19 | recursive_error_pages on; 20 | if ($args != "") { 21 | return 418; 22 | } 23 | 24 | if ($http_cookie ~* "bypasscache" ) { 25 | # if the magic cookie is present, bypass the cache 26 | return 418; 27 | } 28 | 29 | # cached files are all UTF-8 HTMLs 30 | default_type text/html; 31 | charset UTF-8; 32 | 33 | try_files $uri.html @confluence; 34 | } 35 | 36 | location /s/ { 37 | # static contents should be cached in nginx before passed to Confluence 38 | proxy_cache egg; 39 | proxy_ignore_headers Cache-Control; 40 | proxy_cache_valid any 1m; 41 | proxy_cache_valid 200 24h; 42 | proxy_pass @@BACKEND@@; 43 | } 44 | 45 | location / { 46 | # everything else should go straight to Confluence 47 | proxy_pass @@BACKEND@@; 48 | } 49 | 50 | location @confluence { 51 | proxy_pass @@BACKEND@@; 52 | } 53 | 54 | access_log off; 55 | 56 | client_max_body_size 10M; 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Confluence Static Cache Container for jenkins-ci.org 2 | [![Build Status](http://ci.jenkins-ci.org/buildStatus/icon?job=infra_confluence-cache)](http://ci.jenkins-ci.org/view/Infrastructure/job/infra_confluence-cache/) 3 | 4 | This container defines caching HTTP reverse proxy that sits right in front of wiki.jenkins-ci.org. 5 | 6 | Its caching strategy is two folds: 7 | 8 | * Confluence puts static resources in `/s/...`. Cache these resources so that we can cut down 9 | on HTTP request handling threads in Confluence, which in turn prevents database connection 10 | starvation during peak load. 11 | * Read access to Wiki pages are served from statically pre-generated HTML files in conjunction 12 | with [Confluence static cache generator plugin](https://github.com/kohsuke/confluence-static-cache). 13 | This drastically improves the performance of read access to Wiki. 14 | 15 | ## How to run the container 16 | The container execpts `/cache` to point to the cache files generated by Confluence static cache generator plugin. 17 | Do this by `-v someHostDir:/cache`. 18 | 19 | To specify the URL of Confluence, use the `TARGET` environment variable, such as `-e TARGET=http://backend:1234` 20 | 21 | If your backend runs in another container, the easiest way to do it is by linking container, such as 22 | `--link=myConfluenceContainerName:backend -e TARGET=http://backend:8080` 23 | 24 | ## How to develop the container 25 | Run `make image` to build the container. Run `make test` to build the container and then run tests 26 | against it. Tests start another nginx container as the backend to verify the behaviour of the cache. 27 | 28 | If you are developing tests, you can run `make test-setup` to run the test setup, then edit 29 | test source code and run `make test` to quickly run tests. When done, `make test-teardown` to 30 | shut down . 31 | --------------------------------------------------------------------------------