chef-tlc-workflow 0.1.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 (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"