kubetailrb 0.1.0
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.
- checksums.yaml +7 -0
 - data/.pryrc +18 -0
 - data/.rubocop.yml +23 -0
 - data/.ruby-version +1 -0
 - data/CHANGELOG.md +5 -0
 - data/Guardfile +34 -0
 - data/LICENSE.txt +21 -0
 - data/README.md +73 -0
 - data/Rakefile +85 -0
 - data/exe/kubetailrb +6 -0
 - data/journey_log.md +469 -0
 - data/k3d/clock/Dockerfile +5 -0
 - data/k3d/clock/clock.rb +8 -0
 - data/k3d/clock-json/Dockerfile +5 -0
 - data/k3d/clock-json/clock_json.rb +18 -0
 - data/k3d/default.yml +23 -0
 - data/kubetailrb.png +0 -0
 - data/lib/boolean.rb +22 -0
 - data/lib/kubetailrb/cli.rb +24 -0
 - data/lib/kubetailrb/cmd/file.rb +96 -0
 - data/lib/kubetailrb/cmd/help.rb +45 -0
 - data/lib/kubetailrb/cmd/k8s.rb +127 -0
 - data/lib/kubetailrb/cmd/version.rb +18 -0
 - data/lib/kubetailrb/file_reader.rb +122 -0
 - data/lib/kubetailrb/json_formatter.rb +66 -0
 - data/lib/kubetailrb/k8s_opts.rb +38 -0
 - data/lib/kubetailrb/k8s_pod_reader.rb +83 -0
 - data/lib/kubetailrb/k8s_pods_reader.rb +86 -0
 - data/lib/kubetailrb/no_op_formatter.rb +10 -0
 - data/lib/kubetailrb/opts_parser.rb +28 -0
 - data/lib/kubetailrb/validated.rb +19 -0
 - data/lib/kubetailrb/version.rb +5 -0
 - data/lib/kubetailrb/with_k8s_client.rb +25 -0
 - data/lib/kubetailrb.rb +9 -0
 - data/sig/kubetailrb.rbs +4 -0
 - metadata +294 -0
 
| 
         @@ -0,0 +1,45 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Kubetailrb
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Cmd
         
     | 
| 
      
 5 
     | 
    
         
            +
                # Display help.
         
     | 
| 
      
 6 
     | 
    
         
            +
                class Help
         
     | 
| 
      
 7 
     | 
    
         
            +
                  FLAGS = %w[-h --help].freeze
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  def execute
         
     | 
| 
      
 10 
     | 
    
         
            +
                    puts <<~HELP
         
     | 
| 
      
 11 
     | 
    
         
            +
                      Tail your Kubernetes pod logs at the same time.
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                      Usage:
         
     | 
| 
      
 14 
     | 
    
         
            +
                        kubetailrb pod-query [flags]
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                      Flags:
         
     | 
| 
      
 17 
     | 
    
         
            +
                        -v, --version   Display version.
         
     | 
| 
      
 18 
     | 
    
         
            +
                        -h, --help      Display help.
         
     | 
| 
      
 19 
     | 
    
         
            +
                            --tail      The number of lines from the end of the logs to show. Defaults to 10.
         
     | 
| 
      
 20 
     | 
    
         
            +
                        -f, --follow    Output appended data as the file grows.
         
     | 
| 
      
 21 
     | 
    
         
            +
                            --file      Display file content.
         
     | 
| 
      
 22 
     | 
    
         
            +
                        -p, --pretty    Pretty print JSON logs.
         
     | 
| 
      
 23 
     | 
    
         
            +
                        -r, --raw       Only display pod logs.
         
     | 
| 
      
 24 
     | 
    
         
            +
                        -n, --namespace Kubernetes namespace to use.
         
     | 
| 
      
 25 
     | 
    
         
            +
                    HELP
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 29 
     | 
    
         
            +
                    def applicable?(*args)
         
     | 
| 
      
 30 
     | 
    
         
            +
                      missing_args?(*args) || contains_flags?(*args)
         
     | 
| 
      
 31 
     | 
    
         
            +
                    end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                    private
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                    def missing_args?(*args)
         
     | 
| 
      
 36 
     | 
    
         
            +
                      args.nil? || args.empty?
         
     | 
| 
      
 37 
     | 
    
         
            +
                    end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                    def contains_flags?(*args)
         
     | 
| 
      
 40 
     | 
    
         
            +
                      args.any? { |arg| FLAGS.include?(arg) }
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
              end
         
     | 
| 
      
 45 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,127 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'kubetailrb/k8s_pods_reader'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'kubetailrb/json_formatter'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'kubetailrb/no_op_formatter'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Kubetailrb
         
     | 
| 
      
 8 
     | 
    
         
            +
              module Cmd
         
     | 
| 
      
 9 
     | 
    
         
            +
                # Command to read k8s pod logs.
         
     | 
