krane 3.9.1 → 3.9.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43683d4bcd224e46ba5598d9a41ef99867836067620852e0f835712dd602b2a1
4
- data.tar.gz: 3ac6e0f0bd6c4ec7ef5ed5e29021fd5695e3620dfcea1c14aa2e42fd83fdbb2c
3
+ metadata.gz: d13a5afaa821cb112292626c292ce30ffe425d807415376ae41ae7d277093122
4
+ data.tar.gz: a1e741ce9287d33e6f50efeea076363906e3e07e0de037984d626b4a963c9f41
5
5
  SHA512:
6
- metadata.gz: 7c174786771b7b82e47fed0c6f256ac119abe4d39b038d9c7d75dda2718d490622c2c653452d7e0f8edafb026a9e932ab9583986cc6f79d845901070bcabd6ee
7
- data.tar.gz: a261331f47d9d579e1282d92bd182280883ee1aa641fc18f9761743452f6ead060c4fb3900cb4ff8eb724a14df45e522f5931c879fc36ce5d9142943e99d17ed
6
+ metadata.gz: 48523d3a94ca3efb25b04033d187bebf4c911e14fbbe1f7608d340b6f4087d2bee36e51fdfb3d89ea7fff33d74583870cb64ac1ee96be6332212c5e3ca40a8b3
7
+ data.tar.gz: 663967d67f1d685c3b2f7b34e156c7b33a052b78620c8e6a2f568a1998186677dc5cf470cc3a1f4fce73856988a44b17e70315de10eaf52a53405759350e1d00
data/.github/CODEOWNERS CHANGED
@@ -1 +1 @@
1
- * @Shopify/infrastructure-tooling @Shopify/application-infrastructure
1
+ * @Shopify/application-infrastructure
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## next
2
2
 
3
+ ## 3.9.2
4
+ - Parse JSON with `JSON.parse` instead of `MultiJson.load`, and drop the `multi_json` dependency. On Ruby 3.0 and 3.1, `multi_json` 1.19.1 (the newest those versions can install) crashes with `json` 3.x
5
+ - Pod log retrieval now uses the task's `kubeconfig` instead of falling back to `ENV['KUBECONFIG']`
6
+ - Reject namespaces, contexts and kinds that `kubectl` would parse as flags rather than as positional arguments
7
+
3
8
  ## 3.9.1
4
9
  - Test against k8s 1.34
5
10
 
data/krane.gemspec CHANGED
@@ -32,7 +32,6 @@ Gem::Specification.new do |spec|
32
32
  spec.add_dependency("ejson", "~> 1.0")
33
33
  spec.add_dependency("colorize", "~> 0.8")
34
34
  spec.add_dependency("statsd-instrument", ['>= 2.8', "< 3.9"])
35
- spec.add_dependency("multi_json")
36
35
  spec.add_dependency("concurrent-ruby", "~> 1.1")
37
36
  spec.add_dependency("jsonpath", "~> 1.0")
38
37
  spec.add_dependency("thor", ">= 1.0", "< 2.0")
@@ -54,14 +54,14 @@ module Krane
54
54
  end
55
55
 
56
56
  def parse_json(string)
57
- bindings = MultiJson.load(string)
57
+ bindings = JSON.parse(string)
58
58
 
59
59
  unless bindings.is_a?(Hash)
60
60
  raise ArgumentError, "Expected JSON data to be a hash."
61
61
  end
62
62
 
63
63
  bindings
64
- rescue MultiJson::ParseError
64
+ rescue JSON::ParserError
65
65
  nil
66
66
  end
67
67
 
@@ -6,7 +6,7 @@ require 'krane/cli/render_command'
6
6
  require 'krane/cli/restart_command'
7
7
  require 'krane/cli/run_command'
8
8
  require 'krane/cli/version_command'
9
- require 'multi_json'
9
+ require 'json'
10
10
  require 'thor'
11
11
 
12
12
  module Krane
@@ -54,7 +54,7 @@ module Krane
54
54
  @api_path_cache["/"] ||= begin
55
55
  raw_json, err, st = kubectl.run("get", "--raw", base_api_path, attempts: 5, use_namespace: false)
56
56
  paths = if st.success?
57
- MultiJson.load(raw_json)["paths"]
57
+ JSON.parse(raw_json)["paths"]
58
58
  else
59
59
  raise FatalKubeAPIError, "Error retrieving raw path /: #{err}"
60
60
  end
@@ -66,7 +66,7 @@ module Krane
66
66
  @api_path_cache[path] ||= begin
67
67
  raw_json, err, st = kubectl.run("get", "--raw", path, attempts: 2, use_namespace: false)
68
68
  if st.success?
69
- MultiJson.load(raw_json)
69
+ JSON.parse(raw_json)
70
70
  else
