kuber_kit 0.1.6 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -2
  3. data/README.md +16 -3
  4. data/TODO.md +4 -3
  5. data/example/configurations/review.rb +2 -1
  6. data/example/images/app_sources/Dockerfile +1 -1
  7. data/example/images/ruby_app/image.rb +4 -4
  8. data/example/infrastructure/build_servers.rb +8 -0
  9. data/example/services/env_file.rb +5 -1
  10. data/example/services/ruby_app.rb +2 -1
  11. data/kuber_kit.gemspec +1 -0
  12. data/lib/kuber_kit.rb +37 -19
  13. data/lib/kuber_kit/actions/configuration_loader.rb +11 -2
  14. data/lib/kuber_kit/actions/env_file_reader.rb +5 -0
  15. data/lib/kuber_kit/actions/image_compiler.rb +16 -10
  16. data/lib/kuber_kit/actions/kubectl_applier.rb +9 -3
  17. data/lib/kuber_kit/actions/kubectl_attacher.rb +26 -0
  18. data/lib/kuber_kit/actions/service_deployer.rb +38 -0
  19. data/lib/kuber_kit/actions/service_reader.rb +6 -0
  20. data/lib/kuber_kit/actions/template_reader.rb +5 -0
  21. data/lib/kuber_kit/cli.rb +54 -20
  22. data/lib/kuber_kit/configs.rb +24 -22
  23. data/lib/kuber_kit/container.rb +21 -13
  24. data/lib/kuber_kit/core/artifacts/artifact_store.rb +12 -23
  25. data/lib/kuber_kit/core/build_servers/abstract_build_server.rb +21 -0
  26. data/lib/kuber_kit/core/build_servers/build_server.rb +24 -0
  27. data/lib/kuber_kit/core/build_servers/build_server_store.rb +18 -0
  28. data/lib/kuber_kit/core/configuration.rb +10 -4
  29. data/lib/kuber_kit/core/configuration_definition.rb +18 -1
  30. data/lib/kuber_kit/core/configuration_factory.rb +11 -1
  31. data/lib/kuber_kit/core/configuration_store.rb +14 -24
  32. data/lib/kuber_kit/core/context_helper/service_helper.rb +2 -2
  33. data/lib/kuber_kit/core/env_files/env_file_store.rb +8 -23
  34. data/lib/kuber_kit/core/image_store.rb +8 -18
  35. data/lib/kuber_kit/core/registries/registry_store.rb +8 -23
  36. data/lib/kuber_kit/core/service.rb +6 -2
  37. data/lib/kuber_kit/core/service_store.rb +13 -23
  38. data/lib/kuber_kit/core/store.rb +48 -0
  39. data/lib/kuber_kit/core/templates/template_store.rb +12 -23
  40. data/lib/kuber_kit/image_compiler/action_handler.rb +4 -3
  41. data/lib/kuber_kit/image_compiler/build_server_pool.rb +30 -0
  42. data/lib/kuber_kit/image_compiler/build_server_pool_factory.rb +13 -0
  43. data/lib/kuber_kit/image_compiler/image_build_dir_creator.rb +13 -7
  44. data/lib/kuber_kit/image_compiler/image_dependency_resolver.rb +25 -5
  45. data/lib/kuber_kit/preprocessing/file_preprocessor.rb +5 -4
  46. data/lib/kuber_kit/service_deployer/action_handler.rb +1 -1
  47. data/lib/kuber_kit/service_deployer/strategies/kubernetes.rb +10 -3
  48. data/lib/kuber_kit/shell/abstract_shell.rb +4 -0
  49. data/lib/kuber_kit/shell/{bash_commands.rb → commands/bash_commands.rb} +1 -1
  50. data/lib/kuber_kit/shell/{docker_commands.rb → commands/docker_commands.rb} +1 -1
  51. data/lib/kuber_kit/shell/{git_commands.rb → commands/git_commands.rb} +1 -1
  52. data/lib/kuber_kit/shell/commands/kubectl_commands.rb +65 -0
  53. data/lib/kuber_kit/shell/commands/rsync_commands.rb +32 -0
  54. data/lib/kuber_kit/shell/local_shell.rb +24 -5
  55. data/lib/kuber_kit/shell/ssh_session.rb +60 -0
  56. data/lib/kuber_kit/shell/ssh_shell.rb +77 -0
  57. data/lib/kuber_kit/tools/file_presence_checker.rb +6 -2
  58. data/lib/kuber_kit/ui/interactive.rb +8 -0
  59. data/lib/kuber_kit/ui/simple.rb +6 -0
  60. data/lib/kuber_kit/version.rb +2 -2
  61. metadata +34 -12
  62. data/lib/kuber_kit/preprocessing/dir_preprocessor.rb +0 -19
  63. data/lib/kuber_kit/shell/kubectl_commands.rb +0 -42
  64. data/lib/kuber_kit/shell/rsync_commands.rb +0 -20
  65. data/lib/kuber_kit/tools/files_sync.rb +0 -10