| 
      
 10 
     | 
    
         
            +
                class K8s
         
     | 
| 
      
 11 
     | 
    
         
            +
                  DEFAULT_NB_LINES = 10
         
     | 
| 
      
 12 
     | 
    
         
            +
                  DEFAULT_NAMESPACE = 'default'
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  NAMESPACE_FLAGS = %w[-n --namespace].freeze
         
     | 
| 
      
 15 
     | 
    
         
            +
                  TAIL_FLAG = '--tail'
         
     | 
| 
      
 16 
     | 
    
         
            +
                  FOLLOW_FLAGS = %w[-f --follow].freeze
         
     | 
| 
      
 17 
     | 
    
         
            +
                  RAW_FLAGS = %w[-r --raw].freeze
         
     | 
| 
      
 18 
     | 
    
         
            +
                  PRETTY_PRINT_FLAGS = %w[-p --pretty].freeze
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                  attr_reader :reader
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                  def initialize(pod_query:, opts:, formatter:)
         
     | 
| 
      
 23 
     | 
    
         
            +
                    @reader = Kubetailrb::K8sPodsReader.new(pod_query: pod_query, formatter: formatter, opts: opts)
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  def execute
         
     | 
| 
      
 27 
     | 
    
         
            +
                    @reader.read
         
     | 
| 
      
 28 
     | 
    
         
            +
                  end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 31 
     | 
    
         
            +
                    def create(*args)
         
     | 
| 
      
 32 
     | 
    
         
            +
                      new(
         
     | 
| 
      
 33 
     | 
    
         
            +
                        pod_query: parse_pod_query(*args),
         
     | 
| 
      
 34 
     | 
    
         
            +
                        formatter: parse_pretty_print(*args) ? Kubetailrb::JsonFormatter.new : Kubetailrb::NoOpFormatter.new,
         
     | 
| 
      
 35 
     | 
    
         
            +
                        opts: K8sOpts.new(
         
     | 
| 
      
 36 
     | 
    
         
            +
                          namespace: parse_namespace(*args),
         
     | 
| 
      
 37 
     | 
    
         
            +
                          last_nb_lines: parse_nb_lines(*args),
         
     | 
| 
      
 38 
     | 
    
         
            +
                          follow: parse_follow(*args),
         
     | 
| 
      
 39 
     | 
    
         
            +
                          raw: parse_raw(*args)
         
     | 
| 
      
 40 
     | 
    
         
            +
                        )
         
     | 
| 
      
 41 
     | 
    
         
            +
                      )
         
     | 
| 
      
 42 
     | 
    
         
            +
                    end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                    def parse_pod_query(*args)
         
     | 
| 
      
 45 
     | 
    
         
            +
                      # TODO: We could be smarter here? For example, if the pod names are
         
     | 
| 
      
 46 
     | 
    
         
            +
                      # provided at the end of the command, like this:
         
     | 
| 
      
 47 
     | 
    
         
            +
                      #   kubetailrb --tail 3 some-pod
         
     | 
| 
      
 48 
     | 
    
         
            +
                      # The above command will not work because this method will return 3
         
     | 
| 
      
 49 
     | 
    
         
            +
                      # instead of 'some-pod'...
         
     | 
| 
      
 50 
     | 
    
         
            +
                      args.find { |arg| !arg.start_with? '-' }
         
     | 
| 
      
 51 
     | 
    
         
            +
                    end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                    #
         
     | 
| 
      
 54 
     | 
    
         
            +
                    # Parse k8s namespace from arguments provided in the CLI, e.g.
         
     | 
| 
      
 55 
     | 
    
         
            +
                    #
         
     | 
| 
      
 56 
     | 
    
         
            +
                    #   kubetailrb some-pod -n sandbox
         
     | 
| 
      
 57 
     | 
    
         
            +
                    #
         
     | 
| 
      
 58 
     | 
    
         
            +
                    # will return 'sandbox'.
         
     | 
| 
      
 59 
     | 
    
         
            +
                    #
         
     | 
| 
      
 60 
     | 
    
         
            +
                    # Will raise `MissingNamespaceValueError` if the value is not provided:
         
     | 
| 
      
 61 
     | 
    
         
            +
                    #
         
     | 
| 
      
 62 
     | 
    
         
            +
                    #   kubetailrb some-pod -n
         
     | 
| 
      
 63 
     | 
    
         
            +
                    #
         
     | 
| 
      
 64 
     | 
    
         
            +
                    def parse_namespace(*args)
         
     | 
| 
      
 65 
     | 
    
         
            +
                      return DEFAULT_NAMESPACE unless args.any? { |arg| NAMESPACE_FLAGS.include?(arg) }
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                      index = args.find_index { |arg| NAMESPACE_FLAGS.include?(arg) }.to_i
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                      raise MissingNamespaceValueError, "Missing #{NAMESPACE_FLAGS} value." if args[index + 1].nil?
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                      args[index + 1]
         
     | 
