clc-promote 0.4.5 → 0.7.8
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.
- checksums.yaml +4 -4
- data/README.md +18 -8
- data/Rakefile +3 -0
- data/clc-promote.gemspec +3 -2
- data/lib/chef/knife/promote.rb +46 -17
- data/lib/kitchen/provisioner/environment.rb +1 -1
- data/lib/promote.rb +3 -2
- data/lib/promote/config.rb +11 -2
- data/lib/promote/cookbook.rb +17 -5
- data/lib/promote/git_repo.rb +22 -1
- data/lib/promote/node_finder.rb +18 -0
- data/lib/promote/promoter.rb +37 -12
- data/lib/promote/rake_tasks.rb +63 -18
- data/lib/promote/role_file.rb +26 -0
- data/lib/promote/uploader.rb +45 -13
- data/lib/promote/version.rb +2 -2
- data/lib/promote/versioner.rb +44 -9
- data/spec/unit/promote/config_spec.rb +70 -18
- data/spec/unit/promote/promoter_spec.rb +51 -8
- data/spec/unit/promote/uploader_spec.rb +97 -22
- data/spec/unit/promote/versioner_spec.rb +151 -16
- metadata +26 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 028a2d20e003fc5841ed185d5da849033b2879b2
|
4
|
+
data.tar.gz: d1fe5f381fd2ee79759d23a0809b1a7770f1698e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f3601f98c0eab1f4191f60e9bc22221b2207787157da24665f709865b25bc147e5d4af7c0dfebed73b05f2747f4088698fd1cfc2d3a778a68a81c7172645b80
|
7
|
+
data.tar.gz: 07000fd20e24894db386e22bd178ca8268ef0efcab71d71d9d1bde7c9b1cb05844f1f7a3c987acb6281795aedfce08285cda3e10e093dc394834ce006280c61f
|
data/README.md
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# CLC-Promote Gem
|
2
2
|
Drives our CI/CD pipeline and provides functionality for:
|
3
|
-
* Versioning cookbooks, data bags and environments
|
3
|
+
* Versioning cookbooks, data bags, roles, and environments
|
4
4
|
* Manages Cookbook version constraints within environments
|
5
5
|
* Manages the uploading of chef artifacts from CI to the QA chef Server and eventually to the production chef server
|
6
6
|
* Promotes one environment's cookbooks to another
|
7
7
|
|
8
|
-
This functionality is exposed via a
|
8
|
+
This functionality is exposed via a collection of rake tasks used on the CI server and a knife plugin used to perform deployments.
|
9
9
|
|
10
10
|
## Configuration
|
11
11
|
CLC-Promote uses configuration settings to determine the values of:
|
@@ -41,6 +41,7 @@ Setting |
|
|
41
41
|
:cookbook_directory | Defaults to `#{root}/cookbooks`
|
42
42
|
:environment_directory | Defaults to `#{root}/environments`
|
43
43
|
:data_bag_directory | Defaults to `#{root}/data_bags`
|
44
|
+
:role_directory | Defaults to `#{root}/roles`
|
44
45
|
:temp_directory | Defaults to `/tmp/promote`
|
45
46
|
:node_name | **mandatory**
|
46
47
|
:client_key | **mandatory**
|
@@ -48,7 +49,7 @@ Setting |
|
|
48
49
|
|
49
50
|
|
50
51
|
## CLC-Promote Rake Tasks
|
51
|
-
**Note:** See the [below section on versioning](/gems/clc-promote#How are version numbers generated) for details on how
|
52
|
+
**Note:** See the [below section on versioning](/gems/clc-promote#How are version numbers generated) for details on how version numbers are generated.
|
52
53
|
|
53
54
|
### Promote:version_cookbook
|
54
55
|
Bumps the version of an individual cookbook.
|
@@ -62,6 +63,12 @@ Bumps the version of an individual environment file.
|
|
62
63
|
### Promote:version_environments
|
63
64
|
Same as `Promote:version_environment` but iterates all environment files.
|
64
65
|
|
66
|
+
### Promote:version_role
|
67
|
+
Bumps the version of an individual role file.
|
68
|
+
|
69
|
+
### Promote:version_roles
|
70
|
+
Same as `Promote:version_role` but iterates all role files.
|
71
|
+
|
65
72
|
### Promote:version_data_bag
|
66
73
|
Bumps the version of an individual databag entry.
|
67
74
|
DEPRECATED (breaks encrypted data bags and vaults).
|
@@ -79,8 +86,11 @@ Uploads all cookbook versions of an environment to the chef server. **Note**: on
|
|
79
86
|
### Promote:upload_environment
|
80
87
|
Uploads an environment file to the chef server
|
81
88
|
|
89
|
+
### Promote:upload_roles
|
90
|
+
Uploads all role files to the chef server
|
91
|
+
|
82
92
|
### Promote:upload_data_bags
|
83
|
-
Uploads all data
|
93
|
+
Uploads all data bags to the chef server
|
84
94
|
|
85
95
|
### Promote: constrain_environment
|
86
96
|
Given an environment and its environment cookbook, this task edits the environment file and creates cookbook constraints based on the `Berksfile.lock` of the environment cookbook.
|
@@ -95,7 +105,7 @@ Version numbers are based on the last version tag which forms the major and mino
|
|
95
105
|
The knife promote command deploys an environment to production.
|
96
106
|
|
97
107
|
```
|
98
|
-
knife promote environment
|
108
|
+
knife promote environment SOURCE_ENVIRONMENT TARGET_ENVIRONMENT [ --data-bags LIST ]
|
99
109
|
```
|
100
110
|
|
101
111
|
This command performs the following:
|
@@ -103,7 +113,7 @@ This command performs the following:
|
|
103
113
|
1. Copies the cookbook version constraints from source to target
|
104
114
|
2. Commits the constraints to version control
|
105
115
|
3. Uploads the target environment to the QA chef server
|
106
|
-
4. Downloads all data bags from QA
|
116
|
+
4. Downloads all ```secrets_*``` data bags and all roles from QA. Additional data bags can be specified on the command line. Note that data bags containing chef-vault keys ( ```*_keys.json``` ) are always skipped.
|
107
117
|
5. Performs a version diff on all cookbooks between the target environment and production chef server
|
108
|
-
6. Downloads all cookbooks from
|
109
|
-
7.
|
118
|
+
6. Downloads all cookbooks from QA that are not on the production server
|
119
|
+
7. Uploads all new cookbooks, the target environment, databags and roles to the production chef server.
|
data/Rakefile
CHANGED
data/clc-promote.gemspec
CHANGED
@@ -17,8 +17,9 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
18
18
|
|
19
19
|
s.add_runtime_dependency 'clc-git', '~> 1.2', '>= 1.2.8'
|
20
|
-
s.add_runtime_dependency 'berkshelf', '~> 3.2', '>= 3.2.
|
20
|
+
s.add_runtime_dependency 'berkshelf', '~> 3.2', '>= 3.2.3'
|
21
|
+
s.add_runtime_dependency 'highline', '~> 1.6', '>= 1.6.21'
|
21
22
|
|
22
23
|
s.add_development_dependency 'rspec', '~> 3.0', '>= 3.0.0'
|
23
24
|
s.add_development_dependency 'rake', '~> 10.3', '>= 10.3.2'
|
24
|
-
end
|
25
|
+
end
|
data/lib/chef/knife/promote.rb
CHANGED
@@ -6,6 +6,21 @@ module KnifePromote
|
|
6
6
|
|
7
7
|
banner "knife promote environment SOURCE_ENVIRONMENT DESTINATION_ENVIRONMENT"
|
8
8
|
|
9
|
+
option :dry,
|
10
|
+
:long => "--dry-run",
|
11
|
+
:description => "Do not perform actual promote. Just report what would be done.",
|
12
|
+
:default => false
|
13
|
+
|
14
|
+
option :bags,
|
15
|
+
:long => "--data-bags VALUE",
|
16
|
+
:description => "A comma-separated list of data bags to be promoted (in addition to all secrets_* data bags)",
|
17
|
+
:default => ['secrets_*'],
|
18
|
+
:proc => Proc.new { |b|
|
19
|
+
b = "secrets_*,#{b}"
|
20
|
+
b.split(',')
|
21
|
+
}
|
22
|
+
|
23
|
+
|
9
24
|
def run
|
10
25
|
if @name_args.length < 2
|
11
26
|
show_usage
|
@@ -16,40 +31,54 @@ module KnifePromote
|
|
16
31
|
source_env = @name_args[0]
|
17
32
|
dest_env = @name_args[1]
|
18
33
|
|
19
|
-
|
20
|
-
ui.confirm "Are you sure #{source_env} deserves this promotion"
|
21
|
-
|
22
|
-
config = Promote::Config.new({
|
34
|
+
promote_config = Promote::Config.new({
|
23
35
|
:repo_root => File.expand_path("../",Chef::Config[:cookbook_path][0]),
|
24
36
|
:node_name => Chef::Config[:node_name],
|
25
37
|
:client_key => Chef::Config[:client_key],
|
26
|
-
:chef_server_url => Chef::Config[:chef_server_url]
|
38
|
+
:chef_server_url => Chef::Config[:chef_server_url],
|
39
|
+
:temp_directory => '/tmp/promote_staging',
|
40
|
+
:bags => config[:bags]
|
27
41
|
})
|
28
42
|
destination_config = Promote::Config.new({
|
29
|
-
:repo_root =>
|
43
|
+
:repo_root => promote_config.temp_directory,
|
30
44
|
:node_name => Chef::Config[:knife][:promote_prod_user],
|
31
45
|
:client_key => Chef::Config[:knife][:promote_prod_client_key],
|
32
46
|
:chef_server_url => Chef::Config[:knife][:promote_prod_url]
|
33
47
|
})
|
34
48
|
|
35
|
-
|
36
|
-
|
37
|
-
|
49
|
+
# Confirm local git repo is in sync with origin/master
|
50
|
+
source = "environments/#{source_env}.json"
|
51
|
+
dest = "environments/#{dest_env}.json"
|
52
|
+
r = Promote::GitRepo.new(promote_config.repo_root)
|
53
|
+
r.check_sync(source, dest)
|
38
54
|
|
39
|
-
|
55
|
+
ui.info "*** #{source_env} is being promoted to #{dest_env}"
|
56
|
+
ui.confirm "Are you sure this is what you want to do"
|
40
57
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
repo.commit("promoted new constraints from #{source_env} to #{dest_env}", true)
|
58
|
+
promoter = Promote::Promoter.new(promote_config)
|
59
|
+
uploader = Promote::Uploader.new(promote_config)
|
60
|
+
Chef::Config[:verbosity] = 2
|
45
61
|
|
46
|
-
|
62
|
+
# Locally mirror the environment constraints over
|
63
|
+
promoter.promote_to(source_env, dest_env, ui)
|
64
|
+
|
65
|
+
if !config[:dry]
|
66
|
+
ui.info "Committing and pushing changes to #{dest_env} back to git"
|
67
|
+
env_file = File.join(promote_config.environment_directory, "#{dest_env}.json")
|
68
|
+
repo = Promote::GitRepo.new(env_file)
|
69
|
+
repo.commit("promoted new constraints from #{source_env} to #{dest_env}", true)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Uploads the environment to QA
|
73
|
+
ui.info "uploading #{dest_env} to #{promote_config.chef_server_url}"
|
47
74
|
uploader.upload_environment(dest_env)
|
48
75
|
|
49
76
|
promoter.stage_promotion(dest_env, destination_config, ui)
|
50
|
-
promoter.upload_to(dest_env, destination_config, ui)
|
51
77
|
|
52
|
-
|
78
|
+
if !config[:dry]
|
79
|
+
promoter.upload_to(dest_env, destination_config, ui)
|
80
|
+
ui.info "promotion complete. Congratulations #{dest_env} on a well deserved promotion!!"
|
81
|
+
end
|
53
82
|
end
|
54
83
|
end
|
55
84
|
end
|
@@ -18,7 +18,7 @@ module Kitchen
|
|
18
18
|
private
|
19
19
|
|
20
20
|
def create_node
|
21
|
-
node_file = File.join(instance.
|
21
|
+
node_file = File.join(instance.verifier[:test_base_path], "nodes/#{instance.suite.name}.json")
|
22
22
|
if File.exist?(node_file)
|
23
23
|
node = JSON.parse(File.read(node_file))
|
24
24
|
node[:run_list] = config[:run_list]
|
data/lib/promote.rb
CHANGED
@@ -2,7 +2,8 @@ require 'promote/config'
|
|
2
2
|
require 'promote/cookbook'
|
3
3
|
require 'promote/environment_file'
|
4
4
|
require 'promote/git_repo'
|
5
|
-
require 'promote/
|
6
|
-
require 'promote/versioner'
|
5
|
+
require 'promote/node_finder'
|
7
6
|
require 'promote/promoter'
|
7
|
+
require 'promote/uploader'
|
8
8
|
require 'promote/utils'
|
9
|
+
require 'promote/versioner'
|
data/lib/promote/config.rb
CHANGED
@@ -2,13 +2,20 @@ module Promote
|
|
2
2
|
class Config
|
3
3
|
def initialize(options = {})
|
4
4
|
options.each do |k, v|
|
5
|
-
self.send("#{k}=", v) if self.respond_to?(k)
|
5
|
+
self.send("#{k}=", v) if self.respond_to?(k)
|
6
6
|
end
|
7
7
|
self.repo_root ||= Dir.pwd
|
8
8
|
self.cookbook_directory ||= File.join(repo_root, "cookbooks")
|
9
9
|
self.environment_directory ||= File.join(repo_root, "environments")
|
10
10
|
self.data_bag_directory ||= File.join(repo_root, "data_bags")
|
11
|
+
self.role_directory ||= File.join(repo_root, "roles")
|
11
12
|
self.temp_directory ||= "/tmp/promote"
|
13
|
+
self.bags ||= ['secrets_*']
|
14
|
+
end
|
15
|
+
|
16
|
+
def reset_temp_dir
|
17
|
+
FileUtils.rm_rf(temp_directory) if Dir.exist?(temp_directory)
|
18
|
+
Dir.mkdir(temp_directory)
|
12
19
|
end
|
13
20
|
|
14
21
|
def to_hash
|
@@ -27,9 +34,11 @@ module Promote
|
|
27
34
|
attr_accessor :cookbook_directory
|
28
35
|
attr_accessor :environment_directory
|
29
36
|
attr_accessor :data_bag_directory
|
37
|
+
attr_accessor :role_directory
|
30
38
|
attr_accessor :temp_directory
|
31
39
|
attr_accessor :node_name
|
32
40
|
attr_accessor :client_key
|
33
41
|
attr_accessor :chef_server_url
|
42
|
+
attr_accessor :bags
|
34
43
|
end
|
35
|
-
end
|
44
|
+
end
|
data/lib/promote/cookbook.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'berkshelf'
|
2
|
+
require 'digest/sha1'
|
2
3
|
require 'json'
|
3
4
|
|
4
5
|
module Promote
|
@@ -77,15 +78,26 @@ module Promote
|
|
77
78
|
return false
|
78
79
|
end
|
79
80
|
|
81
|
+
def dependency_hash(environment_cookbook_name)
|
82
|
+
cb_changes = sync_latest_app_cookbooks(environment_cookbook_name)
|
83
|
+
hash_src = ""
|
84
|
+
dependencies.each do | k,v |
|
85
|
+
hash_src << "#{k}::#{v}::"
|
86
|
+
end
|
87
|
+
Digest::SHA1.hexdigest(hash_src)
|
88
|
+
end
|
89
|
+
|
80
90
|
def sync_latest_app_cookbooks(environment_cookbook_name)
|
81
91
|
result = {}
|
82
92
|
latest_server_cookbooks = Utils.chef_server_cookbooks(@config, 1)
|
83
93
|
env_cookbook = Cookbook.new(environment_cookbook_name, @config)
|
84
|
-
env_cookbook.metadata_dependencies do |key|
|
85
|
-
|
86
|
-
if
|
87
|
-
|
88
|
-
|
94
|
+
env_cookbook.metadata_dependencies.each do |key|
|
95
|
+
cb_key = key[0]
|
96
|
+
next if cb_key == @name || !latest_server_cookbooks.has_key?(cb_key)
|
97
|
+
latest_version = latest_server_cookbooks[cb_key]['versions'][0]['version']
|
98
|
+
if dependencies.keys.include?(cb_key) && latest_version > dependencies[cb_key].to_s
|
99
|
+
berksfile_update(cb_key)
|
100
|
+
result[cb_key] = latest_version
|
89
101
|
end
|
90
102
|
end
|
91
103
|
result
|
data/lib/promote/git_repo.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'git'
|
2
|
+
|
1
3
|
module Promote
|
2
4
|
class GitRepo
|
3
5
|
def initialize(scope_path)
|
@@ -42,6 +44,25 @@ module Promote
|
|
42
44
|
end
|
43
45
|
end
|
44
46
|
|
47
|
+
def check_sync(source, dest)
|
48
|
+
warnings = []
|
49
|
+
if git.status.changed.to_h.keys.include? source
|
50
|
+
warnings << "File #{source} has uncommitted changes."
|
51
|
+
end
|
52
|
+
git.fetch
|
53
|
+
git.diff('master','origin/master').entries.each do |f|
|
54
|
+
if f.path == source || f.path == dest
|
55
|
+
warnings << "File #{f.path} is not synced with origin/master."
|
56
|
+
end
|
57
|
+
end
|
58
|
+
if warnings.count > 0
|
59
|
+
puts '*** Git sync issues: Fix the following issues and try again:'
|
60
|
+
warnings.each { |w| puts "* #{w}" }
|
61
|
+
exit 1
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
45
66
|
attr_accessor :scope_path
|
46
67
|
|
47
68
|
private
|
@@ -68,4 +89,4 @@ module Promote
|
|
68
89
|
end
|
69
90
|
end
|
70
91
|
end
|
71
|
-
end
|
92
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Promote
|
2
|
+
class NodeFinder
|
3
|
+
def initialize(query, config)
|
4
|
+
@query = query
|
5
|
+
@config = config
|
6
|
+
Chef::Config.reset
|
7
|
+
Chef::Config[:client_key] = config.client_key
|
8
|
+
Chef::Config[:chef_server_url] = config.chef_server_url
|
9
|
+
Chef::Config[:node_name] = config.node_name
|
10
|
+
@searcher = Chef::Search::Query.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def search
|
14
|
+
results = @searcher.search(:node, @query)
|
15
|
+
results[0]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/promote/promoter.rb
CHANGED
@@ -8,44 +8,68 @@ module Promote
|
|
8
8
|
@config = config
|
9
9
|
end
|
10
10
|
|
11
|
-
def promote_to(source_environment, destination_environment)
|
11
|
+
def promote_to(source_environment, destination_environment, ui = nil)
|
12
12
|
source = EnvironmentFile.new(source_environment, config)
|
13
13
|
dest = EnvironmentFile.new(destination_environment, config)
|
14
|
-
|
14
|
+
|
15
|
+
if ui
|
16
|
+
ui.info "Cookbook constraints being promoted to #{destination_environment}"
|
17
|
+
ui.info source.cookbook_versions
|
18
|
+
end
|
19
|
+
|
15
20
|
dest.write_cookbook_versions(source.cookbook_versions)
|
16
21
|
end
|
17
22
|
|
23
|
+
def monitor_promotion(source_environment, destination_environments, probe_interval, ui = nil)
|
24
|
+
destination_environments.each do |env|
|
25
|
+
promote_to(source_environment, env, ui)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
18
29
|
def stage_promotion(promote_environment, destination_config, ui)
|
19
30
|
ui.info "Staging artifacts from #{config.chef_server_url} to #{config.temp_directory}"
|
20
|
-
|
21
|
-
Dir.mkdir(config.temp_directory)
|
31
|
+
config.reset_temp_dir
|
22
32
|
|
23
33
|
deploy_file = "/environments/#{promote_environment}.json"
|
24
34
|
ui.info "Downloading #{deploy_file}..."
|
25
35
|
download_files(config.temp_directory, deploy_file)
|
36
|
+
|
26
37
|
ui.info "Downloading data bags..."
|
27
|
-
|
38
|
+
config.bags.each do |bag|
|
39
|
+
download_files(config.temp_directory, "/data_bags/#{bag}")
|
40
|
+
end
|
41
|
+
|
42
|
+
ui.info "Downloading roles..."
|
43
|
+
download_files(config.temp_directory, "/roles")
|
28
44
|
|
29
|
-
ui.info "Downloading cookbooks..."
|
45
|
+
ui.info "Downloading cookbooks missing on #{destination_config.chef_server_url}..."
|
46
|
+
ui.info "Fetching all versions of cookbooks on #{destination_config.chef_server_url}..."
|
30
47
|
server_versions = Promote::Utils.chef_server_cookbooks(destination_config)
|
48
|
+
ui.info "Fetching the environment file content of #{promote_environment}"
|
31
49
|
env_content = EnvironmentFile.new(promote_environment, Config.new({:repo_root => config.temp_directory}))
|
32
50
|
env_content.cookbook_versions.each do |k,v|
|
51
|
+
ui.info "#{k}-#{v}"
|
33
52
|
if !server_versions.has_key?(k) || (server_versions[k]["versions"].select {|version| version['version'] == v}).empty?
|
34
|
-
ui.info "
|
53
|
+
ui.info "Cookbook missing. Downloading..."
|
35
54
|
download_files(config.temp_directory, "/cookbooks/#{k}", v)
|
55
|
+
else
|
56
|
+
ui.info "Cookbook already exists."
|
36
57
|
end
|
37
|
-
end
|
58
|
+
end
|
38
59
|
end
|
39
60
|
|
40
61
|
def upload_to(promote_environment, destination_config, ui)
|
41
62
|
ui.info "Uploading staged artifacts to #{destination_config.chef_server_url}"
|
42
63
|
uploader = Uploader.new(destination_config)
|
64
|
+
Chef::Config[:verbosity] = 2
|
43
65
|
ui.info("Uploading #{promote_environment} environment file...")
|
44
66
|
uploader.upload_environment(promote_environment)
|
45
67
|
ui.info("Uploading data bags...")
|
46
68
|
uploader.upload_data_bags
|
69
|
+
ui.info("Uploading roles...")
|
70
|
+
uploader.upload_roles
|
47
71
|
ui.info("Uploading cookbooks...")
|
48
|
-
uploader.upload_cookbook_directory(destination_config.cookbook_directory)
|
72
|
+
uploader.upload_cookbook_directory(destination_config.cookbook_directory, ui)
|
49
73
|
end
|
50
74
|
|
51
75
|
private
|
@@ -55,12 +79,13 @@ module Promote
|
|
55
79
|
pattern = Chef::ChefFS::FilePattern.new(path)
|
56
80
|
local = Chef::ChefFS::FileSystem::ChefRepositoryFileSystemRootDir.new(
|
57
81
|
{
|
58
|
-
'environments' => ["#{local_root}/environments"],
|
82
|
+
'environments' => ["#{local_root}/environments"],
|
59
83
|
'data_bags' => ["#{local_root}/data_bags"],
|
84
|
+
'roles' => ["#{local_root}/roles"],
|
60
85
|
'cookbooks' => ["#{local_root}/cookbooks"]
|
61
86
|
}
|
62
87
|
)
|
63
|
-
Chef::ChefFS::FileSystem.copy_to(pattern, fs_config.chef_fs, local,
|
88
|
+
Chef::ChefFS::FileSystem.copy_to(pattern, fs_config.chef_fs, local, nil, Chef::Config)
|
64
89
|
end
|
65
90
|
end
|
66
|
-
end
|
91
|
+
end
|