71
71
  logger.warn("Error retrieving api path: #{err}")
72
72
  {}
@@ -92,7 +92,7 @@ module Krane
92
92
  raw_json, err, st = kubectl.run("get", "CustomResourceDefinition", output: "json", attempts: 5,
93
93
  use_namespace: false)
94
94
  if st.success?
95
- MultiJson.load(raw_json)["items"]
95
+ JSON.parse(raw_json)["items"]
96
96
  else
97
97
  raise FatalKubeAPIError, "Error retrieving CustomResourceDefinition: #{err}"
98
98
  end
@@ -16,8 +16,8 @@ module Krane
16
16
  @printed_latest = false
17
17
  end
18
18
 
19
- def sync
20
- new_logs = fetch_latest
19
+ def sync(kubectl)
20
+ new_logs = fetch_latest(kubectl)
21
21
  return unless new_logs.present?
22
22
  @lines += sort_and_deduplicate(new_logs)
23
23
  end
@@ -47,7 +47,7 @@ module Krane
47
47
 
48
48
  private
49
49
 
50
- def fetch_latest
50
+ def fetch_latest(kubectl)
51
51
  cmd = ["logs", @parent_id, "--container=#{container_name}", "--timestamps"]
52
52
  cmd << if @last_timestamp.present?
53
53
  "--since-time=#{rfc3339_timestamp(@last_timestamp)}"
@@ -58,11 +58,6 @@ module Krane
58
58
  out.encode('UTF-8', invalid: :replace, replace: '').split("\n")
59
59
  end
60
60
 
61
- def kubectl
62
- task_config = TaskConfig.new(@context, @namespace, @logger)
63
- @kubectl ||= Kubectl.new(task_config: task_config, log_failure_by_default: false)
64
- end
65
-
66
61
  def rfc3339_timestamp(time)
67
62
  time.strftime("%FT%T.%N%:z")
68
63
  end
@@ -318,7 +318,7 @@ module Krane
318
318
  @namespace_definition ||= begin
319
319
  definition, _err, st = kubectl.run("get", "namespace", @namespace, use_namespace: false,
320
320
  log_failure: true, raise_if_not_found: true, attempts: 3, output: 'json')
321
- st.success? ? MultiJson.load(definition, symbolize_names: true) : nil
321
+ st.success? ? JSON.parse(definition, symbolize_names: true) : nil
322
322
  end
323
323
  rescue Kubectl::ResourceNotFoundError
324
324
  nil
@@ -352,7 +352,7 @@ module Krane
352
352
  unless st.success?
353
353
  raise EjsonSecretError, "Error retrieving Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET}: #{err}"
354
354
  end
355
- MultiJson.load(out)
355
+ JSON.parse(out)
356
356
  end
357
357
  end
358
358
 
@@ -117,8 +117,8 @@ module Krane
117
117
 
118
118
  def load_ejson_from_file
119
119
  return {} unless File.exist?(@ejson_file)
120
- MultiJson.load(File.read(@ejson_file))
121
- rescue MultiJson::ParseError => e
120
+ JSON.parse(File.read(@ejson_file))
121
+ rescue JSON::ParserError => e
122
122
  raise EjsonSecretError, "Failed to parse encrypted ejson:\n #{e}"
123
123
  end
124
124
 
@@ -139,8 +139,8 @@ module Krane
139
139
  msg = err.presence || out
140
140
  raise EjsonSecretError, msg
141
141
  end
142
- MultiJson.load(out)
143
- rescue MultiJson::ParseError
142
+ JSON.parse(out)
143
+ rescue JSON::ParserError
144
144
  raise EjsonSecretError, "Failed to parse decrypted ejson"
145
145
  end
146
146
 
data/lib/krane/kubectl.rb CHANGED
@@ -86,7 +86,7 @@ module Krane
86
86
  response, _, status = run("version", output: "json", use_namespace: false, log_failure: true, attempts: 2)
87
87
  raise KubectlError, "Could not retrieve kubectl version info" unless status.success?
88
88
 
89
- version_data = MultiJson.load(response)
89
+ version_data = JSON.parse(response)
90
90
  client_version = platform_agnostic_version(version_data.dig("clientVersion", "gitVersion").to_s)
91
91
  server_version = platform_agnostic_version(version_data.dig("serverVersion", "gitVersion").to_s)
92
92
  unless client_version && server_version
@@ -30,9 +30,9 @@ module Krane
30
30
  observed_generation == current_generation
31
31
  end
32
32
 
33
- def fetch_debug_logs
33
+ def fetch_debug_logs(kubectl)
34
34
  most_useful_pod = pods.find(&:deploy_failed?) || pods.find(&:deploy_timed_out?) || pods.first