| 
      
 72 
     | 
    
         
            +
                    end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                    #
         
     | 
| 
      
 75 
     | 
    
         
            +
                    # Parse nb lines from arguments provided in the CLI, e.g.
         
     | 
| 
      
 76 
     | 
    
         
            +
                    #
         
     | 
| 
      
 77 
     | 
    
         
            +
                    #   kubetailrb some-pod --tail 3
         
     | 
| 
      
 78 
     | 
    
         
            +
                    #
         
     | 
| 
      
 79 
     | 
    
         
            +
                    # will return 3.
         
     | 
| 
      
 80 
     | 
    
         
            +
                    #
         
     | 
| 
      
 81 
     | 
    
         
            +
                    # Will raise `MissingNbLinesValueError` if the value is not provided:
         
     | 
| 
      
 82 
     | 
    
         
            +
                    #
         
     | 
| 
      
 83 
     | 
    
         
            +
                    #   kubetailrb some-pod --tail
         
     | 
| 
      
 84 
     | 
    
         
            +
                    #
         
     | 
| 
      
 85 
     | 
    
         
            +
                    # Will raise `InvalidNbLinesValueError` if the provided value is not a
         
     | 
| 
      
 86 
     | 
    
         
            +
                    # number:
         
     | 
| 
      
 87 
     | 
    
         
            +
                    #
         
     | 
| 
      
 88 
     | 
    
         
            +
                    #   kubetailrb some-pod --tail some-string
         
     | 
| 
      
 89 
     | 
    
         
            +
                    #
         
     | 
| 
      
 90 
     | 
    
         
            +
                    def parse_nb_lines(*args)
         
     | 
| 
      
 91 
     | 
    
         
            +
                      return DEFAULT_NB_LINES unless args.include?(TAIL_FLAG)
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                      index = args.find_index { |arg| arg == TAIL_FLAG }.to_i
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                      raise MissingNbLinesValueError, "Missing #{TAIL_FLAG} value." if args[index + 1].nil?
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                      last_nb_lines = args[index + 1].to_i
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                      raise InvalidNbLinesValueError, "Invalid #{TAIL_FLAG} value: #{args[index + 1]}." if last_nb_lines.zero?
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                      last_nb_lines
         
     | 
| 
      
 102 
     | 
    
         
            +
                    end
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                    def parse_follow(*args)
         
     | 
| 
      
 105 
     | 
    
         
            +
                      args.any? { |arg| FOLLOW_FLAGS.include?(arg) }
         
     | 
| 
      
 106 
     | 
    
         
            +
                    end
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                    def parse_raw(*args)
         
     | 
| 
      
 109 
     | 
    
         
            +
                      args.any? { |arg| RAW_FLAGS.include?(arg) }
         
     | 
| 
      
 110 
     | 
    
         
            +
                    end
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                    def parse_pretty_print(*args)
         
     | 
| 
      
 113 
     | 
    
         
            +
                      args.any? { |arg| PRETTY_PRINT_FLAGS.include?(arg) }
         
     | 
| 
      
 114 
     | 
    
         
            +
                    end
         
     | 
| 
      
 115 
     | 
    
         
            +
                  end
         
     | 
| 
      
 116 
     | 
    
         
            +
                end
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                class MissingNbLinesValueError < RuntimeError
         
     | 
| 
      
 119 
     | 
    
         
            +
                end
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                class MissingNamespaceValueError < RuntimeError
         
     | 
| 
      
 122 
     | 
    
         
            +
                end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                class InvalidNbLinesValueError < RuntimeError
         
     | 
| 
      
 125 
     | 
    
         
            +
                end
         
     | 
| 
      
 126 
     | 
    
         
            +
              end
         
     | 
| 
      
 127 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,18 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Kubetailrb
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Cmd
         
     | 
| 
      
 5 
     | 
    
         
            +
                # Get application version.
         
     | 
| 
      
 6 
     | 
    
         
            +
                class Version
         
     | 
| 
      
 7 
     | 
    
         
            +
                  FLAGS = %w[-v --version].freeze
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  def execute
         
     | 
| 
      
 10 
     | 
    
         
            +
                    puts VERSION
         
     | 
| 
      
 11 
     | 
    
         
            +
                  end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  def self.applicable?(*args)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    args.any? { |arg| FLAGS.include?(arg) }
         
     | 
| 
      
 15 
     | 
    
         
            +
                  end
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
              end
         
     | 
| 
      
 18 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,122 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative 'validated'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Kubetailrb
         
     | 
| 
      
 6 
     | 
    
         
            +
              # Read file content
         
     | 
| 
      
 7 
     | 
    
         
            +
              class FileReader
         
     | 
| 
      
 8 
     | 
    
         
            +
                include Validated
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                attr_reader :filepath, :last_nb_lines
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                def initialize(filepath:, last_nb_lines:, follow:)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @filepath = filepath
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @last_nb_lines = last_nb_lines
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @follow = follow
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  validate
         
     | 
