chef-apply 0.1.2 → 0.1.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +14 -14
- data/README.md +29 -14
- data/i18n/en.yml +16 -340
- data/i18n/errors/en.yml +341 -0
- data/lib/chef_apply/action/base.rb +5 -1
- data/lib/chef_apply/action/generate_local_policy.rb +59 -0
- data/lib/chef_apply/action/generate_temp_cookbook.rb +86 -0
- data/lib/chef_apply/action/install_chef/base.rb +38 -16
- data/lib/chef_apply/cli.rb +104 -257
- data/lib/chef_apply/cli/help.rb +69 -0
- data/lib/chef_apply/cli/options.rb +147 -0
- data/lib/chef_apply/cli/validation.rb +99 -0
- data/lib/chef_apply/error.rb +4 -43
- data/lib/chef_apply/errors/standard_error_resolver.rb +45 -0
- data/lib/chef_apply/startup.rb +22 -0
- data/lib/chef_apply/temp_cookbook.rb +23 -12
- data/lib/chef_apply/text.rb +13 -45
- data/lib/chef_apply/text/text_wrapper.rb +86 -0
- data/lib/chef_apply/ui/error_printer.rb +3 -2
- data/lib/chef_apply/ui/terminal.rb +10 -8
- data/lib/chef_apply/version.rb +1 -1
- metadata +10 -3
- data/lib/chef_apply/cli_options.rb +0 -145
@@ -0,0 +1,69 @@
|
|
1
|
+
#
|
2
|
+
# Copyright:: Copyright (c) 2018 Chef Software Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
module ChefApply
|
19
|
+
class CLI
|
20
|
+
module Help
|
21
|
+
T = ChefApply::Text.cli
|
22
|
+
def show_help
|
23
|
+
UI::Terminal.output format_help
|
24
|
+
end
|
25
|
+
|
26
|
+
def format_help
|
27
|
+
help_text = banner.clone # This prevents us appending to the banner text
|
28
|
+
help_text << "\n"
|
29
|
+
help_text << format_flags
|
30
|
+
end
|
31
|
+
|
32
|
+
def format_flags
|
33
|
+
flag_text = "FLAGS:\n"
|
34
|
+
justify_length = 0
|
35
|
+
options.each_value do |spec|
|
36
|
+
justify_length = [justify_length, spec[:long].length + 4].max
|
37
|
+
end
|
38
|
+
options.sort.to_h.each_value do |flag_spec|
|
39
|
+
short = flag_spec[:short] || " "
|
40
|
+
short = short[0, 2] # We only want the flag portion, not the capture portion (if present)
|
41
|
+
if short == " "
|
42
|
+
short = " "
|
43
|
+
else
|
44
|
+
short = "#{short}, "
|
45
|
+
end
|
46
|
+
flags = "#{short}#{flag_spec[:long]}"
|
47
|
+
flag_text << " #{flags.ljust(justify_length)} "
|
48
|
+
ml_padding = " " * (justify_length + 8)
|
49
|
+
first = true
|
50
|
+
flag_spec[:description].split("\n").each do |d|
|
51
|
+
flag_text << ml_padding unless first
|
52
|
+
first = false
|
53
|
+
flag_text << "#{d}\n"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
flag_text
|
57
|
+
end
|
58
|
+
|
59
|
+
def usage
|
60
|
+
T.usage
|
61
|
+
end
|
62
|
+
|
63
|
+
def show_version
|
64
|
+
require "chef_apply/version"
|
65
|
+
UI::Terminal.output T.version.show(ChefApply::VERSION)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
#
|
2
|
+
# Copyright:: Copyright (c) 2018 Chef Software Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require "chef_apply/text"
|
19
|
+
require "chef_apply/action/install_chef"
|
20
|
+
|
21
|
+
# Moving the options into here so the cli.rb file is smaller and easier to read
|
22
|
+
# For options that need to be merged back into the global ChefApply::Config object
|
23
|
+
# we do that with a proc in the option itself. We decided to do that because it is
|
24
|
+
# an easy, straight forward way to merge those options when they do not directly
|
25
|
+
# map back to keys in the Config global. IE, we cannot just do
|
26
|
+
# `ChefApply::Config.merge!(options)` because the keys do not line up, and we do
|
27
|
+
# not want all CLI params merged back into the global config object.
|
28
|
+
# We know that the config is already loaded from the file (or program defaults)
|
29
|
+
# because the `Startup` class was invoked to start the program.
|
30
|
+
module ChefApply
|
31
|
+
class CLI
|
32
|
+
module Options
|
33
|
+
|
34
|
+
T = ChefApply::Text.cli
|
35
|
+
TS = ChefApply::Text.status
|
36
|
+
|
37
|
+
def self.included(klass)
|
38
|
+
klass.banner T.description + "\n" + T.usage_full
|
39
|
+
|
40
|
+
klass.option :version,
|
41
|
+
short: "-v",
|
42
|
+
long: "--version",
|
43
|
+
description: T.version.description,
|
44
|
+
boolean: true
|
45
|
+
|
46
|
+
klass.option :help,
|
47
|
+
short: "-h",
|
48
|
+
long: "--help",
|
49
|
+
description: T.help.description,
|
50
|
+
boolean: true
|
51
|
+
|
52
|
+
# Special note:
|
53
|
+
# config_path is pre-processed in startup.rb, and is shown here only
|
54
|
+
# for purpoess of rendering help text.
|
55
|
+
klass.option :config_path,
|
56
|
+
short: "-c PATH",
|
57
|
+
long: "--config PATH",
|
58
|
+
description: T.default_config_location(ChefApply::Config.default_location),
|
59
|
+
default: ChefApply::Config.default_location,
|
60
|
+
proc: Proc.new { |path| ChefApply::Config.custom_location(path) }
|
61
|
+
|
62
|
+
klass.option :identity_file,
|
63
|
+
long: "--identity-file PATH",
|
64
|
+
short: "-i PATH",
|
65
|
+
description: T.identity_file,
|
66
|
+
proc: (Proc.new do |paths|
|
67
|
+
path = paths
|
68
|
+
unless File.exist?(path)
|
69
|
+
raise OptionValidationError.new("CHEFVAL001", self, path)
|
70
|
+
end
|
71
|
+
path
|
72
|
+
end)
|
73
|
+
|
74
|
+
klass.option :ssl,
|
75
|
+
long: "--[no-]ssl",
|
76
|
+
description: T.ssl.desc(ChefApply::Config.connection.winrm.ssl),
|
77
|
+
boolean: true,
|
78
|
+
default: ChefApply::Config.connection.winrm.ssl,
|
79
|
+
proc: Proc.new { |val| ChefApply::Config.connection.winrm.ssl(val) }
|
80
|
+
|
81
|
+
klass.option :ssl_verify,
|
82
|
+
long: "--[no-]ssl-verify",
|
83
|
+
description: T.ssl.verify_desc(ChefApply::Config.connection.winrm.ssl_verify),
|
84
|
+
boolean: true,
|
85
|
+
default: ChefApply::Config.connection.winrm.ssl_verify,
|
86
|
+
proc: Proc.new { |val| ChefApply::Config.connection.winrm.ssl_verify(val) }
|
87
|
+
|
88
|
+
klass.option :protocol,
|
89
|
+
long: "--protocol <PROTOCOL>",
|
90
|
+
short: "-p",
|
91
|
+
description: T.protocol_description(ChefApply::Config::SUPPORTED_PROTOCOLS.join(" "),
|
92
|
+
ChefApply::Config.connection.default_protocol),
|
93
|
+
default: ChefApply::Config.connection.default_protocol,
|
94
|
+
proc: Proc.new { |val| ChefApply::Config.connection.default_protocol(val) }
|
95
|
+
|
96
|
+
klass.option :user,
|
97
|
+
long: "--user <USER>",
|
98
|
+
description: T.user_description
|
99
|
+
|
100
|
+
klass.option :password,
|
101
|
+
long: "--password <PASSWORD>",
|
102
|
+
description: T.password_description
|
103
|
+
|
104
|
+
klass.option :cookbook_repo_paths,
|
105
|
+
long: "--cookbook-repo-paths PATH",
|
106
|
+
description: T.cookbook_repo_paths,
|
107
|
+
default: ChefApply::Config.chef.cookbook_repo_paths,
|
108
|
+
proc: (Proc.new do |paths|
|
109
|
+
paths = paths.split(",")
|
110
|
+
ChefApply::Config.chef.cookbook_repo_paths(paths)
|
111
|
+
paths
|
112
|
+
end)
|
113
|
+
|
114
|
+
klass.option :install,
|
115
|
+
long: "--[no-]install",
|
116
|
+
default: true,
|
117
|
+
boolean: true,
|
118
|
+
description: T.install_description
|
119
|
+
|
120
|
+
klass.option :sudo,
|
121
|
+
long: "--[no-]sudo",
|
122
|
+
description: T.sudo.flag_description.sudo,
|
123
|
+
boolean: true,
|
124
|
+
default: true
|
125
|
+
|
126
|
+
klass.option :sudo_command,
|
127
|
+
long: "--sudo-command <COMMAND>",
|
128
|
+
default: "sudo",
|
129
|
+
description: T.sudo.flag_description.command
|
130
|
+
|
131
|
+
klass.option :sudo_password,
|
132
|
+
long: "--sudo-password <PASSWORD>",
|
133
|
+
description: T.sudo.flag_description.password
|
134
|
+
|
135
|
+
klass.option :sudo_options,
|
136
|
+
long: "--sudo-options 'OPTIONS...'",
|
137
|
+
description: T.sudo.flag_description.options
|
138
|
+
end
|
139
|
+
|
140
|
+
# I really don't like that mixlib-cli refers to the parsed command line flags in
|
141
|
+
# a hash accesed via the `config` method. Thats just such an overloaded word.
|
142
|
+
def parsed_options
|
143
|
+
config
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
#
|
2
|
+
# Copyright:: Copyright (c) 2018 Chef Software Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require "chef_apply/error"
|
19
|
+
|
20
|
+
module ChefApply
|
21
|
+
class CLI
|
22
|
+
module Validation
|
23
|
+
PROPERTY_MATCHER = /^([a-zA-Z0-9_]+)=(.+)$/
|
24
|
+
CB_MATCHER = '[\w\-]+'
|
25
|
+
|
26
|
+
# The first param is always hostname. Then we either have
|
27
|
+
# 1. A recipe designation
|
28
|
+
# 2. A resource type and resource name followed by any properties
|
29
|
+
def validate_params(params)
|
30
|
+
if params.size < 2
|
31
|
+
raise OptionValidationError.new("CHEFVAL002", self)
|
32
|
+
end
|
33
|
+
if params.size == 2
|
34
|
+
# Trying to specify a recipe to run remotely, no properties
|
35
|
+
cb = params[1]
|
36
|
+
if File.exist?(cb)
|
37
|
+
# This is a path specification, and we know it is valid
|
38
|
+
elsif cb =~ /^#{CB_MATCHER}$/ || cb =~ /^#{CB_MATCHER}::#{CB_MATCHER}$/
|
39
|
+
# They are specifying a cookbook as 'cb_name' or 'cb_name::recipe'
|
40
|
+
else
|
41
|
+
raise OptionValidationError.new("CHEFVAL004", self, cb)
|
42
|
+
end
|
43
|
+
elsif params.size >= 3
|
44
|
+
properties = params[3..-1]
|
45
|
+
properties.each do |property|
|
46
|
+
unless property =~ PROPERTY_MATCHER
|
47
|
+
raise OptionValidationError.new("CHEFVAL003", self, property)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Convert properties in the form k1=v1,k2=v2,kn=vn
|
54
|
+
# into a hash, while validating correct form and format
|
55
|
+
def properties_from_string(string_props)
|
56
|
+
properties = {}
|
57
|
+
string_props.each do |a|
|
58
|
+
key, value = PROPERTY_MATCHER.match(a)[1..-1]
|
59
|
+
value = transform_property_value(value)
|
60
|
+
properties[key] = value
|
61
|
+
end
|
62
|
+
properties
|
63
|
+
end
|
64
|
+
|
65
|
+
# Incoming properties are always read as a string from the command line.
|
66
|
+
# Depending on their type we should transform them so we do not try and pass
|
67
|
+
# a string to a resource property that expects an integer or boolean.
|
68
|
+
def transform_property_value(value)
|
69
|
+
case value
|
70
|
+
when /^0/
|
71
|
+
# when it is a zero leading value like "0777" don't turn
|
72
|
+
# it into a number (this is a mode flag)
|
73
|
+
value
|
74
|
+
when /^\d+$/
|
75
|
+
value.to_i
|
76
|
+
when /(^(\d+)(\.)?(\d+)?)|(^(\d+)?(\.)(\d+))/
|
77
|
+
value.to_f
|
78
|
+
when /true/i
|
79
|
+
true
|
80
|
+
when /false/i
|
81
|
+
false
|
82
|
+
else
|
83
|
+
value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class OptionValidationError < ChefApply::ErrorNoLogs
|
89
|
+
attr_reader :command
|
90
|
+
def initialize(id, calling_command, *args)
|
91
|
+
super(id, *args)
|
92
|
+
# TODO - this is getting cumbersome - move them to constructor options hash in base
|
93
|
+
@decorate = false
|
94
|
+
@command = calling_command
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
data/lib/chef_apply/error.rb
CHANGED
@@ -53,7 +53,7 @@ module ChefApply
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
-
class MultiJobFailure <
|
56
|
+
class MultiJobFailure < ErrorNoLogs
|
57
57
|
attr_reader :jobs
|
58
58
|
def initialize(jobs)
|
59
59
|
super("CHEFMULTI001")
|
@@ -62,47 +62,8 @@ module ChefApply
|
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
-
#
|
66
|
-
#
|
67
|
-
class
|
68
|
-
|
69
|
-
def self.resolve_exception(exception)
|
70
|
-
deps
|
71
|
-
show_log = true
|
72
|
-
show_stack = true
|
73
|
-
case exception
|
74
|
-
when OpenSSL::SSL::SSLError
|
75
|
-
if exception.message =~ /SSL.*verify failed.*/
|
76
|
-
id = "CHEFNET002"
|
77
|
-
show_log = false
|
78
|
-
show_stack = false
|
79
|
-
end
|
80
|
-
when SocketError then id = "CHEFNET001"; show_log = false; show_stack = false
|
81
|
-
end
|
82
|
-
if id.nil?
|
83
|
-
exception
|
84
|
-
else
|
85
|
-
e = ChefApply::Error.new(id, exception.message)
|
86
|
-
e.show_log = show_log
|
87
|
-
e.show_stack = show_stack
|
88
|
-
e
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def self.wrap_exception(original, target_host = nil)
|
93
|
-
resolved_exception = resolve_exception(original)
|
94
|
-
WrappedError.new(resolved_exception, target_host)
|
95
|
-
end
|
96
|
-
|
97
|
-
def self.unwrap_exception(wrapper)
|
98
|
-
resolve_exception(wrapper.contained_exception)
|
99
|
-
end
|
100
|
-
|
101
|
-
def self.deps
|
102
|
-
# Avoid loading additional includes until they're needed
|
103
|
-
require "socket"
|
104
|
-
require "openssl"
|
105
|
-
end
|
65
|
+
# Provide a base type for internal usage errors that should not leak out
|
66
|
+
# but may anyway.
|
67
|
+
class APIError < Error
|
106
68
|
end
|
107
|
-
|
108
69
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ChefApply
|
2
|
+
module Errors
|
3
|
+
# Provides mappings of common errors that we don't explicitly
|
4
|
+
# handle, but can offer expanded help text around.
|
5
|
+
class StandardErrorResolver
|
6
|
+
def self.resolve_exception(exception)
|
7
|
+
deps
|
8
|
+
show_log = true
|
9
|
+
show_stack = true
|
10
|
+
case exception
|
11
|
+
when OpenSSL::SSL::SSLError
|
12
|
+
if exception.message =~ /SSL.*verify failed.*/
|
13
|
+
id = "CHEFNET002"
|
14
|
+
show_log = false
|
15
|
+
show_stack = false
|
16
|
+
end
|
17
|
+
when SocketError then id = "CHEFNET001"; show_log = false; show_stack = false
|
18
|
+
end
|
19
|
+
if id.nil?
|
20
|
+
exception
|
21
|
+
else
|
22
|
+
e = ChefApply::Error.new(id, exception.message)
|
23
|
+
e.show_log = show_log
|
24
|
+
e.show_stack = show_stack
|
25
|
+
e
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.wrap_exception(original, target_host = nil)
|
30
|
+
resolved_exception = resolve_exception(original)
|
31
|
+
WrappedError.new(resolved_exception, target_host)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.unwrap_exception(wrapper)
|
35
|
+
resolve_exception(wrapper.contained_exception)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.deps
|
39
|
+
# Avoid loading additional includes until they're needed
|
40
|
+
require "socket"
|
41
|
+
require "openssl"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/chef_apply/startup.rb
CHANGED
@@ -18,6 +18,8 @@ require "chef_apply/config"
|
|
18
18
|
require "chef_apply/text"
|
19
19
|
require "chef_apply/ui/terminal"
|
20
20
|
require "chef_apply/telemeter/sender"
|
21
|
+
require "chef/log"
|
22
|
+
require "chef/config"
|
21
23
|
module ChefApply
|
22
24
|
class Startup
|
23
25
|
attr_reader :argv
|
@@ -41,6 +43,9 @@ module ChefApply
|
|
41
43
|
# are required.
|
42
44
|
setup_workstation_user_directories
|
43
45
|
|
46
|
+
# Customize behavior of Ruby and any gems around error handling
|
47
|
+
setup_error_handling
|
48
|
+
|
44
49
|
# Startup tasks that may change behavior based on configuration value
|
45
50
|
# must be run after load_config
|
46
51
|
load_config
|
@@ -120,6 +125,17 @@ module ChefApply
|
|
120
125
|
FileUtils.mkdir_p(Config.telemetry_path)
|
121
126
|
end
|
122
127
|
|
128
|
+
def setup_error_handling
|
129
|
+
# In Ruby 2.5+ threads print out to stdout when they raise an exception. This is an agressive
|
130
|
+
# attempt to ensure debugging information is not lost, but in our case it is not necessary
|
131
|
+
# because we handle all the errors ourself. So we disable this to keep output clean.
|
132
|
+
# See https://ruby-doc.org/core-2.5.0/Thread.html#method-c-report_on_exception
|
133
|
+
#
|
134
|
+
# We set this globally so that it applies to all threads we create - we never want any non-UI thread
|
135
|
+
# to render error output to the terminal.
|
136
|
+
Thread.report_on_exception = false
|
137
|
+
end
|
138
|
+
|
123
139
|
def load_config
|
124
140
|
path = custom_config_path
|
125
141
|
Config.custom_location(path) unless path.nil?
|
@@ -145,6 +161,12 @@ module ChefApply
|
|
145
161
|
def setup_logging
|
146
162
|
ChefApply::Log.setup(Config.log.location, Config.log.level.to_sym)
|
147
163
|
ChefApply::Log.info("Initialized logger")
|
164
|
+
|
165
|
+
ChefConfig.logger = ChefApply::Log
|
166
|
+
# Setting the config isn't enough, we need to ensure the logger is initialized
|
167
|
+
# or automatic initialization will still go to stdout
|
168
|
+
Chef::Log.init(ChefApply::Log)
|
169
|
+
Chef::Log.level = ChefApply::Log.level
|
148
170
|
end
|
149
171
|
|
150
172
|
def start_chef_apply
|