@@ -0,0 +1,30 @@
1
+ class KuberKit::ImageCompiler::BuildServerPool
2
+ attr_reader :ssh_shells, :local_shell
3
+
4
+ def initialize(local_shell:, build_servers:, ssh_shell_class:)
5
+ @local_shell = local_shell
6
+ @ssh_shell_class = ssh_shell_class
7
+ @ssh_shells = connect_to_ssh_shells(build_servers)
8
+ end
9
+
10
+ def get_shell
11
+ if @ssh_shells.any?
12
+ @ssh_shells.sample
13
+ else
14
+ @local_shell
15
+ end
16
+ end
17
+
18
+ def disconnect_all
19
+ @ssh_shells.each(&:disconnect)
20
+ end
21
+
22
+ private
23
+ def connect_to_ssh_shells(build_servers)
24
+ build_servers.map do |bs|
25
+ shell = @ssh_shell_class.new
26
+ shell.connect(host: bs.host, user: bs.user, port: bs.port)
27
+ shell
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ class KuberKit::ImageCompiler::BuildServerPoolFactory
2
+ include KuberKit::Import[
3
+ "shell.local_shell",
4
+ ]
5
+
6
+ def create(ssh_shell_class: KuberKit::Shell::SshShell)
7
+ KuberKit::ImageCompiler::BuildServerPool.new(
8
+ local_shell: local_shell,
9
+ build_servers: KuberKit.current_configuration.build_servers,
10
+ ssh_shell_class: ssh_shell_class,
11
+ )
12
+ end
13
+ end
@@ -1,8 +1,8 @@
1
1
  class KuberKit::ImageCompiler::ImageBuildDirCreator
2
2
  include KuberKit::Import[
3
- "preprocessing.dir_preprocessor",
4
3
  "preprocessing.file_preprocessor",
5
4
  "shell.bash_commands",
5
+ "shell.local_shell",
6
6
  "configs"
7
7
  ]
8
8
 
@@ -14,16 +14,22 @@ class KuberKit::ImageCompiler::ImageBuildDirCreator
14
14
  bash_commands.mkdir_p(shell, build_dir)
15
15
 
16
16
  if image.build_context_dir
17
- dir_preprocessor.compile(
18
- shell, image.build_context_dir, build_dir,
19
- context_helper: context_helper
20
- )
17
+ # Sync build context and then preprocess
18
+ shell.sync(image.build_context_dir, build_dir)
19
+
20
+ shell.recursive_list_files(build_dir).each do |file_path|
21
+ file_preprocessor.compile(
22
+ shell, file_path,
23
+ context_helper: context_helper
24
+ )
25
+ end
21
26
  end
22
27
 
28
+ # Sync dockerfile and then preprocess
23
29
  target_dockerfile = File.join(build_dir, configs.image_dockerfile_name)
30
+ shell.sync(image.dockerfile_path, target_dockerfile)
24
31
  file_preprocessor.compile(
25
- shell, image.dockerfile_path,
26
- destination_path: target_dockerfile,
32
+ shell, target_dockerfile,
27
33
  context_helper: context_helper
28
34
  )
29
35
 
@@ -3,20 +3,40 @@ class KuberKit::ImageCompiler::ImageDependencyResolver
3
3
  DependencyNotFoundError = Class.new(KuberKit::NotFoundError)
4
4
 