| 
      
 18 
     | 
    
         
            +
                end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                def read
         
     | 
| 
      
 21 
     | 
    
         
            +
                  # naive_read
         
     | 
| 
      
 22 
     | 
    
         
            +
                  read_with_fd
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                # NOTE: Is there something like `attr_reader` but for boolean?
         
     | 
| 
      
 26 
     | 
    
         
            +
                # Nope, Ruby does not know if a variable is a boolean or not. So it cannot
         
     | 
| 
      
 27 
     | 
    
         
            +
                # create a dedicated method for those booleans.
         
     | 
| 
      
 28 
     | 
    
         
            +
                def follow?
         
     | 
| 
      
 29 
     | 
    
         
            +
                  @follow
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                private
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                def validate
         
     | 
| 
      
 35 
     | 
    
         
            +
                  raise NoSuchFileError, "#{@filepath} not found" unless File.exist?(@filepath)
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  validate_last_nb_lines @last_nb_lines
         
     | 
| 
      
 38 
     | 
    
         
            +
                  validate_boolean @follow, "Invalid follow: #{@follow}."
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                #
         
     | 
| 
      
 42 
     | 
    
         
            +
                # Naive implementation to read the last N lines of a file.
         
     | 
| 
      
 43 
     | 
    
         
            +
                # Does not support `--follow`.
         
     | 
| 
      
 44 
     | 
    
         
            +
                # Took ~1.41s to read a 3.1G file (5M lines).
         
     | 
| 
      
 45 
     | 
    
         
            +
                #
         
     | 
| 
      
 46 
     | 
    
         
            +
                def naive_read
         
     | 
| 
      
 47 
     | 
    
         
            +
                  # Let's us `wc` optimized to count the number of lines!
         
     | 
| 
      
 48 
     | 
    
         
            +
                  nb_lines = `wc -l #{@filepath}`.split.first.to_i
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                  start = nb_lines - @last_nb_lines
         
     | 
| 
      
 51 
     | 
    
         
            +
                  i = 0
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                  File.open(@filepath, 'r').each_line do |line|
         
     | 
| 
      
 54 
     | 
    
         
            +
                    puts line if i >= start
         
     | 
| 
      
 55 
     | 
    
         
            +
                    i += 1
         
     | 
| 
      
 56 
     | 
    
         
            +
                  end
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                #
         
     | 
| 
      
 60 
     | 
    
         
            +
                # Use `seek` to start from the EOF.
         
     | 
| 
      
 61 
     | 
    
         
            +
                # Use `read` to read the content of the file from the given position.
         
     | 
| 
      
 62 
     | 
    
         
            +
                # src: https://renehernandez.io/tutorials/implementing-tail-command-in-ruby/
         
     | 
| 
      
 63 
     | 
    
         
            +
                # Took ~0.13s to read a 3.1G file (5M lines).
         
     | 
| 
      
 64 
     | 
    
         
            +
                #
         
     | 
| 
      
 65 
     | 
    
         
            +
                def read_with_fd
         
     | 
| 
      
 66 
     | 
    
         
            +
                  file = File.open(@filepath)
         
     | 
| 
      
 67 
     | 
    
         
            +
                  update_stats file
         
     | 
| 
      
 68 
     | 
    
         
            +
                  read_last_nb_lines file
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                  if @follow
         
     | 
| 
      
 71 
     | 
    
         
            +
                    loop do
         
     | 
| 
      
 72 
     | 
    
         
            +
                      if file_changed?(file)
         
     | 
| 
      
 73 
     | 
    
         
            +
                        update_stats(file)
         
     | 
| 
      
 74 
     | 
    
         
            +
                        print file.read
         
     | 
| 
      
 75 
     | 
    
         
            +
                      end
         
     | 
| 
      
 76 
     | 
    
         
            +
                    end
         
     | 
| 
      
 77 
     | 
    
         
            +
                  end
         
     | 
| 
      
 78 
     | 
    
         
            +
                ensure
         
     | 
| 
      
 79 
     | 
    
         
            +
                  file&.close
         
     | 
| 
      
 80 
     | 
    
         
            +
                end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                def read_last_nb_lines(file)
         
     | 
| 
      
 83 
     | 
    
         
            +
                  return if File.empty?(file)
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                  pos = 0
         
     | 
| 
      
 86 
     | 
    
         
            +
                  current_line_nb = 0
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                  loop do
         
     | 
| 
      
 89 
     | 
    
         
            +
                    pos -= 1
         
     | 
| 
      
 90 
     | 
    
         
            +
                    # Seek file position from the end.
         
     | 
| 
      
 91 
     | 
    
         
            +
                    file.seek(pos, IO::SEEK_END)
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                    # If we have reached the begining of the file, read all the file.
         
     | 
| 
      
 94 
     | 
    
         
            +
                    # We need to do this check before reading the next byte, otherwise, the
         
     | 
