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.
- data/.gitignore +18 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +93 -0
- data/Rakefile +110 -0
- data/Vagrantfile +33 -0
- data/chef-tlc-workflow.gemspec +52 -0
- data/docs/ApplicationVsLibraryVsForkedCookbooks.md +20 -0
- data/docs/TmpLibrarianHelpers.md +21 -0
- data/docs/TmpWorkflowTasks.md +80 -0
- data/lib/chef-tlc-workflow.rb +5 -0
- data/lib/chef-tlc-workflow/helpers.rb +126 -0
- data/lib/chef-tlc-workflow/version.rb +3 -0
- data/lib/chef-workflow/tasks/tlc.rb +4 -0
- data/lib/chef-workflow/tasks/tlc/deps.rb +100 -0
- data/lib/chef-workflow/tasks/tlc/test.rb +15 -0
- data/test/Gemfile +7 -0
- data/test/ec2-bootstrap/.chef/bootstrap/omnibus-chef.sh +34 -0
- data/test/ec2-bootstrap/.gitignore +3 -0
- data/test/ec2-bootstrap/Mccloudfile +60 -0
- data/test/ec2-bootstrap/Rakefile +57 -0
- data/test/ec2-bootstrap/app_cookbooks.yml +12 -0
- data/test/esx-bootstrap/.chef/knife.rb +7 -0
- data/test/esx-bootstrap/.gitignore +4 -0
- data/test/esx-bootstrap/Rakefile +64 -0
- data/test/esx-bootstrap/app_cookbooks.yml +12 -0
- data/test/esx-bootstrap/nodes/33.33.77.10.json +6 -0
- data/test/local-bootstrap/.gitignore +4 -0
- data/test/local-bootstrap/Rakefile +50 -0
- data/test/local-bootstrap/Vagrantfile +31 -0
- data/test/local-bootstrap/app_cookbooks.yml +12 -0
- data/test/sample-app/CHANGELOG.md +12 -0
- data/test/sample-app/Cheffile +15 -0
- data/test/sample-app/README.md +20 -0
- data/test/sample-app/attributes/default.rb +2 -0
- data/test/sample-app/metadata.rb +10 -0
- data/test/sample-app/recipes/default.rb +27 -0
- data/test/sample-app/templates/default/sample.html.erb +10 -0
- metadata +331 -0
@@ -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,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,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"
|