chef-tlc-workflow 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +7 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +93 -0
  5. data/Rakefile +110 -0
  6. data/Vagrantfile +33 -0
  7. data/chef-tlc-workflow.gemspec +52 -0
  8. data/docs/ApplicationVsLibraryVsForkedCookbooks.md +20 -0
  9. data/docs/TmpLibrarianHelpers.md +21 -0
  10. data/docs/TmpWorkflowTasks.md +80 -0
  11. data/lib/chef-tlc-workflow.rb +5 -0
  12. data/lib/chef-tlc-workflow/helpers.rb +126 -0
  13. data/lib/chef-tlc-workflow/version.rb +3 -0
  14. data/lib/chef-workflow/tasks/tlc.rb +4 -0
  15. data/lib/chef-workflow/tasks/tlc/deps.rb +100 -0
  16. data/lib/chef-workflow/tasks/tlc/test.rb +15 -0
  17. data/test/Gemfile +7 -0
  18. data/test/ec2-bootstrap/.chef/bootstrap/omnibus-chef.sh +34 -0
  19. data/test/ec2-bootstrap/.gitignore +3 -0
  20. data/test/ec2-bootstrap/Mccloudfile +60 -0
  21. data/test/ec2-bootstrap/Rakefile +57 -0
  22. data/test/ec2-bootstrap/app_cookbooks.yml +12 -0
  23. data/test/esx-bootstrap/.chef/knife.rb +7 -0
  24. data/test/esx-bootstrap/.gitignore +4 -0
  25. data/test/esx-bootstrap/Rakefile +64 -0
  26. data/test/esx-bootstrap/app_cookbooks.yml +12 -0
  27. data/test/esx-bootstrap/nodes/33.33.77.10.json +6 -0
  28. data/test/local-bootstrap/.gitignore +4 -0
  29. data/test/local-bootstrap/Rakefile +50 -0
  30. data/test/local-bootstrap/Vagrantfile +31 -0
  31. data/test/local-bootstrap/app_cookbooks.yml +12 -0
  32. data/test/sample-app/CHANGELOG.md +12 -0
  33. data/test/sample-app/Cheffile +15 -0
  34. data/test/sample-app/README.md +20 -0
  35. data/test/sample-app/attributes/default.rb +2 -0
  36. data/test/sample-app/metadata.rb +10 -0
  37. data/test/sample-app/recipes/default.rb +27 -0
  38. data/test/sample-app/templates/default/sample.html.erb +10 -0
  39. metadata +331 -0