| 
      
 95 
     | 
    
         
            +
                    # cursor will be moved to 1.
         
     | 
| 
      
 96 
     | 
    
         
            +
                    break if file.tell.zero?
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                    # Read only one character (or is it byte?).
         
     | 
| 
      
 99 
     | 
    
         
            +
                    char = file.read(1)
         
     | 
| 
      
 100 
     | 
    
         
            +
                    current_line_nb += 1 if char == "\n"
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    break if current_line_nb > @last_nb_lines
         
     | 
| 
      
 103 
     | 
    
         
            +
                  end
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                  update_stats file
         
     | 
| 
      
 106 
     | 
    
         
            +
                  puts file.read
         
     | 
| 
      
 107 
     | 
    
         
            +
                end
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                def update_stats(file)
         
     | 
| 
      
 110 
     | 
    
         
            +
                  @mtime = file.stat.mtime
         
     | 
| 
      
 111 
     | 
    
         
            +
                  @size = file.size
         
     | 
| 
      
 112 
     | 
    
         
            +
                end
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
                def file_changed?(file)
         
     | 
| 
      
 115 
     | 
    
         
            +
                  @mtime != file.stat.mtime || @size != file.size
         
     | 
| 
      
 116 
     | 
    
         
            +
                end
         
     | 
| 
      
 117 
     | 
    
         
            +
              end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
              # NOTE: We can create custom exceptions by extending RuntimeError.
         
     | 
| 
      
 120 
     | 
    
         
            +
              class NoSuchFileError < RuntimeError
         
     | 
| 
      
 121 
     | 
    
         
            +
              end
         
     | 
| 
      
 122 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,66 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Kubetailrb
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Format JSON to human readable.
         
     | 
| 
      
 5 
     | 
    
         
            +
              class JsonFormatter
         
     | 
| 
      
 6 
     | 
    
         
            +
                def format(log)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  json = JSON.parse(log)
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  return format_access_log(json) if access_log?(json)
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  format_application_log(json)
         
     | 
| 
      
 12 
     | 
    
         
            +
                rescue JSON::ParserError
         
     | 
| 
      
 13 
     | 
    
         
            +
                  log
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                private
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                def access_log?(json)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  json.include?('http.response.status_code')
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                def format_access_log(json)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  "#{json["@timestamp"]}#{http_status_code json}#{json["http.request.method"]} #{json["url.path"]}"
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                def format_application_log(json)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  "#{json["@timestamp"]}#{log_level json}#{json["message"]}"
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                def http_status_code(json)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  code = json['http.response.status_code']
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  return "#{blue(" I ")}[#{code}] " if code >= 200 && code < 400
         
     | 
| 
      
 34 
     | 
    
         
            +
                  return "#{yellow(" W ")}[#{code}] " if code >= 400 && code < 500
         
     | 
| 
      
 35 
     | 
    
         
            +
                  return "#{red(" E ")}[#{code}] " if code > 500
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  " #{code} "
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                def log_level(json)
         
     | 
| 
      
 41 
     | 
    
         
            +
                  level = json['log.level']
         
     | 
| 
      
 42 
     | 
    
         
            +
                  return '' if level.nil? || level.strip.empty?
         
     | 
| 
      
 43 
     | 
    
         
            +
                  return blue(' I ') if level == 'INFO'
         
     | 
| 
      
 44 
     | 
    
         
            +
                  return yellow(' W ') if level == 'WARN'
         
     | 
| 
      
 45 
     | 
    
         
            +
                  return red(' E ') if level == 'ERROR'
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                  " #{level} "
         
     | 
| 
      
 48 
     | 
    
         
            +
                end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                def colorize(text, color_code)
         
     | 
| 
      
 51 
     | 
    
         
            +
                  " \e[#{color_code}m#{text}\e[0m "
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                def blue(text)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  colorize(text, '1;30;44')
         
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                def yellow(text)
         
     | 
| 
      
 59 
     | 
    
         
            +
                  colorize(text, '1;30;43')
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                def red(text)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  colorize(text, '1;30;41')
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
              end
         
     | 
| 
      
 66 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,38 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative 'validated'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Kubetailrb
         
     | 
| 
      
 6 
     | 
    
         
            +
              # Options to use for reading k8s pod logs.
         
     | 
| 
      
 7 
     | 
    
         
            +
              class K8sOpts
         
     | 
| 
      
 8 
     | 
    
         
            +
                include Validated
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                attr_reader :namespace, :last_nb_lines
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                def initialize(namespace:, last_nb_lines:, follow:, raw:)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @namespace = namespace
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @last_nb_lines = last_nb_lines
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @follow = follow
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @raw = raw
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  validate
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                def follow?
         
     | 
| 
      
 22 
     | 
    
         
            +
                  @follow
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                def raw?
         
     | 
