├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── Vagrantfile ├── bin ├── console └── setup ├── exe ├── itamae-kondate ├── itamae-kondate-role ├── kondate ├── serverspec-kondate └── serverspec-kondate-role ├── kondate.gemspec ├── lib ├── ext │ ├── itamae │ │ ├── attributes.rb │ │ └── kondate.rb │ └── serverspec │ │ ├── attributes.rb │ │ └── kondate.rb ├── kondate.rb └── kondate │ ├── cli.rb │ ├── config.rb │ ├── error.rb │ ├── hash_ext.rb │ ├── host_plugin │ ├── base.rb │ └── file.rb │ ├── itamae_bootstrap.rb │ ├── property_builder.rb │ ├── property_file.rb │ ├── role_file.rb │ ├── string_util.rb │ ├── templates │ ├── .kondate.conf │ ├── bootstrap.rb │ ├── hosts.yml │ ├── properties │ │ ├── environments │ │ │ ├── .gitkeep │ │ │ └── development.yml │ │ ├── nodes │ │ │ └── .gitkeep │ │ └── roles │ │ │ └── sample.yml │ ├── recipes │ │ ├── middleware │ │ │ └── base │ │ │ │ └── default.rb │ │ └── roles │ │ │ └── .gitkeep │ ├── secrets │ │ └── properties │ │ │ ├── environments │ │ │ └── .gitkeep │ │ │ ├── nodes │ │ │ └── .gitkeep │ │ │ └── roles │ │ │ └── .gitkeep │ └── spec │ │ ├── middleware │ │ ├── .gitkeep │ │ └── base_spec.rb │ │ ├── roles │ │ └── .gitkeep │ │ └── spec_helper.rb │ └── version.rb └── test ├── kondate_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /bootstrap.rb 11 | /recipes/ 12 | /secrets/ 13 | /spec/ 14 | /properties/ 15 | /.kondate.conf 16 | /hosts.yml 17 | .ruby-version 18 | /kondate/ 19 | .vagrant 20 | .tags 21 | /bin/ 22 | /vendor/ 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.2 4 | before_install: gem install bundler -v 1.10.6 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.5.0 2 | 3 | Enhancements: 4 | 5 | * Support --shell option of itamae 6 | * Support --login-shell option of itamae 7 | * Make configurable itamae-kondate and serverspec-kondate options on .kondate.conf 8 | 9 | # 0.4.15 (2018-02-01) 10 | 11 | Changes: 12 | 13 | * Remove `bundle exec` from `bundle exec itamae` because `bundle exec` inside `bundle exec` introcudes some troubles. 14 | 15 | # 0.4.14 (2017-03-27) 16 | 17 | Reverts: 18 | 19 | * Fallback to ENV['USER'] rather than Etc.getlogin if ssh_config[:ssh_user] is not available 20 | 21 | # 0.4.13 (2017-03-27) 22 | 23 | Enhancements: 24 | 25 | * Fix to see all Net::SSH::Config keys in itamae 26 | 27 | # 0.4.12 (2017-03-17) 28 | 29 | Enhancements: 30 | 31 | * Output Net::SSH::Config.for(host) as debug log 32 | 33 | # 0.4.11 (2017-03-09) 34 | 35 | Enhancements: 36 | 37 | * Use Parallel::ProcessorCount instead of Facter gem 38 | 39 | # 0.4.10 (2017-03-09) 40 | 41 | Fixes: 42 | 43 | * Fix Net::SSH::Config ssh_keys 44 | 45 | # 0.4.9 (2017-02-02) 46 | 47 | Enhancements: 48 | 49 | * Add hostname to itamae log prefix without using IO.pipe so that we can use debuggers such as pry 50 | 51 | # 0.4.8 (2017-01-27) 52 | 53 | Enhancements: 54 | 55 | * Add hostname to itamae log prefix 56 | 57 | # 0.4.7 (2017-01-26) 58 | 59 | Fixes: 60 | 61 | * Revert 0.4.5 to avoid No such file or directory ... 62 | 63 | # 0.4.6 (2017-01-25) 64 | 65 | Enhancements: 66 | 67 | * Mask private rsa key 68 | 69 | # 0.4.5 (2017-01-23) 70 | 71 | Fixes: 72 | 73 | * Remove tempfile created 74 | 75 | # 0.4.4 (2016-12-02) 76 | 77 | Changes: 78 | 79 | * Prepare `Kondate::ItamaeBootstrap.bootstrap(context)`. Now, bootstrap.rb should just call it. 80 | 81 | # 0.4.3 (2016-12-02) 82 | 83 | Fixes: 84 | 85 | * Fixe nil error 86 | 87 | # 0.4.2 (2016-12-01) 88 | 89 | Enhancements: 90 | 91 | * Support secret recipes and spec files 92 | 93 | # 0.4.1 (2016-12-01) 94 | 95 | Fixes: 96 | 97 | * The order of exploring possible role files was opposite 98 | 99 | # 0.4.0 (2016-12-01) 100 | 101 | Enhancements: 102 | 103 | * Add a feature to explore possible role files 104 | 105 | # 0.3.3 (2016-11-20) 106 | 107 | Changes: 108 | 109 | * kondate init now requires a target_dir argument, not an option anymore 110 | 111 | # 0.3.2 (2016-11-18) 112 | 113 | Fixes: 114 | 115 | * Revert log message 116 | 117 | # 0.3.1 (2016-11-18) 118 | 119 | Fixes: 120 | 121 | * Fix to remove temporary property files properly 122 | 123 | # 0.3.0 (2016-11-18) 124 | 125 | Enhancements: 126 | 127 | * Add itamae-role and serverspec-role subcommands to run for multiple hosts in parallel 128 | 129 | # 0.2.1 (2016-11-14) 130 | 131 | Fixes: 132 | 133 | * Resolve 'failed to load rake command' when kondate init (thanks to @mazgi) 134 | 135 | # 0.2.0 (2016-04-05) 136 | 137 | Enhancments: 138 | 139 | * Support arbitrary hostinfo by HostPlugin 140 | 141 | # 0.1.9 (2015-12-11) 142 | 143 | Enhancments: 144 | 145 | * Support itamae --profile option 146 | * Support itamae --recipe-graph option 147 | 148 | # 0.1.8 (2015-12-09) 149 | 150 | Changes: 151 | 152 | * kondate generate => kondate init (as itamae init) 153 | 154 | # 0.1.7 (2015-12-06) 155 | 156 | Fixes: 157 | 158 | * Fix typo 159 | 160 | # 0.1.6 (2015-11-30) 161 | 162 | Fixes: 163 | 164 | * Fix regression (avoid nil error) introduced in 0.1.4 165 | 166 | # 0.1.5 (2015-11-30) 167 | 168 | Changes: 169 | 170 | * Add --vagrant option, and stop judging vagrant or not via host name 171 | 172 | # 0.1.4 (2015-11-29) 173 | 174 | Changes: 175 | 176 | * Define global_attributes instead of using attributes['global'] 177 | * Let vagrant? to handle inside each host plugin 178 | 179 | # 0.1.3 (2015-11-26) 180 | 181 | Fixes: 182 | 183 | * Fix to keep global attributes with --recipe 184 | 185 | # 0.1.2 (2015-11-24) 186 | 187 | Enhancments: 188 | 189 | * Support environments properties. 190 | 191 | # 0.1.1 (2015-11-24) 192 | 193 | Changes: 194 | 195 | * Do not open Hash class to add deep_merge, instead use own HashExt class. 196 | 197 | # 0.1.0 (2015-11-23) 198 | 199 | first version 200 | 201 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | gem 'pry' 5 | gem 'pry-nav' 6 | gem 'byebug' 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 sonots 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kondate 2 | 3 | Kondate is yet another nodes management framework for Itamae/Serverspec. 4 | 5 | Kondate provides nodes/roles/attributes/run_lists management feature for Itamae/Serverspec. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'kondate' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install kondate 22 | 23 | ## Usage 24 | 25 | Generate a template directory tree: 26 | 27 | ``` 28 | $ bundle exec kondate init . 29 | ``` 30 | 31 | Run itamae: 32 | 33 | ``` 34 | $ bundle exec kondate itamae 35 | ``` 36 | 37 | Run serverspec: 38 | 39 | ``` 40 | $ bundle exec kondate serverspec 41 | ``` 42 | 43 | Run itamae for multiple hosts of a given role in parallel: 44 | 45 | ``` 46 | $ bundle exec kondate itamae-role 47 | ``` 48 | 49 | Run serverspec for multiple hosts of a given role in parallel: 50 | 51 | ``` 52 | $ bundle exec kondate serverspec-role 53 | ``` 54 | 55 | ## Configuration 56 | 57 | `kondate init` provides a template directory tree such as: 58 | 59 | ``` 60 | . 61 | ├── .kondate.conf # kondate configuration 62 | ├── bootstrap.rb # itamae bootstrap 63 | ├── hosts.yml # manages hostnames and its roles 64 | ├── properties # manages run_lists and attributes 65 | │   ├── nodes # host specific properties 66 | │   ├── roles # role properties 67 | │   └── environments # environment properties 68 | ├── recipes # itamae recipes 69 | │   ├── middleware # middleware recipes 70 | │   │   └── base 71 | │   │   └── default.rb 72 | │   └── roles # role recipes 73 | └── spec # serverspec specs 74 | ├── middleware # middleware recipes specs 75 | │   └── base_spec.rb 76 | ├─ roles # role recipes specs 77 | └── spec_helper.rb 78 | ├── secrets # manages secrets attributes such as passwords 79 | │   ├── properties 80 | │   │   ├── environments 81 | │   │   ├── nodes 82 | │   │   └── roles 83 | │   ├── recipes 84 | │   │   ├── middleware 85 | │   │   └── roles 86 | │   └── spec 87 | │   ├── middleware 88 | │   └── roles 89 | ``` 90 | 91 | ### .kondate.conf 92 | 93 | The default .kondate.conf looks like below: 94 | 95 | ``` 96 | middlware_recipes_dir: recipes/middleware 97 | roles_recipes_dir: recipes/roles 98 | middleware_recipes_serverspec_dir: spec/middleware 99 | roles_recipes_serverspec_dir: spec/roles 100 | nodes_properties_dir: properties/nodes 101 | roles_properties_dir: properties/roles 102 | environments_properties_dir: properties/environments 103 | secret_middlware_recipes_dir: secrets/recipes/middleware 104 | secret_roles_recipes_dir: secrets/recipes/roles 105 | secret_middleware_recipes_serverspec_dir: secrets/spec/middleware 106 | secret_roles_recipes_serverspec_dir: secrets/spec/roles 107 | secret_nodes_properties_dir: secrets/properties/nodes 108 | secret_roles_properties_dir: secrets/properties/roles 109 | secret_environments_properties_dir: secrets/properties/environments 110 | plugin_dir: lib 111 | host_plugin: 112 | type: file 113 | path: hosts.yml 114 | ``` 115 | 116 | You can customize the directory tree with this conf. 117 | 118 | #### Itamae options 119 | 120 | You can configure itamae-kondate options on .kondate.conf, too as: 121 | 122 | ``` 123 | itamae_options: 124 | shell: /bin/bash 125 | ``` 126 | 127 | See `itamae-kondate --help` for option list. 128 | 129 | #### Serverspec options 130 | 131 | You can configure serversepc-kondate options on .kondate.conf, too as: 132 | 133 | ``` 134 | serverspec_options: 135 | vagrant: true 136 | ``` 137 | 138 | See `serverspec-kondate --help` for option list. 139 | 140 | 141 | ### hosts.yml 142 | 143 | The default uses `file` host plugin, and `hosts.yml`. The contents of `hosts.yml` look like below: 144 | 145 | ``` 146 | localhost: [sample] 147 | ``` 148 | 149 | where keys are host names, and values are array of roles. 150 | 151 | ``` 152 | $ bundle exec kondate itamae 153 | ``` 154 | 155 | works as follows: 156 | 157 | 1. obtains a role list from `hosts.yml` 158 | 2. reads `properties/roles/#{role}.yml`, and find recipes and its attributes 159 | 3. runs recipes 160 | 161 | You can create your own host plugin. See `Host Plugin` section for more details. 162 | 163 | ### properties 164 | 165 | Property files are places to write recipes to run and attributes values. 166 | 167 | ``` 168 | ├── properties # manages run_lists and attributes 169 | │   ├── nodes # host specific properties 170 | │   ├── roles # role properties 171 | │   └── environments # environment properties 172 | ``` 173 | 174 | An example looks like below: 175 | 176 | properties/roles/#{role}.yml 177 | 178 | ``` 179 | attributes: 180 | ruby: 181 | versions: [2.2.3] 182 | gems: 183 | 2.2.3: [bundler] 184 | node: 185 | versions: [v0.12.2] 186 | global: v0.12.2 187 | nginx: 188 | ``` 189 | 190 | The attributes variables are accessible like `attrs['ruby']['versions']`, which is equivalent and short version of `node['attributes']['ruby']['versions']` in recipes. 191 | 192 | You can also prepare host-specific property files such as: 193 | 194 | properties/nodes/#{host}.yml 195 | 196 | ``` 197 | attributes: 198 | nginx: 199 | worker_processes: 8 200 | ``` 201 | 202 | In addition, you can also prepare environment property files such as: 203 | 204 | properties/environments/`#{ENV['ENVIRONMENT'] || 'development'}`.yml 205 | 206 | ``` 207 | global_attributes: 208 | aws_region: ap-northeast-1 209 | ``` 210 | 211 | where `global_attributes` is accessible like `global_attrs['aws_region']`, which is equivalent and short version of `node['global_attributes']['aws_region']` in recipes. 212 | 213 | These files are merged on kondate execution in order of `environment` + `role` + `node` (`node` > `role` > `environment` in the strong order). 214 | 215 | ### secret properties 216 | 217 | Secret properties are places to write confidential attributes. 218 | 219 | ``` 220 | ├── secrets # manages secrets attributes such as passwords 221 | │   └── properties 222 | │   ├── nodes 223 | │   ├── roles 224 | │   └── environments 225 | ``` 226 | 227 | An example looks like below: 228 | 229 | secrets/properties/roles/sample.yml 230 | 231 | ``` 232 | attributes: 233 | base: 234 | password: xxxxxxxx 235 | ``` 236 | 237 | These files are merged with property files on kondate execution. 238 | 239 | Hint: I manage secret property files on github private repository. ToDo: support encryption. 240 | 241 | ### recipes 242 | 243 | Put you itamae recipes: 244 | 245 | ``` 246 | ├── recipes # itamae recipes 247 | │   ├── middleware # middleware recipes 248 | │   │   └── base 249 | │   │   └── default.rb 250 | │   └── roles # role recipes 251 | ``` 252 | 253 | `middleware recipes` are usual recipes to write how to install middleware such as `nginx`, `mysql`. 254 | 255 | `role recipes` are places to write role-specific provisioning. I often write recipes to create log directories for my app (role), for example. 256 | 257 | recipes/roles/myapp/default.rb 258 | 259 | ```ruby 260 | directory "/var/log/myapp" do 261 | owner myapp 262 | group myapp 263 | mode 0755 264 | end 265 | ``` 266 | 267 | #### spec 268 | 269 | Put your serverspec specs. 270 | 271 | ``` 272 | └── spec # serverspec specs 273 | ├── middleware # middleware recipes specs 274 | └── roles # role recipes specs 275 | ``` 276 | 277 | It is required that `spec/spec_helper` has lines: 278 | 279 | ```ruby 280 | set :host, ENV['TARGET_HOST'] 281 | set :set_property, YAML.load_file(ENV['TARGET_NODE_FILE']) 282 | ``` 283 | 284 | because these ENVs are passed by `kondate serverspec`. 285 | 286 | Configuring following lines for vagrant is also recommended: 287 | 288 | ``` 289 | if ENV['TARGET_VAGRANT'] 290 | `vagrant up #{host}` 291 | 292 | config = Tempfile.new('', Dir.tmpdir) 293 | config.write(`vagrant ssh-config #{host}`) 294 | config.close 295 | 296 | Net::SSH::Config.for(host, [config.path]) 297 | else 298 | ``` 299 | 300 | `ENV['TARGET_VAGRANT']` is turned on if `kondate serverspec` is executed with `--vagrant` option. 301 | 302 | See [templates/spec/spec_helper.rb](./lib/kondate/templates/spec/spec_helper.rb) for an example. 303 | 304 | ### Exploring role files 305 | 306 | Available version: >= v0.4.0 307 | 308 | Assume `role` is delimited with `-` (you can configure the delimiter) such as `myapp-web-staging`, this feature explores role files in order of: 309 | 310 | 1. myapp-web-staging.yml 311 | 1. myapp-web-base.yml 312 | 1. myapp-web.yml 313 | 1. myapp-base.yml 314 | 1. myapp.yml 315 | 1. base.yml 316 | 317 | This makes it possible to share a property file, for example, `myapp-web.yml` among `myapp-web-staging` and `myapp-web-production` roles. 318 | 319 | To enable this feature, you need to configure .kondate.conf as: 320 | 321 | ``` 322 | explore_role_files: true # default is false 323 | role_delimiter: "-" # default is - 324 | ``` 325 | 326 | ## Host Plugin 327 | 328 | The default reads `hosts.yml` to resolve roles of a host, but 329 | you may want to resolve roles from AWS EC2 `roles` tag, or 330 | you may want to resolve roles from your own host resolver API application. 331 | 332 | Thus, `kondate` provides a plugin system to resolve hosts' roles. 333 | 334 | ### Naming Convention 335 | 336 | You must follow the below naming conventions: 337 | 338 | * gem name: kondate-host_plugin-xxx (xxx_yyy) (if you want to make a gem) 339 | * file name: lib/kondate/host_plugin/xxx.rb (xxx_yyy.rb) 340 | * class name: Kondate::HostPlugin::Xxx (XxxYyy) 341 | 342 | If you want to put your own host plugin locally without publishing a gem, you can configure the location with .kondate.conf as: 343 | 344 | ``` 345 | plugin_dir: lib 346 | ``` 347 | 348 | ### Interface 349 | 350 | What you have to implement are `#initialize`, `#get_environment`, and `#get_roles` methods. 351 | `get_hostinfo` method is an optional method to return arbitrary hostinfo of the host (available from kondate 0.2.0). 352 | Here is an example of file plugin: 353 | 354 | ```ruby 355 | require 'yaml' 356 | 357 | module Kondate 358 | module HostPlugin 359 | # YAML format 360 | # 361 | # host1: [role1, role2] 362 | # host2: [role1, role2] 363 | class File < Base 364 | # @param [HashWithIndifferentAccess] config 365 | def initialize(config) 366 | super 367 | raise ConfigError.new('file: path is not configured') unless config.path 368 | @path = config.path 369 | 370 | @roles_of_host = YAML.load_file(@path) 371 | @hosts_of_role = {} 372 | @roles_of_host.each do |host, roles| 373 | roles.each do |role| 374 | @hosts_of_role[role] ||= [] 375 | @hosts_of_role[role] << host 376 | end 377 | end 378 | end 379 | 380 | # @param [String] host hostname 381 | # @return [String] environment name 382 | def get_environment(host) 383 | ENV['ENVIRONMENT'] || 'development' 384 | end 385 | 386 | # @param [String] host hostname 387 | # @return [Array] array of roles 388 | def get_roles(host) 389 | @roles_of_host[host] 390 | end 391 | 392 | # @param [String] role role 393 | # @return [Array] array of hosts 394 | # 395 | # Available from kondate >= 0.3.0 396 | def get_hosts(role) 397 | @hosts_of_role[role] 398 | end 399 | 400 | # Optional 401 | # 402 | # @param [String] host hostname 403 | # @return [Hash] arbitrary hostinfo 404 | def get_hostinfo(host) 405 | {} 406 | end 407 | end 408 | end 409 | end 410 | ``` 411 | 412 | ### Config 413 | 414 | `config` parameter of `#initialize` is created from the configuration file (.kondate.conf): 415 | 416 | ``` 417 | host_plugin: 418 | type: file 419 | path: hosts.yml 420 | ``` 421 | 422 | `config.type` and `config.path` is available in the above config. 423 | 424 | ## See Also 425 | 426 | * [Itamae meetup #1 で「ぼくのかんがえた Itamae/Serverspec 構成フレームワーク 〜 板前の献立 〜」というトークをしてきた](http://blog.livedoor.jp/sonots/archives/46245484.html) (Japanese) 427 | 428 | ## Development 429 | 430 | ``` 431 | bundle exec exe/kondate init . 432 | vagrant up 433 | ``` 434 | 435 | ``` 436 | bundle exec exe/kondate itamae vagrant-centos --vagrant --role sample 437 | bundle exec exe/kondate serverspec vagrant-centos --vagrant --role sample 438 | ``` 439 | 440 | ## ToDo 441 | 442 | write tests 443 | 444 | ## License 445 | 446 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 447 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /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 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | # All Vagrant configuration is done here. The most common configuration 9 | # options are documented and commented below. For a complete reference, 10 | # please see the online documentation at vagrantup.com. 11 | # Every Vagrant virtual environment requires a box to build off of. 12 | config.vm.box = "centos6.5.3" 13 | config.vm.box_url = 'https://github.com/2creatives/vagrant-centos/releases/download/v6.5.3/centos65-x86_64-20140116.box' # name 14 | config.vm.define "vagrant-centos" 15 | config.ssh.forward_agent = true 16 | config.vm.provider "virtualbox" do |vb| 17 | # Use VBoxManage to customize the VM. For example to change memory: 18 | vb.customize ["modifyvm", :id, "--memory", "1024"] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "kondate" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /exe/itamae-kondate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | CMD = File.join(File.dirname(__FILE__), 'kondate') 4 | if ARGV.size == 0 || ARGV[0] == "-h" || ARGV[0] == "--help" 5 | exec CMD, 'help', 'itamae' 6 | else 7 | exec CMD, 'itamae', *ARGV 8 | end 9 | -------------------------------------------------------------------------------- /exe/itamae-kondate-role: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | CMD = File.join(File.dirname(__FILE__), 'kondate') 4 | if ARGV.size == 0 || ARGV[0] == "-h" || ARGV[0] == "--help" 5 | exec CMD, 'help', 'itamae-role' 6 | else 7 | exec CMD, 'itamae-role', *ARGV 8 | end 9 | -------------------------------------------------------------------------------- /exe/kondate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "kondate/cli" 4 | Kondate::CLI.start(ARGV) 5 | -------------------------------------------------------------------------------- /exe/serverspec-kondate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | CMD = File.join(File.dirname(__FILE__), 'kondate') 4 | if ARGV.size == 0 || ARGV[0] == "-h" || ARGV[0] == "--help" 5 | exec CMD, 'help', 'serverspec' 6 | else 7 | exec CMD, 'serverspec', *ARGV 8 | end 9 | -------------------------------------------------------------------------------- /exe/serverspec-kondate-role: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | CMD = File.join(File.dirname(__FILE__), 'kondate') 4 | if ARGV.size == 0 || ARGV[0] == "-h" || ARGV[0] == "--help" 5 | exec CMD, 'help', 'serverspec-role' 6 | else 7 | exec CMD, 'serverspec-role', *ARGV 8 | end 9 | -------------------------------------------------------------------------------- /kondate.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'kondate/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "kondate" 8 | spec.version = Kondate::VERSION 9 | spec.authors = ["sonots"] 10 | spec.email = ["sonots@gmail.com"] 11 | 12 | spec.summary = %q{Kondate is yet another nodes management framework for Itamae/Serverspec.} 13 | spec.description = %q{Kondate is yet another nodes management framework for Itamae/Serverspec.} 14 | spec.homepage = "https://github.com/sonots/kondate" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_dependency 'itamae' 23 | spec.add_dependency 'serverspec' 24 | spec.add_dependency 'thor' 25 | spec.add_dependency 'highline' 26 | spec.add_dependency 'parallel' 27 | 28 | spec.add_development_dependency "bundler" 29 | spec.add_development_dependency "test-unit" 30 | spec.add_development_dependency "rake" 31 | end 32 | -------------------------------------------------------------------------------- /lib/ext/itamae/attributes.rb: -------------------------------------------------------------------------------- 1 | # top level at recipe 2 | module Itamae 3 | class Recipe 4 | class EvalContext 5 | def attrs 6 | node[:attributes] 7 | end 8 | def global_attrs 9 | node[:global_attributes] 10 | end 11 | end 12 | end 13 | end 14 | 15 | # resource { here } 16 | module Itamae 17 | module Resource 18 | class Base 19 | class EvalContext 20 | def attrs 21 | node[:attributes] 22 | end 23 | def global_attrs 24 | node[:global_attributes] 25 | end 26 | end 27 | end 28 | end 29 | end 30 | 31 | # templates 32 | module Itamae 33 | module Resource 34 | class Template < RemoteFile 35 | class RenderContext 36 | def attrs 37 | node[:attributes] 38 | end 39 | def global_attrs 40 | node[:global_attributes] 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/ext/itamae/kondate.rb: -------------------------------------------------------------------------------- 1 | require 'itamae' 2 | require_relative 'attributes' 3 | 4 | -------------------------------------------------------------------------------- /lib/ext/serverspec/attributes.rb: -------------------------------------------------------------------------------- 1 | module Specinfra 2 | module Helper 3 | module Properties 4 | def attrs 5 | property['attributes'] 6 | end 7 | def global_attrs 8 | property['global_attributes'] 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/ext/serverspec/kondate.rb: -------------------------------------------------------------------------------- 1 | # require 'serverspec' # stack level too deep 2 | require_relative 'attributes' 3 | 4 | -------------------------------------------------------------------------------- /lib/kondate.rb: -------------------------------------------------------------------------------- 1 | module Kondate 2 | ROOT = File.expand_path('../..', __FILE__) 3 | end 4 | 5 | require "kondate/version" 6 | require "kondate/config" 7 | require "kondate/role_file" 8 | require "kondate/property_file" 9 | require "kondate/property_builder" 10 | require "kondate/hash_ext" 11 | require "kondate/string_util" 12 | require "kondate/error" 13 | require "kondate/host_plugin/base" 14 | require "kondate/itamae_bootstrap" 15 | -------------------------------------------------------------------------------- /lib/kondate/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'yaml' 3 | require 'net/ssh' 4 | require "highline/import" 5 | require_relative '../kondate' 6 | require 'fileutils' 7 | require 'shellwords' 8 | require 'find' 9 | require 'parallel' 10 | require 'parallel/processor_count' 11 | 12 | module Kondate 13 | class CLI < Thor 14 | extend Parallel::ProcessorCount 15 | 16 | # cf. http://qiita.com/KitaitiMakoto/items/c6b9d6311c20a3cc21f9 17 | def self.exit_on_failure? 18 | true 19 | end 20 | 21 | class_option :config, :aliases => ["-c"], :type => :string, :default => nil 22 | class_option :dry_run, :type => :boolean, :default => false 23 | # default_command :itamae 24 | 25 | def initialize(args = [], opts = [], config = {}) 26 | super 27 | Config.configure(@options) 28 | end 29 | 30 | desc "init [target_dir]", "Initialize kondate directory tree" 31 | def init(target_dir) 32 | Config.kondate_directories.each do |_, dir| 33 | $stdout.puts "mkdir -p #{File.join(target_dir, dir)}" 34 | FileUtils.mkdir_p(File.join(target_dir, dir)) unless @options[:dry_run] 35 | end 36 | 37 | templates_dir = File.join(Kondate::ROOT, 'lib', 'kondate', 'templates') 38 | templates_dir_length = templates_dir.length 39 | Find.find(templates_dir).select {|f| File.file?(f) }.each do |src| 40 | next if File.basename(src) == '.gitkeep' 41 | dst = File.join(target_dir, src[templates_dir_length+1 .. -1]) 42 | dst_dir = File.dirname(dst) 43 | unless Dir.exist?(dst_dir) 44 | $stdout.puts "mkdir -p #{dst_dir}" 45 | FileUtils.mkdir_p(dst_dir) unless @options[:dry_run] 46 | end 47 | $stdout.puts "cp #{src} #{dst}" 48 | FileUtils.copy(src, dst) unless @options[:dry_run] 49 | end 50 | end 51 | 52 | desc "itamae ", "Execute itamae" 53 | option :role, :type => :array, :default => [] 54 | option :recipe, :type => :array, :default => [] 55 | option :debug, :aliases => ["-d"], :type => :boolean, :default => false 56 | option :confirm, :type => :boolean, :default => true 57 | option :vagrant, :type => :boolean, :default => false 58 | option :profile, :type => :string, :default => nil, :desc => "[EXPERIMENTAL] Save profiling data", :banner => "PATH" 59 | option :recipe_graph, :type => :string, :default => nil, :desc => "[EXPERIMENTAL] Write recipe dependency graph in DOT", :banner => "PATH" 60 | option :shell, :type => :string, :default => Config.itamae_options[:shell] || "/bin/sh" 61 | option :login_shell, :type => :boolean, :default => false 62 | def itamae(host) 63 | with_host(host) {|property_files| do_itamae(host, property_files) } 64 | end 65 | 66 | desc "itamae-role ", "Execute itamae for multiple hosts in the role" 67 | option :role, :type => :array, :default => [] 68 | option :recipe, :type => :array, :default => [] 69 | option :debug, :aliases => ["-d"], :type => :boolean, :default => false 70 | option :confirm, :type => :boolean, :default => true 71 | option :vagrant, :type => :boolean, :default => false 72 | option :profile, :type => :string, :default => nil, :desc => "[EXPERIMENTAL] Save profiling data", :banner => "PATH" 73 | option :recipe_graph, :type => :string, :default => nil, :desc => "[EXPERIMENTAL] Write recipe dependency graph in DOT", :banner => "PATH" 74 | option :parallel, :aliases => ["-p"], :type => :numeric, :default => processor_count 75 | option :shell, :type => :string, :default => Config.itamae_options[:shell] || "/bin/sh" 76 | option :login_shell, :type => :boolean, :default => false 77 | def itamae_role(role) 78 | with_role(role) {|host, property_files| do_itamae(host, property_files) } 79 | end 80 | 81 | desc "serverspec ", "Execute serverspec" 82 | option :role, :type => :array, :default => [] 83 | option :recipe, :type => :array, :default => [] 84 | option :debug, :aliases => ["-d"], :type => :boolean, :default => false 85 | option :confirm, :type => :boolean, :default => true 86 | option :vagrant, :type => :boolean, :default => false 87 | def serverspec(host) 88 | with_host(host) {|property_files| do_serverspec(host, property_files) } 89 | end 90 | 91 | desc "serverspec-role ", "Execute serverspec for multiple hosts in the role" 92 | option :role, :type => :array, :default => [] 93 | option :recipe, :type => :array, :default => [] 94 | option :debug, :aliases => ["-d"], :type => :boolean, :default => false 95 | option :confirm, :type => :boolean, :default => true 96 | option :vagrant, :type => :boolean, :default => false 97 | option :parallel, :aliases => ["-p"], :type => :numeric, :default => processor_count 98 | def serverspec_role(role) 99 | with_role(role) {|host, property_files| do_serverspec(host, property_files) } 100 | end 101 | 102 | private 103 | 104 | def itamae_options 105 | @itamae_options ||= Config.itamae_options.merge(@options) 106 | end 107 | 108 | def serverspec_options 109 | @serverspec_options ||= Config.serverspec_options.merge(@options) 110 | end 111 | 112 | def with_host(host, &block) 113 | property_files = build_property_files(host) 114 | begin 115 | print_property_files(property_files) 116 | if proceed?(property_files) 117 | exit(-1) unless yield(property_files) 118 | end 119 | ensure 120 | clean_property_files(property_files) 121 | end 122 | end 123 | 124 | def with_role(role, &block) 125 | $stdout.puts "Number of parallels is #{@options[:parallel]}" 126 | hosts = Config.host_plugin.get_hosts(role) 127 | if hosts.nil? or hosts.empty? 128 | $stderr.puts 'No host' 129 | exit(1) 130 | end 131 | $stdout.puts "Target hosts are [#{hosts.join(", ")}]" 132 | 133 | property_files_of_host, summarized_property_files, hosts_of_role = build_property_files_of_host(hosts) 134 | begin 135 | print_property_files(summarized_property_files, hosts_of_role) 136 | if proceed?(summarized_property_files) 137 | successes = Parallel.map(hosts, in_processes: @options[:parallel]) do |host| 138 | yield(host, property_files_of_host[host]) 139 | end 140 | exit(-1) unless successes.all? 141 | end 142 | ensure 143 | property_files_of_host.values.each {|property_files| clean_property_files(property_files) } 144 | end 145 | end 146 | 147 | def do_itamae(host, property_files) 148 | env = {} 149 | env['RUBYOPT'] = "-I #{Config.plugin_dir} -r bundler/setup -r ext/itamae/kondate" 150 | property_files.each do |role, property_file| 151 | next if property_file.empty? 152 | command = "itamae ssh" 153 | command << " -h #{host}" 154 | 155 | properties = property_file.load 156 | 157 | if itamae_options[:vagrant] 158 | command << " --vagrant" 159 | else 160 | # itamae itself sees Net:SSH::Config.for(host) 161 | # here, we set ssh config if property file specially specifies 162 | config = Net::SSH::Config.for(host, Net::SSH::Config.default_files) 163 | # itamae fallbacks to Etc.getlogin, but we prefer to fallback to ENV['USER'], then Etc.getlogin 164 | command << " -u #{properties['ssh_user'] || config[:user] || ENV['USER'] || ENV['LOGNAME'] || Etc.getlogin || Etc.getpwuid.name}" 165 | command << " -i #{(Array(properties['ssh_keys']) || []).first}" if properties['ssh_keys'] 166 | command << " -p #{properties['ssh_port']}" if properties['ssh_port'] 167 | end 168 | 169 | command << " -y #{property_file.path}" 170 | command << " -l=debug" if itamae_options[:debug] 171 | command << " --dry-run" if itamae_options[:dry_run] 172 | command << " --profile=#{itamae_options[:profile]}" if itamae_options[:profile] 173 | command << " --recipe-graph=#{itamae_options[:recipe_graph]}" if itamae_options[:recipe_graph] 174 | command << " --shell=#{itamae_options[:shell]}" if itamae_options[:shell] 175 | command << " --login-shell" if itamae_options[:login_shell] 176 | command << " bootstrap.rb" 177 | $stdout.puts "env #{env.map {|k, v| "#{k}=#{v.shellescape}" }.join(' ')} #{command}" 178 | 179 | return false unless system(env, command) 180 | end 181 | true 182 | end 183 | 184 | def do_serverspec(host, property_files) 185 | env = {} 186 | env['TARGET_VAGRANT'] = '1' if serverspec_options[:vagrant] 187 | env['RUBYOPT'] = "-I #{Config.plugin_dir} -r bundler/setup -r ext/serverspec/kondate" 188 | property_files.each do |role, property_file| 189 | next if property_file.empty? 190 | spec_files = property_file.load['attributes'].keys.map {|recipe| 191 | secret_spec_file = File.join(Config.secret_middleware_recipes_serverspec_dir, "#{recipe}_spec.rb") 192 | spec_file = File.join(Config.middleware_recipes_serverspec_dir, "#{recipe}_spec.rb") 193 | File.exist?(secret_spec_file) ? secret_spec_file : spec_file 194 | }.compact 195 | secret_role_spec_file = RoleFile.explore(Config.secret_roles_recipes_serverspec_dir, role, "_spec.rb") 196 | role_spec_file = RoleFile.explore(Config.roles_recipes_serverspec_dir, role, "_spec.rb") 197 | spec_files << (File.exist?(secret_role_spec_file) ? secret_role_spec_file : role_spec_file) 198 | spec_files.select! {|spec| File.exist?(spec) } 199 | 200 | env['TARGET_HOST'] = host 201 | env['TARGET_NODE_FILE'] = property_file.path 202 | command = "rspec #{spec_files.map{|f| f.shellescape }.join(' ')}" 203 | $stdout.puts "env #{env.map {|k, v| "#{k}=#{v.shellescape}" }.join(' ')} #{command}" 204 | 205 | return false unless system(env, command) 206 | end 207 | true 208 | end 209 | 210 | def proceed?(property_files) 211 | if property_files.values.compact.reject(&:empty?).empty? 212 | $stderr.puts "Nothing to run" 213 | false 214 | elsif @options[:confirm] 215 | prompt = ask "Proceed? (y/n):" 216 | prompt == 'y' 217 | else 218 | true 219 | end 220 | end 221 | 222 | def print_property_files(property_files, hosts_of_role = {}) 223 | roles = property_files.keys 224 | if roles.nil? or roles.empty? 225 | $stderr.puts 'No role' 226 | return 227 | end 228 | $stdout.puts "Show property files for roles: [#{roles.join(", ")}]" 229 | 230 | property_files.each do |role, property_file| 231 | hosts = hosts_of_role[role] 232 | if hosts.nil? # itamae 233 | $stdout.print "Show property file for role: #{role}" 234 | else # itamae_role 235 | $stdout.print "Show representative property file for role: #{role}" 236 | $stdout.print " hosts: [#{hosts.join(", ")}]" 237 | end 238 | $stdout.print ", sources: #{property_file.source_files}" 239 | 240 | if property_file.empty? 241 | $stdout.puts " (no attribute, skipped)" 242 | else 243 | $stdout.puts 244 | $stdout.puts property_file.read 245 | end 246 | end 247 | end 248 | 249 | def clean_property_files(property_files) 250 | property_files.values.each do |file| 251 | File.unlink(file) rescue nil 252 | end 253 | end 254 | 255 | # @return [Hash] key value pairs whoses keys are roles and values are path (or nil) 256 | def build_property_files(host) 257 | builder = PropertyBuilder.new(host) 258 | roles = builder.filter_roles(@options[:role]) 259 | 260 | property_files = {} 261 | roles.each do |role| 262 | property_files[role] = builder.install(role, @options[:recipe]) 263 | end 264 | 265 | property_files 266 | end 267 | 268 | def build_property_files_of_host(hosts) 269 | summarized_property_files = {} 270 | property_files_of_host = {} 271 | hosts_of_role = {} 272 | hosts.each do |host| 273 | property_files = build_property_files(host) 274 | property_files_of_host[host] = property_files 275 | property_files.each {|role, property_file| summarized_property_files[role] ||= property_file } 276 | property_files.each {|role, property_file| (hosts_of_role[role] ||= []) << host } 277 | end 278 | [property_files_of_host, summarized_property_files, hosts_of_role] 279 | end 280 | end 281 | end 282 | -------------------------------------------------------------------------------- /lib/kondate/config.rb: -------------------------------------------------------------------------------- 1 | require 'thor/core_ext/hash_with_indifferent_access' 2 | 3 | module Kondate 4 | class Config 5 | class << self 6 | def configure(opts = {}) 7 | @opts = opts 8 | reset 9 | self 10 | end 11 | 12 | def opts 13 | @opts ||= {} 14 | end 15 | 16 | def reset 17 | @config_path = nil 18 | @config = nil 19 | end 20 | 21 | DEFAULT_CONFIG_PATH = '.kondate.conf' 22 | 23 | def config 24 | return @config if @config 25 | if config_path == DEFAULT_CONFIG_PATH && !File.exist?(config_path) 26 | @config = Thor::CoreExt::HashWithIndifferentAccess.new({}) 27 | else 28 | @config = Thor::CoreExt::HashWithIndifferentAccess.new(YAML.load_file(config_path)) 29 | end 30 | end 31 | 32 | def config_path 33 | @config_path ||= opts[:config] || ENV['KONDATE_CONFIG_PATH'] || DEFAULT_CONFIG_PATH 34 | end 35 | 36 | def itamae_options 37 | @itamae_options ||= Thor::CoreExt::HashWithIndifferentAccess.new(config[:itamae_options] || {}) 38 | end 39 | 40 | def serverspec_options 41 | @serverspec_options ||= Thor::CoreExt::HashWithIndifferentAccess.new(config[:serverspec_options] || {}) 42 | end 43 | 44 | def kondate_directories 45 | { 46 | 'middleware_recipes_dir' => middleware_recipes_dir, 47 | 'roles_recipes_dir' => roles_recipes_dir, 48 | 'middleware_recipes_serverspec_dir' => middleware_recipes_serverspec_dir, 49 | 'roles_recipes_serverspec_dir' => roles_recipes_serverspec_dir, 50 | 'nodes_properties_dir' => nodes_properties_dir, 51 | 'roles_properties_dir' => roles_properties_dir, 52 | 'environments_properties_dir' => environments_properties_dir, 53 | 'secret_middleware_recipes_dir' => secret_middleware_recipes_dir, 54 | 'secret_roles_recipes_dir' => secret_roles_recipes_dir, 55 | 'secret_middleware_recipes_serverspec_dir' => secret_middleware_recipes_serverspec_dir, 56 | 'secret_roles_recipes_serverspec_dir' => secret_roles_recipes_serverspec_dir, 57 | 'secret_nodes_properties_dir' => secret_nodes_properties_dir, 58 | 'secret_roles_properties_dir' => secret_roles_properties_dir, 59 | 'secret_environments_properties_dir' => secret_environments_properties_dir, 60 | } 61 | end 62 | 63 | def middleware_recipes_dir 64 | config[:middleware_recipes_dir] || 'recipes/middleware' 65 | end 66 | 67 | def roles_recipes_dir 68 | config[:roles_recipes_dir] || 'recipes/roles' 69 | end 70 | 71 | def middleware_recipes_serverspec_dir 72 | config[:middleware_recipes_serverspec_dir] || 'spec/middleware' 73 | end 74 | 75 | def roles_recipes_serverspec_dir 76 | config[:roles_recipes_serverspec_dir] || 'spec/roles' 77 | end 78 | 79 | def nodes_properties_dir 80 | config[:nodes_properties_dir] || 'properties/nodes' 81 | end 82 | 83 | def roles_properties_dir 84 | config[:roles_properties_dir] || 'properties/roles' 85 | end 86 | 87 | def environments_properties_dir 88 | config[:environments_properties_dir] || 'properties/environments' 89 | end 90 | 91 | def secret_middleware_recipes_dir 92 | config[:secret_middleware_recipes_dir] || 'secrets/recipes/middleware' 93 | end 94 | 95 | def secret_roles_recipes_dir 96 | config[:secret_roles_recipes_dir] || 'secrets/recipes/roles' 97 | end 98 | 99 | def secret_middleware_recipes_serverspec_dir 100 | config[:secret_middleware_recipes_serverspec_dir] || 'secrets/spec/middleware' 101 | end 102 | 103 | def secret_roles_recipes_serverspec_dir 104 | config[:secret_roles_recipes_serverspec_dir] || 'secrets/spec/roles' 105 | end 106 | 107 | def secret_nodes_properties_dir 108 | config[:secret_nodes_properties_dir] || 'secrets/properties/nodes' 109 | end 110 | 111 | def secret_roles_properties_dir 112 | config[:secret_roles_properties_dir] || 'secrets/properties/roles' 113 | end 114 | 115 | def secret_environments_properties_dir 116 | config[:secret_environments_properties_dir] || 'secrets/properties/environments' 117 | end 118 | 119 | def explore_role_files? 120 | !config[:explore_role_files].nil? 121 | end 122 | 123 | def role_delimiter 124 | config[:role_delimiter] || '-' 125 | end 126 | 127 | def plugin_dir 128 | File.expand_path(config[:plugin_dir] || 'lib') 129 | end 130 | 131 | def host_plugin 132 | return @host_plugin if @host_plugin 133 | plugin = Thor::CoreExt::HashWithIndifferentAccess.new(config[:host_plugin] || { 134 | 'type' => 'file', 135 | 'path' => 'hosts.yml' 136 | }) 137 | begin 138 | require File.join(Config.plugin_dir, "kondate/host_plugin/#{plugin.type}") 139 | rescue LoadError => e 140 | require "kondate/host_plugin/#{plugin.type}" 141 | end 142 | class_name = "Kondate::HostPlugin::#{StringUtil.camelize(plugin.type)}" 143 | @host_plugin = Object.const_get(class_name).new(plugin) 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/kondate/error.rb: -------------------------------------------------------------------------------- 1 | module Kondate 2 | class Error < StandardError 3 | end 4 | class ConfigError < StandardError 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/kondate/hash_ext.rb: -------------------------------------------------------------------------------- 1 | # activesupport/core_ext/hash/deep_merge.rb 2 | module Kondate 3 | class HashExt < ::Hash 4 | # Returns a new hash with +self+ and +other_hash+ merged recursively. 5 | # 6 | # h1 = { a: true, b: { c: [1, 2, 3] } } 7 | # h2 = { a: false, b: { x: [3, 4, 5] } } 8 | # 9 | # h1.deep_merge(h2) #=> { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } } 10 | # 11 | # Like with Hash#merge in the standard library, a block can be provided 12 | # to merge values: 13 | # 14 | # h1 = { a: 100, b: 200, c: { c1: 100 } } 15 | # h2 = { b: 250, c: { c1: 200 } } 16 | # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val } 17 | # # => { a: 100, b: 450, c: { c1: 300 } } 18 | def deep_merge(other_hash, &block) 19 | dup.deep_merge!(other_hash, &block) 20 | end 21 | 22 | # Same as +deep_merge+, but modifies +self+. 23 | def deep_merge!(other_hash, &block) 24 | other_hash.each_pair do |current_key, other_value| 25 | this_value = self[current_key] 26 | 27 | self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash) 28 | _this_value = HashExt.new.replace(this_value) 29 | _this_value.deep_merge(other_value, &block).to_h 30 | else 31 | if block_given? && key?(current_key) 32 | block.call(current_key, this_value, other_value) 33 | else 34 | other_value 35 | end 36 | end 37 | end 38 | 39 | self 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/kondate/host_plugin/base.rb: -------------------------------------------------------------------------------- 1 | module Kondate 2 | module HostPlugin 3 | class Base 4 | attr_reader :config 5 | 6 | # @param [HashWithIndifferentAccess] config 7 | def initialize(config) 8 | @config = config 9 | end 10 | 11 | # @param [String] host hostname 12 | # @return [String] environment name 13 | def get_environment(host) 14 | ENV['ENVIRONMENT'] || 'development' 15 | end 16 | 17 | # @param [String] host hostname 18 | # @return [Array] array of roles 19 | def get_roles(host) 20 | raise NotImplementedError 21 | end 22 | 23 | # @param [String] role role 24 | # @return [Array] array of hosts 25 | # 26 | # Available from kondate >= 0.3.0 27 | def get_hosts(role) 28 | raise NotImplementedError 29 | end 30 | 31 | # @param [String] host hostname 32 | # @return [Hash] arbitrary host information 33 | # def get_hostinfo(host) 34 | # raise NotImplementedError 35 | # end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/kondate/host_plugin/file.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Kondate 4 | module HostPlugin 5 | # YAML format 6 | # 7 | # host1: [role1, role2] 8 | # host2: [role1, role2] 9 | class File < Base 10 | # @param [HashWithIndifferentAccess] config 11 | def initialize(config) 12 | super 13 | raise ConfigError.new('file: path is not configured') unless config.path 14 | @path = config.path 15 | 16 | @roles_of_host = YAML.load_file(@path) 17 | @hosts_of_role = {} 18 | @roles_of_host.each do |host, roles| 19 | roles.each do |role| 20 | @hosts_of_role[role] ||= [] 21 | @hosts_of_role[role] << host 22 | end 23 | end 24 | end 25 | 26 | # @param [String] host hostname 27 | # @return [String] environment name 28 | def get_environment(host) 29 | ENV['ENVIRONMENT'] || 'development' 30 | end 31 | 32 | # @param [String] host hostname 33 | # @return [Array] array of roles 34 | def get_roles(host) 35 | @roles_of_host[host] 36 | end 37 | 38 | # @param [String] role role 39 | # @return [Array] array of hosts 40 | # 41 | # Available from kondate >= 0.3.0 42 | def get_hosts(role) 43 | @hosts_of_role[role] 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/kondate/itamae_bootstrap.rb: -------------------------------------------------------------------------------- 1 | module Kondate 2 | class ItamaeBootstrap 3 | # @param [Itamae::Recipe::EvalContext] context itamae context 4 | def self.bootstrap(context) 5 | self.new(context).bootstrap 6 | end 7 | 8 | # @param [Itamae::Recipe::EvalContext] context itamae context 9 | def initialize(context) 10 | @context = context 11 | end 12 | 13 | def bootstrap 14 | $stdout.sync = true 15 | $stderr.sync = true 16 | tweak_logger 17 | bootstrap_middleware_recipes 18 | bootstrap_role_recipes 19 | end 20 | 21 | private 22 | 23 | def node 24 | @context.node 25 | end 26 | 27 | def include_recipe(recipe) 28 | @context.include_recipe(recipe) 29 | end 30 | 31 | def tweak_logger 32 | hostname = node[:hostname] 33 | ::Itamae::Logger::Formatter.class_eval do 34 | define_method(:call) do |severity, datetime, progname, msg| 35 | log = "%s | %s : %s" % [hostname, "%5s" % severity, msg2str(msg)] 36 | 37 | (colored ? colorize(log, severity) : log) + "\n" 38 | end 39 | end 40 | end 41 | 42 | def bootstrap_middleware_recipes 43 | recipes = node['attributes'].keys 44 | recipes.each do |recipe| 45 | secret_recipe_file = File.join(Config.secret_middleware_recipes_dir, recipe, "default.rb") 46 | recipe_file = File.join(Config.middleware_recipes_dir, recipe, "default.rb") 47 | File.exist?(secret_recipe_file) ? include_recipe(secret_recipe_file) : include_recipe(recipe_file) 48 | end 49 | end 50 | 51 | def bootstrap_role_recipes 52 | sep = File::SEPARATOR 53 | secret_role_recipe_file = RoleFile.explore(Config.secret_roles_recipes_dir, node[:role], "#{sep}default.rb") 54 | role_recipe_file = RoleFile.explore(Config.roles_recipes_dir, node[:role], "#{sep}default.rb") 55 | if File.exist?(secret_role_recipe_file) 56 | include_recipe(secret_role_recipe_file) 57 | elsif File.exist?(role_recipe_file) 58 | include_recipe(role_recipe_file) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/kondate/property_builder.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'tempfile' 3 | require_relative 'property_file' 4 | 5 | module Kondate 6 | class PropertyBuilder 7 | attr_reader :host 8 | 9 | def initialize(host) 10 | @host = host 11 | end 12 | 13 | def environment 14 | @environment ||= 15 | begin 16 | Config.host_plugin.get_environment(@host) || '' 17 | rescue => e 18 | $stderr.puts "cannot get environment for host:#{@host}, #{e.class} #{e.message}" 19 | '' 20 | end 21 | end 22 | 23 | def roles 24 | @roles ||= 25 | begin 26 | Config.host_plugin.get_roles(@host) || [] 27 | rescue => e 28 | $stderr.puts "cannot get roles for host:#{@host}, #{e.class} #{e.message}" 29 | [] 30 | end 31 | end 32 | 33 | def hostinfo 34 | @hostinfo ||= 35 | begin 36 | if Config.host_plugin.respond_to?(:get_hostinfo) 37 | Config.host_plugin.get_hostinfo(@host) || {} 38 | else 39 | {} 40 | end 41 | rescue => e 42 | $stderr.puts "cannot get hostinfo for host:#{@host}, #{e.class} #{e.message}" 43 | {} 44 | end 45 | end 46 | 47 | def filter_roles(filters) 48 | return self.roles if filters.nil? or filters.empty? 49 | filters = Array(filters).map {|filter| filter.gsub(':', '-') } 50 | if roles.empty? # maybe, development (vagrant) env 51 | @roles = filters # append specified roles 52 | @roles.each do |role| 53 | file = role_file(role) 54 | unless File.exist?(file) 55 | $stderr.puts "#{file} does not exist, possibly typo?" 56 | exit(1) 57 | end 58 | end 59 | else 60 | if (filters - roles).size > 0 61 | $stderr.puts "cannot specify #{(filters - roles).first}" 62 | exit(1) 63 | end 64 | unless filters.empty? 65 | # filter out for production env 66 | @roles = self.roles & filters 67 | end 68 | end 69 | @roles 70 | end 71 | 72 | def node_file 73 | File.join(Config.nodes_properties_dir, "#{@host}.yml") 74 | end 75 | 76 | def secret_node_file 77 | File.join(Config.secret_nodes_properties_dir, "#{@host}.yml") 78 | end 79 | 80 | def role_file(role) 81 | RoleFile.explore(Config.roles_properties_dir, role, ".yml") 82 | end 83 | 84 | def secret_role_file(role) 85 | RoleFile.explore(Config.secret_roles_properties_dir, role, ".yml") 86 | end 87 | 88 | def environment_file(environment) 89 | File.join(Config.environments_properties_dir, "#{environment}.yml") 90 | end 91 | 92 | def secret_environment_file(environment) 93 | File.join(Config.secret_environments_properties_dir, "#{environment}.yml") 94 | end 95 | 96 | def get_content(yaml_file) 97 | content = File.exist?(yaml_file) ? YAML.load_file(yaml_file) : {} 98 | content.is_a?(Hash) ? content : {} 99 | end 100 | 101 | # Generate tmp node file (for each role) 102 | # 103 | # { environment: environment, role: role, roles: roles } + 104 | # environment_file + secret_environment_file + 105 | # role_file + secret_role_file + 106 | # node_file + node_secret_file 107 | # 108 | # This file is automatically created and removed 109 | def install(role, filter_recipes = nil) 110 | files = [ 111 | environment_file(environment), 112 | secret_environment_file(environment), 113 | role_file(role), 114 | secret_role_file(role), 115 | node_file, 116 | secret_node_file, 117 | ].compact.select {|f| File.readable?(f) } 118 | 119 | property = HashExt.new.deep_merge!({ 120 | 'environment' => environment, 121 | 'role' => role, 122 | 'roles' => roles, 123 | 'hostinfo' => hostinfo, 124 | }) 125 | files.each do |file| 126 | property.deep_merge!(get_content(file)) 127 | end 128 | property['attributes'] ||= {} 129 | 130 | # filter out the recipe 131 | if filter_recipes and !filter_recipes.empty? 132 | property['attributes'].keys.each do |key| 133 | property['attributes'].delete(key) unless filter_recipes.include?(key) 134 | end 135 | end 136 | 137 | if property['attributes'].empty? 138 | PropertyFile.new(nil, files) 139 | else 140 | fp = Tempfile.create("kondate_") 141 | YAML.dump(property.to_h, fp) 142 | fp.close 143 | PropertyFile.new(fp.path, files) 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/kondate/property_file.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Kondate 4 | class PropertyFile 5 | attr_reader :path, :source_files 6 | 7 | def initialize(path, source_files) 8 | @path = path 9 | @source_files = source_files 10 | end 11 | 12 | def read 13 | mask_secrets(File.read(path)) 14 | end 15 | 16 | def load 17 | YAML.load_file(path) 18 | end 19 | 20 | def empty? 21 | @path.nil? 22 | end 23 | 24 | private 25 | 26 | def mask_secrets(str) 27 | str.gsub(/(.*key[^:]*): (.*)$/, '\1: *******'). 28 | gsub(/(.*password[^:]*): (.*)$/, '\1: *******'). 29 | gsub(/(-----BEGIN\s+PRIVATE\s+KEY-----)[0-9A-Za-z+\/=\s\\]+(-----END\s+PRIVATE\s+KEY-----)/m, '\1 xxxxx \2') 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/kondate/role_file.rb: -------------------------------------------------------------------------------- 1 | require_relative 'config' 2 | 3 | module Kondate 4 | class RoleFile 5 | attr_reader :dir, :role, :ext 6 | 7 | def self.explore(dir, role, ext = nil) 8 | self.new(dir, role, ext).explore 9 | end 10 | 11 | def initialize(dir, role, ext = nil) 12 | @dir = dir 13 | @role = role 14 | @ext = ext 15 | end 16 | 17 | # Returns readable role file exploring possible role files. For example, 18 | # if `role` is `myapp-web-staging`, this method explores files as 19 | # 20 | # 1. myapp-web-staging.yml 21 | # 1. myapp-web-base.yml 22 | # 1. myapp-web.yml 23 | # 1. myapp-base.yml 24 | # 1. myapp.yml 25 | # 1. base.yml 26 | # 27 | # @return [String] detected file path or last candidate path 28 | def explore 29 | paths = if Config.explore_role_files? 30 | possible_paths 31 | else 32 | [get_path] 33 | end 34 | paths.find {|path| File.readable?(path) } || paths.last 35 | end 36 | 37 | private 38 | 39 | def get_path(role = nil) 40 | "#{File.join(dir, role || @role)}#{ext}" 41 | end 42 | 43 | def possible_paths 44 | possible_roles.map {|role| get_path(role) } 45 | end 46 | 47 | def possible_roles 48 | parts = role.split('-') 49 | roles = [] 50 | roles << 'base' 51 | roles << parts.shift 52 | parts.each do |part| 53 | last = roles.last 54 | roles << "#{last}-base" 55 | roles << "#{last}-#{part}" 56 | end 57 | roles.reverse! 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/kondate/string_util.rb: -------------------------------------------------------------------------------- 1 | module Kondate 2 | class StringUtil 3 | def self.camelize(string) 4 | string = string.sub(/^[a-z\d]*/) { $&.capitalize } 5 | string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { $2.capitalize } 6 | string.gsub!(/\//, '::') 7 | string 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/kondate/templates/.kondate.conf: -------------------------------------------------------------------------------- 1 | --- 2 | middlware_recipes_dir: recipes/middleware 3 | roles_recipes_dir: recipes/roles 4 | middleware_recipes_serverspec_dir: spec/middleware 5 | roles_recipes_serverspec_dir: spec/roles 6 | nodes_properties_dir: properties/nodes 7 | roles_properties_dir: properties/roles 8 | environments_properties_dir: properties/environments 9 | secret_middlware_recipes_dir: secrets/recipes/middleware 10 | secret_roles_recipes_dir: secrets/recipes/roles 11 | secret_middleware_recipes_serverspec_dir: secrets/spec/middleware 12 | secret_roles_recipes_serverspec_dir: secrets/spec/roles 13 | secret_nodes_properties_dir: secrets/properties/nodes 14 | secret_roles_properties_dir: secrets/properties/roles 15 | secret_environments_properties_dir: secrets/properties/environments 16 | explore_role_files: true 17 | role_delimiter: "-" 18 | plugin_dir: lib 19 | host_plugin: 20 | type: file 21 | path: hosts.yml 22 | -------------------------------------------------------------------------------- /lib/kondate/templates/bootstrap.rb: -------------------------------------------------------------------------------- 1 | require 'kondate' 2 | 3 | Kondate::ItamaeBootstrap.bootstrap(self) 4 | -------------------------------------------------------------------------------- /lib/kondate/templates/hosts.yml: -------------------------------------------------------------------------------- 1 | localhost: [sample] 2 | -------------------------------------------------------------------------------- /lib/kondate/templates/properties/environments/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/kondate/a56d520b08a1a2bf2d992067ef017175401684ef/lib/kondate/templates/properties/environments/.gitkeep -------------------------------------------------------------------------------- /lib/kondate/templates/properties/environments/development.yml: -------------------------------------------------------------------------------- 1 | global_attributes: 2 | aws_region: ap-northeast-1 3 | -------------------------------------------------------------------------------- /lib/kondate/templates/properties/nodes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/kondate/a56d520b08a1a2bf2d992067ef017175401684ef/lib/kondate/templates/properties/nodes/.gitkeep -------------------------------------------------------------------------------- /lib/kondate/templates/properties/roles/sample.yml: -------------------------------------------------------------------------------- 1 | attributes: 2 | base: 3 | message: 'hello, kondate!' 4 | -------------------------------------------------------------------------------- /lib/kondate/templates/recipes/middleware/base/default.rb: -------------------------------------------------------------------------------- 1 | execute "echo #{attrs['base']['message']}" do 2 | command "echo #{attrs['base']['message']}" 3 | end 4 | -------------------------------------------------------------------------------- /lib/kondate/templates/recipes/roles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/kondate/a56d520b08a1a2bf2d992067ef017175401684ef/lib/kondate/templates/recipes/roles/.gitkeep -------------------------------------------------------------------------------- /lib/kondate/templates/secrets/properties/environments/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/kondate/a56d520b08a1a2bf2d992067ef017175401684ef/lib/kondate/templates/secrets/properties/environments/.gitkeep -------------------------------------------------------------------------------- /lib/kondate/templates/secrets/properties/nodes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/kondate/a56d520b08a1a2bf2d992067ef017175401684ef/lib/kondate/templates/secrets/properties/nodes/.gitkeep -------------------------------------------------------------------------------- /lib/kondate/templates/secrets/properties/roles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/kondate/a56d520b08a1a2bf2d992067ef017175401684ef/lib/kondate/templates/secrets/properties/roles/.gitkeep -------------------------------------------------------------------------------- /lib/kondate/templates/spec/middleware/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/kondate/a56d520b08a1a2bf2d992067ef017175401684ef/lib/kondate/templates/spec/middleware/.gitkeep -------------------------------------------------------------------------------- /lib/kondate/templates/spec/middleware/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe file('/tmp') do 4 | it { should be_directory } 5 | end 6 | -------------------------------------------------------------------------------- /lib/kondate/templates/spec/roles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/kondate/a56d520b08a1a2bf2d992067ef017175401684ef/lib/kondate/templates/spec/roles/.gitkeep -------------------------------------------------------------------------------- /lib/kondate/templates/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'serverspec' 2 | require 'net/ssh' 3 | require 'tempfile' 4 | require 'yaml' 5 | 6 | ### required for kondate ##### 7 | host = ENV['TARGET_HOST'] 8 | set :set_property, YAML.load_file(ENV['TARGET_NODE_FILE']) 9 | $stdout.sync = true 10 | $stderr.sync = true 11 | ############################ 12 | 13 | set :backend, :ssh 14 | 15 | if ENV['ASK_SUDO_PASSWORD'] 16 | begin 17 | require 'highline/import' 18 | rescue LoadError 19 | fail "highline is not available. Try installing it." 20 | end 21 | set :sudo_password, ask("Enter sudo password: ") { |q| q.echo = false } 22 | else 23 | set :sudo_password, ENV['SUDO_PASSWORD'] 24 | end 25 | 26 | options = 27 | if ENV['TARGET_VAGRANT'] 28 | `vagrant up #{host}` 29 | 30 | config = Tempfile.new('', Dir.tmpdir) 31 | config.write(`vagrant ssh-config #{host}`) 32 | config.close 33 | 34 | Net::SSH::Config.for(host, [config.path]) 35 | else 36 | o = Net::SSH::Config.for(host) 37 | ssh_config_options = 38 | %w(encryption compression compression_level 39 | timeout forward_agent global_known_hosts_file 40 | auth_methods host_key host_key_alias host_name 41 | keys keys_only hmac auth_methods port proxy 42 | rekey_limit user user_known_hosts_file) 43 | 44 | ssh_config_options.map do |option| 45 | if property[option] 46 | o[option.to_sym] = property[option] 47 | end 48 | end 49 | o 50 | end 51 | 52 | options[:user] ||= Etc.getlogin 53 | 54 | set :host, options[:host_name] || host 55 | set :ssh_options, options 56 | 57 | # Disable sudo 58 | # set :disable_sudo, true 59 | 60 | 61 | # Set environment variables 62 | # set :env, :LANG => 'C', :LC_MESSAGES => 'C' 63 | 64 | # Set PATH 65 | # set :path, '/sbin:/usr/local/sbin:$PATH' 66 | -------------------------------------------------------------------------------- /lib/kondate/version.rb: -------------------------------------------------------------------------------- 1 | module Kondate 2 | VERSION = "0.5.0" 3 | end 4 | -------------------------------------------------------------------------------- /test/kondate_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class KondateTest < Minitest::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil ::Kondate::VERSION 6 | end 7 | 8 | def test_it_does_something_useful 9 | assert false 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'kondate' 3 | 4 | require 'minitest/autorun' 5 | --------------------------------------------------------------------------------