├── .github └── dependabot.yml ├── .gitignore ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── NOTICE ├── README.md ├── config.ru ├── lib ├── app.rb ├── client.rb └── redis_tls.rb ├── manifest.yml ├── spec ├── app_spec.rb ├── redis_getter.rb ├── spec_helper.rb └── support │ ├── ca.crt │ ├── redis.crt │ ├── redis.key │ └── redis_server.rb └── vendor └── cache ├── cf-app-utils-0.6.gem ├── childprocess-4.1.0.gem ├── connection_pool-2.4.1.gem ├── diff-lcs-1.5.0.gem ├── logger-1.5.3.gem ├── mustermann-3.0.0.gem ├── openssl-3.1.0.gem ├── rack-2.2.6.4.gem ├── rack-protection-3.0.6.gem ├── rack-test-2.0.2.gem ├── redis-5.2.0.gem ├── redis-client-0.22.0.gem ├── rspec-3.12.0.gem ├── rspec-core-3.12.1.gem ├── rspec-expectations-3.12.2.gem ├── rspec-mocks-3.12.3.gem ├── rspec-support-3.12.0.gem ├── ruby2_keywords-0.0.5.gem ├── sinatra-3.0.6.gem ├── tilt-2.1.0.gem ├── tttls1.3-0.2.19.gem └── webrick-1.8.1.gem /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rspec 2 | dump.rdb 3 | redis.log 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.4 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.6.0 5 | 6 | cache: bundler 7 | 8 | script: bundle exec rspec 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cf-app-utils' 4 | gem 'sinatra', "3.0.6" 5 | gem 'redis' 6 | gem 'tttls1.3', '>=0.2.15' 7 | gem "webrick", "~> 1.8" 8 | 9 | group :test do 10 | gem 'rack-test' 11 | gem 'rspec' 12 | gem 'childprocess' 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | cf-app-utils (0.6) 5 | childprocess (4.1.0) 6 | connection_pool (2.4.1) 7 | diff-lcs (1.5.0) 8 | logger (1.5.3) 9 | mustermann (3.0.0) 10 | ruby2_keywords (~> 0.0.1) 11 | openssl (3.1.0) 12 | rack (2.2.6.4) 13 | rack-protection (3.0.6) 14 | rack 15 | rack-test (2.0.2) 16 | rack (>= 1.3) 17 | redis (5.2.0) 18 | redis-client (>= 0.22.0) 19 | redis-client (0.22.0) 20 | connection_pool 21 | rspec (3.12.0) 22 | rspec-core (~> 3.12.0) 23 | rspec-expectations (~> 3.12.0) 24 | rspec-mocks (~> 3.12.0) 25 | rspec-core (3.12.1) 26 | rspec-support (~> 3.12.0) 27 | rspec-expectations (3.12.2) 28 | diff-lcs (>= 1.2.0, < 2.0) 29 | rspec-support (~> 3.12.0) 30 | rspec-mocks (3.12.3) 31 | diff-lcs (>= 1.2.0, < 2.0) 32 | rspec-support (~> 3.12.0) 33 | rspec-support (3.12.0) 34 | ruby2_keywords (0.0.5) 35 | sinatra (3.0.6) 36 | mustermann (~> 3.0) 37 | rack (~> 2.2, >= 2.2.4) 38 | rack-protection (= 3.0.6) 39 | tilt (~> 2.0) 40 | tilt (2.1.0) 41 | tttls1.3 (0.2.19) 42 | logger 43 | openssl 44 | webrick (1.8.1) 45 | 46 | PLATFORMS 47 | ruby 48 | 49 | DEPENDENCIES 50 | cf-app-utils 51 | childprocess 52 | rack-test 53 | redis 54 | rspec 55 | sinatra (= 3.0.6) 56 | tttls1.3 (>= 0.2.15) 57 | webrick (~> 1.8) 58 | 59 | BUNDLED WITH 60 | 2.5.3 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | cf-redis-example-app 2 | 3 | Copyright (c) 2014-2017 Pivotal Software, Inc. All Rights Reserved. 4 | 5 | This product is licensed to you under the Apache License, Version 2.0 (the "License"). 6 | You may not use this product except in compliance with the License. 7 | 8 | This product may include a number of subcomponents with separate copyright notices 9 | and license terms. Your use of these subcomponents is subject to the terms and 10 | conditions of the subcomponent's license, as noted in the LICENSE file. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CF Redis Example App [![Build Status](https://travis-ci.org/pivotal-cf/cf-redis-example-app.svg)](https://travis-ci.org/pivotal-cf/cf-redis-example-app) 2 | 3 | This app is an example of how you can consume a Cloud Foundry service within an app. 4 | 5 | It allows you to set, get and delete Redis key/value pairs using RESTful endpoints. 6 | 7 | ### Getting Started 8 | 9 | #### Using Pivotal Redis service 10 | 11 | Install the app by pushing it to your Cloud Foundry and binding with the Pivotal Redis service 12 | 13 | Example: 14 | 15 | $ git clone git@github.com:pivotal-cf/cf-redis-example-app.git 16 | $ cd cf-redis-example-app 17 | $ cf push redis-example-app --no-start 18 | $ cf create-service p-redis dedicated-vm redis 19 | $ cf bind-service redis-example-app redis 20 | $ cf start redis-example-app 21 | 22 | #### Using Redis-Labs CUPS 23 | 24 | Install the app by pushing it to your Cloud Foundry and binding with a Redis-Labs [CUPS](https://docs.cloudfoundry.org/devguide/services/user-provided.html) 25 | 26 | You will need to sign up for [redis-as-a-service](https://redislabs.com/) and then provide the credentials of your redis instance when running cf cups. 27 | 28 | Example: 29 | 30 | $ git clone git@github.com:pivotal-cf/cf-redis-example-app.git 31 | $ cd cf-redis-example-app 32 | $ cf push redis-example-app --no-start 33 | $ cf cups redis -p '{"host":"redis_host","password":"redis_password","port":"redis_port"}' 34 | $ cf bind-service redis-example-app redis 35 | $ cf start redis-example-app 36 | 37 | ### Endpoints 38 | 39 | #### PUT /:key 40 | 41 | Sets the value stored in Redis at the specified key to a value posted in the 'data' field. Example: 42 | 43 | $ export APP=redis-example-app.my-cloud-foundry.com 44 | $ curl -X PUT $APP/foo -d 'data=bar' 45 | success 46 | 47 | 48 | #### GET /:key 49 | 50 | Returns the value stored in Redis at the key specified by the path. Example: 51 | 52 | $ curl -X GET $APP/foo 53 | bar 54 | 55 | #### DELETE /:key 56 | 57 | Deletes a Redis key spcified by the path. Example: 58 | 59 | $ curl -X DELETE $APP/foo 60 | success 61 | 62 | #### GET /config/:item 63 | 64 | Returns the Redis configuration value at the key specified by the path. Example: 65 | 66 | $ curl -X GET $APP/config/max_clients 67 | 100 68 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require './lib/app' 2 | run Sinatra::Application 3 | -------------------------------------------------------------------------------- /lib/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'redis' 3 | require 'cf-app-utils' 4 | require_relative 'redis_tls' 5 | require_relative 'client' 6 | 7 | before do 8 | unless redis_credentials 9 | halt(500, %{ 10 | You must bind a Redis service instance to this application. 11 | 12 | You can run the following commands to create an instance and bind to it: 13 | 14 | $ cf create-service p-redis development redis-instance 15 | $ cf bind-service redis-instance}) 16 | end 17 | end 18 | 19 | put '/:key' do 20 | data = params[:data] 21 | if data 22 | redis_client.set(params[:key], data) 23 | status 201 24 | body 'success' 25 | else 26 | status 400 27 | body 'data field missing' 28 | end 29 | end 30 | 31 | get '/:key' do 32 | value = redis_client.get(params[:key]) 33 | if value 34 | status 200 35 | body value 36 | else 37 | status 404 38 | body 'key not present' 39 | end 40 | end 41 | 42 | get '/status/health' do 43 | status 200 44 | body 'app is running' 45 | end 46 | 47 | get '/config/:item' do 48 | unless params[:item] 49 | status 400 50 | body 'USAGE: GET /config/:item' 51 | return 52 | end 53 | 54 | value = redis_client.config('get', params[:item]) 55 | if value.length < 2 56 | status 404 57 | body "config item #{params[:item]} not found" 58 | return 59 | end 60 | 61 | status 200 62 | body value[1] 63 | end 64 | 65 | delete '/:key' do 66 | result = redis_client.del(params[:key]) 67 | if result > 0 68 | status 410 69 | body 'success' 70 | else 71 | status 404 72 | body 'key not present' 73 | end 74 | end 75 | 76 | get '/tls/v1/:key' do 77 | get_key_with_tls_version(params[:key], 'TLSv1') 78 | end 79 | 80 | get '/tls/v1.1/:key' do 81 | get_key_with_tls_version(params[:key], 'TLSv1_1') 82 | end 83 | 84 | get '/tls/v1.2/:key' do 85 | get_key_with_tls_version(params[:key], 'TLSv1_2') 86 | end 87 | 88 | get '/tls/v1.3/:key' do 89 | get_key_with_tls_version(params[:key], 'TLSv1_3') 90 | end 91 | 92 | def get_key_with_tls_version(key, version) 93 | begin 94 | value = redis_client_tls(version).get(key) 95 | if value 96 | status 200 97 | body value 98 | else 99 | status 404 100 | body 'key not present' 101 | end 102 | rescue StandardError => e 103 | status 418 104 | body 'protocol not supported: '+e.message 105 | end 106 | end 107 | 108 | def redis_client_tls(version='TLSv1') 109 | if redis_credentials.key?('sentinels') 110 | @client ||= ClientRedis.tls_using_sentinel(redis_credentials, version) 111 | else 112 | if version == 'TLSv1_3' 113 | return @client_tls_13 ||= RedisTLS13.new( 114 | host: redis_credentials.fetch('host'), 115 | port: redis_credentials.fetch('tls_port'), 116 | password: redis_credentials.fetch('password') 117 | ) 118 | end 119 | @client ||= ClientRedis.tls(redis_credentials, version) 120 | end 121 | end 122 | 123 | def redis_client 124 | tls_enabled = ENV['tls_enabled'] || false 125 | 126 | if redis_credentials.key?('sentinels') 127 | if tls_enabled 128 | @client ||= ClientRedis.tls_using_sentinel(redis_credentials, version='TLSv1_2') 129 | else 130 | @client ||= ClientRedis.using_sentinel(redis_credentials) 131 | end 132 | elsif tls_enabled 133 | @client ||= ClientRedis.tls(redis_credentials) 134 | else 135 | @client ||= ClientRedis.default(redis_credentials) 136 | end 137 | end 138 | 139 | def redis_credentials 140 | service_name = ENV['service_name'] || "redis" 141 | 142 | if ENV['VCAP_SERVICES'] 143 | all_pivotal_redis_credentials = CF::App::Credentials.find_all_by_all_service_tags(['redis', 'pivotal']) 144 | if all_pivotal_redis_credentials && all_pivotal_redis_credentials.first 145 | all_pivotal_redis_credentials && all_pivotal_redis_credentials.first 146 | else 147 | redis_service_credentials = CF::App::Credentials.find_by_service_name(service_name) 148 | redis_service_credentials 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /lib/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'redis' 3 | require_relative 'redis_tls' 4 | 5 | class ClientRedis 6 | def initialize 7 | end 8 | 9 | def self.default(credentials) 10 | Redis.new( 11 | host: credentials.fetch('host'), 12 | port: credentials.fetch('port'), 13 | password: credentials.fetch('password'), 14 | timeout: 30 15 | ) 16 | end 17 | 18 | def self.using_sentinel(credentials) 19 | Redis.new( 20 | name: credentials.fetch('master_name'), 21 | password: credentials.fetch('password'), 22 | sentinel_password: credentials.fetch('sentinel_password'), 23 | sentinels: credentials.fetch('sentinels').map { | sentinel | { host: sentinel["host"], port: sentinel["port"] } }, 24 | timeout: 30 25 | ) 26 | end 27 | 28 | def self.tls_using_sentinel(credentials, version='') 29 | if version.include?("TLSv1_3") 30 | master_redis = SentinelTLS13.new( 31 | host: credentials.fetch('sentinels').first['host'], 32 | port: credentials.fetch('sentinels').first['tls_port'], 33 | sentinel_password: credentials.fetch("sentinel_password"), 34 | master_name: credentials.fetch('master_name') 35 | ).get_redis_instance 36 | return @client_tls_13 ||= RedisTLS13.new( 37 | host: master_redis[0], 38 | port: master_redis[1], 39 | password: credentials.fetch('password') 40 | ) 41 | else 42 | ssl_params = { 43 | verify_mode: OpenSSL::SSL::VERIFY_NONE, 44 | ssl_version: version, 45 | } 46 | if version.empty? 47 | ssl_params = { 48 | verify_mode: OpenSSL::SSL::VERIFY_NONE 49 | } 50 | end 51 | Redis.new( 52 | name: credentials.fetch('master_name'), 53 | password: credentials.fetch('password'), 54 | sentinel_password: credentials.fetch('sentinel_password'), 55 | sentinels: credentials.fetch('sentinels').map { | sentinel | { host: sentinel["host"], port: sentinel["tls_port"] } }, 56 | ssl: true, 57 | ssl_params: ssl_params, 58 | timeout: 30 59 | ) 60 | end 61 | end 62 | 63 | def self.tls(credentials, version='') 64 | ssl_params = { 65 | verify_mode: OpenSSL::SSL::VERIFY_NONE, 66 | ssl_version: version, 67 | } 68 | 69 | if version.empty? 70 | ssl_params = { 71 | verify_mode: OpenSSL::SSL::VERIFY_NONE 72 | } 73 | end 74 | Redis.new( 75 | host: credentials.fetch('host'), 76 | port: credentials.fetch('tls_port'), 77 | password: credentials.fetch('password'), 78 | ssl: true, 79 | ssl_params: ssl_params, 80 | timeout: 30 81 | ) 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/redis_tls.rb: -------------------------------------------------------------------------------- 1 | require 'tttls1.3' 2 | 3 | class RedisTLS13 4 | attr_reader :host, :port, :password 5 | 6 | def initialize(options) 7 | @host = options.fetch(:host) 8 | @port = options.fetch(:port) 9 | @password = options.fetch(:password) 10 | end 11 | 12 | def get(key) 13 | # no connection pooling for this example 14 | sock = TCPSocket.new(@host, @port) 15 | client = TTTLS13::Client.new(sock, @host) 16 | client.connect 17 | 18 | client.write("AUTH #{password}\r\n") 19 | 20 | resp = client.read 21 | if resp.nil? || !resp.strip.casecmp('+OK').zero? 22 | raise "Invalid password" 23 | end 24 | 25 | client.write("GET #{key}\r\n") 26 | resp = client.read 27 | 28 | 29 | client.close 30 | parse_response(resp) 31 | end 32 | 33 | def parse_response(resp) 34 | if resp.strip.casecmp('$-1').zero? 35 | return nil 36 | end 37 | 38 | # e.g resp: $7"\r\n"a value"\r\n 39 | arr = resp.split("\r\n") 40 | arr[1] 41 | end 42 | end 43 | 44 | class SentinelTLS13 45 | attr_reader :host, :port, :sentinel_password, :master_name 46 | 47 | def initialize(options) 48 | @host = options.fetch(:host) 49 | @port = options.fetch(:port) 50 | @password = options.fetch(:sentinel_password) 51 | @master_name = options.fetch(:master_name) 52 | end 53 | 54 | def get_redis_instance 55 | # no connection pooling for this example 56 | sock = TCPSocket.new(@host, @port) 57 | client = TTTLS13::Client.new(sock, @host) 58 | client.connect 59 | 60 | client.write("AUTH #{@password}\r\n") 61 | resp = client.read 62 | if resp.nil? || !resp.strip.casecmp('+OK').zero? 63 | raise "Invalid password" 64 | end 65 | 66 | client.write("SENTINEL get-master-addr-by-name #{master_name}\n") 67 | resp = client.read 68 | client.close 69 | 70 | parse_sentinel_response(resp) 71 | end 72 | 73 | def parse_sentinel_response(response) 74 | lines = response.split("\r\n") 75 | elements = lines[0][1..-1].to_i 76 | data = [] 77 | i = 1 78 | elements.times do 79 | length = lines[i].split("$")[1].to_i 80 | data << lines[i + 1][0...length] 81 | i += 2 82 | end 83 | data 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: redis-example-app 4 | memory: 256M 5 | instances: 1 6 | path: . 7 | buildpacks: [ruby_buildpack] 8 | -------------------------------------------------------------------------------- /spec/app_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'json' 3 | 4 | require 'app' 5 | require_relative 'redis_getter' 6 | 7 | describe 'app' do 8 | 9 | def app 10 | Sinatra::Application.new 11 | end 12 | 13 | def without_vcap_services 14 | vcap_services = ENV.delete("VCAP_SERVICES") 15 | yield 16 | ENV.store("VCAP_SERVICES", vcap_services) 17 | end 18 | 19 | let(:path) { "/#{key}" } 20 | let(:key) { 'foo' } 21 | 22 | context 'when there is no redis instance bound' do 23 | it 'returns 500 INTERNAL SERVICE ERROR' do 24 | without_vcap_services do 25 | get path 26 | expect(last_response.status).to eq(500) 27 | end 28 | end 29 | 30 | it 'returns binding instructions' do 31 | without_vcap_services do 32 | get path 33 | expect(last_response.body).to match('You must bind a Redis service instance to this application.') 34 | expect(last_response.body).to match('You can run the following commands to create an instance and bind to it:') 35 | expect(last_response.body).to match('\$ cf create-service p-redis development redis-instance') 36 | expect(last_response.body).to match('\$ cf bind-service redis-instance') 37 | end 38 | end 39 | end 40 | 41 | context 'when there is a redis instance bound without tags' do 42 | before do 43 | ENV["VCAP_SERVICES"] ||= { 44 | 'redis' => { 45 | 'name' => 'redis', 46 | 'label' => 'redis', 47 | 'tags' => [], 48 | 'plan' => 'default', 49 | 'credentials' => { 50 | 'password' => REDIS.password, 51 | 'host' => REDIS.host, 52 | 'port' => REDIS.port, 53 | 'tls_port' => REDIS.tls_port 54 | } 55 | } 56 | }.to_json 57 | end 58 | 59 | describe 'PUT /:key' do 60 | context 'with data' do 61 | let(:payload) { { data: 'bar' } } 62 | 63 | it 'returns 201 CREATED' do 64 | put path, payload 65 | expect(last_response.status).to eq(201) 66 | end 67 | 68 | it 'returns "success"' do 69 | put path, payload 70 | expect(last_response.body).to match('success') 71 | end 72 | end 73 | end 74 | end 75 | 76 | context 'when there is a redis instance bound' do 77 | before do 78 | ENV["VCAP_SERVICES"] ||= { 79 | 'redis' => { 80 | 'name' => 'redis', 81 | 'label' => 'redis', 82 | 'tags' => [ 83 | 'redis', 84 | 'pivotal' 85 | ], 86 | 'plan' => 'default', 87 | 'credentials' => { 88 | 'password' => REDIS.password, 89 | 'host' => REDIS.host, 90 | 'port' => REDIS.port, 91 | 'tls_port' => REDIS.tls_port 92 | } 93 | } 94 | }.to_json 95 | 96 | ENV['SSL_CERT_FILE'] = "#{__dir__}/support/ca.crt" 97 | end 98 | 99 | describe 'PUT /:key' do 100 | context 'with data' do 101 | let(:payload) { { data: 'bar' } } 102 | 103 | it 'returns 201 CREATED' do 104 | put path, payload 105 | expect(last_response.status).to eq(201) 106 | end 107 | 108 | it 'returns "success"' do 109 | put path, payload 110 | expect(last_response.body).to match('success') 111 | end 112 | end 113 | 114 | context 'without data' do 115 | let(:payload) { nil } 116 | 117 | it 'returns 400 BAD REQUEST' do 118 | put path, payload 119 | expect(last_response.status).to eq(400) 120 | end 121 | 122 | it 'tells the user to send data' do 123 | put path, payload 124 | expect(last_response.body).to match('data field missing') 125 | end 126 | end 127 | end 128 | 129 | describe 'GET /:key' do 130 | it_behaves_like 'redis get endpoint' 131 | end 132 | 133 | describe '/tls/v1.2/:key' do 134 | let(:path) { "/tls/v1.2/#{key}" } 135 | 136 | it_behaves_like 'redis get endpoint' 137 | end 138 | 139 | describe '/tls/v1.3/:key' do 140 | let(:path) { "/tls/v1.3/#{key}" } 141 | 142 | it_behaves_like 'redis get endpoint' 143 | end 144 | 145 | describe 'GET /config/:item' do 146 | context 'when the configuration item exists' do 147 | it 'returns 200' do 148 | get '/config/maxclients' 149 | expect(last_response.status).to eq 200 150 | end 151 | 152 | it 'returns the config value' do 153 | get '/config/maxclients' 154 | expect(last_response.body).to match /^\d+$/ 155 | end 156 | end 157 | 158 | context 'when the configuration item does not exist' do 159 | it 'returns 404' do 160 | get '/config/treeplatypus' 161 | expect(last_response.status).to eq 404 162 | end 163 | end 164 | end 165 | 166 | describe 'DELETE /:key' do 167 | context 'when the key does not exist' do 168 | let(:key) { 'nonexistant' } 169 | 170 | it 'returns 404 NOT FOUND' do 171 | delete path 172 | expect(last_response.status).to eq(404) 173 | end 174 | 175 | it 'reports that the key is not present' do 176 | delete path 177 | expect(last_response.body).to match('key not present') 178 | end 179 | end 180 | 181 | context 'when the key exists' do 182 | let(:value) { 'a value' } 183 | let(:payload) { { data: value } } 184 | 185 | before do 186 | put path, payload 187 | end 188 | 189 | it 'returns 410 ' do 190 | delete path 191 | expect(last_response.status).to eq(410) 192 | end 193 | 194 | it 'returns success' do 195 | delete path 196 | expect(last_response.body).to eq('success') 197 | end 198 | 199 | it 'deletes the key' do 200 | delete path 201 | get path 202 | expect(last_response.status).to eq(404) 203 | end 204 | end 205 | end 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /spec/redis_getter.rb: -------------------------------------------------------------------------------- 1 | require 'rack/test' 2 | 3 | RSpec.shared_examples "redis get endpoint" do 4 | context 'when the key does not exist' do 5 | let(:key) { 'nonexistant' } 6 | 7 | it 'returns 404 NOT FOUND' do 8 | get path 9 | expect(last_response.status).to eq(404) 10 | end 11 | 12 | it 'reports that the key is not present' do 13 | get path 14 | expect(last_response.body).to match('key not present') 15 | end 16 | end 17 | 18 | context 'when the key exists' do 19 | let(:value) { 'a value' } 20 | let(:payload) { { data: value } } 21 | 22 | before do 23 | put path, payload 24 | end 25 | 26 | it 'returns 200 OK' do 27 | get path 28 | expect(last_response.status).to eq(200) 29 | end 30 | 31 | it 'returns the value' do 32 | get path 33 | expect(last_response.body).to eq(value) 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rack/test' 2 | RSpec.configure do |config| 3 | config.include Rack::Test::Methods 4 | end 5 | 6 | if ENV.has_key?('VCAP_SERVICES') 7 | puts "Not starting local redis-server, using the one defined in VCAP_SERVICES" 8 | else 9 | require 'support/redis_server' 10 | REDIS = RedisServer.new( 11 | host: "localhost", 12 | port: 6380, 13 | tls_port: 16380, 14 | password: "p4ssw0rd" 15 | ) 16 | 17 | RSpec.configure do |config| 18 | config.before(:suite) do 19 | REDIS.start 20 | end 21 | 22 | config.after(:suite) do 23 | REDIS.stop 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/support/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE5jCCAs4CCQDqs1MSDJNhITANBgkqhkiG9w0BAQsFADA1MRMwEQYDVQQKDApS 3 | ZWRpcyBUZXN0MR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjEw 4 | OTAzMTY0NDQzWhcNMzEwOTAxMTY0NDQzWjA1MRMwEQYDVQQKDApSZWRpcyBUZXN0 5 | MR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEB 6 | AQUAA4ICDwAwggIKAoICAQDSN4xnNNBi5d4q72Hh6Z9doiU5HIyV5VlXE3zmNVyX 7 | T8dBed91criaPG2E1ab0HV0kgImbpjQ3z6XhaNliST8cR3MfG+uK/1vRzydUaV4d 8 | cvBRvLej5096UvZMbmSzY/rEg17kZ6Crzfd1h3nCGYuOAo1Sntk/PwSH5mveG6wo 9 | fnq20CEVDGcXjxiiThpmTTZL4BJVXGPtFvwQ5hAzUuyVXsT9yliHbu5VoSofsGLv 10 | sxCWzDHxdWIhv9RMgbt8aj2pHO4RHY51mXugP8XwFRpIiHgwCvrllDHb2uRKgq1/ 11 | 40LxY50P/UStPAy6/RKK/eN58htOGRZctV3xsqbYFcMDD/P+7Gl3ZoJrP/ZrfdMk 12 | 4lwVnpAMwN/nP4bjI/nySpx+hi/KjgTKmhZxvfKN3xLoX+KWgAB6c2gC73G8eiSy 13 | w/0xdUO7pCIVTaHvbw7YO2F1p6jaJGTCRfIjTi0gwLgCQrcTfDs2Kv21E8wKQunT 14 | ses9osByOiY1awq07r3Ph372TBYsHm2oaGYukP5FIH0S64zgf9xiXgCrnES3h64g 15 | Mz1W63D4DP4DYFDX2psoN7WZKnWQXyF+cT22D5+GrtIJ9vfAv5pfbdFP2v2lXw0b 16 | ghgQA+4WDuyINiBMU2uUjRrXlLXKbRgq4Lc2+zBIap0fTALoXCxyB4PnOsM3nJUg 17 | JwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQA9VOjAOf63Ea98JwWezVArfmlJBF1D 18 | wX94xnRfmt88YJ3rmNXC3ohVsRrJyypjZX/al4AfaNX7lLPOYIixLWc+9a+6rBBb 19 | OQCHKcATqrJ8K1G4x2D+dZ4GoCqKFM1u02zqeOGMPD+zwPnQFqfFb8SrUy83GwCq 20 | DLRh3+hOzKvNFE20UQYEeABbw6VoPEAHvLNxphlz9CvFxgbZiD8kRKosAeKe2B4p 21 | f0Pj5NhIXNZ0/nqOU6j8az7fxXxi33pmmL4eoMP/V6ZtWVsIADZ8hsudSfixmCsv 22 | ZXgydTTGsnnLk+MsgTT6W9MRB1i0Yrv5ljDDfpGicKP+7SCpTkhOAGyXNWDgsIN6 23 | vDp20/fvgzFRdO5wdn2Tf3Oh0ZWuXQTaAy64ohOwdcxXpzLjzGYc8iz5TySVhEFq 24 | sMjRSqVBfCPslyqYfOSrn/F7fWCj6wvNQIMgsdGiqRKaomHxoI31fdgaFhethXq6 25 | dwp/9sTlqgpVXdqbjNWAjIuROb797MXGvwLbhUqFOkbXJxVIewCQZIxyubCKhub3 26 | qvWfoFEOWhQvq6XHT72h4yDiJdf9gkWc/I00TOJ/KOkBj9mJdSf0J2n1cY2ufKoY 27 | IIH+yTOPCnUOT95jpeIG7SGEUJyWmJidY5OfIrpTw/oQEnljDx6h+xLVBhuvN2p4 28 | 4kdAqAPdyFMpjQ== 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /spec/support/redis.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID+zCCAeOgAwIBAgIJAJoKJVJMubsrMA0GCSqGSIb3DQEBCwUAMDUxEzARBgNV 3 | BAoMClJlZGlzIFRlc3QxHjAcBgNVBAMMFUNlcnRpZmljYXRlIEF1dGhvcml0eTAe 4 | Fw0yMTA5MDMxNjQ0NDNaFw0yMjA5MDMxNjQ0NDNaMCsxEzARBgNVBAoMClJlZGlz 5 | IFRlc3QxFDASBgNVBAMMC1NlcnZlci1vbmx5MIIBIjANBgkqhkiG9w0BAQEFAAOC 6 | AQ8AMIIBCgKCAQEA3ARiEeg23T7hCr14LjVImcq6sFrDQhW5K2eUsu52QkpofTNi 7 | XNGSvObrTDMZC+mjLp8IGuA0yiBixAjJQJfUs/jkiK6/PijQmf92kL/RQKSgNyfe 8 | pMhVNjj1U+CoHymAR2OThEFJ0a0uDkeP0wpAxKFrCifbU0GOH4uQz1DGMAWtX4Oh 9 | NTiaLgW30KT5AQL5DoSAJ4KBb/jcNcmy4IuK3JX6kk0SNcwvHp0FI9n48U2cs4Er 10 | 5gZPbd3lUmV4tbRx3BwaufcM/MJS9bC/xYJTWKVk7PUsduKcTZToyZgy+sbQAmTD 11 | 3SuQv4VFtLILNImmBoVLo0tUVZrtJ2IF3rhB9QIDAQABoxgwFjAUBgNVHREEDTAL 12 | gglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggIBAF9ZYkwnnnfVIqLHjBzA/G9R 13 | /wPpEn47m80WWM/mBFhWWbGrxEqf3hKDptGW62L51UGSG7VXunVirnD+KjwKRFKB 14 | 1SheMWbtmedB4btuvsoyWFEq/epdiHlY+VF9zfokYRmWep0EUoS8p3e3v2s+b2wv 15 | mFelPq+6QzruoXlzRikjqbykJW1cPBG4feuKnJh1Sf9OtrU3d45rSaOKJNUTWjhk 16 | Smm8xlbVGs9YOeVmLPjdSnMWIQiZjeCX+jSR1UxJMDS/DDwz+ccPzcTcfBgIbWpT 17 | W1PLLXkw43LqGI1aq/GtIQs5H8ySaVo8zM8VKojRos9uGWu+lGaDUFrhBjts3LEo 18 | LO+HHSDhJ+S0GDISdf8jKko6leFoWR/MRY/AzdqylvRflSAl7uRlaguLAEfQKVMu 19 | CI3fJyIktfHSipKQJcuoBJSZjkf+G4jkkgYzJi9QW6yNYOHrmBVNEb6ublnNKO1o 20 | YXIRnpn6WFoWgVLeLBwSS06rOnP/Z6VCpQGwIdXCHBPPhZa2YP6QpDkQaV4vnNCi 21 | Eaj1bVwg/9PI1qw87GBraIt3DZAhTjHCw8MhnuEfL/JqxExg5IGWfLymXgkrbgSU 22 | c3UPRFvHV/qvyJz1BoIrdoKaHSnrGseZtxyDLdpp/x6yS+KoEcGSeO8umROeUKmG 23 | G7Fg+UM1TojNbGK41L6q 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /spec/support/redis.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA3ARiEeg23T7hCr14LjVImcq6sFrDQhW5K2eUsu52QkpofTNi 3 | XNGSvObrTDMZC+mjLp8IGuA0yiBixAjJQJfUs/jkiK6/PijQmf92kL/RQKSgNyfe 4 | pMhVNjj1U+CoHymAR2OThEFJ0a0uDkeP0wpAxKFrCifbU0GOH4uQz1DGMAWtX4Oh 5 | NTiaLgW30KT5AQL5DoSAJ4KBb/jcNcmy4IuK3JX6kk0SNcwvHp0FI9n48U2cs4Er 6 | 5gZPbd3lUmV4tbRx3BwaufcM/MJS9bC/xYJTWKVk7PUsduKcTZToyZgy+sbQAmTD 7 | 3SuQv4VFtLILNImmBoVLo0tUVZrtJ2IF3rhB9QIDAQABAoIBAQCerOaRpZjT7YJd 8 | vZHMOY93ovcLMxo5MfOOszBR75sIaiN7kDBzYo3igOPX285HgiO2/ujYgTRxRfQV 9 | D4UdeaLkfS9MyEKSsNrpBY3cDQr8BP6raKywyO5zoZaE/LQjbUnfspf0PSXg+3YI 10 | xGGcwXs+lG/fLimGnPPEGmLAsa+Nd2QdR2yhr6gqKQ7HWdCjGMZaIZCUMDACFIaB 11 | PCLZaRmdEbZEoe8dVfrx+cD7a8wuecRf8aGUCk943gHCbF/xRma4k/xArnaNwcuL 12 | XG6FgLDLWqHOCI+qxOps41jolt8puXjHy0wwlWwvxWTFlCwxJ+l0qE8YrUK31xV+ 13 | V+gME1FtAoGBAPcSWFwjPFqeDhG/sz9wutwKcrdPFJ2mMUd7GzaJ+j+yy+HanoSv 14 | PCr7b9lArVCPcBrOPgLHMDhmXs3PakQ4epcWhuIpB4ju1rIBtQVVFGQWRosYxk57 15 | U9I5kVpAOimfsfHO5po/1pKgZoJHT23BQrZD2zHpnjYId5iJj+IIopB/AoGBAOP3 16 | wccQyAflGsHdnc+G7PG6xYOjPiTxyWXN0FTd6NpXZ0vqZ3RvnmUT+uMTVkzYiThg 17 | baSc+fWKHjfs+hkfZekZE4d/z5dQPaKEe+6ZJHNRigdOP2Cl+6Obg5JI9U8Tgeqa 18 | W7LsTwk37G1TMqZY8rMNQNCgnPyQFYy7be/XFbOLAoGAdrVZvusvFFrcZ6qzdMe1 19 | AAQFGg3k2dn/01hSuuGQWwqM6vcfMqD/R6eHFdr9areAJWDyamNJx89nvXrqk9yP 20 | CgjIxeDwvdoDTYOEdgtqwKvzOOGuZgbbPyZpr/Mr/tO7q3K9WV9SuSBYIEAi6Zjx 21 | dKElr9WPLmbjD2cIupFrigECgYEAjgS/V8CAnUqBTN0/fxXTbxSoD8YW5PUsPpvl 22 | ebl1YvCojYbORJ900guMdQKK9PzBH7oTFEYnAmy1PqoHdLc2yeyxWeyp6t42R5S5 23 | kH8fUiguETcSDKfbCKXNZcjF6imA+iKHlgPZiSipxcHa//7R0/IhutcUddNdWXs6 24 | lfwsrNkCgYA8xr2hJBlFr8XbImbKGCb4fJh+sdZvj6qamcGxeKn133pmR33XSZWT 25 | 5Bc6MtqbQJouXILJwfLKh+WAQ0BeftI0JkxW362cKj5CaNWtykDc8C6t6v86NW44 26 | TIzFT90koJo69gIuKmpga4UKeLB04fGod8JF+a/uEYaO1P+QfuXjeQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /spec/support/redis_server.rb: -------------------------------------------------------------------------------- 1 | require "childprocess" 2 | 3 | class RedisServer 4 | attr_reader :host, :port, :password, :tls_port 5 | 6 | def initialize(options) 7 | @host = options.fetch(:host) 8 | @port = options.fetch(:port) 9 | @tls_port = options.fetch(:tls_port) 10 | @password = options.fetch(:password) 11 | 12 | @process = ChildProcess.build("redis-server", 13 | "--port", port.to_s, 14 | "--requirepass", password, 15 | "--tls-port", tls_port.to_s, 16 | "--tls-cert-file", "#{__dir__}/redis.crt", 17 | "--tls-key-file", "#{__dir__}/redis.key", 18 | "--tls-ca-cert-file", "#{__dir__}/ca.crt", 19 | "--tls-auth-clients", "no") 20 | 21 | @process.io.stdout = @process.io.stderr = File.open('redis.log', 'w+') 22 | end 23 | 24 | def start 25 | process.start 26 | rescue ChildProcess::Error 27 | fail "redis-server could not start. Do you have it installed and on your PATH?" 28 | exit 1 29 | end 30 | 31 | def stop 32 | process.stop if process.alive? 33 | end 34 | 35 | private 36 | 37 | attr_reader :process 38 | end 39 | -------------------------------------------------------------------------------- /vendor/cache/cf-app-utils-0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/cf-app-utils-0.6.gem -------------------------------------------------------------------------------- /vendor/cache/childprocess-4.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/childprocess-4.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/connection_pool-2.4.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/connection_pool-2.4.1.gem -------------------------------------------------------------------------------- /vendor/cache/diff-lcs-1.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/diff-lcs-1.5.0.gem -------------------------------------------------------------------------------- /vendor/cache/logger-1.5.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/logger-1.5.3.gem -------------------------------------------------------------------------------- /vendor/cache/mustermann-3.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/mustermann-3.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/openssl-3.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/openssl-3.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/rack-2.2.6.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/rack-2.2.6.4.gem -------------------------------------------------------------------------------- /vendor/cache/rack-protection-3.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/rack-protection-3.0.6.gem -------------------------------------------------------------------------------- /vendor/cache/rack-test-2.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/rack-test-2.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/redis-5.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/redis-5.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/redis-client-0.22.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/redis-client-0.22.0.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-3.12.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/rspec-3.12.0.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-core-3.12.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/rspec-core-3.12.1.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-expectations-3.12.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/rspec-expectations-3.12.2.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-mocks-3.12.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/rspec-mocks-3.12.3.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-support-3.12.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/rspec-support-3.12.0.gem -------------------------------------------------------------------------------- /vendor/cache/ruby2_keywords-0.0.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/ruby2_keywords-0.0.5.gem -------------------------------------------------------------------------------- /vendor/cache/sinatra-3.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/sinatra-3.0.6.gem -------------------------------------------------------------------------------- /vendor/cache/tilt-2.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/tilt-2.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/tttls1.3-0.2.19.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/tttls1.3-0.2.19.gem -------------------------------------------------------------------------------- /vendor/cache/webrick-1.8.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-cf/cf-redis-example-app/50646cd0d9b5ce16be0278f6f09e6ed190b4302f/vendor/cache/webrick-1.8.1.gem --------------------------------------------------------------------------------