| 
      
 26 
     | 
    
         
            +
                  @raw
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                private
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def validate
         
     | 
| 
      
 32 
     | 
    
         
            +
                  raise_if_blank @namespace, 'Namespace not set.'
         
     | 
| 
      
 33 
     | 
    
         
            +
                  validate_last_nb_lines @last_nb_lines
         
     | 
| 
      
 34 
     | 
    
         
            +
                  validate_boolean @follow, "Invalid follow: #{@follow}."
         
     | 
| 
      
 35 
     | 
    
         
            +
                  validate_boolean @raw, "Invalid raw: #{@raw}."
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
              end
         
     | 
| 
      
 38 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,83 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative 'with_k8s_client'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative 'validated'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative 'json_formatter'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Kubetailrb
         
     | 
| 
      
 8 
     | 
    
         
            +
              # Read Kubernetes pod logs.
         
     | 
| 
      
 9 
     | 
    
         
            +
              class K8sPodReader
         
     | 
| 
      
 10 
     | 
    
         
            +
                include Validated
         
     | 
| 
      
 11 
     | 
    
         
            +
                include WithK8sClient
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                attr_reader :pod_name, :opts
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                def initialize(pod_name:, formatter:, opts:, k8s_client: nil)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  validate(pod_name, formatter, opts)
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  @k8s_client = k8s_client
         
     | 
| 
      
 19 
     | 
    
         
            +
                  @pod_name = pod_name
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @formatter = formatter
         
     | 
| 
      
 21 
     | 
    
         
            +
                  @opts = opts
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                def read
         
     | 
| 
      
 25 
     | 
    
         
            +
                  pod_logs = read_pod_logs
         
     | 
| 
      
 26 
     | 
    
         
            +
                  unless @opts.follow?
         
     | 
| 
      
 27 
     | 
    
         
            +
                    print_logs pod_logs
         
     | 
| 
      
 28 
     | 
    
         
            +
                    return
         
     | 
| 
      
 29 
     | 
    
         
            +
                  end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                  # NOTE: The watch method from kubeclient does not accept `tail_lines`
         
     | 
| 
      
 32 
     | 
    
         
            +
                  # argument, so I had to resort to some hack... by using the first log to
         
     | 
| 
      
 33 
     | 
    
         
            +
                  # print out. Not ideal, since it's not really the N last nb lines, and
         
     | 
| 
      
 34 
     | 
    
         
            +
                  # assume every logs are different, which may not be true.
         
     | 
| 
      
 35 
     | 
    
         
            +
                  # But it does the job for most cases.
         
     | 
| 
      
 36 
     | 
    
         
            +
                  first_log_to_display = pod_logs.to_s.split("\n").first
         
     | 
| 
      
 37 
     | 
    
         
            +
                  should_print_logs = false
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                  k8s_client.watch_pod_log(@pod_name, @opts.namespace) do |line|
         
     | 
| 
      
 40 
     | 
    
         
            +
                    # NOTE: Is it good practice to update a variable that is outside of a
         
     | 
| 
      
 41 
     | 
    
         
            +
                    # block? Can we do better?
         
     | 
| 
      
 42 
     | 
    
         
            +
                    should_print_logs = true if line == first_log_to_display
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                    print_logs(line) if should_print_logs
         
     | 
| 
      
 45 
     | 
    
         
            +
                  end
         
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                private
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                def validate(pod_name, formatter, opts)
         
     | 
| 
      
 51 
     | 
    
         
            +
                  raise_if_blank pod_name, 'Pod name not set.'
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                  raise ArgumentError, 'Formatter not set.' if formatter.nil?
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                  raise ArgumentError, 'Opts not set.' if opts.nil?
         
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                def print_logs(logs)
         
     | 
| 
      
 59 
     | 
    
         
            +
                  if logs.to_s.include?("\n")
         
     | 
| 
      
 60 
     | 
    
         
            +
                    logs.to_s.split("\n").each { |log| print_logs(log) }
         
     | 
| 
      
 61 
     | 
    
         
            +
                    return
         
     | 
| 
      
 62 
     | 
    
         
            +
                  end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                  if @opts.raw?
         
     | 
| 
      
 65 
     | 
    
         
            +
                    puts @formatter.format(logs)
         
     | 
| 
      
 66 
     | 
    
         
            +
                  else
         
     | 
| 
      
 67 
     | 
    
         
            +
                    puts "#{@pod_name} - #{@formatter.format logs}"
         
     | 
| 
      
 68 
     | 
    
         
            +
                  end
         
     | 
| 
      
 69 
     | 
    
         
            +
                  $stdout.flush
         
     | 
| 
      
 70 
     | 
    
         
            +
                end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                def read_pod_logs
         
     | 
| 
      
 73 
     | 
    
         
            +
                  # The pod may still not up/ready, so small hack to retry 120 times (number
         
     | 
| 
      
 74 
     | 
    
         
            +
                  # taken randomly) until the pod returns its logs.
         
     | 