35
- most_useful_pod.fetch_debug_logs
35
+ most_useful_pod.fetch_debug_logs(kubectl)
36
36
  end
37
37
 
38
38
  def print_debug_logs?
@@ -29,8 +29,8 @@ module Krane
29
29
  @latest_rs.present?
30
30
  end
31
31
 
32
- def fetch_debug_logs
33
- @latest_rs.fetch_debug_logs
32
+ def fetch_debug_logs(kubectl)
33
+ @latest_rs.fetch_debug_logs(kubectl)
34
34
  end
35
35
 
36
36
  def deploy_succeeded?
@@ -28,12 +28,12 @@ module Krane
28
28
  logger: logger, statsd_tags: statsd_tags)
29
29
  end
30
30
 
31
- def sync(_cache)
31
+ def sync(cache)
32
32
  super
33
33
  raise_predates_deploy_error if exists? && unmanaged? && !deploy_started?
34
34
 
35
35
  if exists?
36
- logs.sync if unmanaged?
36
+ logs.sync(cache.kubectl) if unmanaged?
37
37
  update_container_statuses(@instance_data["status"])
38
38
  else # reset
39
39
  @containers.each(&:reset_status)
@@ -93,8 +93,8 @@ module Krane
93
93
  "#{phase_failure_message} #{container_problems}".strip.presence
94
94
  end
95
95
 
96
- def fetch_debug_logs
97
- logs.sync
96
+ def fetch_debug_logs(kubectl)
97
+ logs.sync(kubectl)
98
98
  logs
99
99
  end
100
100
 
@@ -18,7 +18,7 @@ module Krane
18
18
  own_events.merge(most_useful_pod.fetch_events(kubectl))
19
19
  end
20
20
 
21
- def fetch_debug_logs
21
+ def fetch_debug_logs(kubectl)
22
22
  logs = Krane::RemoteLogs.new(
23
23
  logger: @logger,
24
24
  parent_id: id,
@@ -26,7 +26,7 @@ module Krane
26
26
  namespace: @namespace,
27
27
  context: @context
28
28
  )
29
- logs.sync
29
+ logs.sync(kubectl)
30
30
  logs
31
31
  end
32
32
 
@@ -35,6 +35,11 @@ module Krane
35
35
  If you have reason to believe it will succeed, retry the deploy to continue to monitor the rollout.
36
36
  MSG
37
37
 
38
+ # Kubernetes kinds begin with a letter, and may contain dashes after that (the API server
39
+ # lowercases the kind and validates it as a DNS-1035 label). Only the first character matters
40
+ # here: a leading dash would make kubectl parse the kind as a flag rather than a resource kind.
41
+ KIND_FORMAT = /\A[A-Za-z]/
42
+
38
43
  ALLOWED_DEPLOY_METHOD_OVERRIDES = %w(create replace replace-force)
39
44
  DEPLOY_METHOD_OVERRIDE_ANNOTATION = "deploy-method-override"
40
45
  TIMEOUT_OVERRIDE_ANNOTATION = "timeout-override"
@@ -91,6 +96,10 @@ module Krane
91
96
  raise InvalidTemplateError.new("Template is missing required field 'kind'", content: debug_content)
92
97
  end
93
98
 
99
+ unless definition["kind"].match?(KIND_FORMAT)
100
+ raise InvalidTemplateError.new("Template's 'kind' must begin with a letter", content: debug_content)
101
+ end
102
+
94
103
  if definition.dig('metadata', 'name').blank? && definition.dig('metadata', 'generateName').blank?
95
104
  raise InvalidTemplateError.new("Template must specify one of 'metadata.name' or 'metadata.generateName'",
96
105
  content: debug_content)
@@ -250,7 +259,7 @@ module Krane
250
259
 
251
260
  def sync_debug_info(kubectl)
252
261
  @debug_events = fetch_events(kubectl) unless ENV[DISABLE_FETCHING_EVENT_INFO]
253
- @debug_logs = fetch_debug_logs if print_debug_logs? && !ENV[DISABLE_FETCHING_LOG_INFO]
262
+ @debug_logs = fetch_debug_logs(kubectl) if print_debug_logs? && !ENV[DISABLE_FETCHING_LOG_INFO]
254
263
  end
255
264
 
256
265
  def debug_message(cause = nil, info_hash = {})
@@ -23,8 +23,8 @@ module Krane
23
23
  @container_logs.all?(&:empty?)
24
24
  end
25
25
 
26
- def sync
27
- @container_logs.each(&:sync)
26
+ def sync(kubectl)
27
+ @container_logs.each { |cl| cl.sync(kubectl) }
28
28
  end
29
29
 
30
30
  def print_latest
@@ -4,6 +4,8 @@ require 'concurrent/hash'
4
4
 
