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.
@@ -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