| 
      
 75 
     | 
    
         
            +
                  120.times do
         
     | 
| 
      
 76 
     | 
    
         
            +
                    return k8s_client.get_pod_log(@pod_name, @opts.namespace, tail_lines: @opts.last_nb_lines)
         
     | 
| 
      
 77 
     | 
    
         
            +
                  rescue Kubeclient::HttpError => e
         
     | 
| 
      
 78 
     | 
    
         
            +
                    puts e.message
         
     | 
| 
      
 79 
     | 
    
         
            +
                    sleep 1
         
     | 
| 
      
 80 
     | 
    
         
            +
                  end
         
     | 
| 
      
 81 
     | 
    
         
            +
                end
         
     | 
| 
      
 82 
     | 
    
         
            +
              end
         
     | 
| 
      
 83 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,86 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative 'k8s_opts'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative 'k8s_pod_reader'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative 'with_k8s_client'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Kubetailrb
         
     | 
| 
      
 8 
     | 
    
         
            +
              # Read multiple pod logs.
         
     | 
| 
      
 9 
     | 
    
         
            +
              class K8sPodsReader
         
     | 
| 
      
 10 
     | 
    
         
            +
                include Validated
         
     | 
| 
      
 11 
     | 
    
         
            +
                include WithK8sClient
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                attr_reader :pod_query, :opts
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                def initialize(pod_query:, formatter:, opts:, k8s_client: nil)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  validate(pod_query, formatter, opts)
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  @k8s_client = k8s_client
         
     | 
| 
      
 19 
     | 
    
         
            +
                  @pod_query = Regexp.new(pod_query)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @formatter = formatter
         
     | 
| 
      
 21 
     | 
    
         
            +
                  @opts = opts
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                def read
         
     | 
| 
      
 25 
     | 
    
         
            +
                  pods = find_pods
         
     | 
| 
      
 26 
     | 
    
         
            +
                  watch_for_new_pod_events if @opts.follow?
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  threads = pods.map do |pod|
         
     | 
| 
      
 29 
     | 
    
         
            +
                    # NOTE: How much memory does a Ruby Thread takes? Can we spawn hundreds
         
     | 
| 
      
 30 
     | 
    
         
            +
                    # to thoudsands of Threads without issue?
         
     | 
| 
      
 31 
     | 
    
         
            +
                    Thread.new { create_reader(pod.metadata.name).read }
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  # NOTE: '&:' is a shorthand way of calling 'join' method on each thread.
         
     | 
| 
      
 35 
     | 
    
         
            +
                  # It's equivalent to: threads.each { |thread| thread.join }
         
     | 
| 
      
 36 
     | 
    
         
            +
                  threads.each(&:join)
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                private
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                def validate(pod_query, formatter, opts)
         
     | 
| 
      
 42 
     | 
    
         
            +
                  raise_if_blank pod_query, 'Pod query not set.'
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  raise ArgumentError, 'Formatter not set.' if formatter.nil?
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  raise ArgumentError, 'Opts not set.' if opts.nil?
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                def find_pods
         
     | 
| 
      
 50 
     | 
    
         
            +
                  k8s_client
         
     | 
| 
      
 51 
     | 
    
         
            +
                    .get_pods(namespace: @opts.namespace)
         
     | 
| 
      
 52 
     | 
    
         
            +
                    .select { |pod| applicable?(pod) }
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                def create_reader(pod_name)
         
     | 
| 
      
 56 
     | 
    
         
            +
                  K8sPodReader.new(
         
     | 
| 
      
 57 
     | 
    
         
            +
                    k8s_client: k8s_client,
         
     | 
| 
      
 58 
     | 
    
         
            +
                    pod_name: pod_name,
         
     | 
| 
      
 59 
     | 
    
         
            +
                    formatter: @formatter,
         
     | 
| 
      
 60 
     | 
    
         
            +
                    opts: @opts
         
     | 
| 
      
 61 
     | 
    
         
            +
                  )
         
     | 
| 
      
 62 
     | 
    
         
            +
                end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                #
         
     | 
| 
      
 65 
     | 
    
         
            +
                # Watch any pod events, and if there's another pod that validates the pod
         
     | 
| 
      
 66 
     | 
    
         
            +
                # query, then let's read the pod logs!
         
     | 
| 
      
 67 
     | 
    
         
            +
                #
         
     | 
| 
      
 68 
     | 
    
         
            +
                def watch_for_new_pod_events
         
     | 
| 
      
 69 
     | 
    
         
            +
                  k8s_client.watch_pods(namespace: @opts.namespace) do |notice|
         
     | 
| 
      
 70 
     | 
    
         
            +
                    if new_pod_event?(notice) && applicable?(notice.object)
         
     | 
| 
      
 71 
     | 
    
         
            +
                      # NOTE: We are in another thread (are we?), so no sense to use
         
     | 