5
5
  module Krane
6
6
  class ResourceCache
7
+ attr_reader :kubectl
8
+
7
9
  delegate :namespace, :context, :logger, to: :@task_config
8
10
 
9
11
  def initialize(task_config)
@@ -64,7 +66,7 @@ module Krane
64
66
  raise KubectlError unless st.success?
65
67
 
66
68
  instances = {}
67
- MultiJson.load(raw_json)["items"].each do |resource|
69
+ JSON.parse(raw_json)["items"].each do |resource|
68
70
  resource_name = resource.dig("metadata", "name")
69
71
  instances[resource_name] = resource
70
72
  end
@@ -251,7 +251,7 @@ module Krane
251
251
  # For resources that rely on a generateName attribute, we get the `name` from the result of the call to `create`
252
252
  # We must explicitly set this name value so that the `apply` step for pruning can run successfully
253
253
  if status.success? && resource.uses_generate_name?
254
- resource.use_generated_name(MultiJson.load(out))
254
+ resource.use_generated_name(JSON.parse(out))
255
255
  end
256
256
 
257
257
  [err, status]
@@ -11,7 +11,7 @@ module Krane
11
11
  def from_annotation(conditions_string)
12
12
  return new(default_conditions) if conditions_string.downcase.strip == "true"
13
13
 
14
- conditions = MultiJson.load(conditions_string).slice('success_conditions', 'failure_conditions')
14
+ conditions = JSON.parse(conditions_string).slice('success_conditions', 'failure_conditions')
15
15
  conditions.deep_symbolize_keys!
16
16
 
17
17
  # Create JsonPath objects
@@ -26,7 +26,7 @@ module Krane
26
26
  end
27
27
 
28
28
  new(conditions)
29
- rescue MultiJson::ParseError => e
29
+ rescue JSON::ParserError => e
30
30
  raise RolloutConditionsError, "Rollout conditions are not valid JSON: #{e}"
31
31
  rescue StandardError => e
32
32
  raise RolloutConditionsError,
@@ -9,6 +9,10 @@ module Krane
9
9
  validate_namespace_exists
10
10
  ).freeze
11
11
 
12
+ # Kubernetes namespaces are DNS-1123 labels. Anything else, in particular a value starting with
13
+ # a dash, would be parsed by kubectl as a flag rather than as a resource name.
14
+ NAMESPACE_FORMAT = /\A[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\z/
15
+
12
16
  delegate :context, :namespace, :logger, to: :@task_config
13
17
 
14
18
  def initialize(task_config, kubectl, kubeclient_builder, only: nil)
@@ -44,6 +48,12 @@ module Krane
44
48
  return @errors << "Context can not be blank"
45
49
  end
46
50
 
51
+ # A context is passed to kubectl as a bare positional argument; a leading dash would make
52
+ # kubectl parse it as a flag. Context names are otherwise too varied to constrain further.
53
+ if context.start_with?("-")
54
+ return @errors << "Context can not start with a dash"
55
+ end
56
+
47
57
  _, err, st = @kubectl.run("config", "get-contexts", context, "-o", "name",
48
58
  use_namespace: false, use_context: false, log_failure: false, attempts: 2)
49
59
 
@@ -70,6 +80,10 @@ module Krane
70
80
  return @errors << "Namespace can not be blank"
71
81
  end
72
82
 
83
+ unless namespace.match?(NAMESPACE_FORMAT)
84
+ return @errors << "Namespace is not a valid Kubernetes namespace name"
85
+ end
86
+
73
87
  _, err, st = @kubectl.run("get", "namespace", "-o", "name", namespace,
74
88
  use_namespace: false, log_failure: false, attempts: 3)
75
89
 
data/lib/krane/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Krane
3
- VERSION = "3.9.1"
3
+ VERSION = "3.9.2"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: krane
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.9.1
4
+ version: 3.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katrina Verey
@@ -101,20 +101,6 @@ dependencies:
101
101
  - - "<"
102
102
  - !ruby/object:Gem::Version
103
103
  version: '3.9'
104
- - !ruby/object:Gem::Dependency
105
- name: multi_json
106
- requirement: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- type: :runtime
112
- prerelease: false
113
- version_requirements: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
104
  - !ruby/object:Gem::Dependency
119
105
  name: concurrent-ruby
120
106
  requirement: !ruby/object:Gem::Requirement
@@ -525,7 +511,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
525
511
  - !ruby/object:Gem::Version
526
512
  version: '0'
527
513
  requirements: []
528
- rubygems_version: 3.7.2
514
+ rubygems_version: 4.0.21
529
515
  specification_version: 4
530
516
  summary: A command line tool that helps you ship changes to a Kubernetes namespace
531
517
  and understand the result