summaryrefslogtreecommitdiff
path: root/lib/chef/knife/core/ui.rb
blob: 484c3ab3deeb84d961f2daac42ae21b7d3620bc1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Christopher Brown (<cb@chef.io>)
# Author:: Daniel DeLeo (<dan@chef.io>)
# Copyright:: Copyright 2009-2016, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "forwardable"
require "chef/platform/query_helpers"
require "chef/knife/core/generic_presenter"
require "tempfile"

class Chef
  class Knife

    #==Chef::Knife::UI
    # The User Interaction class used by knife.
    class UI

      extend Forwardable

      attr_reader :stdout
      attr_reader :stderr
      attr_reader :stdin
      attr_reader :config

      attr_reader :presenter

      def_delegator :@presenter, :format_list_for_display
      def_delegator :@presenter, :format_for_display
      def_delegator :@presenter, :format_cookbook_list_for_display

      def initialize(stdout, stderr, stdin, config)
        @stdout, @stderr, @stdin, @config = stdout, stderr, stdin, config
        @presenter = Chef::Knife::Core::GenericPresenter.new(self, config)
      end

      # Creates a new +presenter_class+ object and uses it to format structured
      # data for display. By default, a Chef::Knife::Core::GenericPresenter
      # object is used.
      def use_presenter(presenter_class)
        @presenter = presenter_class.new(self, config)
      end

      def highline
        @highline ||= begin
          require "highline"
          HighLine.new
        end
      end

      # Prints a message to stdout. Aliased as +info+ for compatibility with
      # the logger API.
      def msg(message)
        stdout.puts message
      rescue Errno::EPIPE => e
        raise e if @config[:verbosity] >= 2
        exit 0
      end

      # Prints a msg to stderr. Used for info, warn, error, and fatal.
      def log(message)
        stderr.puts message
      rescue Errno::EPIPE => e
        raise e if @config[:verbosity] >= 2
        exit 0
      end

      alias :info :log
      alias :err :log

      # Print a warning message
      def warn(message)
        log("#{color('WARNING:', :yellow, :bold)} #{message}")
      end

      # Print an error message
      def error(message)
        log("#{color('ERROR:', :red, :bold)} #{message}")
      end

      # Print a message describing a fatal error.
      def fatal(message)
        log("#{color('FATAL:', :red, :bold)} #{message}")
      end

      def color(string, *colors)
        if color?
          highline.color(string, *colors)
        else
          string
        end
      end

      # Should colored output be used? For output to a terminal, this is
      # determined by the value of `config[:color]`. When output is not to a
      # terminal, colored output is never used
      def color?
        Chef::Config[:color] && stdout.tty?
      end

      def ask(*args, &block)
        highline.ask(*args, &block)
      end

      def list(*args)
        highline.list(*args)
      end

      # Formats +data+ using the configured presenter and outputs the result
      # via +msg+. Formatting can be customized by configuring a different
      # presenter. See +use_presenter+
      def output(data)
        msg @presenter.format(data)
      end

      # Determines if the output format is a data interchange format, i.e.,
      # JSON or YAML
      def interchange?
        @presenter.interchange?
      end

      def ask_question(question, opts = {})
        question += "[#{opts[:default]}] " if opts[:default]

        if opts[:default] && config[:defaults]
          opts[:default]
        else
          stdout.print question
          a = stdin.readline.strip

          if opts[:default]
            a.empty? ? opts[:default] : a
          else
            a
          end
        end
      end

      def pretty_print(data)
        stdout.puts data
      rescue Errno::EPIPE => e
        raise e if @config[:verbosity] >= 2
        exit 0
      end

      # Hash -> Hash
      # Works the same as edit_data but
      # returns a hash rather than a JSON string/Fully inflated object
      def edit_hash(hash)
        raw = edit_data(hash, false)
        Chef::JSONCompat.parse(raw)
      end

      def edit_data(data, parse_output = true, object_class: nil)
        output = Chef::JSONCompat.to_json_pretty(data)
        if !config[:disable_editing]
          Tempfile.open([ "knife-edit-", ".json" ]) do |tf|
            tf.sync = true
            tf.puts output
            tf.close
            raise "Please set EDITOR environment variable. See https://docs.chef.io/knife_using.html for details." unless system("#{config[:editor]} #{tf.path}")

            output = IO.read(tf.path)
          end
        end

        if parse_output
          if object_class.nil?
            raise ArgumentError, "Please pass in the object class to hydrate or use #edit_hash"
          else
            object_class.from_hash(Chef::JSONCompat.parse(output))
          end
        else
          output
        end
      end

      def edit_object(klass, name)
        object = klass.load(name)

        output = edit_data(object, object_class: klass)

        # Only make the save if the user changed the object.
        #
        # Output JSON for the original (object) and edited (output), then parse
        # them without reconstituting the objects into real classes
        # (create_additions=false). Then, compare the resulting simple objects,
        # which will be Array/Hash/String/etc.
        #
        # We wouldn't have to do these shenanigans if all the editable objects
        # implemented to_hash, or if to_json against a hash returned a string
        # with stable key order.
        object_parsed_again = Chef::JSONCompat.parse(Chef::JSONCompat.to_json(object))
        output_parsed_again = Chef::JSONCompat.parse(Chef::JSONCompat.to_json(output))
        if object_parsed_again != output_parsed_again
          output.save
          msg("Saved #{output}")
        else
          msg("Object unchanged, not saving")
        end
        output(format_for_display(object)) if config[:print_after]
      end

      def confirmation_instructions(default_choice)
        case default_choice
        when true
          "? (Y/n) "
        when false
          "? (y/N) "
        else
          "? (Y/N) "
        end
      end

      # See confirm method for argument information
      def confirm_without_exit(question, append_instructions = true, default_choice = nil)
        return true if config[:yes]

        stdout.print question
        stdout.print confirmation_instructions(default_choice) if append_instructions

        answer = stdin.readline
        answer.chomp!

        case answer
        when "Y", "y"
          true
        when "N", "n"
          msg("You said no, so I'm done here.")
          false
        when ""
          unless default_choice.nil?
            default_choice
          else
            msg("I have no idea what to do with '#{answer}'")
            msg("Just say Y or N, please.")
            confirm_without_exit(question, append_instructions, default_choice)
          end
        else
          msg("I have no idea what to do with '#{answer}'")
          msg("Just say Y or N, please.")
          confirm_without_exit(question, append_instructions, default_choice)
        end
      end

      #
      # Not the ideal signature for a function but we need to stick with this
      # for now until we get a chance to break our API in Chef 12.
      #
      # question => Question to print  before asking for confirmation
      # append_instructions => Should print '? (Y/N)' as instructions
      # default_choice => Set to true for 'Y', and false for 'N' as default answer
      #
      def confirm(question, append_instructions = true, default_choice = nil)
        unless confirm_without_exit(question, append_instructions, default_choice)
          exit 3
        end
        true
      end

    end
  end
end