engineyard-jenkins 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +87 -0
  4. data/History.md +32 -0
  5. data/README.md +186 -0
  6. data/Rakefile +24 -0
  7. data/bin/ey-jenkins +7 -0
  8. data/engineyard-jenkins.gemspec +33 -0
  9. data/features/install.feature +53 -0
  10. data/features/install_server.feature +69 -0
  11. data/features/step_definitions/api_steps.rb +10 -0
  12. data/features/step_definitions/common_steps.rb +211 -0
  13. data/features/step_definitions/fixture_project_steps.rb +14 -0
  14. data/features/step_definitions/jenkins_steps.rb +9 -0
  15. data/features/support/common.rb +51 -0
  16. data/features/support/engineyard.rb +24 -0
  17. data/features/support/env.rb +14 -0
  18. data/features/support/matchers.rb +10 -0
  19. data/fixtures/cookbooks/main/recipes/default.rb +1 -0
  20. data/fixtures/cookbooks/redis/recipes/default.rb +0 -0
  21. data/fixtures/jenkins_boot_sequence/jenkins_booting.html +1 -0
  22. data/fixtures/jenkins_boot_sequence/jenkins_ready.html +1 -0
  23. data/fixtures/jenkins_boot_sequence/pre_jenkins_booting.html +1 -0
  24. data/fixtures/projects/rails/Gemfile +3 -0
  25. data/fixtures/projects/rails/Gemfile.lock +10 -0
  26. data/fixtures/projects/rails/Rakefile +4 -0
  27. data/lib/engineyard-jenkins.rb +4 -0
  28. data/lib/engineyard-jenkins/appcloud_env.rb +49 -0
  29. data/lib/engineyard-jenkins/cli.rb +134 -0
  30. data/lib/engineyard-jenkins/cli/install_generator.rb +55 -0
  31. data/lib/engineyard-jenkins/cli/install_generator/templates/attributes.rb.tt +17 -0
  32. data/lib/engineyard-jenkins/cli/install_generator/templates/cookbooks/main/attributes/recipe.rb +3 -0
  33. data/lib/engineyard-jenkins/cli/install_generator/templates/cookbooks/main/definitions/ey_cloud_report.rb +6 -0
  34. data/lib/engineyard-jenkins/cli/install_generator/templates/cookbooks/main/libraries/ruby_block.rb +40 -0
  35. data/lib/engineyard-jenkins/cli/install_generator/templates/cookbooks/main/libraries/run_for_app.rb +12 -0
  36. data/lib/engineyard-jenkins/cli/install_generator/templates/recipes.rb +95 -0
  37. data/lib/engineyard-jenkins/cli/install_server_generator.rb +25 -0
  38. data/lib/engineyard-jenkins/cli/install_server_generator/templates/attributes.rb.tt +3 -0
  39. data/lib/engineyard-jenkins/cli/install_server_generator/templates/cookbooks/jenkins_master/recipes/default.rb +95 -0
  40. data/lib/engineyard-jenkins/cli/install_server_generator/templates/cookbooks/jenkins_master/templates/default/init.sh.erb +26 -0
  41. data/lib/engineyard-jenkins/cli/install_server_generator/templates/cookbooks/jenkins_master/templates/default/proxy.conf.erb +20 -0
  42. data/lib/engineyard-jenkins/cli/install_server_generator/templates/cookbooks/main/attributes/recipe.rb +3 -0
  43. data/lib/engineyard-jenkins/cli/install_server_generator/templates/cookbooks/main/definitions/ey_cloud_report.rb +6 -0
  44. data/lib/engineyard-jenkins/cli/install_server_generator/templates/cookbooks/main/libraries/ruby_block.rb +40 -0
  45. data/lib/engineyard-jenkins/cli/install_server_generator/templates/cookbooks/main/libraries/run_for_app.rb +12 -0
  46. data/lib/engineyard-jenkins/cli/install_server_generator/templates/cookbooks/main/recipes/default.rb +1 -0
  47. data/lib/engineyard-jenkins/thor-ext/actions/directory.rb +33 -0
  48. data/lib/engineyard-jenkins/version.rb +5 -0
  49. data/spec/appcloud_env_spec.rb +75 -0
  50. data/spec/spec_helper.rb +4 -0
  51. metadata +254 -0
