kube_deploy_tools 3.0.5

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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +177 -0
  3. data/README.md +142 -0
  4. data/bin/deploy +60 -0
  5. data/bin/generate +28 -0
  6. data/bin/kdt +17 -0
  7. data/bin/make_configmap +20 -0
  8. data/bin/publish +28 -0
  9. data/bin/push +57 -0
  10. data/bin/render_deploys_hook +18 -0
  11. data/bin/templater +34 -0
  12. data/bin/upgrade +23 -0
  13. data/lib/kube_deploy_tools.rb +17 -0
  14. data/lib/kube_deploy_tools/artifact_registry.rb +30 -0
  15. data/lib/kube_deploy_tools/artifact_registry/driver.rb +13 -0
  16. data/lib/kube_deploy_tools/artifact_registry/driver_artifactory.rb +155 -0
  17. data/lib/kube_deploy_tools/artifact_registry/driver_base.rb +37 -0
  18. data/lib/kube_deploy_tools/artifact_registry/driver_gcs.rb +120 -0
  19. data/lib/kube_deploy_tools/built_artifacts_file.rb +28 -0
  20. data/lib/kube_deploy_tools/concurrency.rb +18 -0
  21. data/lib/kube_deploy_tools/deferred_summary_logging.rb +69 -0
  22. data/lib/kube_deploy_tools/deploy.rb +215 -0
  23. data/lib/kube_deploy_tools/deploy/options.rb +114 -0
  24. data/lib/kube_deploy_tools/deploy_config_file.rb +286 -0
  25. data/lib/kube_deploy_tools/deploy_config_file/deep_merge.rb +38 -0
  26. data/lib/kube_deploy_tools/deploy_config_file/util.rb +39 -0
  27. data/lib/kube_deploy_tools/errors.rb +5 -0
  28. data/lib/kube_deploy_tools/file_filter.rb +43 -0
  29. data/lib/kube_deploy_tools/formatted_logger.rb +59 -0
  30. data/lib/kube_deploy_tools/generate.rb +145 -0
  31. data/lib/kube_deploy_tools/generate/options.rb +66 -0
  32. data/lib/kube_deploy_tools/image_registry.rb +30 -0
  33. data/lib/kube_deploy_tools/image_registry/driver.rb +18 -0
  34. data/lib/kube_deploy_tools/image_registry/driver/aws.rb +121 -0
  35. data/lib/kube_deploy_tools/image_registry/driver/base.rb +50 -0
  36. data/lib/kube_deploy_tools/image_registry/driver/gcp.rb +71 -0
  37. data/lib/kube_deploy_tools/image_registry/driver/login.rb +26 -0
  38. data/lib/kube_deploy_tools/image_registry/driver/noop.rb +15 -0
  39. data/lib/kube_deploy_tools/image_registry/image.rb +17 -0
  40. data/lib/kube_deploy_tools/kdt.rb +52 -0
  41. data/lib/kube_deploy_tools/kubectl.rb +25 -0
  42. data/lib/kube_deploy_tools/kubernetes_resource.rb +57 -0
  43. data/lib/kube_deploy_tools/kubernetes_resource/deployment.rb +56 -0
  44. data/lib/kube_deploy_tools/make_configmap.rb +51 -0
  45. data/lib/kube_deploy_tools/make_configmap/options.rb +39 -0
  46. data/lib/kube_deploy_tools/object.rb +11 -0
  47. data/lib/kube_deploy_tools/publish.rb +40 -0
  48. data/lib/kube_deploy_tools/publish/options.rb +34 -0
  49. data/lib/kube_deploy_tools/push.rb +129 -0
  50. data/lib/kube_deploy_tools/push/options.rb +46 -0
  51. data/lib/kube_deploy_tools/render_deploys_hook.rb +95 -0
  52. data/lib/kube_deploy_tools/shellrunner.rb +46 -0
  53. data/lib/kube_deploy_tools/tag.rb +33 -0
  54. data/lib/kube_deploy_tools/templater.rb +63 -0
  55. data/lib/kube_deploy_tools/templater/options.rb +74 -0
  56. data/lib/kube_deploy_tools/version.rb +3 -0
  57. metadata +191 -0