@@ -0,0 +1,5 @@
1
+ require "chef-tlc-workflow/version"
2
+
3
+ module ChefTLCWorkflow
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,126 @@
1
+ module ChefTLCWorkflow
2
+
3
+ module Helpers
4
+
5
+ #
6
+ # reads the direct dependencies defined in `metadata.rb` and
7
+ # returns a map of dependency => version, e.g.:
8
+ #
9
+ # { 'foo' => '1.0.0', 'bar' => '0.1.0' }
10
+ #
11
+ def self.read_metadata_deps
12
+ require 'chef/cookbook/metadata'
13
+ metadata = ::Chef::Cookbook::Metadata.new
14
+ metadata.from_file "metadata.rb"
15
+ metadata.dependencies
16
+ end
17
+
18
+ #
19
+ # reads the direct dependencies defined in `Cheffile` and
20
+ # returns a map of dependency => version, e.g.:
21
+ #
22
+ # { 'foo' => '1.0.0', 'bar' => '0.1.0' }
23
+ #
24
+ def self.read_cheffile_deps
25
+ require 'librarian/chef/environment'
26
+ env = ::Librarian::Chef::Environment.new
27
+ deps = env.spec.dependencies
28
+ Hash[deps.map { |dep| [dep.name, dep.requirement.to_s] }]
29
+ end
30
+
31
+ #
32
+ # resolves the dependencies as specified in `Cheffile` and
33
+ # returns a map of dependency => version (including the
34
+ # transitive ones), e.g.:
35
+ #
36
+ # { 'foo' => '1.0.0', 'bar' => '0.1.0', 'baz_which_depends_on_bar' => '1.5.0' }
37
+ #
38
+ def self.read_and_resolve_cheffile_deps
39
+ require 'librarian/chef/environment'
40
+ env = ::Librarian::Chef::Environment.new
41
+ deps = env.resolver.resolve(env.spec).manifests
42
+ Hash[deps.map { |dep| [dep.name, dep.version.to_s] }]
43
+ end
44
+
45
+ #
46
+ # returns the direct dependencies defined in `metadata.rb` as an
47
+ # array of triples:
48
+ #
49
+ # [[<cookbook_name>, <cookbook_version>, <location>], ...]
50
+ #
51
+ # where `<location>` is `nil`, unless the `locations_yml`
52
+ # parameter is given and the specified file contains a location
53
+ # mapping for `<cookbook_name>`
54
+ #
55
+ # Example usage in `Cheffile`:
56
+ #
57
+ # require 'chef-tlc-workflow/helpers'
58
+ #
59
+ # ChefTLCWorkflow::Helpers::from_metadata.each do |cb_name, cb_version, location|
60
+ # cookbook cb_name, cb_version, location
61
+ # end
62
+ #
63
+ def self.from_metadata(locations_yml = nil)
64
+ read_metadata_deps.to_a.map do |cb_name, cb_version|
65
+ if locations_yml
66
+ inferred_location = resolve_location_from_file(locations_yml, cb_name, cb_version)
67
+ else
68
+ inferred_location = nil
69
+ end
70
+ [cb_name, cb_version, inferred_location]
71
+ end
72
+ end
73
+
74
+ #
75
+ # reads in the YAML file containing the application cookbooks and
76
+ # returns either all application cookbooks defined there or a specific
77
+ # ones if the `name` and/or `version` parameters are given to filter
78
+ # the selection.
79
+ #
80
+ # Example .yml file:
81
+ #
82
+ # - name: "sample-app"
83
+ # version: "0.1.0"
84
+ # git: "https://github.com/tknerr/sample-app-tlc.git"
85
+ # ref: "v0.1.0"
86
+ # - name: "sample-app"
87
+ # version: "0.2.0"
88
+ # path: "../sample-app"
89
+ #
90
+ #
91
+ def self.read_app_cookbooks(yml_file, name = nil, version = nil)
92
+ # TODO: validate format
93
+ app_cookbooks = YAML.load_file yml_file
94
+ app_cookbooks.select! { |ac| ac['name'] == name } if name
95
+ app_cookbooks.select! { |ac| ac['version'] == version } if version
96
+ app_cookbooks
97
+ end
98
+
99
+ #
100
+ # parse input string "<app-cookbook-name>@<version>" into
101
+ # a `[name, version]` pair by splitting at the '@' character
102
+ #
103
+ def self.parse_name_and_version(string)
104
+ if string == nil
105
+ [nil, nil]
106
+ else
107
+ string.split '@'
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def self.resolve_location_from_file(locations_yml, cb_name, cb_version)
114
+ cookbook_index = YAML.load_file(locations_yml)
115
+ if cookbook_index[cb_name]
116
+ default_opts = Hash.new
117
+ default_opts[:git] = cookbook_index[cb_name][:git] if cookbook_index[cb_name][:git]
118
+ default_opts[:ref] = cookbook_index[cb_name][:ref] if cookbook_index[cb_name][:ref]
119
+ version_specific_opts = cookbook_index[cb_name][cb_version] || {}
120
+ return default_opts.merge version_specific_opts
121
+ else
122
+ return nil
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,3 @@
1
+ module ChefTLCWorkflow
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'chef-workflow-tasklib'
2
+
3
+ chef_workflow_task 'tlc/deps'
4
+ chef_workflow_task 'tlc/test'
@@ -0,0 +1,100 @@
1
+
2
+ namespace :tlc do
3
+ namespace :deps do
4
+
5
+ require 'chef-tlc-workflow/helpers'
6
+
7
+ #
8
+ # resolve dependencies using librarian
9
+ #
10
+ task :resolve do
11
+ require 'fileutils'
12
+ FileUtils.rm_rf('Cheffile.lock')
13
+ sh "librarian-chef clean"
14
+ sh "librarian-chef install"
15
+ end
16
+
17
+ #
18
+ # check if dependencies in metadata.rb and Cheffile are consistent
19
+ #
20
+ task :check do
21
+ errors = []
22
+ metadata_deps = ChefTLCWorkflow::Helpers::read_metadata_deps
23
+ resolved_cheffile_deps = ChefTLCWorkflow::Helpers::read_and_resolve_cheffile_deps
24
+ resolved_cheffile_deps.each do | dep, version |
25
+ if metadata_deps.has_key?(dep)
26
+ metadata_ver = ::Gem::Requirement.new(metadata_deps[dep])
27
+ cheffile_ver = ::Gem::Requirement.new(version)
28
+ if metadata_ver != cheffile_ver
29
+ errors << "dependency version for '#{dep}' is inconsistent: '#{metadata_ver}' vs '#{cheffile_ver}'!"
30
+ end
31
+ else
32
+ errors << "dependency '#{dep}' is missing in metadata.rb!"
33
+ end
34
+ end
35
+ puts errors.empty? ? "everything OK" : errors
36
+ end
37
+
38
+ #
39
+ # resolve an application cookbook from `app_cookbooks.yml` with all its dependencies
40
+ #
41
+ task :resolve_app_cookbook, [:app_cookbook] do |t, args|
42
+ name, version = ChefTLCWorkflow::Helpers::parse_name_and_version(args[:app_cookbook])
43
+ app_cookbooks = ChefTLCWorkflow::Helpers::read_app_cookbooks("app_cookbooks.yml", name, version)
44
+ app_cookbooks.each do |app_cookbook|
45
+ resolve_app_cookbook(app_cookbook)
46
+ end
47
+ end
48
+
49
+ #
50
+ # resolve an application cookbook with all it's dependenices
51
+ # whilst honoring the application cookbook's Cheffile:
52
+ #
53
+ # 1. clone (:git,:ref) or copy (:path) the app cookbook to './tmp'
54
+ # 2. resolve dependencies (inlcuding app cookbook itself) as defined in
55
+ # the app cookbook's Cheffile to './cookbooks/<app-cookbook-name>-<version>'
56
+ #
57
+ #
58
+ def self.resolve_app_cookbook(app_cookbook)
59
+ name = app_cookbook['name']
60
+ version = app_cookbook['version']
61
+ git_loc = app_cookbook['git']
62
+ git_ref = app_cookbook['ref']
63
+ path_loc = app_cookbook['path']
64
+ has_git_loc = git_loc != nil
65
+ has_path_loc = path_loc != nil
66
+
67
+ fail "must specify either `git` or `path` location for #{name}" if !has_path_loc && !has_git_loc
68
+ fail "must not specify both `git` and `path` location for #{name}" if has_path_loc && has_git_loc
69
+
70
+ target_dir = "cookbooks/#{name}-#{version}"
71
+ tmp_dir = "tmp/tlc/#{name}-#{version}"
72
+
73
+ # clone / copy to temp dir
74
+ FileUtils.rm_rf tmp_dir
75
+ FileUtils.mkdir_p tmp_dir
76
+ if has_git_loc
77
+ sh "git clone -b #{git_ref || 'master'} #{git_loc} #{tmp_dir}"
78
+ elsif has_path_loc
79
+ FileUtils.cp_r cookbook_files_to_copy(path_loc), tmp_dir
80
+ end
81
+
82
+ # resolve deps from tmp_dir into target_dir
83
+ fail "No Cheffile found in '#{tmp_dir}'" unless File.exist? "#{tmp_dir}/Cheffile"
84
+ FileUtils.rm_rf target_dir
85
+ FileUtils.mkdir_p target_dir
86
+ sh "cd #{tmp_dir} && librarian-chef install --path #{File.absolute_path(target_dir)}"
87
+
88
+ # copy application cookbook itself if it was not reference in Cheffile using `:path => '.'`
89
+ app_cookbook_in_targetdir = "#{target_dir}/#{name}"
90
+ unless File.exist? app_cookbook_in_targetdir
91
+ FileUtils.mkdir_p app_cookbook_in_targetdir
92
+ FileUtils.cp_r Dir.glob("#{tmp_dir}/*"), app_cookbook_in_targetdir
93
+ end
94
+ end
95
+
96
+ def self.cookbook_files_to_copy(path)
97
+ Dir.glob("#{path}/*").reject {|p| p.end_with? '/tmp' }
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,15 @@
1
+
2
+ namespace :tlc do
3
+ namespace :test do
4
+
5
+ #
6
+ # destroy the default Vagrant VM, resolve dependencies and converge the default Vagrant VM
7
+ #
8
+ task :converge do
9
+ sh "vagrant destroy -f"
10
+ Rake::Task["tlc:deps:resolve"].invoke
11
+ sh "vagrant up"
12
+ end
13
+ end
14
+ end
15
+
data/test/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source :rubygems
2
+
3
+ source 'https://gems.gemfury.com/hUe8s8nSyzxs7JMMSZV8/' # vagrant-1.0.5.1
4
+ source 'https://gems.gemfury.com/psBbdHx94zqZrvxpiVmm/' # librarian-0.0.26.2
5
+
6
+ gem "chef-tlc-workflow", "0.1.0"
7
+ #:path => "#{File.dirname(__FILE__)}/../../chef-tlc-workflow"
@@ -0,0 +1,34 @@
1
+ #!/bin/bash -ex
2
+
3
+ if which curl 2>/dev/null; then
4
+ HTTP_GET_CMD="curl -L"
5
+ else
6
+ HTTP_GET_CMD="wget -qO-"
7
+ fi
8
+
9
+ # read/parse user-data
10
+ USERDATA=`$HTTP_GET_CMD http://169.254.169.254/latest/user-data`
11
+ CHEF_VERSION=`echo $USERDATA | tr -s ',' '\n' | grep "chef_version" | cut -d'=' -f2`
12
+ FQDN=`echo $USERDATA | tr -s ',' '\n' | grep "fqdn" | cut -d'=' -f2`
13
+
14
+ # install Chef
15
+ if [ -z "$CHEF_VERSION" ]; then
16
+ $HTTP_GET_CMD https://www.opscode.com/chef/install.sh | sudo bash -s
17
+ else
18
+ $HTTP_GET_CMD https://www.opscode.com/chef/install.sh | sudo bash -s -- -v $CHEF_VERSION
19
+ fi
20
+
21
+ # set proper hostname
22
+ if [ "$FQDN" ]; then
23
+ HOSTNAME=`echo $FQDN | sed -r 's/\..*//'`
24
+ if [ "`grep "127.0.1.1" /etc/hosts`" ]; then
25
+ sudo sed -r -i "s/^(127[.]0[.]1[.]1[[:space:]]+).*$/\\1$FQDN $HOSTNAME/" /etc/hosts
26
+ else
27
+ sudo sed -r -i "s/^(127[.]0[.]0[.]1[[:space:]]+localhost[[:space:]]*)$/\\1\n127.0.1.1 $FQDN $HOSTNAME/" /etc/hosts
28
+ fi
29
+ sudo sed -i "s/.*$/$HOSTNAME/" /etc/hostname
30
+ sudo hostname -F /etc/hostname
31
+ fi
32
+
33
+ # XXX: ensure that file_cache_path configured in solo.rb exists (Mccloud does not create it)
34
+ sudo mkdir -p /var/chef-solo
@@ -0,0 +1,3 @@
1
+ cookbooks/
2
+ tmp/
3
+ Cheffile.lock
@@ -0,0 +1,60 @@
1
+ Mccloud::Config.run do |config|
2
+
3
+ # identity / namespace
4
+ config.mccloud.prefix="mccloud"
5
+ config.mccloud.environment="tlc"
6
+ config.mccloud.identity=ENV['USERNAME']
7
+
8
+ # define AWS cloud provider for EU-West region
9
+ config.provider.define "aws-eu-west" do |provider_config|
10
+ provider_config.provider.flavor = :aws
11
+ provider_config.provider.options = { }
12
+ provider_config.provider.region = "eu-west-1"
13
+ provider_config.provider.check_keypairs = true
14
+ provider_config.provider.check_security_groups = true
15
+ provider_config.provider.namespace = "mccloud-tlc-#{ENV['USERNAME']}"
16
+ end
17
+
18
+ # ***********************************************
19
+ # VM Definitions
20
+ # ***********************************************
21
+
22
+ config.vm.define "sample-app" do |config|
23
+
24
+ # official Ubuntu 12.04 AMI
25
+ config.vm.ami = "ami-524e4726"
26
+ config.vm.provider= "aws-eu-west"
27
+ config.vm.flavor = "m1.small"
28
+ config.vm.zone = "eu-west-1c"
29
+ config.vm.user = "ubuntu"
30
+
31
+ # NOTE: the security groups must exist (e.g. create it beforehand via AWS console)
32
+ config.vm.security_groups = [ "mccloud", "http" ]
33
+
34
+ # see http://fog.io/1.1.2/rdoc/Fog/Compute/AWS/Servers.html
35
+ # and https://github.com/fog/fog/blob/v1.1.2/lib/fog/aws/requests/compute/run_instances.rb
36
+ config.vm.create_options = {
37
+ :user_data => "chef_version=10.18.2-1,fqdn=sample-app.example.com"
38
+ }
39
+
40
+ # NOTE: the keypair (for logging in to the VM) must exist (e.g. create it beforehand via AWS console)
41
+ config.vm.key_name = "mccloud-key-tlc"
42
+ config.vm.private_key_path = "#{ENV['HOME']}/.ssh/mccloud_rsa"
43
+ config.vm.public_key_path = "#{ENV['HOME']}/.ssh/mccloud_rsa.pub"
44
+
45
+ # bootstrap template (runs at first-boot only)
46
+ config.vm.bootstrap = ".chef/bootstrap/omnibus-chef.sh"
47
+
48
+ # provisioning sript (runs on each mccloud up or provision)
49
+ config.vm.provision :chef_solo do |chef|
50
+ chef.cookbooks_path = [ "cookbooks/sample-app-0.1.0" ]
51
+ chef.log_level = "info"
52
+ chef.add_recipe "sample-app"
53
+ chef.json.merge!({
54
+ :sample_app => {
55
+ :words_of_wisdom => "YEAH! I did it for the Lulz!"
56
+ }
57
+ })
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,57 @@
1
+ require 'bundler/setup'
2
+ require 'fileutils'
3
+ require 'chef-workflow/tasks/tlc/deps'
4
+
5
+ desc "resolve application cookbook with all its dependencies"
6
+ task :resolve_deps, [:app_cookbook] do |t, args|
7
+ Rake::Task["tlc:deps:resolve_app_cookbook"].invoke(args[:app_cookbook])
8
+ end
9
+
10
+ desc "bring up the mccloud \"VM\" as configured in the Mccloudfile"
11
+ task :up, [:vm_name] do |t, args|
12
+ vm_name = get_required_args(args, :vm_name)
13
+ sh "mccloud up #{vm_name}"
14
+ end
15
+
16
+ desc "provision the mccloud \"VM\" with the provisioners as defined in the Mccloudfile"
17
+ task :provision, [:vm_name] do |t, args|
18
+ vm_name = get_required_args(args, :vm_name)
19
+ sh "mccloud provision #{vm_name}"
20
+ end
21
+
22
+ desc "destroy the mccloud \"VM\" with the given name"
23
+ task :destroy, [:vm_name] do |t, args|
24
+ vm_name = get_required_args(args, :vm_name)
25
+ sh "mccloud destroy #{vm_name}"
26
+ end
27
+
28
+ desc "ssh into the mccloud \"VM\" with the given name"
29
+ task :ssh, [:vm_name] do |t, args|
30
+ vm_name = get_required_args(args, :vm_name)
31
+ sh "mccloud ssh #{vm_name}"
32
+ end
33
+
34
+ desc "show status of all mccloud \"VMs\" defined in the Mccloudfile"
35
+ task :status do
36
+ sh "mccloud status"
37
+ end
38
+
39
+ desc "returns the ip address of the mccloud \"VM\" with the given name"
40
+ task :get_ip, [:vm_name] do |t, args|
41
+ vm_name = get_required_args(args, :vm_name)
42
+ # TODO: filter out IP of given vm rather than printing status for all
43
+ sh "mccloud status"
44
+ end
45
+
46
+
47
+ #
48
+ # helper methods below
49
+ #
50
+ def get_required_args(args, *param_keys)
51
+ param_values = Array.new
52
+ param_keys.each do |param_key|
53
+ fail "parameter #{param_key.to_sym} is required" unless args[param_key.to_sym]
54
+ param_values << args[param_key.to_sym]
55
+ end
56
+ param_values.size == 1 ? param_values[0] : param_values
57
+ end
@@ -0,0 +1,12 @@
1
+
2
+ - name: "sample-app"
3
+ version: "0.1.0"
4
+ path: "../sample-app"
5
+ # git: "https://github.com/tknerr/sample-app-tlc.git"
6
+ # ref: "v0.1.0"
7
+
8
+ - name: "sample-app"
9
+ version: "0.2.0"
10
+ path: "../sample-app"
11
+ # git: "https://github.com/tknerr/sample-app-tlc.git"
12
+ # ref: "v0.2.0"