engineyard-jenkins 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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