chef-apply 0.1.2 → 0.1.15

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.
@@ -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
@@ -53,7 +53,7 @@ module ChefApply
53
53
  end
54
54
  end
55
55
 
56
- class MultiJobFailure < ChefApply::ErrorNoLogs
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
- # Provides mappings of common errors that we don't explicitly
66
- # handle, but can offer expanded help text around.
67
- class StandardErrorResolver
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
@@ -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