@@ -0,0 +1,134 @@
1
+ require 'thor'
2
+ require 'engineyard-jenkins/thor-ext/actions/directory'
3
+ require 'engineyard-jenkins/appcloud_env'
4
+
5
+ module Engineyard
6
+ module Jenkins
7
+ class CLI < Thor
8
+
9
+ desc "install PROJECT_PATH", "Install Jenkins node/slave recipes into your project."
10
+ def install(project_path)
11
+ require 'engineyard-jenkins/cli/install_generator'
12
+ Engineyard::Jenkins::InstallGenerator.start(ARGV.unshift(project_path))
13
+ end
14
+
15
+ desc "install_server [PROJECT_PATH]", "Install Jenkins CI into an AppCloud environment."
16
+ method_option :verbose, :aliases => ["-V"], :desc => "Display more output"
17
+ method_option :environment, :aliases => ["-e"], :desc => "Environment in which to deploy this application", :type => :string
18
+ method_option :account, :aliases => ["-c"], :desc => "Name of the account you want to deploy in"
19
+ # Generates a chef recipe cookbook, uploads it to AppCloud, and waits until Jenkins CI has launched
20
+ def install_server(project_path=nil)
21
+ environments = Engineyard::Jenkins::AppcloudEnv.new.find_environments(options)
22
+ if environments.size == 0
23
+ no_environments_discovered and return
24
+ elsif environments.size > 1
25
+ too_many_environments_discovered(environments) and return
26
+ end
27
+
28
+ env_name, account_name, environment = environments.first
29
+ if environment.instances.first
30
+ public_hostname = environment.instances.first.public_hostname
31
+ status = environment.instances.first.status
32
+ end
33
+
34
+ temp_project_path = File.expand_path(project_path || File.join(Dir.tmpdir, "temp_jenkins_server"))
35
+ shell.say "Temp installation dir: #{temp_project_path}" if options[:verbose]
36
+
37
+ FileUtils.mkdir_p(temp_project_path)
38
+ FileUtils.chdir(temp_project_path) do
39
+ # 'install_server' generator
40
+ require 'engineyard-jenkins/cli/install_server_generator'
41
+ Engineyard::Jenkins::InstallServerGenerator.start(ARGV.unshift(temp_project_path))
42
+
43
+ say ""
44
+ say "Uploading to "; say "'#{env_name}' ", :yellow; say "environment on "; say "'#{account_name}' ", :yellow; say "account..."
45
+ require 'engineyard/cli/recipes'
46
+ environment.upload_recipes
47
+
48
+ if status == "running"
49
+ say "Environment is rebuilding..."
50
+ environment.run_custom_recipes
51
+ watch_page_while public_hostname, 80, "/" do |req|
52
+ req.body !~ /Please wait while Jenkins is getting ready to work/
53
+ end
54
+
55
+ say ""
56
+ say "Jenkins is starting..."
57
+ watch_page_while public_hostname, 80, "/" do |req|
58
+ req.body =~ /Please wait while Jenkins is getting ready to work/
59
+ end
60
+
61
+ require 'jenkins'
62
+ require 'jenkins/config'
63
+ ::Jenkins::Config.config["base_uri"] = public_hostname
64
+ ::Jenkins::Config.store!
65
+
66
+ say ""
67
+ say "Done! Jenkins CI hosted at "; say "http://#{public_hostname}", :green
68
+ else
69
+ say ""
70
+ say "Almost there..."
71
+ say "* Boot your environment via https://cloud.engineyard.com", :yellow
72
+ end
73
+ end
74
+ end
75
+
76
+ desc "version", "show version information"
77
+ def version
78
+ require 'engineyard-jenkins/version'
79
+ shell.say Engineyard::Jenkins::VERSION
80
+ end
81
+
82
+ map "-v" => :version, "--version" => :version, "-h" => :help, "--help" => :help
83
+
84
+ private
85
+ def say(msg, color = nil)
86
+ color ? shell.say(msg, color) : shell.say(msg)
87
+ end
88
+
89
+ def display(text)
90
+ shell.say text
91
+ exit
92
+ end
93
+
94
+ def error(text)
95
+ shell.say "ERROR: #{text}", :red
96
+ exit
97
+ end
98
+
99
+ def no_environments_discovered
100
+ say "No environments with name jenkins, jenkins_server, jenkins_production, jenkins_server_production.", :red
101
+ say "Either:"
102
+ say " * Create an AppCloud environment called jenkins, jenkins_server, jenkins_production, jenkins_server_production"
103
+ say " * Use --environment/--account flags to select AppCloud environment"
104
+ end
105
+
106
+ def too_many_environments_discovered(environments)
107
+ say "Multiple environments possible, please be more specific:", :red
108
+ say ""
109
+ environments.each do |env_name, account_name, environment|
110
+ say " ey-jenkins install_server --environment "; say "'#{env_name}' ", :yellow;
111
+ say "--account "; say "'#{account_name}'", :yellow
112
+ end
113
+ end
114
+
115
+ def watch_page_while(host, port, path)
116
+ waiting = true
117
+ while waiting
118
+ begin
119
+ Net::HTTP.start(host, port) do |http|
120
+ req = http.get(path)
121
+ waiting = yield req
122
+ end
123
+ sleep 1; print '.'; $stdout.flush
124
+ rescue SocketError => e
125
+ sleep 1; print 'x'; $stdout.flush
126
+ rescue Exception => e
127
+ puts e.message
128
+ sleep 1; print '.'; $stdout.flush
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,55 @@
1
+ require 'thor/group'
2
+
3
+ module Engineyard
4
+ module Jenkins
5
+ class InstallGenerator < Thor::Group
6
+ include Thor::Actions
7
+
8
+ argument :project_path
9
+
10
+ def self.source_root
11
+ File.join(File.dirname(__FILE__), "install_generator", "templates")
12
+ end
13
+
14
+ def install_cookbooks
15
+ file = "cookbooks/main/recipes/default.rb"
16
+ unless File.exists?(File.join(destination_root, "cookbooks/main/recipes/default.rb"))
17
+ directory "cookbooks"
18
+ end
19
+ end
20
+
21
+ def attributes
22
+ template "attributes.rb.tt", "cookbooks/jenkins_slave/attributes/default.rb"
23
+ end
24
+
25
+ def recipe
26
+ copy_file "recipes.rb", "cookbooks/jenkins_slave/recipes/default.rb"
27
+ end
28
+
29
+ def enable_recipe
30
+ file = "cookbooks/main/recipes/default.rb"
31
+ enable_cmd = "\nrequire_recipe 'jenkins_slave'"
32
+ if File.exists?(file_path = File.join(destination_root, file))
33
+ append_file file, enable_cmd
34
+ else
35
+ create_file file, enable_cmd
36
+ end
37
+ end
38
+
39
+ def readme
40
+ say ""
41
+ say "Finally:"
42
+ say "* edit "; say "cookbooks/jenkins_slave/attributes/default.rb ", :yellow; say "as necessary."
43
+ say "* run: "; say "ey recipes upload ", :green; say "# use --environment(-e) & --account(-c)"
44
+ say "* run: "; say "ey recipes apply ", :green; say "# to select environment"
45
+ say "* "; say "Boot your environment ", :yellow; say "if not already booted."
46
+ say "When the recipe completes, your project will commence its first build on Jenkins CI."
47
+ end
48
+
49
+ private
50
+ def say(msg, color = nil)
51
+ color ? shell.say(msg, color) : shell.say(msg)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,17 @@
1
+ #
2
+ # Cookbook Name:: jenkins_slave
3
+ # Recipe:: default
4
+ #
5
+
6
+ jenkins_slave({
7
+ :master => {
8
+ :host => "ec2-174-129-24-134.compute-1.amazonaws.com",
9
+ :port => 80,
10
+ :public_key => "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6AWDDDJcsIrY0KA99KPg+UmSjxjPz7+Eu9mO5GaSNn0vvVdsgrgjkh+35AS9k8Gn/DPaQJoNih+DpY5ZHsuY1zlvnvvk+hsCUHOATngARNs6yQMf2IrQqf38SlBPJ/xjt4oopLyqZuZ59xbFMFa0Yr/B7cCpxNpeIMCbwmc8YOtztOG1ZazlxB6eMTwp1V25TxFPh3PqUz9s37NmBEhkRiEyiJzlDSrKwz2y+77VWztQByM30lYAEXc5GwJD1LTaQwlv/thjhwveAzKLIpxzC5TbUjii7L+4iJF/JrjtXAEYmkegXj6lGBpRIdwXTYWMm3jG6gG+MV2nfWmocDzg3Q==",
11
+ :master_key_location => "/home/deploy/.ssh/id_rsa"
12
+ },
13
+ :gem => {
14
+ :install => "jenkins --pre",
15
+ :version => "jenkins-0.3.0.beta.16"
16
+ }
17
+ })
@@ -0,0 +1,3 @@
1
+ recipes('main')
2
+ owner_name(@attribute[:users].first[:username])
3
+ owner_pass(@attribute[:users].first[:password])
@@ -0,0 +1,6 @@
1
+ define :ey_cloud_report do
2
+ execute "reporting for #{params[:name]}" do
3
+ command "ey-enzyme --report '#{params[:message]}'"
4
+ epic_fail true
5
+ end
6
+ end
@@ -0,0 +1,40 @@
1
+
2
+ class Chef
3
+ class Resource
4
+ class RubyBlock < Chef::Resource
5
+ def initialize(name, collection=nil, node=nil)
6
+ super(name, collection, node)
7
+ @resource_name = :ruby_block
8
+ @action = :create
9
+ @allowed_actions.push(:create)
10
+ end
11
+
12
+ def block(&block)
13
+ if block
14
+ @block = block
15
+ else
16
+ @block
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+
24
+ class Chef
25
+ class Provider
26
+ class RubyBlock < Chef::Provider
27
+ def load_current_resource
28
+ Chef::Log.debug(@new_resource.inspect)
29
+ true
30
+ end
31
+
32
+ def action_create
33
+ @new_resource.block.call
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ Chef::Platform.platforms[:default].merge! :ruby_block => Chef::Provider::RubyBlock
40
+
@@ -0,0 +1,12 @@
1
+ class Chef
2
+ class Recipe
3
+ def run_for_app(*apps, &block)
4
+ apps.map! {|a| a.to_s }
5
+ node[:applications].map{|k,v| [k,v] }.sort_by {|a,b| a }.each do |name, app_data|
6
+ if apps.include?(name)
7
+ block.call(name, app_data)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,95 @@
1
+ #
2
+ # Cookbook Name:: jenkins_slave
3
+ # Recipe:: default
4
+ #
5
+
6
+ env_name = node[:environment][:name]
7
+ framework_env = node[:environment][:framework_env]
8
+ username = node[:users].first[:username]
9
+
10
+ if ['solo','app_master'].include?(node[:instance_role]) && env_name =~ /(ci|jenkins_slave)$/
11
+ gem_package "bundler" do
12
+ action :install
13
+ end
14
+
15
+ execute "install_jenkins_in_resin" do
16
+ command "/usr/local/ey_resin/ruby/bin/gem install #{node[:jenkins_slave][:gem][:install]}"
17
+ not_if { FileTest.directory?("/usr/local/ey_resin/ruby/gems/1.8/gems/#{node[:jenkins_slave][:gem][:version]}") }
18
+ end
19
+
20
+ ruby_block "authorize_jenkins_master_key" do
21
+ authorized_keys = "/home/#{node[:users].first[:username]}/.ssh/authorized_keys"
22
+ block do
23
+ File.open(authorized_keys, "a") do |f|
24
+ f.puts node[:jenkins_slave][:master][:public_key]
25
+ end
26
+ end
27
+ not_if "grep '#{node[:jenkins_slave][:master][:public_key]}' #{authorized_keys}"
28
+ end
29
+
30
+ execute "setup-git-config-for-tagging" do
31
+ command %Q{ sudo su #{username} -c "git config --global user.email 'you@example.com' && git config --global user.name 'You are Special'" }
32
+ not_if %Q{ sudo su #{username} -c "git config user.email" }
33
+ end
34
+
35
+ ruby_block "add-slave-to-master" do
36
+ block do
37
+ Gem.clear_paths
38
+ require "jenkins"
39
+ require "jenkins/config"
40
+
41
+ Jenkins::Api.setup_base_url(node[:jenkins_slave][:master])
42
+
43
+ Jenkins::Api.delete_node(env_name)
44
+
45
+ # Tell master about this slave
46
+ Jenkins::Api.add_node(
47
+ :name => env_name,
48
+ :description => "Automatically added by Engine Yard AppCloud for environment #{env_name}",
49
+ :slave_host => node[:engineyard][:environment][:instances].first[:public_hostname],
50
+ :slave_user => username,
51
+ :executors => [node[:applications].size, 1].max,
52
+ :label => node[:applications].keys.join(" ")
53
+ )
54
+ end
55
+ action :create
56
+ end
57
+
58
+ ruby_block "tell-master-about-new-jobs" do
59
+ block do
60
+ begin
61
+ job_names = Jenkins::Api.job_names
62
+ app_names = node[:applications].keys
63
+ apps_to_add = app_names - job_names
64
+
65
+ # Tell server about each application
66
+ apps_to_add.each do |app_name|
67
+ data = node[:applications][app_name]
68
+
69
+ # job_config = Jenkins::JobConfigBuilder.new("rails") do |c|
70
+ job_config = Jenkins::JobConfigBuilder.new do |c|
71
+ c.scm = data[:repository_name]
72
+ c.assigned_node = app_name
73
+ c.envfile = "/data/#{app_name}/shared/config/git-env"
74
+ c.steps = [
75
+ [:build_shell_step, "bundle install"],
76
+ [:build_ruby_step, <<-RUBY.gsub(/^ /, '')],
77
+ appcloud_database = "/data/#{app_name}/shared/config/database.yml"
78
+ FileUtils.cp appcloud_database, "config/database.yml"
79
+ RUBY
80
+ [:build_shell_step, "bundle exec rake db:schema:load RAILS_ENV=#{framework_env} RACK_ENV=#{framework_env}"],
81
+ [:build_shell_step, "bundle exec rake RAILS_ENV=#{framework_env} RACK_ENV=#{framework_env}"]
82
+ ]
83
+ end
84
+
85
+ Jenkins::Api.create_job(app_name, job_config)
86
+ Jenkins::Api.build_job(app_name)
87
+ end
88
+ rescue Errno::ECONNREFUSED, Errno::EAFNOSUPPORT
89
+ raise Exception, "No connection available to the Jenkins server (#{Jenkins::Api.base_uri})."
90
+ end
91
+ end
92
+ action :create
93
+ end
94
+
95
+ end
@@ -0,0 +1,25 @@
1
+ require 'thor/group'
2
+
3
+ module Engineyard
4
+ module Jenkins
5
+ class InstallServerGenerator < Thor::Group
6
+ include Thor::Actions
7
+
8
+ class_option :plugins, :aliases => '-p', :desc => 'additional Jenkins CI plugins (comma separated)'
9
+
10
+ def self.source_root
11
+ File.join(File.dirname(__FILE__), "install_server_generator", "templates")
12
+ end
13
+
14
+ def cookbooks
15
+ directory "cookbooks"
16
+ end
17
+
18
+ def attributes
19
+ @plugins = %w[git github rake ruby greenballs envfile] + (options[:plugins] || '').strip.split(/\s*,\s*/)
20
+ template "attributes.rb.tt", "cookbooks/jenkins_master/attributes/default.rb"
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ jenkins_master({
2
+ :plugins => %w[<%= @plugins.join(" ") %>]
3
+ })
@@ -0,0 +1,95 @@
1
+ #
2
+ # Cookbook Name:: jenkins
3
+ # Recipe:: default
4
+ #
5
+
6
+ # Using manual jenkins for now not jenkins gem. No ebuild seems to exist.
7
+ # Based on http://bit.ly/9Y852l
8
+
9
+ # You can use this in combination with http://github.com/bjeanes/ey_jenkins_proxy
10
+ # to serve jenkins publicly on a Jenkins-only EY instance. This is so you don't have to
11
+ # find a simple app to run on the instance in lieu of an actual staging/production site.
12
+ # Alternatively, set up nginx asa reverse proxy manually.
13
+
14
+ # We'll assume running jenkins under the default username
15
+ jenkins_user = node[:users].first[:username]
16
+ jenkins_port = 8082 # change this in your proxy if modified
17
+ jenkins_home = "/data/jenkins-ci"
18
+ jenkins_pid = "#{jenkins_home}/tmp/pid"
19
+ plugins = node[:jenkins_master][:plugins]
20
+
21
+ if ['solo'].include?(node[:instance_role])
22
+ gem_package "bundler" do
23
+ source "http://gemcutter.org"
24
+ action :install
25
+ end
26
+
27
+ execute "setup-git-config-for-tagging" do
28
+ command %Q{ sudo su #{jenkins_user} -c "git config --global user.email 'you@example.com' && git config --global user.name 'You are Special'" }
29
+ not_if %Q{ sudo su #{jenkins_user} -c "git config user.email" }
30
+ end
31
+
32
+ %w[logs tmp war plugins .].each do |dir|
33
+ directory "#{jenkins_home}/#{dir}" do
34
+ owner jenkins_user
35
+ group jenkins_user
36
+ mode 0755 unless dir == "war"
37
+ action :create
38
+ recursive true
39
+ end
40
+ end
41
+
42
+ remote_file "#{jenkins_home}/jenkins.war" do
43
+ source "http://jenkins-ci.org/latest/jenkins.war"
44
+ owner jenkins_user
45
+ group jenkins_user
46
+ not_if { FileTest.exists?("#{jenkins_home}/jenkins.war") }
47
+ end
48
+
49
+ template "/etc/init.d/jenkins" do
50
+ source "init.sh.erb"
51
+ owner "root"
52
+ group "root"
53
+ mode 0755
54
+ variables(
55
+ :user => jenkins_user,
56
+ :port => jenkins_port,
57
+ :home => jenkins_home,
58
+ :pid => jenkins_pid
59
+ )
60
+ end
61
+
62
+ plugins.each do |plugin|
63
+ remote_file "#{jenkins_home}/plugins/#{plugin}.hpi" do
64
+ source "http://jenkins-ci.org/latest/#{plugin}.hpi"
65
+ owner jenkins_user
66
+ group jenkins_user
67
+ not_if { FileTest.exists?("#{jenkins_home}/plugins/#{plugin}.hpi") }
68
+ end
69
+
70
+ end
71
+
72
+ template "/data/nginx/servers/jenkins_reverse_proxy.conf" do
73
+ source "proxy.conf.erb"
74
+ owner jenkins_user
75
+ group jenkins_user
76
+ mode 0644
77
+ variables(
78
+ :port => jenkins_port
79
+ )
80
+ end
81
+
82
+ execute "ensure-jenkins-is-running" do
83
+ command "/etc/init.d/jenkins restart"
84
+ end
85
+
86
+ execute "Restart nginx" do
87
+ command "/etc/init.d/nginx restart"
88
+ end
89
+
90
+ execute "Generate key pair for slaves" do
91
+ key_path = "/home/#{node[:users].first[:username]}/.ssh/id_rsa"
92
+ command "ssh-keygen -f #{key_path} -N ''"
93
+ not_if { FileTest.exists?(key_path) }
94
+ end
95
+ end