├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── infrataster ├── ci └── jenkins_build ├── example ├── .gitignore ├── .rspec ├── Berksfile ├── Gemfile ├── README.md ├── Vagrantfile ├── cookbooks │ ├── app │ │ ├── files │ │ │ └── default │ │ │ │ └── index.html │ │ ├── metadata.rb │ │ └── recipes │ │ │ └── default.rb │ ├── db │ │ ├── files │ │ │ └── default │ │ │ │ └── my.cnf │ │ ├── metadata.rb │ │ └── recipes │ │ │ └── default.rb │ └── proxy │ │ ├── metadata.rb │ │ ├── recipes │ │ └── default.rb │ │ └── templates │ │ └── default │ │ ├── app.erb │ │ └── static.erb └── spec │ ├── spec_helper.rb │ └── web_spec.rb ├── infrataster.gemspec ├── lib ├── infrataster.rb └── infrataster │ ├── cli.rb │ ├── contexts.rb │ ├── contexts │ ├── base_context.rb │ ├── capybara_context.rb │ ├── http_context.rb │ └── no_resource_context.rb │ ├── faraday_middlewares.rb │ ├── faraday_middlewares │ └── follow_redirects.rb │ ├── fixtures │ ├── Gemfile.erb │ ├── Rakefile.erb │ ├── Vagrantfile.erb │ ├── app_spec.rb.erb │ └── spec_helper.rb.erb │ ├── helpers.rb │ ├── helpers │ ├── resource_helper.rb │ └── rspec_helper.rb │ ├── resources.rb │ ├── resources │ ├── base_resource.rb │ ├── capybara_resource.rb │ ├── http_resource.rb │ └── server_resource.rb │ ├── rspec.rb │ ├── server.rb │ └── version.rb └── spec ├── integration ├── capybara_spec.rb ├── http_spec.rb ├── other_spec.rb ├── spec_helper.rb └── vm │ ├── .gitignore │ ├── Berksfile │ ├── Vagrantfile │ ├── app │ ├── Gemfile │ ├── app.rb │ └── config.ru │ └── recipes │ ├── app │ ├── default.rb │ └── files │ │ └── etc │ │ └── supervisor │ │ └── conf.d │ │ └── rackup.conf │ └── proxy │ ├── default.rb │ ├── files │ ├── etc │ │ └── nginx │ │ │ ├── .htpasswd │ │ │ ├── server.crt │ │ │ └── server.key │ └── usr │ │ └── share │ │ └── nginx │ │ └── html │ │ ├── auth │ │ └── index.html │ └── templates │ └── etc │ └── nginx │ └── sites-available │ └── integration-test.erb └── unit ├── lib └── infrataster │ ├── contexts │ └── http_context_spec.rb │ ├── resources │ ├── http_resource_spec.rb │ └── server_resource_spec.rb │ └── server_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | vendor/browsermob-proxy 19 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Infrataster Changelog 2 | 3 | ## v0.3.2 4 | 5 | * `http` resource: `faraday_middlewares` option 6 | * `http` resource: `follow_redirects` option 7 | 8 | ## v0.3.1 9 | 10 | * Accept DNS name as server address. 11 | 12 | ## v0.3.0 13 | 14 | * [Fix deprecation of example_group in metadata (by @otahi)](https://github.com/ryotarai/infrataster/pull/64) 15 | 16 | ## v0.2.6 17 | 18 | * [Inflate response body by gzip if inflate_gzip of http resource is true.] 19 | 20 | ## v0.2.5 21 | 22 | * [body option of http resource](https://github.com/ryotarai/infrataster/pull/57) 23 | 24 | ## v0.2.4 25 | 26 | * [Consider how to fetch the current example in RSpec v2.14.x (by @gongo)](https://github.com/ryotarai/infrataster/pull/56) 27 | 28 | ## v0.2.3 29 | 30 | * [http resource supports basic auth. (by @winebarrel)](https://github.com/ryotarai/infrataster/pull/54) 31 | 32 | ## v0.2.2 33 | 34 | * Server can be defined by block. (https://github.com/ryotarai/infrataster/pull/51 by @otahi) 35 | 36 | ## v0.2.1 37 | 38 | * `http` resource support `ssl` option which is passed to Faraday.new (by @SnehaM) 39 | 40 | ## v0.2.0 41 | 42 | * No change 43 | 44 | ## v0.2.0.beta1 45 | 46 | * Support RSpec 3.x 47 | * Some deprecation warnings remain. 48 | * RSpec 2.x is supported too. 49 | 50 | ## v0.1.13 51 | 52 | * Make Infrataster's methods available in before(:all) block. (by @KitaitiMakoto) 53 | 54 | ## v0.1.12 55 | 56 | * Add infrataster command to create template directory. 57 | * Specify host in a option passed to Server.define. (by @KitaitiMakoto) 58 | * Add Server#ssh method. (by @KitaitiMakoto) 59 | 60 | ## v0.1.11 61 | 62 | * Slight bug fixes 63 | 64 | ## v0.1.10 65 | 66 | * rspec ~> 2.0 67 | 68 | ## v0.1.9 69 | 70 | * Add "fuzzy IP address" feature which determine IP address by CIDR or netmask. 71 | 72 | ## v0.1.8 73 | 74 | * Use Poltergeist's header manipulation instead of BrowserMob Proxy. Remove BrowserMob Proxy dependency. 75 | 76 | ## v0.1.7 77 | 78 | * Fix a key name in a config passed to Net::SSH.start. (by @rrreeeyyy) 79 | 80 | ## v0.1.6 81 | 82 | * Implement `Server#ssh_exec` which executes a command on the server via SSH. 83 | 84 | ## v0.1.5 85 | 86 | * Extract mysql resource to [infrataster-plugin-mysql](https://github.com/ryotarai/infrataster-plugin-mysql). 87 | 88 | ## v0.1.4 89 | 90 | * Include RSpec::Matchers to use be_xxx matchers. (by @KitaitiMakoto) 91 | * Don't raise any error for subjects which is not related to Infrataster. (by @KitaitiMakoto) 92 | 93 | ## v0.1.3 94 | 95 | * Don't create multiple phantomjs and browsermob proxy. `capybara` resources become faster. 96 | 97 | ## v0.1.2 98 | 99 | * Use poltergeist (PhantomJS) as capybara driver instead of selenium-webdriver. 100 | 101 | ## v0.1.1 102 | 103 | * Http resources accept `method`, `params` and `headers` options. (Issue #7) 104 | 105 | ## v0.1.0 106 | 107 | * Initial release 108 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in infrataster.gemspec 4 | gemspec 5 | 6 | gem 'vagrant', github: 'mitchellh/vagrant' 7 | gem 'vagrant-itamae' 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ryota Arai 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Infrataster 2 | [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/ryotarai/infrataster?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 3 | 4 | [![Gem Version](https://badge.fury.io/rb/infrataster.png)](http://badge.fury.io/rb/infrataster) 5 | [![Code Climate](https://codeclimate.com/github/ryotarai/infrataster.png)](https://codeclimate.com/github/ryotarai/infrataster) 6 | 7 | Infrastructure Behavior Testing Framework. 8 | 9 | 10 | ## Basic Usage with Vagrant 11 | 12 | First, create `Gemfile`: 13 | 14 | ```ruby 15 | source 'https://rubygems.org' 16 | 17 | gem 'infrataster' 18 | ``` 19 | 20 | Install gems: 21 | 22 | ``` 23 | $ bundle install 24 | ``` 25 | 26 | Install Vagrant: [Official Docs](http://docs.vagrantup.com/v2/installation/index.html) 27 | 28 | Create Vagrantfile: 29 | 30 | ```ruby 31 | # Vagrantfile 32 | Vagrant.configure("2") do |config| 33 | config.vm.box = "hashicorp/precise64" 34 | 35 | config.vm.define :proxy do |c| 36 | c.vm.network "private_network", ip: "192.168.33.10" 37 | c.vm.network "private_network", ip: "172.16.33.10", virtualbox__intnet: "infrataster-example" 38 | end 39 | 40 | config.vm.define :app do |c| 41 | c.vm.network "private_network", ip: "172.16.33.11", virtualbox__intnet: "infrataster-example" 42 | end 43 | end 44 | ``` 45 | 46 | Start VMs: 47 | 48 | ``` 49 | $ vagrant up 50 | ``` 51 | 52 | Initialize rspec directory: 53 | 54 | ``` 55 | $ rspec --init 56 | create spec/spec_helper.rb 57 | create .rspec 58 | ``` 59 | 60 | `require 'infrataster/rspec'` and define target servers for testing in `spec/spec_helper.rb`: 61 | 62 | ```ruby 63 | # spec/spec_helper.rb 64 | require 'infrataster/rspec' 65 | 66 | Infrataster::Server.define( 67 | :proxy, # name 68 | '192.168.0.0/16', # ip address 69 | vagrant: true # for vagrant VM 70 | ) 71 | Infrataster::Server.define( 72 | :app, # name 73 | '172.16.0.0/16', # ip address 74 | vagrant: true, # for vagrant VM 75 | from: :proxy # access to this machine via SSH port forwarding from proxy 76 | ) 77 | 78 | # Code generated by `rspec --init` is following... 79 | ``` 80 | Or 81 | 82 | ```ruby 83 | # spec/spec_helper.rb 84 | require 'infrataster/rspec' 85 | 86 | Infrataster::Server.define(:proxy) do |server| 87 | server.address = '192.168.0.0/16' 88 | server.vagrant = true 89 | end 90 | Infrataster::Server.define(:app) do |server| 91 | server.address = '172.16.0.0/16' 92 | server.vagrant = true 93 | server.from = :proxy 94 | end 95 | 96 | # Code generated by `rspec --init` is following... 97 | ``` 98 | 99 | Then, you can write spec files: 100 | 101 | ```ruby 102 | # spec/example_spec.rb 103 | require 'spec_helper' 104 | 105 | describe server(:app) do 106 | describe http('http://app') do 107 | it "responds content including 'Hello Sinatra'" do 108 | expect(response.body).to include('Hello Sinatra') 109 | end 110 | it "responds as 'text/html'" do 111 | expect(response.headers['content-type']).to eq("text/html") 112 | end 113 | end 114 | end 115 | ``` 116 | 117 | Run tests: 118 | 119 | ``` 120 | $ bundle exec rspec 121 | 2 examples, 2 failures 122 | ``` 123 | 124 | Currently, the tests failed because the VM doesn't respond to HTTP request. 125 | 126 | It's time to write provisioning instruction like Chef's cookbooks or Puppet's manifests! 127 | 128 | ## Server 129 | 130 | "Server" is a server you tests. This supports Vagrant, which is very useful to run servers for testing. Of course, you can test real servers. 131 | 132 | You should define servers in `spec_helper.rb` like the following: 133 | 134 | ```ruby 135 | Infrataster::Server.define( 136 | # Name of the server, this will be used in the spec files. 137 | :proxy, 138 | # IP address of the server 139 | '192.168.0.0/16', 140 | # If the server is provided by vagrant and this option is true, 141 | # SSH configuration to connect to this server is got from `vagrant ssh-config` command automatically. 142 | vagrant: true, 143 | ) 144 | 145 | Infrataster::Server.define( 146 | # Name of the server, this will be used in the spec files. 147 | :app, 148 | # IP address of the server 149 | '172.16.0.0/16', 150 | # If the server is provided by vagrant and this option is true, 151 | # SSH configuration to connect to this server is got from `vagrant ssh-config` command automatically. 152 | vagrant: true, 153 | # Which gateway is used to connect to this server by SSH port forwarding? 154 | from: :proxy, 155 | # options for resources 156 | mysql: {user: 'app', password: 'app'}, 157 | ) 158 | ``` 159 | 160 | You can specify SSH configuration manually too: 161 | 162 | ```ruby 163 | Infrataster::Server.define( 164 | # ... 165 | ssh: {host_name: 'hostname', user: 'testuser', keys: ['/path/to/id_rsa']} 166 | ) 167 | ``` 168 | 169 | ### fuzzy IP address 170 | 171 | Infrataster has "fuzzy IP address" feature. You can pass IP address which has netmask (= CIDR) to `Infrataster::Server#define`. This needs `vagrant` option or `ssh` option which has `host_name` because this fetches all IP address via SSH and find the matching one. 172 | 173 | ```ruby 174 | Infrataster::Server.define( 175 | :app, 176 | # find IP address matching 172.16.0.0 ~ 172.16.255.255 177 | '172.16.0.0/16', 178 | ) 179 | ``` 180 | 181 | Of course, you can set fully-specified IP address too. 182 | 183 | ```ruby 184 | Infrataster::Server.define( 185 | :app, 186 | '172.16.11.22', 187 | # or 188 | '172.16.11.22/32', 189 | ) 190 | ``` 191 | 192 | ### #ssh_exec 193 | 194 | You can execute a command on the server like the following: 195 | 196 | ```ruby 197 | describe server(:proxy) do 198 | let(:time) { Time.now } 199 | before do 200 | current_server.ssh_exec "echo 'Hello' > /tmp/test-#{time.to_i}" 201 | end 202 | it "executes a command on the current server" do 203 | result = current_server.ssh_exec("cat /tmp/test-#{time.to_i}") 204 | expect(result.chomp).to eq('Hello') 205 | end 206 | end 207 | ``` 208 | 209 | This is useful to test cases which depends on the status of the server. 210 | 211 | ## Resources 212 | 213 | "Resource" is what you test by Infrataster. For instance, the following code describes `http` resource. 214 | 215 | ```ruby 216 | describe server(:app) do 217 | describe http('http://example.com') do 218 | it "responds content including 'Hello Sinatra'" do 219 | expect(response.body).to include('Hello Sinatra') 220 | end 221 | end 222 | end 223 | ``` 224 | 225 | ### `http` resource 226 | 227 | `http` resource tests HTTP response when sending HTTP request. 228 | It accepts `method`, `params` and `header` as options. 229 | 230 | ```ruby 231 | describe server(:app) do 232 | describe http( 233 | 'http://app.example.com', 234 | method: :post, 235 | params: {'foo' => 'bar'}, 236 | headers: {'USER' => 'VALUE'} 237 | ) do 238 | it "responds with content including 'app'" do 239 | expect(response.body).to include('app') 240 | 241 | # `response` is a instance of `Faraday::Response` 242 | # See: https://github.com/lostisland/faraday/blob/master/lib/faraday/response.rb 243 | end 244 | end 245 | 246 | # Gzip support 247 | describe http('http://app.example.com/gzipped') do 248 | it "responds with content deflated by gzip" do 249 | expect(response.headers['content-encoding']).to eq('gzip') 250 | end 251 | end 252 | 253 | describe http('http://app.example.com/gzipped', inflate_gzip: true) do 254 | it "responds with content inflated automatically" do 255 | expect(response.headers['content-encoding']).to be_nil 256 | expect(response.body).to eq('plain text') 257 | end 258 | end 259 | 260 | # Redirects 261 | describe http('http://app.example.com/redirect', follow_redirects: true) do 262 | it "follows redirects" do 263 | expect(response.status).to eq(200) 264 | end 265 | end 266 | 267 | # Custom Faraday middleware 268 | describe http('http://app.example.com', faraday_middlewares: [ 269 | YourMiddleware, 270 | [YourMiddleware, options] 271 | ]) do 272 | it "uses the middlewares" do 273 | expect(response.status).to eq(200) 274 | end 275 | end 276 | end 277 | ``` 278 | 279 | ### `capybara` resource 280 | 281 | `capybara` resource tests your web application by simulating real user's interaction. 282 | 283 | ```ruby 284 | describe server(:app) do 285 | describe capybara('http://app.example.com') do 286 | it 'shows food list' do 287 | visit '/' 288 | click_link 'Foods' 289 | expect(page).to have_content 'Yummy Soup' 290 | end 291 | end 292 | end 293 | ``` 294 | 295 | ### `mysql_query` resource 296 | 297 | `mysql_query` resource is now in [infrataster-plugin-mysql](https://github.com/ryotarai/infrataster-plugin-mysql). 298 | 299 | ### `pgsql_query` resource 300 | 301 | `pgsql_query` resource sends a query to PostgreSQL server. 302 | 303 | `pgsql_query` is provided by [infrataster-plugin-pgsql](https://github.com/SnehaM/infrataster-plugin-pgsql) by [@SnehaM](https://github.com/SnehaM). 304 | 305 | ### `dns` resource 306 | 307 | `dns` resource sends a query to DNS server. 308 | 309 | `dns` is provided by [infrataster-plugin-dns](https://github.com/otahi/infrataster-plugin-dns) by [@otahi](https://github.com/otahi). 310 | 311 | ### `memcached` resource 312 | 313 | `memcached` resource sends a query to memcached server. 314 | 315 | `memcached` is provided by [infrataster-plugin-memecached](https://github.com/rahulkhengare/infrataster-plugin-memcached) by [@rahulkhengare](https://github.com/rahulkhengare). 316 | 317 | ### `redis` resource 318 | 319 | `redis` resource sends a query to redis server. 320 | 321 | `redis` is provided by [infrataster-plugin-redis](https://github.com/rahulkhengare/infrataster-plugin-redis) by [@rahulkhengare](https://github.com/rahulkhengare). 322 | 323 | ### `firewall` resource 324 | 325 | `firewall` resource tests your firewalls. 326 | 327 | `firewall` is provided by [infrataster-plugin-firewall](https://github.com/otahi/infrataster-plugin-firewall) by [@otahi](https://github.com/otahi). 328 | 329 | ## Example 330 | 331 | * [example](example) 332 | * [spec/integration](spec/integration) 333 | 334 | ## Tests 335 | 336 | ### Unit Tests 337 | 338 | Unit tests are under `spec/unit` directory. 339 | 340 | ``` 341 | $ bundle exec rake spec:unit 342 | ``` 343 | 344 | ### Integration Tests 345 | 346 | Integration tests are under `spec/integration` directory. 347 | 348 | ``` 349 | $ bundle exec rake spec:integration:prepare 350 | $ bundle exec rake spec:integration 351 | ``` 352 | 353 | ## Presentations and Articles 354 | 355 | * https://speakerdeck.com/ryotarai/introducing-infrataster 356 | 357 | 358 | Introducing Infrataster 359 | 360 | 361 | * https://speakerdeck.com/ryotarai/infrataster-infra-behavior-testing-framework-number-oedo04 362 | * [Infratasterでリバースプロキシのテストをする](http://techlife.cookpad.com/entry/2014/11/19/151557) 363 | * Google-Translated: [Testing reverse proxy with Infrataster](https://translate.google.com/translate?hl=ja&sl=ja&tl=en&u=http%3A%2F%2Ftechlife.cookpad.com%2Fentry%2F2014%2F11%2F19%2F151557) 364 | 365 | ## Changelog 366 | 367 | [Changelog](CHANGELOG.md) 368 | 369 | ## Contributing 370 | 371 | 1. Fork it ( http://github.com/ryotarai/infrataster/fork ) 372 | 2. Create your feature branch (`git checkout -b my-new-feature`) 373 | 3. Commit your changes (`git commit -am 'Add some feature'`) 374 | 4. Push to the branch (`git push origin my-new-feature`) 375 | 5. Create new Pull Request 376 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | require "open-uri" 4 | 5 | def yellow(str) 6 | "\e[33m#{str}\e[m" 7 | end 8 | 9 | ENV['VAGRANT_CWD'] = File.expand_path('spec/integration/vm') 10 | 11 | 12 | desc 'Run unit and integration tests' 13 | task :spec => ['spec:unit', 'spec:integration'] 14 | 15 | namespace :spec do 16 | RSpec::Core::RakeTask.new("unit") do |task| 17 | task.pattern = "./spec/unit{,/*/**}/*_spec.rb" 18 | end 19 | 20 | RSpec::Core::RakeTask.new("integration") do |task| 21 | task.pattern = "./spec/integration{,/*/**}/*_spec.rb" 22 | end 23 | 24 | namespace :integration do 25 | integration_dir = 'spec/integration' 26 | 27 | desc 'Clean' 28 | task :clean => ['vagrant:destroy'] do 29 | end 30 | 31 | desc 'Prepare' 32 | task :prepare => ['vagrant:up'] do 33 | end 34 | 35 | namespace :vagrant do 36 | task :up do 37 | puts yellow('Starting VM...') 38 | system 'vagrant', 'up' 39 | end 40 | 41 | task :destroy do 42 | puts yellow('Destroying VM...') 43 | system 'vagrant', 'destroy', '-f' 44 | end 45 | 46 | task :provision do 47 | puts yellow('Provisioning VM...') 48 | system 'vagrant', 'provision' 49 | end 50 | 51 | task :ssh, :name do |t, args| 52 | system 'vagrant', 'ssh', args[:name] 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /bin/infrataster: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'infrataster/cli' 4 | 5 | Infrataster::CLI.start(ARGV) 6 | 7 | -------------------------------------------------------------------------------- /ci/jenkins_build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | def exec(cmd, exit_if_fail = true) 4 | env = {'CI' => 'true'} 5 | system env, cmd 6 | if exit_if_fail && $?.exitstatus != 0 7 | puts "`#{cmd}` failed" 8 | exit $?.exitstatus 9 | end 10 | 11 | $?.exitstatus 12 | end 13 | 14 | exec 'rm -f Gemfile.lock' 15 | exec 'env USE_SYSTEM_GECODE=1 bundle install --path=vendor/bundle --jobs=4' 16 | 17 | exec 'bundle exec rake spec:unit' 18 | 19 | exec 'bundle exec rake spec:integration:prepare' 20 | status = exec('bundle exec rake spec:integration', false) 21 | exec 'bundle exec rake spec:integration:clean' 22 | exit status 23 | 24 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /Gemfile.lock 2 | /Berksfile.lock 3 | /.vagrant 4 | /vendor 5 | -------------------------------------------------------------------------------- /example/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /example/Berksfile: -------------------------------------------------------------------------------- 1 | source "https://supermarket.chef.io" 2 | cookbook 'apt' 3 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'infrataster', path: '..' 4 | gem 'infrataster-plugin-mysql' 5 | 6 | gem 'berkshelf', github: 'berkshelf/berkshelf' 7 | 8 | gem 'pry' 9 | gem 'pry-byebug' 10 | gem 'pry-rescue' 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Infrataster Example 2 | 3 | ``` 4 | $ cd example 5 | $ bundle install 6 | $ bundle exec berks vendor vendor/cookbooks 7 | $ vagrant up 8 | $ bundle exec rspec 9 | ``` 10 | 11 | -------------------------------------------------------------------------------- /example/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | COOKBOOK_PATH = ['./cookbooks', './vendor/cookbooks'] 8 | APP_IP = "172.16.33.11" 9 | 10 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 11 | config.vm.box = "hashicorp/precise64" 12 | 13 | config.vm.define :proxy do |c| 14 | c.vm.network "private_network", ip: "192.168.33.10" 15 | c.vm.network "private_network", ip: "172.16.33.10", virtualbox__intnet: "infrataster-example" 16 | 17 | c.vm.provision "chef_solo" do |chef| 18 | chef.cookbooks_path = COOKBOOK_PATH 19 | chef.add_recipe "proxy" 20 | chef.json = {'app_ip' => APP_IP} 21 | end 22 | end 23 | 24 | config.vm.define :app do |c| 25 | c.vm.network "private_network", ip: APP_IP, virtualbox__intnet: "infrataster-example" 26 | 27 | c.vm.provision "chef_solo" do |chef| 28 | chef.cookbooks_path = COOKBOOK_PATH 29 | chef.add_recipe "app" 30 | chef.json = {} 31 | end 32 | end 33 | 34 | config.vm.define :db do |c| 35 | c.vm.network "private_network", ip: '172.16.33.12', virtualbox__intnet: "infrataster-example" 36 | 37 | c.vm.provision "chef_solo" do |chef| 38 | chef.cookbooks_path = COOKBOOK_PATH 39 | chef.add_recipe "db" 40 | chef.json = {'app_ip' => APP_IP} 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /example/cookbooks/app/files/default/index.html: -------------------------------------------------------------------------------- 1 | This is app server. 2 | -------------------------------------------------------------------------------- /example/cookbooks/app/metadata.rb: -------------------------------------------------------------------------------- 1 | name 'app' 2 | depends 'apt' 3 | -------------------------------------------------------------------------------- /example/cookbooks/app/recipes/default.rb: -------------------------------------------------------------------------------- 1 | include_recipe 'apt' 2 | 3 | package "nginx" 4 | 5 | service 'nginx' do 6 | action :start 7 | end 8 | 9 | cookbook_file '/usr/share/nginx/www/index.html' do 10 | mode '0644' 11 | end 12 | 13 | -------------------------------------------------------------------------------- /example/cookbooks/db/files/default/my.cnf: -------------------------------------------------------------------------------- 1 | # 2 | # The MySQL database server configuration file. 3 | # 4 | # You can copy this to one of: 5 | # - "/etc/mysql/my.cnf" to set global options, 6 | # - "~/.my.cnf" to set user-specific options. 7 | # 8 | # One can use all long options that the program supports. 9 | # Run program with --help to get a list of available options and with 10 | # --print-defaults to see which it would actually understand and use. 11 | # 12 | # For explanations see 13 | # http://dev.mysql.com/doc/mysql/en/server-system-variables.html 14 | 15 | # This will be passed to all mysql clients 16 | # It has been reported that passwords should be enclosed with ticks/quotes 17 | # escpecially if they contain "#" chars... 18 | # Remember to edit /etc/mysql/debian.cnf when changing the socket location. 19 | [client] 20 | port = 3306 21 | socket = /var/run/mysqld/mysqld.sock 22 | 23 | # Here is entries for some specific programs 24 | # The following values assume you have at least 32M ram 25 | 26 | # This was formally known as [safe_mysqld]. Both versions are currently parsed. 27 | [mysqld_safe] 28 | socket = /var/run/mysqld/mysqld.sock 29 | nice = 0 30 | 31 | [mysqld] 32 | # 33 | # * Basic Settings 34 | # 35 | user = mysql 36 | pid-file = /var/run/mysqld/mysqld.pid 37 | socket = /var/run/mysqld/mysqld.sock 38 | port = 3306 39 | basedir = /usr 40 | datadir = /var/lib/mysql 41 | tmpdir = /tmp 42 | lc-messages-dir = /usr/share/mysql 43 | skip-external-locking 44 | # 45 | # Instead of skip-networking the default is now to listen only on 46 | # localhost which is more compatible and is not less secure. 47 | bind-address = 0.0.0.0 48 | # 49 | # * Fine Tuning 50 | # 51 | key_buffer = 16M 52 | max_allowed_packet = 16M 53 | thread_stack = 192K 54 | thread_cache_size = 8 55 | # This replaces the startup script and checks MyISAM tables if needed 56 | # the first time they are touched 57 | myisam-recover = BACKUP 58 | #max_connections = 100 59 | #table_cache = 64 60 | #thread_concurrency = 10 61 | # 62 | # * Query Cache Configuration 63 | # 64 | query_cache_limit = 1M 65 | query_cache_size = 16M 66 | # 67 | # * Logging and Replication 68 | # 69 | # Both location gets rotated by the cronjob. 70 | # Be aware that this log type is a performance killer. 71 | # As of 5.1 you can enable the log at runtime! 72 | #general_log_file = /var/log/mysql/mysql.log 73 | #general_log = 1 74 | # 75 | # Error log - should be very few entries. 76 | # 77 | log_error = /var/log/mysql/error.log 78 | # 79 | # Here you can see queries with especially long duration 80 | #log_slow_queries = /var/log/mysql/mysql-slow.log 81 | #long_query_time = 2 82 | #log-queries-not-using-indexes 83 | # 84 | # The following can be used as easy to replay backup logs or for replication. 85 | # note: if you are setting up a replication slave, see README.Debian about 86 | # other settings you may need to change. 87 | #server-id = 1 88 | #log_bin = /var/log/mysql/mysql-bin.log 89 | expire_logs_days = 10 90 | max_binlog_size = 100M 91 | #binlog_do_db = include_database_name 92 | #binlog_ignore_db = include_database_name 93 | # 94 | # * InnoDB 95 | # 96 | # InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/. 97 | # Read the manual for more InnoDB related options. There are many! 98 | # 99 | # * Security Features 100 | # 101 | # Read the manual, too, if you want chroot! 102 | # chroot = /var/lib/mysql/ 103 | # 104 | # For generating SSL certificates I recommend the OpenSSL GUI "tinyca". 105 | # 106 | # ssl-ca=/etc/mysql/cacert.pem 107 | # ssl-cert=/etc/mysql/server-cert.pem 108 | # ssl-key=/etc/mysql/server-key.pem 109 | 110 | 111 | 112 | [mysqldump] 113 | quick 114 | quote-names 115 | max_allowed_packet = 16M 116 | 117 | [mysql] 118 | #no-auto-rehash # faster start of mysql but no tab completition 119 | 120 | [isamchk] 121 | key_buffer = 16M 122 | 123 | # 124 | # * IMPORTANT: Additional settings that can override those from this file! 125 | # The files must end with '.cnf', otherwise they'll be ignored. 126 | # 127 | !includedir /etc/mysql/conf.d/ 128 | -------------------------------------------------------------------------------- /example/cookbooks/db/metadata.rb: -------------------------------------------------------------------------------- 1 | name 'db' 2 | depends 'apt' 3 | -------------------------------------------------------------------------------- /example/cookbooks/db/recipes/default.rb: -------------------------------------------------------------------------------- 1 | include_recipe 'apt' 2 | 3 | package 'mysql-server' 4 | 5 | service 'mysql' do 6 | supports(:restart => true) 7 | end 8 | 9 | cookbook_file '/etc/mysql/my.cnf' do 10 | notifies :restart, 'service[mysql]' 11 | end 12 | 13 | execute "mysql -uroot -e \"GRANT ALL PRIVILEGES ON *.* TO 'app'@'#{node['app_ip']}' IDENTIFIED BY 'app';\"" 14 | 15 | -------------------------------------------------------------------------------- /example/cookbooks/proxy/metadata.rb: -------------------------------------------------------------------------------- 1 | name 'proxy' 2 | depends 'apt' 3 | -------------------------------------------------------------------------------- /example/cookbooks/proxy/recipes/default.rb: -------------------------------------------------------------------------------- 1 | include_recipe 'apt' 2 | 3 | package "nginx" do 4 | action :install 5 | end 6 | 7 | service "nginx" do 8 | action :start 9 | supports(:reload => true) 10 | end 11 | 12 | file "/etc/nginx/sites-enabled/default" do 13 | action :delete 14 | notifies :reload, 'service[nginx]' 15 | end 16 | 17 | %w!app static!.each do |app| 18 | template "/etc/nginx/sites-available/#{app}" do 19 | notifies :reload, 'service[nginx]' 20 | end 21 | 22 | link "/etc/nginx/sites-enabled/#{app}" do 23 | to "/etc/nginx/sites-available/#{app}" 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /example/cookbooks/proxy/templates/default/app.erb: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | root /usr/share/nginx/www; 5 | index index.html index.htm; 6 | 7 | server_name app; 8 | 9 | location / { 10 | proxy_pass http://<%= node['app_ip'] %>; 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /example/cookbooks/proxy/templates/default/static.erb: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | root /usr/share/nginx/www; 5 | index index.html index.htm; 6 | 7 | server_name static; 8 | 9 | location / { 10 | try_files $uri $uri/ /index.html; 11 | } 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'infrataster/rspec' 2 | require 'infrataster-plugin-mysql' 3 | 4 | Infrataster::Server.define( 5 | :proxy, 6 | '192.168.0.0/16', 7 | vagrant: true, 8 | ) 9 | Infrataster::Server.define( 10 | :app, 11 | '172.16.0.0/16', 12 | vagrant: true, 13 | from: :proxy 14 | ) 15 | Infrataster::Server.define( 16 | :db, 17 | '172.16.0.0/16', 18 | vagrant: true, 19 | from: :app, 20 | mysql: {user: 'app', password: 'app'} 21 | ) 22 | 23 | RSpec.configure do |config| 24 | config.treat_symbols_as_metadata_keys_with_true_values = true 25 | config.run_all_when_everything_filtered = true 26 | config.filter_run :focus 27 | 28 | config.order = 'random' 29 | end 30 | 31 | -------------------------------------------------------------------------------- /example/spec/web_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe server(:app) do 4 | describe http('http://app') do 5 | it "responds content including 'This is app server'" do 6 | expect(response.body).to include('This is app server') 7 | end 8 | it "responds as 'text/html'" do 9 | expect(response['content-type']).to eq('text/html') 10 | end 11 | it "responds OK 200" do 12 | expect(response.status).to eq(200) 13 | end 14 | end 15 | describe capybara('http://app') do 16 | it "responds content including 'This is app server'" do 17 | visit '/' 18 | expect(page).to have_content('This is app server') 19 | end 20 | end 21 | end 22 | 23 | describe server(:db) do 24 | describe mysql_query('SHOW STATUS') do 25 | it 'responds uptime' do 26 | row = results.find {|r| r['Variable_name'] == 'Uptime' } 27 | expect(row['Value'].to_i).to be > 0 28 | end 29 | end 30 | end 31 | 32 | describe server(:proxy) do 33 | describe http('http://app') do 34 | it "responds content including 'This is app server'" do 35 | expect(response.body).to include('This is app server') 36 | end 37 | it "responds as 'text/html'" do 38 | expect(response['content-type']).to eq('text/html') 39 | end 40 | end 41 | describe http('http://static') do 42 | it "responds content including 'Welcome to nginx!'" do 43 | expect(response.body).to include('Welcome to nginx!') 44 | end 45 | it "responds as 'text/html'" do 46 | expect(response['content-type']).to eq('text/html') 47 | end 48 | end 49 | describe capybara('http://app') do 50 | it "responds content including 'This is app server'" do 51 | visit '/' 52 | expect(page).to have_content('This is app server') 53 | end 54 | end 55 | describe capybara('http://static') do 56 | it "responds content including 'Welcome to nginx!'" do 57 | visit '/' 58 | expect(page).to have_content('Welcome to nginx!') 59 | end 60 | end 61 | end 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /infrataster.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'infrataster/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "infrataster" 8 | spec.version = Infrataster::VERSION 9 | spec.authors = ["Ryota Arai"] 10 | spec.email = ["ryota.arai@gmail.com"] 11 | spec.summary = %q{Infrastructure Behavior Testing Framework} 12 | spec.homepage = "https://github.com/ryotarai/infrataster" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0").delete_if do |path| 16 | path.start_with?('example/') || 17 | path.start_with?('ci/') 18 | end 19 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_runtime_dependency "rspec", ['>= 2.0', '< 4.0'] 24 | spec.add_runtime_dependency "net-ssh" 25 | spec.add_runtime_dependency "net-ssh-gateway" 26 | spec.add_runtime_dependency "capybara" 27 | spec.add_runtime_dependency "poltergeist" 28 | spec.add_runtime_dependency "faraday" 29 | spec.add_runtime_dependency "faraday_middleware", '>= 0.10.0' 30 | spec.add_runtime_dependency "thor" 31 | 32 | spec.add_development_dependency "bundler", "~> 1.5" 33 | spec.add_development_dependency "rake" 34 | spec.add_development_dependency "itamae" 35 | end 36 | -------------------------------------------------------------------------------- /lib/infrataster.rb: -------------------------------------------------------------------------------- 1 | require "infrataster/version" 2 | require "infrataster/resources" 3 | require "infrataster/server" 4 | require "infrataster/helpers" 5 | require "infrataster/contexts" 6 | require "infrataster/faraday_middlewares" 7 | require 'logger' 8 | 9 | module Infrataster 10 | Logger = ::Logger.new($stdout) 11 | if ENV['INFRATASTER_LOG'] 12 | Logger.level = ::Logger.const_get(ENV['INFRATASTER_LOG'].upcase) 13 | else 14 | Logger.level = ::Logger::ERROR 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/infrataster/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'fileutils' 3 | require 'erb' 4 | 5 | module Infrataster 6 | class CLI < Thor 7 | desc "init", "Initialize Infrataster specs." 8 | option :path, type: :string, default: '.' 9 | def init 10 | path = File.expand_path(options[:path]) 11 | FileUtils.mkdir_p(path) 12 | create_file_from_template(File.expand_path("Gemfile", path)) 13 | create_file_from_template(File.expand_path("Rakefile", path)) 14 | FileUtils.mkdir(File.expand_path("spec", path)) 15 | if ask_yes_or_no("Use Vagrant?") 16 | create_file_from_template(File.expand_path("spec/Vagrantfile", path)) 17 | end 18 | create_file_from_template(File.expand_path("spec/spec_helper.rb", path)) 19 | create_file_from_template(File.expand_path("spec/app_spec.rb", path)) 20 | end 21 | 22 | private 23 | def ask_yes_or_no(question) 24 | print "#{question} (y/N): " 25 | answer = $stdin.gets 26 | if answer =~ /^y/i 27 | true 28 | else 29 | false 30 | end 31 | end 32 | 33 | def create_file_from_template(path) 34 | basename = File.basename(path) 35 | 36 | if File.exist?(path) 37 | puts "#{basename} exists already. Skip." 38 | return 39 | end 40 | 41 | open(path, 'w') do |f| 42 | f.write(ERB.new(File.read(File.expand_path("../fixtures/#{basename}.erb", __FILE__))).result) 43 | end 44 | 45 | puts "Created: #{basename}" 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/infrataster/contexts.rb: -------------------------------------------------------------------------------- 1 | require 'infrataster/resources' 2 | require 'infrataster/contexts/base_context' 3 | require 'infrataster/contexts/no_resource_context' 4 | require 'infrataster/contexts/http_context' 5 | require 'infrataster/contexts/capybara_context' 6 | 7 | module Infrataster 8 | module Contexts 9 | class << self 10 | def from_example(example) 11 | eg = example_group(example) 12 | 13 | server_resource = find_described(Resources::ServerResource, eg) 14 | resource = find_described(Resources::BaseResource, eg) 15 | 16 | unless server_resource || resource 17 | # There is neither server_resource or resource 18 | return nil 19 | end 20 | 21 | if server_resource && !resource 22 | # Server is found but resource is not found 23 | return Contexts::NoResourceContext.new(server_resource.server) 24 | end 25 | 26 | resource.context_class.new(server_resource.server, resource) 27 | end 28 | 29 | private 30 | 31 | def example_group(example) 32 | if RSpec::Core::Version::STRING.start_with?('2') 33 | example.metadata[:example_group] 34 | else 35 | example.example_group 36 | end 37 | end 38 | 39 | def find_described(resource_class, example_group) 40 | arg = example_group_arg(example_group) 41 | if arg.is_a?(resource_class) 42 | arg 43 | else 44 | parent_eg = parent_example_group(example_group) 45 | find_described(resource_class, parent_eg) if parent_eg 46 | end 47 | end 48 | 49 | def parent_example_group(example_group) 50 | if RSpec::Core::Version::STRING.start_with?('2') 51 | example_group[:example_group] 52 | else 53 | example_group.parent_groups[1] 54 | end 55 | end 56 | 57 | def example_group_arg(example_group) 58 | if RSpec::Core::Version::STRING.start_with?('2') 59 | example_group[:description_args].first 60 | else 61 | example_group.metadata[:description_args].first 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/infrataster/contexts/base_context.rb: -------------------------------------------------------------------------------- 1 | module Infrataster 2 | module Contexts 3 | class BaseContext 4 | attr_reader :server 5 | attr_reader :resource 6 | 7 | def initialize(server, resource) 8 | @server = server 9 | @resource = resource 10 | end 11 | 12 | def current_server 13 | @server 14 | end 15 | end 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /lib/infrataster/contexts/capybara_context.rb: -------------------------------------------------------------------------------- 1 | require 'capybara' 2 | require 'capybara/rspec/matchers' 3 | require 'capybara/poltergeist' 4 | 5 | module Infrataster 6 | module Contexts 7 | class CapybaraContext < BaseContext 8 | CAPYBARA_DRIVER_NAME = :infrataster_driver 9 | 10 | def self.session 11 | @session ||= prepare_session 12 | end 13 | 14 | def self.prepare_session 15 | Capybara.register_driver CAPYBARA_DRIVER_NAME do |app| 16 | Capybara::Poltergeist::Driver.new( 17 | app, 18 | ) 19 | end 20 | Capybara::Session.new(CAPYBARA_DRIVER_NAME) 21 | end 22 | 23 | def initialize(*args) 24 | super(*args) 25 | end 26 | 27 | def session 28 | self.class.session 29 | end 30 | 31 | def page 32 | session 33 | end 34 | 35 | def before_each(example) 36 | example.example_group_instance.extend(Capybara::RSpecMatchers) 37 | 38 | session.driver.headers = {"Host" => resource.uri.host} 39 | 40 | address, port = server.forward_port(resource.uri.port) 41 | Capybara.app_host = "http://#{address}:#{port}" 42 | end 43 | 44 | Capybara::Session::DSL_METHODS.each do |method| 45 | define_method method do |*args, &block| 46 | page.send method, *args, &block 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/infrataster/contexts/http_context.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | require 'faraday_middleware' 3 | 4 | module Infrataster 5 | module Contexts 6 | class HttpContext < BaseContext 7 | def response 8 | server.forward_port(resource.uri.port) do |address, port| 9 | url = "#{resource.uri.scheme}://#{address}:#{port}" 10 | options = {:url => url} 11 | 12 | if resource.uri.scheme == 'https' 13 | options[:ssl] = resource.ssl_option 14 | end 15 | 16 | host = determine_host(address) 17 | 18 | conn = Faraday.new(options) do |faraday| 19 | faraday.request :url_encoded 20 | faraday.response :logger, Logger 21 | middlewares(host => address).each do |middleware| 22 | faraday.use(*middleware) 23 | end 24 | faraday.adapter Faraday.default_adapter 25 | faraday.basic_auth(*resource.basic_auth) if resource.basic_auth 26 | end 27 | 28 | conn.public_send(resource.method) do |req| 29 | resource.params.each_pair do |k, v| 30 | req.params[k] = v 31 | end 32 | req.headers['Host'] = host 33 | resource.headers.each_pair do |k, v| 34 | req.headers[k] = v 35 | end 36 | 37 | req.body = resource.body if resource.body 38 | 39 | req.url resource.uri.path 40 | end 41 | end 42 | end 43 | 44 | def determine_host(default) 45 | resource.uri.host || (server.options[:http] && server.options[:http][:host]) || default 46 | end 47 | 48 | private 49 | 50 | def middlewares(host_mapping) 51 | host_mapping = resource.host_mapping.merge(host_mapping) 52 | 53 | ms = resource.faraday_middlewares.dup 54 | if resource.follow_redirects? 55 | ms << [Infrataster::FaradayMiddlewares::FollowRedirects, host_mapping: host_mapping] 56 | end 57 | if resource.inflate_gzip? 58 | ms << [FaradayMiddleware::Gzip] 59 | end 60 | 61 | ms 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/infrataster/contexts/no_resource_context.rb: -------------------------------------------------------------------------------- 1 | module Infrataster 2 | module Contexts 3 | class NoResourceContext < BaseContext 4 | def initialize(server) 5 | super(server, nil) 6 | end 7 | end 8 | end 9 | end 10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/infrataster/faraday_middlewares.rb: -------------------------------------------------------------------------------- 1 | require 'infrataster/faraday_middlewares/follow_redirects' 2 | 3 | module Infrataster 4 | module FaradayMiddlewares 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/infrataster/faraday_middlewares/follow_redirects.rb: -------------------------------------------------------------------------------- 1 | require 'faraday_middleware' 2 | 3 | module Infrataster 4 | module FaradayMiddlewares 5 | class FollowRedirects < ::FaradayMiddleware::FollowRedirects 6 | def update_env(env, request_body, response) 7 | super.tap do |e| 8 | if replacement = @options[:host_mapping][e[:url].hostname] 9 | e[:request_headers]['Host'] = e[:url].hostname 10 | e[:url].hostname = replacement 11 | end 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/infrataster/fixtures/Gemfile.erb: -------------------------------------------------------------------------------- 1 | # Generated by `infrataster init` 2 | source "https://rubygems.org" 3 | 4 | gem "infrataster" 5 | 6 | # If you would like to test mysql, uncomment the next line. 7 | # gem "infrataster-plugin-mysql" 8 | -------------------------------------------------------------------------------- /lib/infrataster/fixtures/Rakefile.erb: -------------------------------------------------------------------------------- 1 | # Generated by `infrataster init` 2 | 3 | require "rspec/core/rake_task" 4 | 5 | def exec_and_abort_if_fail(cmd) 6 | system cmd 7 | unless $?.exitstatus == 0 8 | $stderr.puts "'#{cmd}' failed." 9 | abort 10 | end 11 | end 12 | 13 | desc 'Run tests' 14 | task :spec => ['spec:integration'] 15 | 16 | namespace :spec do 17 | RSpec::Core::RakeTask.new("integration") do |task| 18 | task.pattern = "./spec/{,/*/**}/*_spec.rb" 19 | end 20 | 21 | desc 'Prepare' 22 | task :prepare do 23 | exec_and_abort_if_fail '/usr/bin/vagrant up' 24 | exec_and_abort_if_fail '/usr/bin/vagrant provision' 25 | end 26 | 27 | desc 'Provision' 28 | task :provision do 29 | exec_and_abort_if_fail '/usr/bin/vagrant provision' 30 | end 31 | 32 | desc 'Restart VMs' 33 | task :restart do 34 | exec_and_abort_if_fail '/usr/bin/vagrant reload --provision' 35 | end 36 | 37 | desc 'Clean' 38 | task :clean do 39 | exec_and_abort_if_fail '/usr/bin/vagrant destroy -f' 40 | end 41 | end 42 | 43 | -------------------------------------------------------------------------------- /lib/infrataster/fixtures/Vagrantfile.erb: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | # 4 | # Generated by `infrataster init` 5 | 6 | VAGRANTFILE_API_VERSION = "2" 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | config.vm.box = "hashicorp/precise64" 9 | 10 | config.vm.define :app do |c| 11 | c.vm.network "private_network", ip: "192.168.44.10" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/infrataster/fixtures/app_spec.rb.erb: -------------------------------------------------------------------------------- 1 | # Generated by `infrataster init` 2 | require 'spec_helper' 3 | 4 | describe server(:app) do 5 | describe http('http://app') do 6 | it "responds OK 200" do 7 | expect(response.status).to eq(200) 8 | end 9 | end 10 | end 11 | 12 | -------------------------------------------------------------------------------- /lib/infrataster/fixtures/spec_helper.rb.erb: -------------------------------------------------------------------------------- 1 | # Generated by `infrataster init` 2 | require 'infrataster/rspec' 3 | 4 | ENV['VAGRANT_CWD'] = File.dirname(__FILE__) 5 | 6 | Infrataster::Server.define( 7 | :app, 8 | '192.168.0.0/16', 9 | vagrant: true, 10 | ) 11 | 12 | RSpec.configure do |config| 13 | config.treat_symbols_as_metadata_keys_with_true_values = true 14 | config.run_all_when_everything_filtered = true 15 | config.filter_run :focus 16 | 17 | config.order = 'random' 18 | end 19 | -------------------------------------------------------------------------------- /lib/infrataster/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'infrataster/helpers/resource_helper' 2 | require 'infrataster/helpers/rspec_helper' 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/infrataster/helpers/resource_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'infrataster/resources' 3 | 4 | module Infrataster 5 | module Helpers 6 | module ResourceHelper 7 | if RSpec::Version::STRING.start_with?('2.') 8 | include RSpec::Matchers 9 | end 10 | 11 | def server(*args) 12 | Resources::ServerResource.new(*args) 13 | end 14 | 15 | def http(*args) 16 | Resources::HttpResource.new(*args) 17 | end 18 | 19 | def mysql_query(*args) 20 | Resources::MysqlQueryResource.new(*args) 21 | end 22 | 23 | def capybara(*args) 24 | Resources::CapybaraResource.new(*args) 25 | end 26 | end 27 | end 28 | end 29 | 30 | -------------------------------------------------------------------------------- /lib/infrataster/helpers/rspec_helper.rb: -------------------------------------------------------------------------------- 1 | module Infrataster 2 | module Helpers 3 | module RSpecHelper 4 | def method_missing(method, *args) 5 | if current_infrataster_context.respond_to?(method) 6 | return current_infrataster_context.public_send(method, *args) 7 | end 8 | 9 | super 10 | end 11 | 12 | def current_infrataster_context 13 | @infrataster_context 14 | end 15 | end 16 | end 17 | end 18 | 19 | 20 | -------------------------------------------------------------------------------- /lib/infrataster/resources.rb: -------------------------------------------------------------------------------- 1 | require 'infrataster/resources/base_resource' 2 | require 'infrataster/resources/server_resource' 3 | require 'infrataster/resources/http_resource' 4 | require 'infrataster/resources/capybara_resource' 5 | -------------------------------------------------------------------------------- /lib/infrataster/resources/base_resource.rb: -------------------------------------------------------------------------------- 1 | module Infrataster 2 | module Resources 3 | class BaseResource 4 | # do nothing 5 | def name 6 | self.class.name.split('::').last[0...(-1 * 'Resource'.size)] 7 | end 8 | 9 | def context_class 10 | Contexts.const_get("#{name}Context") 11 | end 12 | end 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /lib/infrataster/resources/capybara_resource.rb: -------------------------------------------------------------------------------- 1 | require 'infrataster/resources/base_resource' 2 | require 'uri' 3 | 4 | module Infrataster 5 | module Resources 6 | class CapybaraResource < BaseResource 7 | Error = Class.new(StandardError) 8 | 9 | attr_reader :uri 10 | 11 | def initialize(url) 12 | @uri = URI.parse(url) 13 | end 14 | 15 | def to_s 16 | "capybara '#{@uri}'" 17 | end 18 | end 19 | end 20 | end 21 | 22 | -------------------------------------------------------------------------------- /lib/infrataster/resources/http_resource.rb: -------------------------------------------------------------------------------- 1 | require 'infrataster/resources/base_resource' 2 | require 'uri' 3 | 4 | module Infrataster 5 | module Resources 6 | class HttpResource < BaseResource 7 | Error = Class.new(StandardError) 8 | 9 | attr_reader :uri, :options 10 | 11 | def initialize(url_str, options = {}) 12 | @options = {params: {}, method: :get, headers: {}}.merge(options) 13 | @uri = URI.parse(url_str) 14 | if @uri.scheme 15 | unless %w!http https!.include?(@uri.scheme) 16 | raise Error, "The provided url, '#{@uri}', is not http or https." 17 | end 18 | else 19 | @uri = URI::HTTP.build([@uri.userinfo, @uri.host, @uri.port, @uri.path, @uri.query, @uri.fragment]) 20 | end 21 | end 22 | 23 | def to_s 24 | "http '#{@uri}' with #{@options}" 25 | end 26 | 27 | def params 28 | @options[:params] 29 | end 30 | 31 | def method 32 | valid_methods = [:get, :head, :delete, :post, :put, :patch] 33 | unless valid_methods.include?(@options[:method]) 34 | raise Error, "#{@options[:method]} is not supported HTTP method." 35 | end 36 | 37 | @options[:method] 38 | end 39 | 40 | def headers 41 | @options[:headers] 42 | end 43 | 44 | def body 45 | @options[:body] 46 | end 47 | 48 | def ssl_option 49 | @options[:ssl] 50 | end 51 | 52 | def basic_auth 53 | @options[:basic_auth] 54 | end 55 | 56 | def inflate_gzip? 57 | !!@options[:inflate_gzip] 58 | end 59 | 60 | def follow_redirects? 61 | !!@options[:follow_redirects] 62 | end 63 | 64 | def faraday_middlewares 65 | (@options[:faraday_middlewares] || []).map do |m| 66 | Array(m) 67 | end 68 | end 69 | 70 | def host_mapping 71 | @options[:host_mapping] || {} 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/infrataster/resources/server_resource.rb: -------------------------------------------------------------------------------- 1 | module Infrataster 2 | module Resources 3 | class ServerResource 4 | Error = Class.new(StandardError) 5 | 6 | attr_reader :name 7 | 8 | def initialize(name) 9 | @name = name 10 | end 11 | 12 | def to_s 13 | desc = "server '#{server.name}'" 14 | desc += " from '#{server.from.name}'" if server.from 15 | desc 16 | end 17 | 18 | def address 19 | server.address 20 | end 21 | 22 | def server 23 | Server.find_by_name(@name) 24 | end 25 | end 26 | end 27 | end 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/infrataster/rspec.rb: -------------------------------------------------------------------------------- 1 | require 'infrataster' 2 | require 'rspec' 3 | 4 | include Infrataster::Helpers::ResourceHelper 5 | 6 | RSpec.configure do |config| 7 | config.include Infrataster::Helpers::RSpecHelper 8 | 9 | fetch_current_example = RSpec.respond_to?(:current_example) ? 10 | proc { RSpec.current_example } 11 | : proc { |context| context.example } 12 | 13 | config.before(:all) do 14 | @infrataster_context = Infrataster::Contexts.from_example(self.class) 15 | end 16 | 17 | config.before(:each) do 18 | current_example = fetch_current_example.call(self) 19 | @infrataster_context = Infrataster::Contexts.from_example(current_example) 20 | @infrataster_context.before_each(current_example) if @infrataster_context.respond_to?(:before_each) 21 | end 22 | 23 | config.after(:each) do 24 | current_example = fetch_current_example.call(self) 25 | @infrataster_context.after_each(current_example) if @infrataster_context.respond_to?(:after_each) 26 | end 27 | 28 | config.after(:all) do 29 | Infrataster::Server.defined_servers.each do |server| 30 | server.shutdown_gateway 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/infrataster/server.rb: -------------------------------------------------------------------------------- 1 | require 'tmpdir' 2 | require 'net/ssh' 3 | require 'net/ssh/gateway' 4 | require 'ipaddr' 5 | require 'shellwords' 6 | require 'ostruct' 7 | 8 | module Infrataster 9 | class Server 10 | Error = Class.new(StandardError) 11 | 12 | class << self 13 | 14 | def define(name, *args, &block) 15 | address = args.shift 16 | options = args.any? ? args.shift : {} 17 | if block 18 | st = OpenStruct.new 19 | block.call(st) 20 | address = st.address if st.address 21 | st.each_pair { |k, v| options[k] = v unless k == :address } 22 | end 23 | @@servers << Server.new(name, address, options) 24 | end 25 | 26 | def defined_servers 27 | @@servers 28 | end 29 | 30 | def clear_defined_servers 31 | @@servers = [] 32 | end 33 | 34 | def find_by_name(name) 35 | server = @@servers.find {|s| s.name == name } 36 | unless server 37 | raise Error, "Server definition for '#{name}' is not found." 38 | end 39 | server 40 | end 41 | 42 | Server.clear_defined_servers 43 | end 44 | 45 | attr_reader :name, :address, :options 46 | 47 | def initialize(name, address, options = {}) 48 | @name, @options = name, options 49 | @address = determine_address(address) 50 | @gateway = nil 51 | end 52 | 53 | def from 54 | if @options[:from] 55 | Server.find_by_name(@options[:from]) 56 | end 57 | end 58 | 59 | def find_available_port 60 | # find available local port 61 | server = TCPServer.new('127.0.0.1', 0) 62 | available_port = server.addr[1] 63 | server.close 64 | 65 | available_port 66 | end 67 | 68 | def gateway 69 | @gateway ||= Net::SSH::Gateway.new(*ssh_start_args) 70 | end 71 | 72 | def shutdown_gateway 73 | if @gateway 74 | @gateway.shutdown! 75 | @gateway = nil 76 | end 77 | end 78 | 79 | def forward_port(port, &block) 80 | host, forwarded_port = _forward_port(port) 81 | if block_given? 82 | return_value = block.call(host, forwarded_port) 83 | from.gateway.close(forwarded_port) if from 84 | return_value 85 | else 86 | [host, forwarded_port] 87 | end 88 | end 89 | 90 | def ssh(&block) 91 | Net::SSH.start(*ssh_start_args) do |ssh| 92 | block.call(ssh) 93 | end 94 | end 95 | 96 | def ssh_exec(cmd, &block) 97 | result = nil 98 | ssh do |ssh| 99 | result = ssh.exec!(cmd, &block) 100 | end 101 | result 102 | end 103 | 104 | def ssh_start_args 105 | @ssh_start_args ||= _ssh_start_args 106 | end 107 | 108 | private 109 | 110 | def _forward_port(port) 111 | if from 112 | local_port = from.gateway.open(@address, port, find_available_port) 113 | ['127.0.0.1', local_port] 114 | else 115 | # no need to forward port 116 | [@address, port] 117 | end 118 | end 119 | 120 | def _ssh_start_args 121 | config = {} 122 | 123 | if @options[:ssh] 124 | config = @options[:ssh] 125 | config[:host_name] ||= @address 126 | elsif @options[:vagrant] 127 | vagrant_name = if @options[:vagrant] != true 128 | @options[:vagrant] 129 | else 130 | @name 131 | end 132 | config = ssh_config_for_vagrant(vagrant_name) 133 | else 134 | raise Error, "Can't get configuration to connect to the server via SSH. Please set ssh option or vagrant option." 135 | end 136 | 137 | [config[:host_name], config[:user], config] 138 | end 139 | 140 | def ssh_config_for_vagrant(name) 141 | config = nil 142 | 143 | Dir.mktmpdir do |dir| 144 | output = File.join(dir, 'ssh-config') 145 | Bundler.with_clean_env do 146 | `vagrant ssh-config #{Shellwords.shellescape(name)} > #{Shellwords.shellescape(output)}` 147 | if $?.exitstatus != 0 148 | raise Error, "`vagrant ssh-config` failed. Please check if VMs are running or not." 149 | end 150 | end 151 | config = Net::SSH::Config.for(@name.to_s, [output]) 152 | end 153 | 154 | config 155 | end 156 | 157 | def fetch_all_addresses 158 | result = [] 159 | uname = ssh_exec('uname -s') 160 | case uname 161 | when /(FreeBSD|OpenBSD)/ 162 | ssh_exec('/sbin/ifconfig').each_line do |line| 163 | # inet 169.254.252.26 netmask 0xfffffffc broadcast 169.254.252.27 (regular interface) 164 | # inet 10.100.255.2 --> 10.100.255.1 netmask 0xffffffff (tunnel interface) 165 | if %r{inet (\S+) (?:-->[ ].+[ ])?netmask} =~ line 166 | result << $1 167 | end 168 | end 169 | else 170 | ssh_exec('/sbin/ip addr').each_line do |line| 171 | # Linux 172 | # inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0 173 | if %r{inet ([^/]+)} =~ line 174 | result << $1 175 | end 176 | end 177 | end 178 | 179 | result 180 | end 181 | 182 | def determine_address(address) 183 | begin 184 | ipaddr = IPAddr.new(address) 185 | rescue IPAddr::InvalidAddressError 186 | return address 187 | end 188 | 189 | Logger.debug("Determining ip address...") 190 | 191 | if ipaddr.to_range.begin == ipaddr.to_range.end 192 | # subnet mask is 255.255.255.255 193 | return ipaddr.to_s 194 | end 195 | 196 | all_addresses = fetch_all_addresses 197 | Logger.debug(all_addresses) 198 | 199 | matched = all_addresses.select do |a| 200 | ipaddr.include?(a) 201 | end 202 | Logger.debug(matched) 203 | 204 | if matched.empty? 205 | raise Error, "No IP address matching #{ipaddr} is not found." 206 | elsif matched.size > 1 207 | raise Error, "Multiple IP addresses matching #{ipaddr} are found." 208 | end 209 | 210 | matched.first 211 | end 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /lib/infrataster/version.rb: -------------------------------------------------------------------------------- 1 | module Infrataster 2 | VERSION = "0.3.2" 3 | end 4 | -------------------------------------------------------------------------------- /spec/integration/capybara_spec.rb: -------------------------------------------------------------------------------- 1 | require 'integration/spec_helper' 2 | 3 | describe server(:proxy) do 4 | describe capybara('http://app.example.com') do 5 | it 'has content "app"' do 6 | visit '/' 7 | expect(page).to have_content('app') 8 | end 9 | end 10 | describe capybara('http://static.example.com') do 11 | it 'has content "static"' do 12 | visit '/' 13 | expect(page).to have_content('static') 14 | end 15 | end 16 | end 17 | 18 | describe server(:app) do 19 | describe capybara('http://app.example.com') do 20 | it 'has content "app"' do 21 | visit '/' 22 | expect(page).to have_content('app') 23 | end 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /spec/integration/http_spec.rb: -------------------------------------------------------------------------------- 1 | require 'integration/spec_helper' 2 | require 'json' 3 | 4 | describe server(:proxy) do 5 | describe http('http://app.example.com') do 6 | it "sends GET request with Host header" do 7 | expect(response.body).to include('app') 8 | end 9 | end 10 | 11 | describe http('http://static.example.com') do 12 | it "sends GET request with Host header" do 13 | expect(response.body).to include('static') 14 | end 15 | end 16 | 17 | describe http('http://static.example.com', headers: {"Accept-Encoding" => "gzip"}) do 18 | it "gets response compressed by gzip" do 19 | expect(response.headers['content-encoding']).to eq('gzip') 20 | end 21 | end 22 | 23 | describe http('http://static.example.com', headers: {"Accept-Encoding" => "gzip"}, inflate_gzip: true) do 24 | it "gets response inflated by gzip" do 25 | expect(response.headers['content-encoding']).to be_nil 26 | expect(response.body).to eq("This is static site.\n") 27 | end 28 | end 29 | 30 | describe http('http://static.example.com/auth') do 31 | it "sends GET request without basic auth" do 32 | expect(response.status).to eq 401 33 | end 34 | end 35 | 36 | describe http('http://static.example.com/auth', basic_auth: ['dummy', 'dummy']) do 37 | it "sends GET request with basic auth" do 38 | expect(response.status).to eq 200 39 | expect(response.body).to include('auth') 40 | end 41 | end 42 | 43 | describe http('http://static.example.com/redirect') do 44 | it "doesn't follow redirects" do 45 | expect(response.status).to eq 302 46 | end 47 | end 48 | 49 | describe http('http://static.example.com/redirect', follow_redirects: true) do 50 | it "follows redirects" do 51 | expect(response.status).to eq 200 52 | expect(response.body).to eq("This is static site.\n") 53 | end 54 | end 55 | 56 | describe http('https://static.example.com/redirect', follow_redirects: true, ssl: {verify: false}) do 57 | it "follows redirects" do 58 | expect(response.status).to eq 200 59 | expect(response.body).to eq("This is static site.\n") 60 | end 61 | end 62 | end 63 | 64 | describe server(:app) do 65 | let(:body_as_json) { JSON.parse(response.body) } 66 | 67 | describe http('http://app.example.com') do 68 | it "sends GET request" do 69 | expect(response.body).to include('app') 70 | end 71 | end 72 | 73 | describe http('http://app.example.com/path/to/resource', params: {'foo' => 'bar'}, headers: {'USER' => 'VALUE'}) do 74 | it "sends GET request with params" do 75 | expect(body_as_json['method']).to eq('GET') 76 | expect(body_as_json['path']).to eq('/path/to/resource') 77 | expect(body_as_json['params']).to eq({"foo" => "bar"}) 78 | expect(body_as_json['headers']['USER']).not_to be_empty 79 | expect(body_as_json['headers']['USER']).to eq('VALUE') 80 | end 81 | end 82 | 83 | describe http('http://app.example.com', method: :post, params: {'foo' => 'bar'}, headers: {'USER' => 'VALUE'}) do 84 | it "sends POST request with params" do 85 | expect(body_as_json['method']).to eq('POST') 86 | expect(body_as_json['params']).to eq({"foo" => "bar"}) 87 | expect(body_as_json['headers']['USER']).not_to be_empty 88 | expect(body_as_json['headers']['USER']).to eq('VALUE') 89 | end 90 | end 91 | 92 | describe http('http://app.example.com', method: :post, body: {'foo' => 'bar'}, headers: {'USER' => 'VALUE'}) do 93 | it "sends POST request with body entity" do 94 | expect(body_as_json['method']).to eq('POST') 95 | expect(body_as_json['body']).to eq("foo=bar") 96 | expect(body_as_json['headers']['USER']).not_to be_empty 97 | expect(body_as_json['headers']['USER']).to eq('VALUE') 98 | end 99 | end 100 | 101 | describe http('http://app.example.com', method: :post, body: {'foo' => 'bar'}.to_json, headers: {'USER' => 'VALUE'}) do 102 | it "sends POST request with body entity as JSON format" do 103 | expect(body_as_json['method']).to eq('POST') 104 | expect(body_as_json['body']).to eq({"foo" => "bar"}.to_json) 105 | expect(body_as_json['headers']['USER']).not_to be_empty 106 | expect(body_as_json['headers']['USER']).to eq('VALUE') 107 | end 108 | end 109 | 110 | describe http('/path/to/resource') do 111 | it "sends GET request with scheme and host" do 112 | expect(body_as_json['headers']['HOST']).to eq('example.com') 113 | end 114 | end 115 | end 116 | 117 | describe server(:example_com) do 118 | describe http('https://example.com') do 119 | it "is example domain" do 120 | expect(response.body).to include('Example Domain') 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /spec/integration/other_spec.rb: -------------------------------------------------------------------------------- 1 | require 'integration/spec_helper' 2 | 3 | describe 'Normal subject not related to Infrataster' do 4 | it "doesn't raise any error" do 5 | expect do 6 | subject 7 | end.not_to raise_error 8 | end 9 | end 10 | 11 | describe server(:proxy) do 12 | let(:time) { Time.now } 13 | before :all do 14 | @before_all_time = Time.now 15 | current_server.ssh_exec "echo 'Hello once' > /tmp/test-once-#{@before_all_time.to_i}" 16 | end 17 | before do 18 | current_server.ssh_exec "echo 'Hello' > /tmp/test-#{time.to_i}" 19 | end 20 | it "executes a command on the current server" do 21 | result = current_server.ssh_exec("cat /tmp/test-#{time.to_i}") 22 | expect(result.chomp).to eq('Hello') 23 | end 24 | it "connects to the current server via SSH" do 25 | current_server.ssh do |ssh| 26 | expect(ssh.exec!('echo -n Hello')).to eq('Hello') 27 | end 28 | end 29 | it "executes a command on the current server in before all block" do 30 | result = current_server.ssh_exec("cat /tmp/test-once-#{@before_all_time.to_i}") 31 | expect(result.chomp).to eq('Hello once') 32 | end 33 | 34 | end 35 | 36 | -------------------------------------------------------------------------------- /spec/integration/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'infrataster/rspec' 2 | 3 | Infrataster::Server.define( 4 | :proxy, 5 | '192.168.0.0/16', 6 | vagrant: true, 7 | ) 8 | Infrataster::Server.define( 9 | :app, 10 | '172.16.0.0/16', 11 | vagrant: true, 12 | from: :proxy, 13 | http: {host: 'example.com'}, 14 | ) 15 | Infrataster::Server.define( 16 | :example_com, 17 | 'example.com', 18 | ) 19 | 20 | RSpec.configure do |config| 21 | config.order = 'random' 22 | end 23 | -------------------------------------------------------------------------------- /spec/integration/vm/.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | Berksfile.lock 3 | /node.json 4 | -------------------------------------------------------------------------------- /spec/integration/vm/Berksfile: -------------------------------------------------------------------------------- 1 | source "http://api.berkshelf.com" 2 | cookbook 'apt' 3 | -------------------------------------------------------------------------------- /spec/integration/vm/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | require 'vagrant-itamae' 5 | 6 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 7 | VAGRANTFILE_API_VERSION = "2" 8 | 9 | INTERNAL_ADDRESSES = { 10 | 'proxy' => '172.16.44.10', 11 | 'app' => '172.16.44.11', 12 | } 13 | 14 | node_json = File.join(__dir__, 'node.json') 15 | File.write(node_json, {'addresses' => INTERNAL_ADDRESSES}.to_json) 16 | 17 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 18 | config.vm.box = "ubuntu/trusty64" 19 | 20 | config.vm.define :proxy do |c| 21 | c.vm.network "private_network", ip: "192.168.44.10" 22 | c.vm.network "private_network", ip: INTERNAL_ADDRESSES['proxy'], virtualbox__intnet: "infrataster-integration-test" 23 | 24 | c.vm.provision :itamae do |c| 25 | c.sudo = true 26 | c.recipes = [File.join(__dir__, 'recipes/proxy/default.rb')] 27 | c.json = node_json 28 | end 29 | end 30 | 31 | config.vm.define :app do |c| 32 | c.vm.network "private_network", ip: INTERNAL_ADDRESSES['app'], virtualbox__intnet: "infrataster-integration-test" 33 | 34 | c.vm.provision :itamae do |c| 35 | c.sudo = true 36 | c.recipes = [File.join(__dir__, 'recipes/app/default.rb')] 37 | c.json = node_json 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/integration/vm/app/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sinatra' 4 | 5 | -------------------------------------------------------------------------------- /spec/integration/vm/app/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'json' 3 | 4 | get '/' do 5 | result.to_json 6 | end 7 | 8 | get '/path/to/resource' do 9 | result.to_json 10 | end 11 | 12 | post '/' do 13 | result.to_json 14 | end 15 | 16 | def result 17 | { 18 | 'app' => 'sinatra', 19 | 'method' => request.request_method, 20 | 'path' => request.path_info, 21 | 'params' => params, 22 | 'body' => request.body.read, 23 | 'headers' => RequestWrapper.new(request).headers, 24 | } 25 | end 26 | 27 | class RequestWrapper 28 | def initialize(request) 29 | @request = request 30 | end 31 | 32 | def headers 33 | headers = @request.env.select do |k, v| 34 | k.start_with?('HTTP_') 35 | end.map do |k, v| 36 | [k[5..-1], v] 37 | end 38 | Hash[headers] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/integration/vm/app/config.ru: -------------------------------------------------------------------------------- 1 | require './app' 2 | run Sinatra::Application 3 | -------------------------------------------------------------------------------- /spec/integration/vm/recipes/app/default.rb: -------------------------------------------------------------------------------- 1 | execute "sed -i -e 's|http://archive.ubuntu.com/ubuntu|mirror://mirrors.ubuntu.com/mirrors.txt|g' /etc/apt/sources.list" 2 | 3 | execute 'apt-get update' 4 | 5 | package 'python-software-properties' 6 | 7 | execute 'apt-add-repository -y ppa:brightbox/ruby-ng && apt-get update' 8 | 9 | package 'build-essential' 10 | 11 | package 'ruby2.2' 12 | 13 | package 'ruby2.2-dev' 14 | 15 | gem_package "bundler" 16 | 17 | execute 'bundle install' do 18 | cwd '/vagrant/app' 19 | end 20 | 21 | # supervisor 22 | package 'supervisor' 23 | 24 | execute 'reload supervisor' do 25 | command 'supervisorctl reload' 26 | action :nothing 27 | end 28 | 29 | remote_file '/etc/supervisor/conf.d/rackup.conf' do 30 | notifies :run, 'execute[reload supervisor]' 31 | end 32 | 33 | execute 'supervisorctl restart rackup' 34 | -------------------------------------------------------------------------------- /spec/integration/vm/recipes/app/files/etc/supervisor/conf.d/rackup.conf: -------------------------------------------------------------------------------- 1 | [program:rackup] 2 | directory=/vagrant/app 3 | command=rackup -o 0.0.0.0 -p 80 config.ru 4 | -------------------------------------------------------------------------------- /spec/integration/vm/recipes/proxy/default.rb: -------------------------------------------------------------------------------- 1 | execute "sed -i -e 's|http://archive.ubuntu.com/ubuntu|mirror://mirrors.ubuntu.com/mirrors.txt|g' /etc/apt/sources.list" 2 | 3 | execute 'apt-get update' 4 | 5 | package 'nginx' 6 | 7 | service 'nginx' do 8 | action :start 9 | end 10 | 11 | file '/etc/nginx/sites-enabled/default' do 12 | action :delete 13 | end 14 | 15 | template '/etc/nginx/sites-available/integration-test' do 16 | notifies :restart, 'service[nginx]' 17 | end 18 | 19 | link '/etc/nginx/sites-enabled/integration-test' do 20 | to '/etc/nginx/sites-available/integration-test' 21 | end 22 | 23 | %w!server.key server.crt!.each do |name| 24 | remote_file "/etc/nginx/#{name}" do 25 | notifies :restart, 'service[nginx]' 26 | end 27 | end 28 | 29 | remote_file '/usr/share/nginx/html/index.html' do 30 | mode '644' 31 | end 32 | 33 | remote_file '/usr/share/nginx/html/auth' do 34 | mode '644' 35 | end 36 | 37 | remote_file '/etc/nginx/.htpasswd' do 38 | mode '644' 39 | end 40 | 41 | -------------------------------------------------------------------------------- /spec/integration/vm/recipes/proxy/files/etc/nginx/.htpasswd: -------------------------------------------------------------------------------- 1 | dummy:$apr1$xLfour3i$43BqMHRaaXxY8eRzmBtKr1 2 | -------------------------------------------------------------------------------- /spec/integration/vm/recipes/proxy/files/etc/nginx/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBjCCAe4CCQDNVnu9+HqZhTANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB 3 | VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 4 | cyBQdHkgTHRkMB4XDTE1MTEyNDE1MDgxOVoXDTI1MTEyMTE1MDgxOVowRTELMAkG 5 | A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 6 | IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 7 | AK8LwaN6dZ/CtK0nf33BZ16bFieqCIApqSziZzVqkARu5kOPtacB9QieJpSf/yJS 8 | DHFZbcSKZ5d+9Ga/noN92U2JM+tZ54NeQJWny2GgxPrhH9VYgWegBNIsHBu4FAha 9 | JVq2IFtkRRmJQX0gDd2BcfQ5AQgTWT/rh7O4rrSgESbmN27yrMqH0H3FI0mz3+mX 10 | 5pMrq5GepSL9bMMvH8vyeJHNIhIijhKoSwanZn/8LvvLfXcnVr58eOfQCmwhigbx 11 | FKdZ9EO7lJ06U0nR0JBHBrnOpFunITgTHBMQBUjBNm6j80F+OtkCB2QlvHhTCDHd 12 | onaw6D8ntep6MhoXbduqlOcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAPg2ftIdr 13 | LazJXjMhQOZt4PccT5TilbxpbR2xosacxpCWKv2ll5SIdWDNb1AMvDR1l4oE5OKA 14 | ow+pqecMps6CW/aqPGkSS25kycuETOOjqCCHbS2DZOLKXlulFLgPkmjoJH5aJo9B 15 | Uf5Ywdu1V6kVVv8AnBEbAVA1kMGp6ueO4R3UpSPGWc5PHu+FDzs+d7+yEX/G31oa 16 | OExETGvcTWsCzHjYUSI7oz2FpeVuCAz5WlbONxInS83Q1PXXoWSd641H9wzbAVSO 17 | 4Rxf39ZaJDiKWzjuBBxKyatL/v9ErqS+aZ+NlAUzVL8a6CMcRI2miENu+F1+djCG 18 | kkDhYNRTsyV0YA== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /spec/integration/vm/recipes/proxy/files/etc/nginx/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEArwvBo3p1n8K0rSd/fcFnXpsWJ6oIgCmpLOJnNWqQBG7mQ4+1 3 | pwH1CJ4mlJ//IlIMcVltxIpnl370Zr+eg33ZTYkz61nng15AlafLYaDE+uEf1ViB 4 | Z6AE0iwcG7gUCFolWrYgW2RFGYlBfSAN3YFx9DkBCBNZP+uHs7iutKARJuY3bvKs 5 | yofQfcUjSbPf6ZfmkyurkZ6lIv1swy8fy/J4kc0iEiKOEqhLBqdmf/wu+8t9dydW 6 | vnx459AKbCGKBvEUp1n0Q7uUnTpTSdHQkEcGuc6kW6chOBMcExAFSME2bqPzQX46 7 | 2QIHZCW8eFMIMd2idrDoPye16noyGhdt26qU5wIDAQABAoIBABETS4Nf+cNo4ekW 8 | NkiVKjk/Vazu2D9wUPFw8HJ0f7H/A3VJuoWdGkMJ8meDKAc4C8Cu4ZdPjQVpO/wd 9 | 6x+WkjEPFkJ6Faxp8FDo+lz+qthyR4GvTYhHQtEhvmgxz7p02Ln9lDB2GouAKSRE 10 | kTWvX+db/C7GfGlQ137/oAM8rg+mMirjRpITG1K3P+cot3+aPWbUgB7UC5mszHiB 11 | KidQpw3lG89/LQz3XJ6rctJLAQVzInUrZrz0/6kshVI76Gg8u5ioDJujYl4R9v5B 12 | xbfCIVq/HTHMLj5kteiWtCFhSfJiBGkn0bqVEhMn9J761vDYghUvSRLs+G++Z3PM 13 | xTt/rBECgYEA1ZQ66T6Y7CibeRqNkBI7mLR8XXya6NDtcZbz1j/k2F6tVCZKIZcT 14 | BfHoAdrlaqhxY61wExYeJcT2SSOSxSY8cFzdNH5UiGv1XoVXLWX6EjXPXbPcgzLW 15 | ri6aGxjTakXjDhuoJiPgOMBQrgSF+Vb1c8zRT2flR9pG3TnhNzMuLzkCgYEA0dA/ 16 | p3Y+CubmNMyoOkjvbMfnXklin0zM+2ahSuOtQ/84kDCFZBufIx9lg+OvM3gQ3UUE 17 | MS7i2FCdB16xV1onIjaPR182CKaBGXBQ/indv74pfAmQO1RdSwrWjT4B7SseFdWK 18 | Ctc3pRDACtnarRSsFFO5dwkSe6ZKglE/GhicxR8CgYA+1Oy/0XqLfya96eHbkIGX 19 | 2wD7P70S4OP5lr8FOKbKQJAdQWZtm1QtW401foAfcI5rFIr0En2r/tW1jpGPDkCZ 20 | jbhFySR1Pc1hkTikUmvPD6WBm3gz4LDnbS5I6nTHvuedLuW40rUT/P+/MfV39O0g 21 | tBCfm6EIEl7mljB7XMMpgQKBgQCrmqoVa45/GC89DA6j7Td2P2IrKdZx+DzHej99 22 | ow35yWsX87eGE1lrPqim4LhRXmhcsbAmEfUTe4uTlY1yppfPUdSKSYNGO/jnN4fm 23 | +ZMW2o/PlXrUK8JKOEkXKJomwtdOgGLxTumCJlsMBPboDTpZPck5ZMArRe02/QCh 24 | dXPzfQKBgD3s0D//+QTT0cwRdgGi9Mn1FWkjKfZVrLhjlwrdMOMTFB690LBGYm7Z 25 | 0Sd0aFVkfHGsHyl0MF5XfKBGIjIreI/yrncYJ81CEeWCUG7E+PjNu5ahkB15zarP 26 | 3jskXc5ccH0UsJC+/fUvbwAssptoCCpkeU5qcMvMrboip6VkW7+a 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /spec/integration/vm/recipes/proxy/files/usr/share/nginx/html/auth: -------------------------------------------------------------------------------- 1 | auth 2 | -------------------------------------------------------------------------------- /spec/integration/vm/recipes/proxy/files/usr/share/nginx/html/index.html: -------------------------------------------------------------------------------- 1 | This is static site. 2 | -------------------------------------------------------------------------------- /spec/integration/vm/recipes/proxy/templates/etc/nginx/sites-available/integration-test.erb: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen 443 ssl; 4 | 5 | ssl_certificate /etc/nginx/server.crt; 6 | ssl_certificate_key /etc/nginx/server.key; 7 | 8 | gzip on; 9 | gzip_min_length 0; 10 | 11 | root /usr/share/nginx/html; 12 | index index.html index.htm; 13 | 14 | server_name static.example.com; 15 | 16 | location / { 17 | try_files $uri $uri/ /index.html; 18 | } 19 | 20 | location = /auth { 21 | auth_basic "Restricted"; 22 | auth_basic_user_file /etc/nginx/.htpasswd; 23 | } 24 | 25 | location = /redirect { 26 | return 302 $scheme://$server_name/; 27 | } 28 | } 29 | 30 | server { 31 | listen 80; 32 | 33 | root /usr/share/nginx/html; 34 | index index.html index.htm; 35 | 36 | server_name app.example.com; 37 | 38 | location / { 39 | proxy_pass http://<%= node['addresses']['app'] %>; 40 | } 41 | } 42 | 43 | 44 | # another virtual host using mix of IP-, name-, and port-based configuration 45 | # 46 | #server { 47 | # listen 8000; 48 | # listen somename:8080; 49 | # server_name somename alias another.alias; 50 | # root html; 51 | # index index.html index.htm; 52 | # 53 | # location / { 54 | # try_files $uri $uri/ /index.html; 55 | # } 56 | #} 57 | 58 | 59 | # HTTPS server 60 | # 61 | #server { 62 | # listen 443; 63 | # server_name localhost; 64 | # 65 | # root html; 66 | # index index.html index.htm; 67 | # 68 | # ssl on; 69 | # ssl_certificate cert.pem; 70 | # ssl_certificate_key cert.key; 71 | # 72 | # ssl_session_timeout 5m; 73 | # 74 | # ssl_protocols SSLv3 TLSv1; 75 | # ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP; 76 | # ssl_prefer_server_ciphers on; 77 | # 78 | # location / { 79 | # try_files $uri $uri/ /index.html; 80 | # } 81 | #} 82 | -------------------------------------------------------------------------------- /spec/unit/lib/infrataster/contexts/http_context_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit/spec_helper' 2 | 3 | module Infrataster 4 | module Contexts 5 | describe HttpContext do 6 | context "with relative URI resource" do 7 | let(:address) { '127.0.0.1' } 8 | let(:port) { 80 } 9 | let(:resource) { Resources::HttpResource.new('/path/to/resource') } 10 | subject { described_class.new(server, resource) } 11 | 12 | context "with http host definition" do 13 | let(:server) { Server.new('example.com', address, http: {:host => 'example.com'}) } 14 | 15 | it "complements host with server definition" do 16 | expect(subject.determine_host(address)).to eq('example.com') 17 | end 18 | end 19 | 20 | context "with no http host definition" do 21 | let(:server) { Server.new('example.com', address) } 22 | 23 | it "complements host with ip address" do 24 | expect(subject.determine_host(address)).to eq('127.0.0.1') 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/unit/lib/infrataster/resources/http_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit/spec_helper' 2 | 3 | module Infrataster 4 | module Resources 5 | describe HttpResource do 6 | context "with no scheme URI" do 7 | it "complements scheme" do 8 | resource = HttpResource.new('/path/to/resource') 9 | expect(resource.uri.scheme).to eq('http') 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/unit/lib/infrataster/resources/server_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit/spec_helper' 2 | 3 | module Infrataster 4 | module Resources 5 | describe ServerResource do 6 | context 'when invoked address' do 7 | it 'returns an address' do 8 | Server.define(:app, '127.0.0.1') 9 | i = described_class.new(:app) 10 | expect(i.address).to eq('127.0.0.1') 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/lib/infrataster/server_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit/spec_helper' 2 | 3 | module Infrataster 4 | describe Server do 5 | describe "self.define" do 6 | it "adds a Server instance to defined_servers" do 7 | described_class.define('name', '127.0.0.1', {}) 8 | servers = described_class.defined_servers 9 | expect(servers.size).to eq(1) 10 | expect(servers[0].name).to eq('name') 11 | expect(servers[0].address).to eq('127.0.0.1') 12 | expect(servers[0].options).to eq({}) 13 | end 14 | it "adds a Server instance to defined_servers with block" do 15 | described_class.define(:app) do |server| 16 | server.address = '127.0.0.1' 17 | server.vagrant = true 18 | server.from = :proxy 19 | end 20 | described_class.define(:proxy) do |server| 21 | server.address = '127.0.0.1' 22 | server.vagrant = true 23 | end 24 | servers = described_class.defined_servers 25 | expect(servers.size).to eq(2) 26 | expect(servers[0].name).to eq(:app) 27 | expect(servers[0].address).to eq('127.0.0.1') 28 | expect(servers[0].options[:vagrant]).to eq(true) 29 | expect(servers[0].from).to eq(servers[1]) 30 | end 31 | end 32 | 33 | describe "self.find_by_name" do 34 | it "finds a server by name" do 35 | described_class.define('name', '127.0.0.1', {}) 36 | server = described_class.find_by_name('name') 37 | expect(server.name).to eq('name') 38 | expect(server.address).to eq('127.0.0.1') 39 | expect(server.options).to eq({}) 40 | end 41 | end 42 | 43 | describe "#from" do 44 | it "returns 'from' server instance" do 45 | described_class.define('proxy', '127.0.0.1', {}) 46 | described_class.define('app', '127.0.0.1', from: 'proxy') 47 | app_server = described_class.find_by_name('app') 48 | expect(app_server.from.name).to eq('proxy') 49 | end 50 | end 51 | 52 | describe "#_ssh_start_args" do 53 | context "with ssh option" do 54 | context "when options[:ssh][:host] is set" do 55 | it 'returns args for SSH.start' do 56 | server = Server.new('name', '127.0.0.1', ssh: {host_name: 'host', user: 'user'}) 57 | expect(server.send(:_ssh_start_args)). 58 | to eq(['host', 'user', {host_name: 'host', user: 'user'}]) 59 | end 60 | end 61 | 62 | context "when options[:ssh][:host] is not set" do 63 | it 'returns args for SSH.start' do 64 | server = Server.new('name', '127.0.0.1', ssh: {user: 'user'}) 65 | expect(server.send(:_ssh_start_args)). 66 | to eq(['127.0.0.1', 'user', {host_name: '127.0.0.1', user: 'user'}]) 67 | end 68 | end 69 | end 70 | 71 | context "with vagrant option" do 72 | context "when vagrant option is true" do 73 | it 'returns args for SSH.start' do 74 | server = Server.new('name', '127.0.0.1', vagrant: true) 75 | expect(server).to receive(:ssh_config_for_vagrant).with('name'). 76 | and_return({host_name: 'host', user: 'user'}) 77 | expect(server.send(:_ssh_start_args)). 78 | to eq(['host', 'user', {host_name: 'host', user: 'user'}]) 79 | end 80 | end 81 | 82 | context "when vagrant option is not true but truthy" do 83 | it 'returns args for SSH.start' do 84 | server = Server.new('name', '127.0.0.1', vagrant: 'vagrant_vm_name') 85 | expect(server).to receive(:ssh_config_for_vagrant).with('vagrant_vm_name'). 86 | and_return({host_name: 'host', user: 'user'}) 87 | expect(server.send(:_ssh_start_args)). 88 | to eq(['host', 'user', {host_name: 'host', user: 'user'}]) 89 | end 90 | end 91 | end 92 | 93 | context "otherwise" do 94 | it 'raises an error' do 95 | server = Server.new('name', '127.0.0.1') 96 | expect do 97 | server.send(:_ssh_start_args) 98 | end.to raise_error(Server::Error) 99 | end 100 | end 101 | end 102 | end 103 | end 104 | 105 | -------------------------------------------------------------------------------- /spec/unit/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'infrataster' 2 | 3 | # This file was generated by the `rspec --init` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # Require this file using `require "spec_helper"` to ensure that it is only 6 | # loaded once. 7 | # 8 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 9 | RSpec.configure do |config| 10 | config.treat_symbols_as_metadata_keys_with_true_values = true 11 | config.run_all_when_everything_filtered = true 12 | config.filter_run :focus 13 | 14 | # Run specs in random order to surface order dependencies. If you find an 15 | # order dependency and want to debug it, you can fix the order by providing 16 | # the seed, which is printed after each run. 17 | # --seed 1234 18 | config.order = 'random' 19 | 20 | config.before(:each) do 21 | Infrataster::Server.clear_defined_servers 22 | end 23 | end 24 | --------------------------------------------------------------------------------