| 
      
 72 
     | 
    
         
            +
                      # 'Thread.join' here.
         
     | 
| 
      
 73 
     | 
    
         
            +
                      Thread.new { create_reader(notice.object.metadata.name).read }
         
     | 
| 
      
 74 
     | 
    
         
            +
                    end
         
     | 
| 
      
 75 
     | 
    
         
            +
                  end
         
     | 
| 
      
 76 
     | 
    
         
            +
                end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                def applicable?(pod)
         
     | 
| 
      
 79 
     | 
    
         
            +
                  pod.metadata.name.match?(@pod_query)
         
     | 
| 
      
 80 
     | 
    
         
            +
                end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                def new_pod_event?(notice)
         
     | 
| 
      
 83 
     | 
    
         
            +
                  notice.type == 'ADDED' && notice.object.kind == 'Pod'
         
     | 
| 
      
 84 
     | 
    
         
            +
                end
         
     | 
| 
      
 85 
     | 
    
         
            +
              end
         
     | 
| 
      
 86 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,28 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative 'cmd/file'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative 'cmd/help'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative 'cmd/k8s'
         
     | 
| 
      
 6 
     | 
    
         
            +
            require_relative 'cmd/version'
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            module Kubetailrb
         
     | 
| 
      
 9 
     | 
    
         
            +
              # Parse CLI arguments and flags.
         
     | 
| 
      
 10 
     | 
    
         
            +
              # NOTE: We could use the standard library optparse (OptionParser) or a more
         
     | 
| 
      
 11 
     | 
    
         
            +
              # comprehensive tool like Thor to achieve this, but that would defeat the
         
     | 
| 
      
 12 
     | 
    
         
            +
              # purpose of learning by implementing it ourselves.
         
     | 
| 
      
 13 
     | 
    
         
            +
              class OptsParser
         
     | 
| 
      
 14 
     | 
    
         
            +
                def initialize(*args)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @args = *args
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                def parse
         
     | 
| 
      
 19 
     | 
    
         
            +
                  return Cmd::Help.new if Cmd::Help.applicable?(*@args)
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  return Cmd::Version.new if Cmd::Version.applicable?(*@args)
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  return Cmd::File.create(*@args) if Cmd::File.applicable?(*@args)
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  Cmd::K8s.create(*@args)
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
              end
         
     | 
| 
      
 28 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,19 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Kubetailrb
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Add behaviors to validate the invariants.
         
     | 
| 
      
 5 
     | 
    
         
            +
              module Validated
         
     | 
| 
      
 6 
     | 
    
         
            +
                def raise_if_blank(arg, error_message)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  raise ArgumentError, error_message if arg.nil? || arg.strip&.empty?
         
     | 
| 
      
 8 
     | 
    
         
            +
                end
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                def validate_last_nb_lines(last_nb_lines)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  last_nb_lines_valid = last_nb_lines.is_a?(Integer) && last_nb_lines.positive?
         
     | 
| 
      
 12 
     | 
    
         
            +
                  raise ArgumentError, "Invalid last_nb_lines: #{last_nb_lines}." unless last_nb_lines_valid
         
     | 
| 
      
 13 
     | 
    
         
            +
                end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                def validate_boolean(follow, error_message)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  raise ArgumentError, error_message unless follow.is_a?(Boolean)
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,25 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'kubeclient'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Kubetailrb
         
     | 
| 
      
 6 
     | 
    
         
            +
              # Add behavior to get a k8s client by using composition.
         
     | 
| 
      
 7 
     | 
    
         
            +
              # NOTE: Is it the idiomatic way? Or shall I use a factory? Or is there a
         
     | 
| 
      
 8 
     | 
    
         
            +
              # better way?
         
     | 
| 
      
 9 
     | 
    
         
            +
              module WithK8sClient
         
     | 
| 
      
 10 
     | 
    
         
            +
                def k8s_client
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @k8s_client ||= create_k8s_client
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                def create_k8s_client
         
     | 
| 
      
 15 
     | 
    
         
            +
                  config = Kubeclient::Config.read(ENV['KUBECONFIG'] || "#{ENV["HOME"]}/.kube/config")
         
     | 
| 
      
 16 
     | 
    
         
            +
                  context = config.context
         
     | 
| 
      
 17 
     | 
    
         
            +
                  Kubeclient::Client.new(
         
     | 
| 
      
 18 
     | 
    
         
            +
                    context.api_endpoint,
         
     | 
| 
      
 19 
     | 
    
         
            +
                    'v1',
         
     | 
| 
      
 20 
     | 
    
         
            +
                    ssl_options: context.ssl_options,
         
     | 
| 
      
 21 
     | 
    
         
            +
                    auth_options: context.auth_options
         
     | 
| 
      
 22 
     | 
    
         
            +
                  )
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/kubetailrb.rb
    ADDED
    
    
    
        data/sig/kubetailrb.rbs
    ADDED