kubeclient_exec 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e26fa27357945aaf399dd95d64016c5e210c050812c25c5da4243bcbb6e2917c
4
+ data.tar.gz: c98b695f374b80b688206096fe50614d3edd0f2d7f09fa1abe3a0f043015d76e
5
+ SHA512:
6
+ metadata.gz: 86b3b7d38cfbbdeb90479185da1d958f517774263a6f5638008bb2cceae7dc0e412a629883bea7080ccf306d4df703ae643ef0685c06cd98729c5f1f4b8bc42b
7
+ data.tar.gz: 4f8adcd36dc6dfa16390330eb5eec1d11bfcf17d7f702adb6fc3a11fd5d5ba165dffab2ce46401bf38b178c8c6a466160b4152098e196fdb2a0564fe821021fa
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'tar'
4
+
5
+ module KubeclientExec
6
+ module Copy
7
+ include Tar
8
+
9
+ DEFAULT_CP_OPTIONS = {
10
+ container: nil,
11
+ reverse_direction: false,
12
+ suppress_errors: true,
13
+ tls: {
14
+ cert_chain_file: nil,
15
+ private_key_file: nil,
16
+ verify_peer: true
17
+ }
18
+ }
19
+
20
+ def cp_pod(local_path, remote_path, name, namespace, options: {})
21
+ # Reverse merge with the default options
22
+ options.merge!(Copy::DEFAULT_CP_OPTIONS) { |_, option, _| option }
23
+
24
+ if options[:reverse_direction]
25
+ cp_from_pod(local_path, remote_path, name, namespace, options)
26
+ else
27
+ cp_to_pod(local_path, remote_path, name, namespace, options)
28
+ end
29
+ end
30
+
31
+ private
32
+ def cp_to_pod(local_path, remote_path, name, namespace, options)
33
+ copy_file = false
34
+
35
+ if File.file?(local_path)
36
+ copy_file = true
37
+ elsif !File.directory?(local_path)
38
+ raise 'Did not find local path!'
39
+ end
40
+
41
+ tar_file = tar(local_path, remote_path, copy_file)
42
+
43
+ if copy_file
44
+ exec_pod("tar xf - -C #{remote_path.split('/')[0...-1].join('/')}", name, namespace, options: { tty: false }.merge!(options)) do |executor|
45
+ executor.write(tar_file.string)
46
+
47
+ # Feels like there should be a better way for this
48
+ EM.add_periodic_timer(0.1) do
49
+ if executor.done?
50
+ executor.stop
51
+ end
52
+ end
53
+ end
54
+ else
55
+ exec_pod("tar xf - -C #{remote_path}", name, namespace, options: { tty: false }.merge!(options)) do |executor|
56
+ executor.write(tar_file.string)
57
+
58
+ # Feels like there should be a better way for this
59
+ EM.add_periodic_timer(0.1) do
60
+ if executor.done?
61
+ executor.stop
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def cp_from_pod(local_path, remote_path, name, namespace, options)
69
+ result = nil
70
+
71
+ exec_pod("tar cf - #{remote_path}", name, namespace, options: { tty: false }.merge!(options)) do |executor|
72
+ count = 0
73
+
74
+ executor.on_stdout do |data|
75
+ if count == 1
76
+ if local_path.is_a? String
77
+ untar(StringIO.new(data), local_path)
78
+ elsif local_path == :single_result
79
+ result = single_untar(StringIO.new(data))
80
+ end
81
+ executor.stop
82
+ end
83
+
84
+ count += 1
85
+ end
86
+ end
87
+
88
+ result
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'rubygems/package'
5
+
6
+ # Greatly inspired by Github.com/sinisterchipmunk
7
+ # Source: https://gist.github.com/sinisterchipmunk/1335041/5be4e6039d899c9b8cca41869dc6861c8eb71f13
8
+ #
9
+ # Copyright (C) 2011 by Colin MacKenzie IV
10
+ #
11
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ # of this software and associated documentation files (the "Software"), to deal
13
+ # in the Software without restriction, including without limitation the rights
14
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ # copies of the Software, and to permit persons to whom the Software is
16
+ # furnished to do so, subject to the following conditions:
17
+ #
18
+ # The above copyright notice and this permission notice shall be included in
19
+ # all copies or substantial portions of the Software.
20
+ #
21
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
+ # THE SOFTWARE.
28
+ module KubeclientExec
29
+ module Copy
30
+ module Tar
31
+ def tar(src, dst, copy_file)
32
+ tar_file = StringIO.new
33
+
34
+ Gem::Package::TarWriter.new(tar_file) do |tar|
35
+ if copy_file
36
+ relative_file = dst.split('/').last
37
+
38
+ tar.add_file relative_file, File.stat(src).mode do |tf|
39
+ File.open(src, "rb") { |f| tf.write f.read }
40
+ end
41
+ else
42
+ Dir[File.join(src, "**/*")].each do |file|
43
+ mode = File.stat(file).mode
44
+ relative_file = file.sub /^#{Regexp::escape src}\/?/, ''
45
+
46
+ if File.directory?(file)
47
+ tar.mkdir relative_file, mode
48
+ else
49
+ tar.add_file relative_file, mode do |tf|
50
+ File.open(file, "rb") { |f| tf.write f.read }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ tar_file.rewind
58
+ tar_file
59
+ end
60
+
61
+ def untar(io, destination)
62
+ Gem::Package::TarReader.new(io) do |tar|
63
+ tar.each do |tarfile|
64
+ destination_file = File.join(destination, tarfile.full_name)
65
+
66
+ if tarfile.directory?
67
+ FileUtils.mkdir_p(destination_file)
68
+ else
69
+ destination_directory = File.dirname(destination_file)
70
+
71
+ FileUtils.mkdir_p destination_directory unless File.directory?(destination_directory)
72
+
73
+ File.open(destination_file, "wb") do |f|
74
+ f.write(tarfile.read)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ def single_untar(io)
82
+ Gem::Package::TarReader.new(io) do |tar|
83
+ tar.each do |tarfile|
84
+ return tarfile.read
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'executor'
4
+
5
+ module KubeclientExec
6
+ module Execute
7
+ DEFAULT_EXEC_OPTIONS = {
8
+ container: nil,
9
+ stdin: true,
10
+ stdout: true,
11
+ stderror: true,
12
+ tty: true,
13
+ suppress_errors: true,
14
+ tls: {
15
+ cert_chain_file: nil,
16
+ private_key_file: nil,
17
+ verify_peer: true
18
+ }
19
+ }
20
+
21
+ def exec_pod(command, name, namespace, options: {}, &block)
22
+ ns_prefix = build_namespace_prefix(namespace)
23
+ client = rest_client["#{ns_prefix}pods/#{name}/exec"]
24
+ url = URI.parse(client.url)
25
+
26
+ # Reverse merge with the default options
27
+ options.merge!(Execute::DEFAULT_EXEC_OPTIONS) { |_, option, _| option }
28
+
29
+ kubeclient_options = { headers: @headers, tls: options[:tls] }
30
+
31
+ url.query = (options.filter { |k| ![:suppress_errors, :tls].include?(k) }.compact.map { |k, v| "#{k}=#{v}"} << command.split(' ').map { |c| "command=#{c}"}).join('&')
32
+
33
+ if url.to_s.start_with?('https')
34
+ url = "wss" + url.to_s[5..-1]
35
+ end
36
+
37
+ last_output = { last_stdout: nil, last_stderr: nil }
38
+
39
+ EM.run do
40
+ executor = if block_given?
41
+ Executor.new(command, url, kubeclient_options, options) do |executor|
42
+ block.call(executor)
43
+ end
44
+ else
45
+ Executor.new(command, url, kubeclient_options, options.merge!(mode: :adhoc))
46
+ end
47
+
48
+ EM.add_shutdown_hook do
49
+ last_output[:last_stdout] = executor.last_stdout
50
+ last_output[:last_stderr] = executor.last_stderr
51
+ end
52
+ end
53
+
54
+ last_output
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eventmachine'
4
+
5
+ module KubeclientExec
6
+ module Execute
7
+ class Executor
8
+ EXEC_STDIN = 0
9
+ EXEC_STDOUT = 1
10
+ EXEC_STDERR = 2
11
+ EXEC_DCKERR = 3 # Not sure about this one
12
+
13
+ attr_reader :last_stdout, :last_stderr, :last_command
14
+
15
+ def initialize(command, url, kubeclient_options, options, &block)
16
+ @last_command = command
17
+ @url = url
18
+ @kubeclient_options = kubeclient_options
19
+ @options = options
20
+ @on_open = block
21
+ @suppress_errors = options[:suppress_errors]
22
+
23
+ if options[:mode] == :adhoc
24
+ @on_stdout = ->(_) { stop }
25
+ @on_stderr = ->(_) { stop }
26
+ end
27
+
28
+ setup
29
+ end
30
+
31
+ def execute(command)
32
+ raise 'ws not initialized' unless @ws
33
+
34
+ @last_command = command
35
+
36
+ @ws.send((command + "\n").unpack("C*").unshift(EXEC_STDIN))
37
+ end
38
+
39
+ def write(content)
40
+ raise 'ws not initialized' unless @ws
41
+
42
+ @ws.send(content.unpack("C*").unshift(EXEC_STDIN))
43
+ end
44
+
45
+ def on_stdout(&block)
46
+ @on_stdout = block
47
+ end
48
+
49
+ def on_stderr(&block)
50
+ @on_stderr = block
51
+ end
52
+
53
+ def on_close(&block)
54
+ @on_close = block
55
+ end
56
+
57
+ def stop
58
+ @ws.close if @ws
59
+ @on_close.call if @on_close
60
+ EM.stop_event_loop
61
+ end
62
+
63
+ def done?
64
+ @ws.instance_variable_get(:@driver).instance_variable_get(:@queue).empty?
65
+ end
66
+
67
+ private
68
+ def setup
69
+ @ws = Faye::WebSocket::Client.new(@url, nil, {
70
+ ping: 10,
71
+ headers: @kubeclient_options[:headers],
72
+ tls: {
73
+ cert_chain_file: @kubeclient_options[:tls][:cert_chain_file],
74
+ private_key_file: @kubeclient_options[:tls][:private_key_file],
75
+ verify_peer: @kubeclient_options[:tls][:verify_peer],
76
+ }
77
+ })
78
+
79
+ @ws.on(:message) do |msg|
80
+ if msg.type == :close
81
+ stop
82
+ return
83
+ end
84
+
85
+ next if msg.data.empty?
86
+
87
+ type = msg.data.shift
88
+ content = msg.data.pack("C*").force_encoding('utf-8')
89
+
90
+ if content.empty?
91
+ if @options[:mode] == :adhoc
92
+ @last_stdout = 1 if type == EXEC_STDOUT
93
+ @last_stderr = 1 if type == EXEC_STDERR || EXEC_DCKERR
94
+ stop
95
+ end
96
+ end
97
+
98
+ case type
99
+ when EXEC_STDOUT
100
+ @last_stdout = content
101
+ @on_stdout.call(content) if @on_stdout
102
+ when EXEC_STDERR, EXEC_DCKERR
103
+ @last_stderr = content
104
+ @on_stderr.call(content) if @on_stderr
105
+ else
106
+ raise "Unsupported or Unknown channel"
107
+ end
108
+ end
109
+
110
+ @ws.on(:error) do |event|
111
+ raise "Error: #{event.inspect}" unless @suppress_errors
112
+ end
113
+
114
+ @ws.on(:close) do
115
+ stop
116
+ end
117
+
118
+ @ws.on(:open) do
119
+ @on_open.call(self) if @on_open
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kubeclient'
4
+ require 'faye/websocket'
5
+ require_relative 'kubeclient_exec/execute/execute'
6
+ require_relative 'kubeclient_exec/copy/copy'
7
+
8
+ module KubeclientExec
9
+ include KubeclientExec::Execute
10
+ include KubeclientExec::Copy
11
+ end
12
+
13
+ Kubeclient::Client.include(KubeclientExec)
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kubeclient_exec
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wout Ceulemans
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: kubeclient
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.11.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.11.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: faye-websocket
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.11.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.11.3
41
+ description:
42
+ email: me@wout.dev
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/kubeclient_exec.rb
48
+ - lib/kubeclient_exec/copy/copy.rb
49
+ - lib/kubeclient_exec/copy/tar.rb
50
+ - lib/kubeclient_exec/execute/execute.rb
51
+ - lib/kubeclient_exec/execute/executor.rb
52
+ homepage: https://github.com/WoutDev/kubeclient_exec
53
+ licenses:
54
+ - MIT
55
+ metadata: {}
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 3.5.3
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: An extension to the kubeclient gem that adds exec_pod and cp_pod functionality.
75
+ test_files: []