Class: Krane::Kubectl

Inherits:
Object
  • Object
show all
Defined in:
lib/krane/kubectl.rb

Defined Under Namespace

Classes: ResourceNotFoundError

Constant Summary collapse

ERROR_MATCHERS =
{
  not_found: /NotFound/,
  client_timeout: /Client\.Timeout exceeded while awaiting headers/,
  empty: /\A\z/,
  context_deadline: /context deadline exceeded/,
}
DEFAULT_TIMEOUT =
15
MAX_RETRY_DELAY =
16
SERVER_DRY_RUN_MIN_VERSION =
"1.13"
ALLOW_LIST_MIN_VERSION =
"1.26"

Instance Method Summary collapse

Constructor Details

#initialize(task_config:, log_failure_by_default:, default_timeout: DEFAULT_TIMEOUT, output_is_sensitive_default: false) ⇒ Kubectl

Returns a new instance of Kubectl.



21
22
23
24
25
26
27
# File 'lib/krane/kubectl.rb', line 21

def initialize(task_config:, log_failure_by_default:, default_timeout: DEFAULT_TIMEOUT,
  output_is_sensitive_default: false)
  @task_config = task_config
  @log_failure_by_default = log_failure_by_default
  @default_timeout = default_timeout
  @output_is_sensitive_default = output_is_sensitive_default
end

Instance Method Details

#allowlist_flagObject



116
117
118
119
120
121
122
# File 'lib/krane/kubectl.rb', line 116

def allowlist_flag
  if client_version >= Gem::Version.new(ALLOW_LIST_MIN_VERSION)
    "--prune-allowlist"
  else
    "--prune-whitelist"
  end
end

#client_versionObject



100
101
102
# File 'lib/krane/kubectl.rb', line 100

def client_version
  version_info[:client]
end

#dry_run_flagObject



112
113
114
# File 'lib/krane/kubectl.rb', line 112

def dry_run_flag
  "--dry-run=server"
end

#retry_delay(attempt) ⇒ Object



78
79
80
81
# File 'lib/krane/kubectl.rb', line 78

def retry_delay(attempt)
  # exponential backoff starting at 1s with cap at 16s, offset by up to 0.5s
  [2**(attempt - 1), MAX_RETRY_DELAY].min - Random.rand(0.5).round(1)
end

#run(*args, log_failure: nil, use_context: true, use_namespace: true, output: nil, raise_if_not_found: false, attempts: 1, output_is_sensitive: nil, retry_whitelist: nil) ⇒ Object

Raises:

  • (ArgumentError)


29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/krane/kubectl.rb', line 29

def run(*args, log_failure: nil, use_context: true, use_namespace: true, output: nil,
  raise_if_not_found: false, attempts: 1, output_is_sensitive: nil, retry_whitelist: nil)
  raise ArgumentError, "namespace is required" if namespace.blank? && use_namespace
  log_failure = @log_failure_by_default if log_failure.nil?
  output_is_sensitive = @output_is_sensitive_default if output_is_sensitive.nil?
  cmd = build_command_from_options(args, use_namespace, use_context, output)
  out, err, st = nil

  (1..attempts).to_a.each do |current_attempt|
    logger.debug("Running command (attempt #{current_attempt}): #{cmd.join(' ')}")
    env = { 'KUBECONFIG' => kubeconfig }
    out, err, st = Open3.capture3(env, *cmd)

    # https://github.com/Shopify/krane/issues/395
    unless out.valid_encoding?
      out = out.dup.force_encoding(Encoding::UTF_8)
    end

    if logger.debug? && !output_is_sensitive
      # don't do the gsub unless we're going to print this
      logger.debug("Kubectl out: " + out.gsub(/\s+/, ' '))
    end

    break if st.success?
    raise(ResourceNotFoundError, err) if err.match(ERROR_MATCHERS[:not_found]) && raise_if_not_found

    if log_failure
      warning = if current_attempt == attempts
        "The following command failed (attempt #{current_attempt}/#{attempts})"
      elsif retriable_err?(err, retry_whitelist)
        "The following command failed and will be retried (attempt #{current_attempt}/#{attempts})"
      else
        "The following command failed and cannot be retried"
      end
      logger.warn("#{warning}: #{Shellwords.join(cmd)}")
      logger.warn(err) unless output_is_sensitive
    else
      logger.debug("Kubectl err: #{output_is_sensitive ? '<suppressed sensitive output>' : err}")
    end
    StatsD.client.increment('kubectl.error', 1, tags: { context: context, namespace: namespace, cmd: cmd[1],
                                                        max_attempt: attempts, current_attempt: current_attempt })

    break unless retriable_err?(err, retry_whitelist) && current_attempt < attempts
    sleep(retry_delay(current_attempt))
  end

  [out.chomp, err.chomp, st]
end

#server_dry_run_enabled?Boolean

Returns:

  • (Boolean)


108
109
110
# File 'lib/krane/kubectl.rb', line 108

def server_dry_run_enabled?
  server_version >= Gem::Version.new(SERVER_DRY_RUN_MIN_VERSION)
end

#server_versionObject



104
105
106
# File 'lib/krane/kubectl.rb', line 104

def server_version
  version_info[:server]
end

#version_infoObject



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/krane/kubectl.rb', line 83

def version_info
  @version_info ||=
    begin
      response, _, status = run("version", output: "json", use_namespace: false, log_failure: true, attempts: 2)
      raise KubectlError, "Could not retrieve kubectl version info" unless status.success?

      version_data = MultiJson.load(response)
      client_version = platform_agnostic_version(version_data.dig("clientVersion", "gitVersion").to_s)
      server_version = platform_agnostic_version(version_data.dig("serverVersion", "gitVersion").to_s)
      unless client_version && server_version
        raise KubectlError, "Received invalid kubectl version data: #{version_data}"
      end

      { client: client_version, server: server_version }
    end
end