5
5
  include KuberKit::Import[
6
- "core.image_store"
6
+ "core.image_store",
7
+ "configs"
7
8
  ]
9
+
10
+ Contract Or[Symbol, ArrayOf[Symbol]], Proc => Any
11
+ def each_with_deps(image_names, &block)
12
+ compile_limit = configs.compile_simultaneous_limit
13
+
14
+ resolved_dependencies = []
15
+ next_dependencies = get_next(image_names, limit: compile_limit)
16
+
17
+ while (next_dependencies - resolved_dependencies).any?
18
+ block.call(next_dependencies)
19
+ resolved_dependencies += next_dependencies
20
+ next_dependencies = get_next(image_names, resolved: resolved_dependencies, limit: compile_limit)
21
+ end
22
+
23
+ block.call(image_names - resolved_dependencies)
24
+ end
8
25
 
9
- Contract Any, KeywordArgs[
10
- resolved: Optional[ArrayOf[Symbol]]
26
+ Contract Or[Symbol, ArrayOf[Symbol]], KeywordArgs[
27
+ resolved: Optional[ArrayOf[Symbol]],
28
+ limit: Optional[Maybe[Num]]
11
29
  ] => Any
12
- def get_next(image_names, resolved: [])
30
+ def get_next(image_names, resolved: [], limit: nil)
13
31
  deps = Array(image_names).map { |i| get_recursive_deps(i) }.flatten.uniq
14
32
 
15
33
  ready_to_resolve = deps.select do |dep_name|
16
34
  unresolved_deps = get_deps(dep_name) - resolved
17
35
  unresolved_deps.empty?
18
36
  end
19
- ready_to_resolve - resolved
37
+ unresolved_deps = ready_to_resolve - resolved
38
+ unresolved_deps = unresolved_deps.take(limit) if limit
39
+ unresolved_deps
20
40
  end
21
41
 
22
42
  def get_recursive_deps(image_name, dependency_tree: [])
@@ -1,6 +1,7 @@
1
1
  class KuberKit::Preprocessing::FilePreprocessor
2
2
  include KuberKit::Import[
3
- "preprocessing.text_preprocessor"
3
+ "preprocessing.text_preprocessor",
4
+ "shell.bash_commands"
4
5
  ]
5
6
 
6
7
  PreprocessingError = Class.new(KuberKit::Error)
@@ -8,7 +9,7 @@ class KuberKit::Preprocessing::FilePreprocessor
8
9
  def compile(shell, source_path, destination_path: nil, context_helper: nil)
9
10
  destination_path ||= source_path
10
11
 
11
- prepare_destination_dir(destination_path)
12
+ prepare_destination_dir(shell, destination_path)
12
13
 
13
14
  template = shell.read(source_path)
14
15
  content = text_preprocessor.compile(template, context_helper: context_helper)
@@ -27,7 +28,7 @@ class KuberKit::Preprocessing::FilePreprocessor
27
28
  raise PreprocessingError, "Error while processing template #{source_path}.\r\n#{message}"
28
29
  end
29
30
 
30
- def prepare_destination_dir(destination_path)
31
- FileUtils.mkdir_p(File.dirname(destination_path))
31
+ def prepare_destination_dir(shell, destination_path)
32
+ bash_commands.mkdir_p(shell, File.dirname(destination_path))
32
33
  end
33
34
  end
@@ -11,6 +11,6 @@ class KuberKit::ServiceDeployer::ActionHandler
11
11
 
12
12
  strategy_name = strategy_detector.call(service)
13
13
 
14
- deployer.restart(shell, service, strategy_name)
14
+ deployer.deploy(shell, service, strategy_name)
15
15
  end
16
16
  end
@@ -11,8 +11,15 @@ class KuberKit::ServiceDeployer::Strategies::Kubernetes < KuberKit::ServiceDeplo
11
11
  config_path = "#{configs.service_config_dir}/#{service.name}.yml"
12
12
  shell.write(config_path, service_config)
13
13
 
14
- kubeconfig_path = KuberKit.current_configuration.kubeconfig_path
15
- kubectl_commands.apply_file(shell, config_path, kubeconfig_path: kubeconfig_path)
16
- kubectl_commands.rolling_restart(shell, service.uri, kubeconfig_path: kubeconfig_path)
14
+ kubeconfig_path = KuberKit.current_configuration.kubeconfig_path
15
+ deploy_namespace = KuberKit.current_configuration.deploy_namespace
16
+
17
+ kubectl_commands.apply_file(shell, config_path, kubeconfig_path: kubeconfig_path, namespace: deploy_namespace)
18
+
19
+ deployment_restart_enabled = service.attribute(:deployment_restart_enabled, default: true)
20
+ deployment_restart_name = service.attribute(:deployment_restart_name, default: service.uri)
21
+ if deployment_restart_enabled
22
+ kubectl_commands.rolling_restart(shell, deployment_restart_name, kubeconfig_path: kubeconfig_path, namespace: deploy_namespace)
23
+ end
17
24
  end
18
25
  end
@@ -17,4 +17,8 @@ class KuberKit::Shell::AbstractShell
17
17
  def recursive_list_files(path, name: nil)
18
18
  raise KuberKit::NotImplementedError, "must be implemented"
19
19
  end
20
+
21
+ def sync(local_path, remote_path, exclude: nil)
22
+ raise KuberKit::NotImplementedError, "must be implemented"
23
+ end
20
24
  end
@@ -1,4 +1,4 @@
1
- class KuberKit::Shell::BashCommands
1
+ class KuberKit::Shell::Commands::BashCommands
2
2
  def rm(shell, path)
3
3
  shell.exec!(%Q{rm "#{path}"})
4
4
  end
@@ -1,4 +1,4 @@
1
- class KuberKit::Shell::DockerCommands
1
+ class KuberKit::Shell::Commands::DockerCommands
2
2
  def build(shell, build_dir, args = [])
3
3
  default_args = ["--rm=true"]
4
4
  args_list = (default_args + args).join(" ")
@@ -1,4 +1,4 @@
1
- class KuberKit::Shell::GitCommands
1
+ class KuberKit::Shell::Commands::GitCommands
2
2
  def get_remote_url(shell, git_repo_path, remote_name: "origin")
3
3
  shell.exec! [
4
4
  "cd #{git_repo_path}",
@@ -0,0 +1,65 @@
1
+ require 'json'
2
+ require 'shellwords'
3
+
4
+ class KuberKit::Shell::Commands::KubectlCommands
5
+ def kubectl_run(shell, command_list, kubeconfig_path: nil, namespace: nil, interactive: false)
6
+ command_parts = []
7
+ if kubeconfig_path
8
+ command_parts << "KUBECONFIG=#{kubeconfig_path}"
9
+ end
10
+
11
+ command_parts << "kubectl"
12
+
13
+ if namespace
14
+ command_parts << "-n #{namespace}"
15
+ end
16
+
17
+ command_parts += Array(command_list)
18
+
19
+ # TODO: investigate how to do it with shell.
20
+ if interactive
21
+ system(command_parts.join(" "))
22
+ else
23
+ shell.exec!(command_parts.join(" "))
24
+ end
25
+ end
26
+
27
+ def apply_file(shell, file_path, kubeconfig_path: nil, namespace: nil)
28
+ kubectl_run(shell, "apply -f #{file_path}", kubeconfig_path: kubeconfig_path, namespace: namespace)
29
+ end
30
+
31
+ def exec(shell, pod_name, command, args: nil, kubeconfig_path: nil, interactive: false, namespace: nil)
32
+ command_parts = []
33
+ command_parts << "exec"
34
+
35
+ if args
36
+ command_parts << args
37
+ end
38
+
39
+ command_parts << pod_name
40
+ command_parts << "-- #{command}"
41
+ kubectl_run(shell, command_parts, kubeconfig_path: kubeconfig_path, interactive: interactive, namespace: namespace)
42
+ end
43
+
44
+ def rolling_restart(shell, deployment_name, kubeconfig_path: nil, namespace: namespace)
45
+ patch_deployment(shell, deployment_name, {
46
+ spec: {
47
+ template: {
48
+ metadata: {
49
+ labels: {
50
+ redeploy: "$(date +%s)"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }, kubeconfig_path: kubeconfig_path, namespace: namespace)
56
+ end
57
+
58
+ def patch_deployment(shell, deployment_name, specs, kubeconfig_path: nil, namespace: nil)
59
+ specs_json = JSON.dump(specs).gsub('"', '\"')
60
+
61
+ command = %Q{patch deployment #{deployment_name} -p "#{specs_json}"}
62
+
63
+ kubectl_run(shell, command, kubeconfig_path: kubeconfig_path, namespace: namespace)
64
+ end
65
+ end
@@ -0,0 +1,32 @@
1
+ class KuberKit::Shell::Commands::RsyncCommands
2
+ def rsync(shell, source_path, target_path, target_host: nil, exclude: nil, delete: true)
3
+ # Add a trailing slash to directory to have behavior similar to CP command
4
+ if path_is_directory?(source_path) && !source_path.end_with?("/")
5
+ source_path = "#{source_path}/"
6
+ end
7
+
8
+ if target_host
9
+ destination = "#{target_host}:#{target_path}"
10
+ else
11
+ destination = target_path
12
+ end
13
+
14
+ args = [source_path, destination]
15
+ if exclude
16
+ Array(exclude).each do |e|
17
+ args << "--exclude=#{e}"
18
+ end
19
+ end
20
+
21
+ if delete
22
+ args << "--delete"
23
+ end
24
+
25
+ shell.exec!(%Q{rsync -a #{args.join(' ')}})
26
+ end
27
+
28
+ private
29
+ def path_is_directory?(path)
30
+ File.directory?(path)
31
+ end
32
+ end
@@ -3,21 +3,24 @@ require 'fileutils'
3
3
  class KuberKit::Shell::LocalShell < KuberKit::Shell::AbstractShell
4
4
  include KuberKit::Import[
5
5
  "tools.logger",
6
- "shell.command_counter"
6
+ "shell.command_counter",
7
+ "shell.rsync_commands",
7
8
  ]
8
9
 
9
- def exec!(command)
10
+ def exec!(command, log_command: true)
10
11
  command_number = command_counter.get_number.to_s.rjust(2, "0")
11
12
 
12
- logger.info("Executed command [#{command_number}]: #{command.to_s.cyan}")
13
+ if log_command
14
+ logger.info("Execute: [#{command_number}]: #{command.to_s.cyan}")
15
+ end
13
16
 
14
17
  result = nil
15
18
  IO.popen(command, err: [:child, :out]) do |io|
16
19
  result = io.read.chomp.strip
17
20
  end
18
21
 
19
- if result && result != ""
20
- logger.info("Finished command [#{command_number}] with result: \n#{result.grey}")
22
+ if result && result != "" && log_command
23
+ logger.info("Finished [#{command_number}] with result: \n#{result.grey}")
21
24
  end
22
25
 
23
26
  if $?.exitstatus != 0
@@ -27,6 +30,10 @@ class KuberKit::Shell::LocalShell < KuberKit::Shell::AbstractShell
27
30
  result
28
31
  end
29
32
 
33
+ def sync(local_path, remote_path, exclude: nil)
34
+ rsync_commands.rsync(self, local_path, remote_path, exclude: exclude)
35
+ end
36
+
30
37
  def read(file_path)
31
38
  File.read(file_path)
32
39
  end
@@ -41,6 +48,18 @@ class KuberKit::Shell::LocalShell < KuberKit::Shell::AbstractShell
41
48
  true
42
49
  end
43
50
 
51
+ def delete(file_path)
52
+ exec!("rm #{file_path}")
53
+ end
54
+
55
+ def file_exists?(file_path)
56
+ exec!("test -f #{file_path} && echo 'true' || echo 'false'", log_command: false) == 'true'
57
+ end
58
+
59
+ def dir_exists?(dir_path)
60
+ exec!("test -d #{dir_path} && echo 'true' || echo 'false'", log_command: false) == 'true'
61
+ end
62
+
44
63
  def recursive_list_files(path, name: nil)
45
64
  command = %Q{find -L #{path} -type f}
46
65
  command += " -name #{name}" if name
@@ -0,0 +1,60 @@
1
+ require 'net/ssh'
2
+
3
+ class KuberKit::Shell::SshSession
4
+ SshSessionError = Class.new(KuberKit::Error)
5
+
6
+ attr_reader :session, :host, :user, :port
7
+
8
+ def initialize(host:, user:, port:)
9
+ @host = host
10
+ @user = user
11
+ @port = port
12
+ @session = Net::SSH.start(host, user, {port: port})
13
+ end
14
+
15
+ def connected?
16
+ !!@session
17
+ end
18
+
19
+ def disconnect
20
+ return unless connected?
21
+ @session.close
22
+ @session = nil
23
+ end
24
+
25
+ def exec!(command)
26
+ stdout_data = ''
27
+ stderr_data = ''
28
+ exit_code = nil
29
+ channel = session.open_channel do |ch|
30
+ ch.exec(command) do |ch, success|
31
+ if !success
32
+ raise SshSessionError, "Shell command failed: #{command}\r\n#{stdout_data}\r\n#{stderr_data}"
33
+ end
34
+
35
+ channel.on_data do |ch,data|
36
+ stdout_data += data
37
+ end
38
+
39
+ channel.on_extended_data do |ch,type,data|
40
+ stderr_data += data
41
+ end
42
+
43
+ channel.on_request('exit-status') do |ch,data|
44
+ exit_code = data.read_long
45
+ end
46
+ end
47
+ end
48
+
49
+ channel.wait
50
+ session.loop
51
+
52
+ stdout_data = stdout_data.chomp.strip
53
+
54
+ if exit_code != 0
55
+ raise SshSessionError, "Shell command failed: #{command}\r\n#{stdout_data}\r\n#{stderr_data}"
56
+ end
57
+
58
+ stdout_data
59
+ end
60
+ end
@@ -0,0 +1,77 @@
1
+ require 'tempfile'
2
+
3
+ class KuberKit::Shell::SshShell < KuberKit::Shell::LocalShell
4
+ include KuberKit::Import[
5
+ "tools.logger",
6
+ "shell.command_counter",
7
+ "shell.rsync_commands",
8
+ "shell.local_shell"
9
+ ]
10
+
11
+ def connect(host:, user:, port:)
12
+ @ssh_session = KuberKit::Shell::SshSession.new(host: host, user: user, port: port)
13
+ end
14
+
15
+ def connected?
16
+ @ssh_session && @ssh_session.connected?
17
+ end
18
+
19
+ def disconnect
20
+ @ssh_session.disconnect if @ssh_session
21
+ end
22
+
23
+ def exec!(command, log_command: true)
24
+ command_number = command_counter.get_number.to_s.rjust(2, "0")
25
+
26
+ if log_command
27
+ logger.info("#{ssh_session.host.green} > Execute: [#{command_number}]: #{command.to_s.cyan}")
28
+ end
29
+
30
+ result = ssh_session.exec!(command)
31
+
32
+ if result && result != "" && log_command
33
+ logger.info("#{ssh_session.host.green} > Finished [#{command_number}] with result: \n#{result.grey}")
34
+ end
35
+
36
+ result
37
+ rescue KuberKit::Shell::SshSession::SshSessionError => e
38
+ raise ShellError.new(e.message)
39
+ end
40
+
41
+ def sync(local_path, remote_path, exclude: nil)
42
+ rsync_commands.rsync(
43
+ local_shell, local_path, remote_path,
44
+ target_host: "#{ssh_session.user}@#{ssh_session.host}",
45
+ exclude: exclude
46
+ )
47
+ end
48
+
49
+ def read(file_path)
50
+ exec!("cat #{file_path}")
51
+ end
52
+
53
+ def write(file_path, content)
54
+ Tempfile.create do |file|
55
+ file << content
56
+ file.flush
57
+ sync(file.path, file_path)
58
+ end
59
+
60
+ logger.info("Created file #{file_path.to_s.cyan}\r\n#{content.grey}")
61
+
62
+ true
63
+ end
64
+
65
+ private
66
+ def ssh_session
67
+ unless connected?
68
+ raise ArgumentError, "ssh session is not created, please call #connect"
69
+ end
70
+
71
+ @ssh_session
72
+ end
73
+
74
+ def ensure_directory_exists(file_path)
75
+ exec!("mkdir -p #{file_path}")
76
+ end
77
+ end