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