require 'tty-prompt' require 'pastel' require 'uri' require 'io/console' require_relative 'config' require_relative 'spinner' module Kontena module Cli module Common def logger return @logger if @logger @logger = Logger.new(ENV["DEBUG"] ? STDERR : STDOUT) @logger.level = ENV["DEBUG"].nil? ? Logger::INFO : Logger::DEBUG @logger.progname = 'COMMON' @logger end def pastel @pastel ||= Pastel.new(enabled: $stdout.tty?) end def running_silent? self.respond_to?(:silent?) && self.silent? end def running_verbose? self.respond_to?(:verbose?) && self.verbose? end # Puts that puts even when self.silent? def sputs(*msgs) ::Kernel.puts(*msgs) end # Print that prints even when self.silent? def sprint(*msgs) ::Kernel.print(*msgs) end # Print that doesn't print if self.silent? def print(*msgs) if running_silent? logger.debug(msgs.join) else super(*msgs) end end # Puts that doesn't puts if self.silent? def puts(*msgs) if running_silent? msgs.compact.each { |msg| logger.debug(msg) } elsif Thread.main['spinners'] && !Thread.main['spinners'].empty? Thread.main['spinner_msgs'] ||= [] msgs.each { |msg| Thread.main['spinner_msgs'] << msg } else super(*msgs) end end # Only output message if in verbose mode def vputs(msg = nil) if running_verbose? puts msg elsif ENV["DEBUG"] && msg logger.debug msg end end # Only show spinner when in verbose mode def vspinner(msg, &block) return vfakespinner(msg) unless block_given? if running_verbose? spinner(msg, &block) else yield end end # Like vspinner but without actually running any block def vfakespinner(msg, success: true) if !running_verbose? logger.debug msg return end puts " [#{ success ? 'done'.colorize(:green) : 'fail'.colorize(:red)}] #{msg}" end def warning(msg) warning = pastel.yellow('warn') STDERR.puts " [#{warning}] #{msg}" end def exit_with_error(msg, code = 1) error = pastel.red('error') STDERR.puts " [#{error}] #{msg}" exit code end def config Kontena::Cli::Config.instance end def require_api_url config.require_current_master.url end def require_token retried ||= false config.require_current_master_token rescue Kontena::Cli::Config::TokenExpiredError if retried raise ArgumentError, "Current master access token has expired and refresh failed." else logger.debug "Access token expired, trying to refresh" retried = true client.refresh_token && retry end end ## Invalidate refresh_token # @param [Kontena::Cli::Config::Server] server def use_refresh_token(server) return unless server.token return unless server.token.refresh_token return if server.token.expired? client = Kontena::Client.new(server.url, server.token) logger.debug "Trying to invalidate refresh token on #{server.name}" client.refresh_token rescue logger.debug "Refreshing failed: #{$!} : #{$!.message}" end def require_current_master config.require_current_master end def require_current_account config.require_current_account end def current_account config.current_account end def kontena_account @kontena_account ||= config.find_account(ENV['KONTENA_ACCOUNT'] || 'kontena') end def cloud_auth? return false unless kontena_account return false unless kontena_account.token return false unless kontena_account.token.access_token true end def api_url_version client.server_version end def cloud_client @cloud_client ||= Kontena::Client.new(kontena_account.url, kontena_account.token, prefix: '/') end def reset_cloud_client @cloud_client = nil end def client(token = nil, api_url = nil) if token.kind_of?(String) token = Kontena::Cli::Config::Token.new(access_token: token) end @client ||= Kontena::Client.new( api_url || require_current_master.url, token || require_current_master.token ) end def reset_client @client = nil end def settings_filename config.config_filename end def settings config end def api_url config.require_current_master.url end def current_grid=(grid) config.current_grid=(grid) end def require_current_grid config.require_current_grid end def clear_current_grid current_master.delete_field(:grid) if require_current_master.respond_to?(:grid) config.write end def current_grid (self.respond_to?(:grid) ? self.grid : nil) || config.current_grid end def current_master_index config.find_server_index(require_current_master.name) end def current_master config.current_master end def current_master=(master_alias) config.current_master = master_alias end def error(message = "Error") prompt.error(message) exit(1) end def ask(question = "") prompt.ask(question) end def yes?(question = "") prompt.yes?(question) end def prompt ::Kontena.prompt end def confirm_command(name, message = nil) if self.respond_to?(:force?) && self.force? return end puts message if message puts "Destructive command. To proceed, type \"#{name}\" or re-run this command with --force option." ask("Enter '#{name}' to confirm: ") == name || error("Confirmation did not match #{name}. Aborted command.") end def confirm(message = 'Destructive command. You can skip this prompt by running this command with --force option. Are you sure?') if self.respond_to?(:force?) && self.force? return end prompt.yes?(message) || error('Aborted command.') end def api_url=(api_url) config.current_master.url = api_url config.write end def access_token=(token) require_current_master.token.access_token = token config.write end def add_master(server_name, master_info) config.add_server(master_info.merge('name' => server_name)) end def spinner(msg, &block) Kontena::Cli::Spinner.spin(msg, &block) end def any_key_to_continue_with_timeout(timeout=9) return nil if running_silent? return nil unless $stdout.tty? start_time = Time.now.to_i end_time = start_time + timeout Thread.main['any_key.timed_out'] = false msg = "Press any key to continue or ctrl-c to cancel.. (Automatically continuing in ? seconds)" reader_thread = Thread.new do Thread.main['any_key.char'] = STDIN.getch end countdown_thread = Thread.new do time_left = timeout while time_left > 0 && Thread.main['any_key.char'].nil? print "\r#{pastel.bright_white("#{msg.sub("?", time_left.to_s)}")} " time_left = end_time - Time.now.to_i sleep 0.1 end print "\r#{' ' * msg.length} \r" reader_thread.kill if reader_thread.alive? end countdown_thread.join if Thread.main['any_key.char'] == "\u0003" error "Canceled" end end def any_key_to_continue(timeout = nil) return nil if running_silent? return nil unless $stdout.tty? return any_key_to_continue_with_timeout(timeout) if timeout msg = "Press any key to continue or ctrl-c to cancel.. " print pastel.bright_cyan("#{msg}") char = STDIN.getch print "\r#{' ' * msg.length}\r" if char == "\u0003" error "Canceled" end end def display_account_login_info if kontena_account if kontena_account.token && kontena_account.token.access_token begin puts [ pastel.green("Authenticated to Kontena Cloud at"), pastel.yellow(kontena_account.url), pastel.green("as"), pastel.yellow(kontena_account.username) ].join(' ') rescue end else puts pastel.cyan("Not authenticated to Kontena Cloud") end end end def display_master_login_info server = config.current_master if server if server.token && server.token.access_token puts [ pastel.green('Authenticated to Kontena Master'), pastel.yellow(server.name), pastel.green('at'), pastel.yellow(server.url), pastel.green('as'), pastel.yellow(server.token.username || server.username) ].join(' ') else puts pastel.cyan("Not authenticated to current master #{server.name}") end else puts pastel.cyan("Current master not selected") end end def display_login_info(only: nil) display_master_login_info unless only == :account display_account_login_info unless only == :master end def display_logo puts File.read(File.expand_path('../../../../LOGO', __FILE__)) end end end end