@@ -0,0 +1,121 @@
1
+ require 'json'
2
+
3
+ require_relative 'base'
4
+ require_relative '../image'
5
+ require 'kube_deploy_tools/deploy_config_file/util'
6
+ require 'kube_deploy_tools/shellrunner'
7
+
8
+ module KubeDeployTools
9
+ class ImageRegistry::Driver::Aws < ImageRegistry::Driver::Base
10
+ include DeployConfigFileUtil
11
+
12
+ def initialize(h)
13
+ super(h)
14
+
15
+ check_and_err(
16
+ @registry.config&.has_key?('region'),
17
+ "Expected .image_registries['#{@name}'] to have .config.region to be set "\
18
+ "for the AWS image registry driver, but .config.region is not set "\
19
+ "e.g. .config.region = 'us-west-2'"
20
+ )
21
+ end
22
+
23
+ def push_image(image)
24
+ create_repository(image.repository) unless repository_exists?(image.repository)
25
+ super(image)
26
+ end
27
+
28
+ def authorize_command
29
+ login_cmd = get_docker_login
30
+ raise "Unexpected login command: #{login_cmd}" if login_cmd.first(2) != ['docker', 'login']
31
+ login_cmd
32
+ end
33
+
34
+ def unauthorize
35
+ end
36
+
37
+ def delete_image(image, dryrun)
38
+ # In the AWS driver, the 'delete many' primitive is the primary one.
39
+ delete_images([image], dryrun)
40
+ end
41
+
42
+ def delete_images(images, dryrun)
43
+ # Aggregate images by repository and call aws ecr batch-delete-image
44
+ # once per repository.
45
+ ids_by_repository = {}
46
+ images.each do |image|
47
+ repository, tag = split_full_image_id(image)
48
+ item = {'imageTag': tag}
49
+ if ids_by_repository[repository].nil?
50
+ ids_by_repository[repository] = [item]
51
+ else
52
+ ids_by_repository[repository].push(item)
53
+ end
54
+ end
55
+
56
+ # JSON format documented here:
57
+ # https://docs.aws.amazon.com/cli/latest/reference/ecr/batch-delete-image.html
58
+ ids_by_repository.each do |repository, image_ids|
59
+ # batch-delete-image has a threshold of 100 image_ids at a time
60
+ image_chunks = image_ids.each_slice(100).to_a
61
+
62
+ image_chunks.each do |images|
63
+ cmd = [
64
+ 'aws', 'ecr', 'batch-delete-image',
65
+ '--repository-name', repository,
66
+ '--region', @registry.config.fetch('region'),
67
+ '--image-ids', images.to_json,
68
+ ]
69
+
70
+ if dryrun
71
+ Logger.info("Would run: #{cmd}")
72
+ else
73
+ Shellrunner.check_call(*cmd)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ private
80
+ def get_docker_login
81
+ args = Shellrunner.check_call('aws', 'ecr', 'get-login', '--region', @registry.config.fetch('region')).split
82
+
83
+ # Remove '-e' and subsequent argument
84
+ # This compensates for --no-include-email not being recognized in the Ubuntu packaged awscli
85
+ # and not usable unless you upgrade
86
+ i = args.index('-e')
87
+ if !i.nil?
88
+ # delete '-e'
89
+ args.delete_at(i)
90
+ # delete whatever value is after (usually 'none')
91
+ args.delete_at(i)
92
+ end
93
+
94
+ args
95
+ end
96
+
97
+ def repository_exists?(repository)
98
+ _, _, status = Shellrunner.run_call('aws', 'ecr', 'describe-repositories', '--repository-names', repository, '--region', @registry.config.fetch('region'))
99
+ status.success?
100
+ end
101
+
102
+ def create_repository(repository)
103
+ Shellrunner.check_call('aws', 'ecr', 'create-repository', '--repository-name', repository, '--region', @registry.config.fetch('region'))
104
+ end
105
+
106
+ def split_full_image_id(image_id)
107
+ # Create syntax suitable for aws ecr subcommand.
108
+ # Example: 12345678.dkr.ecr.amazonaws.com/my_app:deadbeef-123
109
+ # splits into ('my_app', 'deadbeef-123') after verifying that the
110
+ # prefix is the expected one for this driver instance.
111
+ repo_with_prefix, tag = image_id.split(':', 2)
112
+ prefix, repository = repo_with_prefix.split('/', 2)
113
+
114
+ # Sanity check, as the resultant command line uses the region to specify
115
+ # the prefix implicitly.
116
+ raise "This driver can't delete images from #{prefix}, only #{@registry.prefix}" unless prefix == @registry.prefix
117
+
118
+ return repository, tag
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,50 @@
1
+ require 'kube_deploy_tools/formatted_logger'
2
+ require 'kube_deploy_tools/shellrunner'
3
+
4
+ require 'kube_deploy_tools/image_registry/image'
5
+
6
+ # Abstract Driver class that specific implementations inherit
7
+ module KubeDeployTools
8
+ class ImageRegistry
9
+ module Driver
10
+ class Base
11
+ def initialize(registry:)
12
+ @registry = registry
13
+ end
14
+
15
+ def push_image(image)
16
+ Shellrunner.check_call('docker', 'push', image.full_tag)
17
+ end
18
+
19
+ def authorize
20
+ Logger.info "performing registry login for #{@registry.prefix}"
21
+ Shellrunner.check_call(*authorize_command, print_cmd: false)
22
+ end
23
+
24
+ def authorize_command
25
+ raise "#{self.class}#authorize_command needs explicit implementation"
26
+ end
27
+
28
+ def unauthorize
29
+ Logger.info "Performing registry unauthorization for #{@registry.prefix}"
30
+ Shellrunner.check_call(*unauthorize_command, print_cmd: false)
31
+ end
32
+
33
+ def unauthorize_command
34
+ raise "#{self.class}#unauthorize_command needs explicit implementation"
35
+ end
36
+
37
+ def delete_images(images, dryrun)
38
+ # Naive default implementation.
39
+ images.each do |image|
40
+ delete_image(image, dryrun)
41
+ end
42
+ end
43
+
44
+ def delete_image(image, dryrun)
45
+ raise "#{self.class}#delete_image needs explicit implementation"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,71 @@
1
+ require_relative 'base'
2
+ require 'tmpdir'
3
+
4
+ module KubeDeployTools
5
+ class ImageRegistry::Driver::Gcp < ImageRegistry::Driver::Base
6
+ def initialize(registry:)
7
+ super
8
+
9
+ @gcloud_config_dir = Dir.mktmpdir
10
+ @activated = false
11
+ end
12
+
13
+ def authorize
14
+ # Always prefer and activate a service account under a protected namespace if present.
15
+ if @activated
16
+ return
17
+ elsif ENV.member?('GOOGLE_APPLICATION_CREDENTIALS')
18
+ raise "Failed to activate service account" unless activate_service_account()[2].success?
19
+ @activated = true
20
+ else
21
+ user = current_user
22
+ if ! user.empty?
23
+ Logger.info "Skipping Google activation, using current user #{user}"
24
+ @activated = true
25
+ else
26
+ raise 'No usable Google authorization for pushing images; specify GOOGLE_APPLICATION_CREDENTIALS?'
27
+ end
28
+ end
29
+ end
30
+
31
+ # Delete temporary config dir for gcloud authentication
32
+ def unauthorize
33
+ Logger.info "Cleaning up authorization for #{@registry.prefix}"
34
+ FileUtils.rm_rf(@gcloud_config_dir) unless @gcloud_config_dir.nil?
35
+ end
36
+
37
+ def delete_image(image_id, dryrun)
38
+ # Need the id path to be [HOSTNAME]/[PROJECT-ID]/[IMAGE]
39
+ if dryrun
40
+ Logger.info("DRYRUN: delete gcp image #{image_id}")
41
+ else
42
+ # --quiet removes the user-input component
43
+ _, err, status = Shellrunner.run_call('gcloud', 'container', 'images', 'delete', '--quiet', image_id, '--force-delete-tags')
44
+ if !status.success?
45
+ # gcloud gives a deceptive error msg when the image does not exist
46
+ if err.include?('is not a valid name')
47
+ Logger.warn("Image #{image_id} does not exist, skipping")
48
+ else
49
+ raise "gcloud image deletion failed!"
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+ # activate gcloud with svc json keys on Jenkins
57
+ def activate_service_account
58
+ keypath = ENV.fetch('GOOGLE_APPLICATION_CREDENTIALS')
59
+ Logger.info("Authorizing using temp directory #{@gcloud_config_dir} and credentials #{keypath}")
60
+
61
+ ENV['XDG_CONFIG_HOME'] = @gcloud_config_dir
62
+ ENV['CLOUDSDK_CONFIG']= File.join(@gcloud_config_dir, 'gcloud')
63
+
64
+ Shellrunner.run_call('gcloud', 'auth', 'activate-service-account', '--key-file', keypath)
65
+ end
66
+
67
+ def current_user
68
+ Shellrunner.run_call('gcloud', 'config', 'list', 'account', '--format', "value(core.account)")[0]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,26 @@
1
+ require_relative 'base'
2
+ require_relative '../image'
3
+
4
+ module KubeDeployTools
5
+ class ImageRegistry::Driver::Login < ImageRegistry::Driver::Base
6
+ # This driver expects the following to be set in the @registry hash:
7
+ # username_var: set to a string which is the env var containing the docker
8
+ # registry username
9
+ # password_var: set to a string which is the env var containing the docker
10
+ # registry password
11
+ # prefix: passed directly to docker login
12
+ def authorize_command
13
+ ['docker', 'login',
14
+ '--username', ENV.fetch(@registry.config.fetch('username_var')),
15
+ '--password', ENV.fetch(@registry.config.fetch('password_var')),
16
+ @registry.prefix]
17
+ end
18
+
19
+ def delete_image(image, dryrun)
20
+ raise 'not implemented'
21
+ end
22
+
23
+ def unauthorize
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'base'
2
+
3
+ # Noop driver, does nothing!
4
+ module KubeDeployTools
5
+ class ImageRegistry::Driver::Noop < ImageRegistry::Driver::Base
6
+ def push_image(image)
7
+ end
8
+
9
+ def authorize
10
+ end
11
+
12
+ def unauthorize
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module KubeDeployTools
2
+ class Push
3
+ class Image
4
+ attr_accessor :registry, :repository, :tag
5
+ def initialize(registry, repository, tag)
6
+ registry += '/' unless registry.end_with?('/')
7
+ @registry = registry
8
+ @repository = repository
9
+ @tag = tag
10
+ end
11
+
12
+ def full_tag
13
+ "#{registry}#{repository}:#{tag}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+ require 'kube_deploy_tools/version'
2
+ require 'kube_deploy_tools/formatted_logger'
3
+
4
+ module KubeDeployTools
5
+ class Kdt
6
+ DESCRIPTIONS = {
7
+ 'deploy' => 'Releases all Kubernetes resources in a deploy artifact with |kubectl apply|',
8
+ 'push' => 'Tags and pushes images to defined image registries',
9
+ 'generate' => 'Generates artifacts based on templates in kubernetes/ and your deploy.yaml.',
10
+ 'publish' => 'Publishes generated artifacts to your artifact store.',
11
+ 'upgrade' => 'Upgrades a KDT 1.x deploy.yml to a KDT 2.x deploy.yaml',
12
+ }
13
+
14
+ def initialize(path, args)
15
+ KubeDeployTools::Logger.logger = KubeDeployTools::FormattedLogger.build
16
+
17
+ @path = path
18
+ @args = args
19
+ end
20
+
21
+ def bins_names
22
+ @bins ||= Dir["#{@path}/*"].map { |x| File.basename(x) } - ['kdt']
23
+ end
24
+
25
+ def display_bins
26
+ # Print full runtime version
27
+ version = Gem.loaded_specs["kube_deploy_tools"].version
28
+ puts "kube_deploy_tools #{version}"
29
+
30
+ bins_names.each do |bin|
31
+ spaces_count = 25 - bin.size
32
+ puts "-> #{bin}#{' ' * spaces_count}| #{DESCRIPTIONS[bin]}"
33
+ end
34
+ end
35
+
36
+ def execute!
37
+ bin = @args.first
38
+
39
+ raise "command '#{bin}' is not a valid command" unless valid_bin?(bin)
40
+ bin_with_path = "#{@path}/#{bin}"
41
+ bin_args = @args[1..-1]
42
+
43
+ # calling exec with multiple args will prevent shell expansion
44
+ # https://ruby-doc.org/core/Kernel.html#method-i-exec
45
+ exec bin_with_path, *bin_args
46
+ end
47
+
48
+ def valid_bin?(bin)
49
+ bins_names.include?(bin)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ require 'kube_deploy_tools/object'
2
+ require 'kube_deploy_tools/shellrunner'
3
+
4
+ module KubeDeployTools
5
+ class Kubectl
6
+ def initialize(
7
+ context:,
8
+ kubeconfig:)
9
+ @context = context
10
+ @kubeconfig = kubeconfig
11
+
12
+ raise ArgumentError, "context is required" if context.empty?
13
+ end
14
+
15
+ def run(*args, print_cmd: true, timeout: nil)
16
+ args = args.unshift("kubectl")
17
+ args.push("--context=#{@context}")
18
+ args.push("--kubeconfig=#{@kubeconfig}") if @kubeconfig.present?
19
+ args.push("--request-timeout=#{timeout}") if timeout.present?
20
+
21
+ Shellrunner.run_call(*args, print_cmd: print_cmd)
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,57 @@
1
+ require 'tempfile'
2
+
3
+ module KubeDeployTools
4
+ class KubernetesResource
5
+ attr_accessor :definition,
6
+ :kind,
7
+ :name,
8
+ :namespace,
9
+ :annotations
10
+
11
+ def self.build(filepath: nil, definition:, kubectl:)
12
+ opts = { filepath: filepath, definition: definition, kubectl: kubectl }
13
+ # Find the corresponding class for the Kubernetes resource, if available
14
+ if ["Deployment"].include?(definition["kind"])
15
+ klass = KubeDeployTools.const_get(definition["kind"])
16
+ klass.new(**opts)
17
+ else
18
+ # Otherwise initialize here if no class exists for this Kubernetes
19
+ # resource kind
20
+ inst = new(**opts)
21
+ inst.kind = definition["kind"]
22
+ inst
23
+ end
24
+ end
25
+
26
+ def initialize(filepath:, definition:, kubectl:)
27
+ @filepath = filepath
28
+ @definition = definition
29
+ @kubectl = kubectl
30
+
31
+ @namespace = definition.dig('metadata', 'namespace')
32
+ @name = definition.dig('metadata', 'name')
33
+ @kind = definition['kind']
34
+ @annotations = definition.dig('metadata', 'annotations')
35
+ end
36
+
37
+ def filepath
38
+ @filepath ||= file.path
39
+ end
40
+
41
+ def file
42
+ @file ||= create_definition_tempfile
43
+ end
44
+
45
+ def create_definition_tempfile
46
+ file = Tempfile.new(["#{@namespace}-#{@kind}-#{@name}", ".yaml"])
47
+ file.write(YAML.dump(@definition))
48
+ file
49
+ ensure
50
+ file&.close
51
+ end
52
+
53
+ def sync
54
+ # unimplemented
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,56 @@
1
+ require 'json'
2
+
3
+ require 'kube_deploy_tools/formatted_logger'
4
+ require 'kube_deploy_tools/object'
5
+
6
+ module KubeDeployTools
7
+ class Deployment < KubernetesResource
8
+ TIMEOUT = '60s'
9
+
10
+ attr_accessor :found,
11
+ :local_replicas,
12
+ :remote_replicas,
13
+ :recorded_replicas
14
+
15
+ def sync
16
+ @local_replicas = @definition["spec"]["replicas"]
17
+
18
+ raw_json, _err, st = @kubectl.run("get", "-f", filepath, "--output=json", print_cmd: false, timeout: TIMEOUT)
19
+ @found = st.success?
20
+
21
+ if st.success?
22
+ deployment_data = JSON.parse(raw_json)
23
+ @remote_replicas = deployment_data["spec"]["replicas"]
24
+ end
25
+
26
+ raw_json, _err, st = @kubectl.run("apply", "view-last-applied", "-f", filepath, "--output=json", print_cmd: false, timeout: TIMEOUT)
27
+ if st.success?
28
+ raw_json = fix_kubectl_apply_view_last_applied_output(raw_json)
29
+ deployment_data = JSON.parse(raw_json)
30
+ @recorded_replicas = deployment_data["spec"]["replicas"]
31
+ end
32
+ end
33
+
34
+ def warn_replicas_mismatch
35
+ if @found
36
+ if @local_replicas.present? && @local_replicas.to_i != @remote_replicas.to_i
37
+ warning = "Deployment replica count mismatch! Will scale deployment/#{@name} from #{@remote_replicas} to #{@local_replicas}"
38
+ Logger.warn(warning)
39
+ elsif @local_replicas.nil? && !@recorded_replicas.nil?
40
+ # Check if we're converting to a replicaless Deployment
41
+ warning = "Deployment replica count mismatch! Will scale deployment/#{@name} from #{@remote_replicas} to 1. Run `kubectl apply set-last-applied -f #{@filepath}` first."
42
+ Logger.warn(warning)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ # In kubectl version <= 1.7, `kubectl apply view-last-applied` may
50
+ # produces an invalid JSON output, which we must sanitize.
51
+ #
52
+ # The upstream fix is in kubect >= 1.8:
53
+ # https://github.com/kubernetes/kubernetes/commit/7c656ab4d2ca41e07db9f90c99ee360b0d48c651
54
+ def fix_kubectl_apply_view_last_applied_output(json)
55
+ json.sub(/\!\"\(MISSING\)/, '"')
56
+ end