kube_deploy_tools 3.0.5

Sign up to get free protection for your applications and to get access to all the features.
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