tobsch-krane 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.buildkite/pipeline.nightly.yml +43 -0
- data/.github/probots.yml +2 -0
- data/.gitignore +20 -0
- data/.rubocop.yml +17 -0
- data/.shopify-build/VERSION +1 -0
- data/.shopify-build/kubernetes-deploy.yml +53 -0
- data/1.0-Upgrade.md +185 -0
- data/CHANGELOG.md +431 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +164 -0
- data/Gemfile +16 -0
- data/ISSUE_TEMPLATE.md +25 -0
- data/LICENSE.txt +21 -0
- data/README.md +655 -0
- data/Rakefile +36 -0
- data/bin/ci +21 -0
- data/bin/setup +16 -0
- data/bin/test +47 -0
- data/dev.yml +28 -0
- data/dev/flamegraph-from-tests +35 -0
- data/exe/krane +5 -0
- data/krane.gemspec +44 -0
- data/lib/krane.rb +7 -0
- data/lib/krane/bindings_parser.rb +88 -0
- data/lib/krane/cli/deploy_command.rb +75 -0
- data/lib/krane/cli/global_deploy_command.rb +54 -0
- data/lib/krane/cli/krane.rb +91 -0
- data/lib/krane/cli/render_command.rb +41 -0
- data/lib/krane/cli/restart_command.rb +34 -0
- data/lib/krane/cli/run_command.rb +54 -0
- data/lib/krane/cli/version_command.rb +13 -0
- data/lib/krane/cluster_resource_discovery.rb +113 -0
- data/lib/krane/common.rb +23 -0
- data/lib/krane/concerns/template_reporting.rb +29 -0
- data/lib/krane/concurrency.rb +18 -0
- data/lib/krane/container_logs.rb +106 -0
- data/lib/krane/deferred_summary_logging.rb +95 -0
- data/lib/krane/delayed_exceptions.rb +14 -0
- data/lib/krane/deploy_task.rb +363 -0
- data/lib/krane/deploy_task_config_validator.rb +29 -0
- data/lib/krane/duration_parser.rb +27 -0
- data/lib/krane/ejson_secret_provisioner.rb +154 -0
- data/lib/krane/errors.rb +28 -0
- data/lib/krane/formatted_logger.rb +57 -0
- data/lib/krane/global_deploy_task.rb +210 -0
- data/lib/krane/global_deploy_task_config_validator.rb +12 -0
- data/lib/krane/kubeclient_builder.rb +156 -0
- data/lib/krane/kubectl.rb +120 -0
- data/lib/krane/kubernetes_resource.rb +621 -0
- data/lib/krane/kubernetes_resource/cloudsql.rb +43 -0
- data/lib/krane/kubernetes_resource/config_map.rb +22 -0
- data/lib/krane/kubernetes_resource/cron_job.rb +18 -0
- data/lib/krane/kubernetes_resource/custom_resource.rb +87 -0
- data/lib/krane/kubernetes_resource/custom_resource_definition.rb +98 -0
- data/lib/krane/kubernetes_resource/daemon_set.rb +90 -0
- data/lib/krane/kubernetes_resource/deployment.rb +213 -0
- data/lib/krane/kubernetes_resource/horizontal_pod_autoscaler.rb +65 -0
- data/lib/krane/kubernetes_resource/ingress.rb +18 -0
- data/lib/krane/kubernetes_resource/job.rb +60 -0
- data/lib/krane/kubernetes_resource/network_policy.rb +22 -0
- data/lib/krane/kubernetes_resource/persistent_volume_claim.rb +80 -0
- data/lib/krane/kubernetes_resource/pod.rb +269 -0
- data/lib/krane/kubernetes_resource/pod_disruption_budget.rb +23 -0
- data/lib/krane/kubernetes_resource/pod_set_base.rb +71 -0
- data/lib/krane/kubernetes_resource/pod_template.rb +20 -0
- data/lib/krane/kubernetes_resource/replica_set.rb +92 -0
- data/lib/krane/kubernetes_resource/resource_quota.rb +22 -0
- data/lib/krane/kubernetes_resource/role.rb +22 -0
- data/lib/krane/kubernetes_resource/role_binding.rb +22 -0
- data/lib/krane/kubernetes_resource/secret.rb +24 -0
- data/lib/krane/kubernetes_resource/service.rb +104 -0
- data/lib/krane/kubernetes_resource/service_account.rb +22 -0
- data/lib/krane/kubernetes_resource/stateful_set.rb +70 -0
- data/lib/krane/label_selector.rb +42 -0
- data/lib/krane/oj.rb +4 -0
- data/lib/krane/options_helper.rb +39 -0
- data/lib/krane/remote_logs.rb +60 -0
- data/lib/krane/render_task.rb +118 -0
- data/lib/krane/renderer.rb +118 -0
- data/lib/krane/resource_cache.rb +68 -0
- data/lib/krane/resource_deployer.rb +265 -0
- data/lib/krane/resource_watcher.rb +171 -0
- data/lib/krane/restart_task.rb +228 -0
- data/lib/krane/rollout_conditions.rb +103 -0
- data/lib/krane/runner_task.rb +212 -0
- data/lib/krane/runner_task_config_validator.rb +18 -0
- data/lib/krane/statsd.rb +65 -0
- data/lib/krane/task_config.rb +22 -0
- data/lib/krane/task_config_validator.rb +96 -0
- data/lib/krane/template_sets.rb +173 -0
- data/lib/krane/version.rb +4 -0
- data/pull_request_template.md +8 -0
- data/screenshots/deploy-demo.gif +0 -0
- data/screenshots/migrate-logs.png +0 -0
- data/screenshots/missing-secret-fail.png +0 -0
- data/screenshots/success.png +0 -0
- data/screenshots/test-output.png +0 -0
- metadata +375 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'krane'
|
4
|
+
require 'thor'
|
5
|
+
require 'krane/cli/version_command'
|
6
|
+
require 'krane/cli/restart_command'
|
7
|
+
require 'krane/cli/run_command'
|
8
|
+
require 'krane/cli/render_command'
|
9
|
+
require 'krane/cli/deploy_command'
|
10
|
+
require 'krane/cli/global_deploy_command'
|
11
|
+
|
12
|
+
module Krane
|
13
|
+
module CLI
|
14
|
+
class Krane < Thor
|
15
|
+
TIMEOUT_EXIT_CODE = 70
|
16
|
+
FAILURE_EXIT_CODE = 1
|
17
|
+
|
18
|
+
package_name "Krane"
|
19
|
+
|
20
|
+
def self.expand_options(task_options)
|
21
|
+
task_options.each { |option_name, config| method_option(option_name, config) }
|
22
|
+
end
|
23
|
+
|
24
|
+
desc("render", "Render templates")
|
25
|
+
expand_options(RenderCommand::OPTIONS)
|
26
|
+
def render
|
27
|
+
rescue_and_exit do
|
28
|
+
RenderCommand.from_options(options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
desc("version", "Prints the version")
|
33
|
+
expand_options(VersionCommand::OPTIONS)
|
34
|
+
def version
|
35
|
+
VersionCommand.from_options(options)
|
36
|
+
end
|
37
|
+
|
38
|
+
desc("restart NAMESPACE CONTEXT", "Restart the pods in one or more deployments")
|
39
|
+
expand_options(RestartCommand::OPTIONS)
|
40
|
+
def restart(namespace, context)
|
41
|
+
rescue_and_exit do
|
42
|
+
RestartCommand.from_options(namespace, context, options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
desc("run NAMESPACE CONTEXT", "Run a pod that exits upon completing a task")
|
47
|
+
expand_options(RunCommand::OPTIONS)
|
48
|
+
def run_command(namespace, context)
|
49
|
+
rescue_and_exit do
|
50
|
+
RunCommand.from_options(namespace, context, options)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
desc("deploy NAMESPACE CONTEXT", "Ship resources to a namespace")
|
55
|
+
expand_options(DeployCommand::OPTIONS)
|
56
|
+
def deploy(namespace, context)
|
57
|
+
rescue_and_exit do
|
58
|
+
DeployCommand.from_options(namespace, context, options)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
desc("global-deploy CONTEXT", "Ship non-namespaced resources to a cluster")
|
63
|
+
expand_options(GlobalDeployCommand::OPTIONS)
|
64
|
+
def global_deploy(context)
|
65
|
+
rescue_and_exit do
|
66
|
+
GlobalDeployCommand.from_options(context, options)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.exit_on_failure?
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def rescue_and_exit
|
77
|
+
yield
|
78
|
+
rescue ::Krane::DeploymentTimeoutError
|
79
|
+
exit(TIMEOUT_EXIT_CODE)
|
80
|
+
rescue ::Krane::FatalDeploymentError
|
81
|
+
exit(FAILURE_EXIT_CODE)
|
82
|
+
rescue ::Krane::DurationParser::ParsingError => e
|
83
|
+
STDERR.puts(<<~ERROR_MESSAGE)
|
84
|
+
Error parsing duration
|
85
|
+
#{e.message}. Duration must be a full ISO8601 duration or time value (e.g. 300s, 10m, 1h)
|
86
|
+
ERROR_MESSAGE
|
87
|
+
exit(FAILURE_EXIT_CODE)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Krane
|
4
|
+
module CLI
|
5
|
+
class RenderCommand
|
6
|
+
OPTIONS = {
|
7
|
+
"bindings" => { type: :array, banner: "foo=bar abc=def", desc: 'Bindings for erb' },
|
8
|
+
"filenames" => { type: :array, banner: 'config/deploy/production config/deploy/my-extra-resource.yml',
|
9
|
+
required: false, default: [], aliases: 'f', desc: 'Directories and files to render' },
|
10
|
+
"stdin" => { type: :boolean, desc: "Read resources from stdin", default: false },
|
11
|
+
"current-sha" => { type: :string, banner: "SHA", desc: "Expose SHA `current_sha` in ERB bindings",
|
12
|
+
lazy_default: '' },
|
13
|
+
}
|
14
|
+
|
15
|
+
def self.from_options(options)
|
16
|
+
require 'krane/render_task'
|
17
|
+
require 'krane/bindings_parser'
|
18
|
+
require 'krane/options_helper'
|
19
|
+
|
20
|
+
bindings_parser = ::Krane::BindingsParser.new
|
21
|
+
options[:bindings]&.each { |b| bindings_parser.add(b) }
|
22
|
+
|
23
|
+
# never mutate options directly
|
24
|
+
filenames = options[:filenames].dup
|
25
|
+
filenames << "-" if options[:stdin]
|
26
|
+
if filenames.empty?
|
27
|
+
raise Thor::RequiredArgumentMissingError, 'At least one of --filenames or --stdin must be set'
|
28
|
+
end
|
29
|
+
|
30
|
+
::Krane::OptionsHelper.with_processed_template_paths(filenames, render_erb: true) do |paths|
|
31
|
+
renderer = ::Krane::RenderTask.new(
|
32
|
+
current_sha: options['current-sha'],
|
33
|
+
filenames: paths,
|
34
|
+
bindings: bindings_parser.parse,
|
35
|
+
)
|
36
|
+
renderer.run!(stream: STDOUT)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Krane
|
4
|
+
module CLI
|
5
|
+
class RestartCommand
|
6
|
+
DEFAULT_RESTART_TIMEOUT = '300s'
|
7
|
+
OPTIONS = {
|
8
|
+
"deployments" => { type: :array, banner: "list of deployments",
|
9
|
+
desc: "List of workload names to restart" },
|
10
|
+
"global-timeout" => { type: :string, banner: "duration", default: DEFAULT_RESTART_TIMEOUT,
|
11
|
+
desc: "Max duration to monitor workloads correctly restarted" },
|
12
|
+
"selector" => { type: :string, banner: "'label=value'",
|
13
|
+
desc: "Select workloads by selector(s)" },
|
14
|
+
"verify-result" => { type: :boolean, default: true,
|
15
|
+
desc: "Verify workloads correctly restarted" },
|
16
|
+
}
|
17
|
+
|
18
|
+
def self.from_options(namespace, context, options)
|
19
|
+
require 'krane/restart_task'
|
20
|
+
selector = ::Krane::LabelSelector.parse(options[:selector]) if options[:selector]
|
21
|
+
restart = ::Krane::RestartTask.new(
|
22
|
+
namespace: namespace,
|
23
|
+
context: context,
|
24
|
+
global_timeout: ::Krane::DurationParser.new(options["global-timeout"]).parse!.to_i,
|
25
|
+
)
|
26
|
+
restart.run!(
|
27
|
+
deployments: options[:deployments],
|
28
|
+
selector: selector,
|
29
|
+
verify_result: options["verify-result"]
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Krane
|
4
|
+
module CLI
|
5
|
+
class RunCommand
|
6
|
+
DEFAULT_RUN_TIMEOUT = '300s'
|
7
|
+
|
8
|
+
OPTIONS = {
|
9
|
+
"global-timeout" => {
|
10
|
+
type: :string,
|
11
|
+
banner: "duration",
|
12
|
+
desc: "Timeout error is raised if the pod runs for longer than the specified number of seconds",
|
13
|
+
default: DEFAULT_RUN_TIMEOUT,
|
14
|
+
},
|
15
|
+
"arguments" => {
|
16
|
+
type: :string,
|
17
|
+
banner: '"ARG1 ARG2 ARG3"',
|
18
|
+
desc: "Override the default arguments for the command with a space-separated list of arguments",
|
19
|
+
},
|
20
|
+
"verify-result" => { type: :boolean, desc: "Wait for completion and verify pod success", default: true },
|
21
|
+
"command" => { type: :array, desc: "Override the default command in the container image" },
|
22
|
+
"template" => {
|
23
|
+
type: :string,
|
24
|
+
desc: "The template file you'll be rendering",
|
25
|
+
required: true,
|
26
|
+
aliases: :f,
|
27
|
+
},
|
28
|
+
"env-vars" => {
|
29
|
+
type: :string,
|
30
|
+
banner: "VAR=val,FOO=bar",
|
31
|
+
desc: "A Comma-separated list of env vars",
|
32
|
+
default: '',
|
33
|
+
},
|
34
|
+
}
|
35
|
+
|
36
|
+
def self.from_options(namespace, context, options)
|
37
|
+
require "krane/runner_task"
|
38
|
+
runner = ::Krane::RunnerTask.new(
|
39
|
+
namespace: namespace,
|
40
|
+
context: context,
|
41
|
+
global_timeout: ::Krane::DurationParser.new(options["global-timeout"]).parse!.to_i,
|
42
|
+
)
|
43
|
+
|
44
|
+
runner.run!(
|
45
|
+
verify_result: options['verify-result'],
|
46
|
+
template: options['template'],
|
47
|
+
command: options['command'],
|
48
|
+
arguments: options['arguments']&.split(" "),
|
49
|
+
env_vars: options['env-vars'].split(','),
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Krane
|
4
|
+
class ClusterResourceDiscovery
|
5
|
+
delegate :namespace, :context, :logger, to: :@task_config
|
6
|
+
|
7
|
+
def initialize(task_config:, namespace_tags: [])
|
8
|
+
@task_config = task_config
|
9
|
+
@namespace_tags = namespace_tags
|
10
|
+
end
|
11
|
+
|
12
|
+
def crds
|
13
|
+
@crds ||= fetch_crds.map do |cr_def|
|
14
|
+
CustomResourceDefinition.new(namespace: namespace, context: context, logger: logger,
|
15
|
+
definition: cr_def, statsd_tags: @namespace_tags)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def prunable_resources(namespaced:)
|
20
|
+
black_list = %w(Namespace Node ControllerRevision)
|
21
|
+
api_versions = fetch_api_versions
|
22
|
+
|
23
|
+
fetch_resources(namespaced: namespaced).uniq { |r| r['kind'] }.map do |resource|
|
24
|
+
next unless resource['verbs'].one? { |v| v == "delete" }
|
25
|
+
next if black_list.include?(resource['kind'])
|
26
|
+
group_versions = api_versions[resource['apigroup'].to_s]
|
27
|
+
version = version_for_kind(group_versions, resource['kind'])
|
28
|
+
[resource['apigroup'], version, resource['kind']].compact.join("/")
|
29
|
+
end.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
# kubectl api-resources -o wide returns 5 columns
|
33
|
+
# NAME SHORTNAMES APIGROUP NAMESPACED KIND VERBS
|
34
|
+
# SHORTNAMES and APIGROUP may be blank
|
35
|
+
# VERBS is an array
|
36
|
+
# serviceaccounts sa <blank> true ServiceAccount [create delete deletecollection get list patch update watch]
|
37
|
+
def fetch_resources(namespaced: false)
|
38
|
+
command = %w(api-resources)
|
39
|
+
command << "--namespaced=#{namespaced}"
|
40
|
+
raw, _, st = kubectl.run(*command, output: "wide", attempts: 5,
|
41
|
+
use_namespace: false)
|
42
|
+
if st.success?
|
43
|
+
rows = raw.split("\n")
|
44
|
+
header = rows[0]
|
45
|
+
resources = rows[1..-1]
|
46
|
+
full_width_field_names = header.downcase.scan(/[a-z]+[\W]*/)
|
47
|
+
cursor = 0
|
48
|
+
fields = full_width_field_names.each_with_object({}) do |name, hash|
|
49
|
+
start = cursor
|
50
|
+
cursor = start + name.length
|
51
|
+
# Last field should consume the remainder of the line
|
52
|
+
cursor = 0 if full_width_field_names.last == name.strip
|
53
|
+
hash[name.strip] = [start, cursor - 1]
|
54
|
+
end
|
55
|
+
resources.map do |resource|
|
56
|
+
resource = fields.map { |k, (s, e)| [k.strip, resource[s..e].strip] }.to_h
|
57
|
+
# Manually parse verbs: "[get list]" into %w(get list)
|
58
|
+
resource["verbs"] = resource["verbs"][1..-2].split
|
59
|
+
resource
|
60
|
+
end
|
61
|
+
else
|
62
|
+
[]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# kubectl api-versions returns a list of group/version strings e.g. autoscaling/v2beta2
|
69
|
+
# A kind may not exist in all versions of the group.
|
70
|
+
def fetch_api_versions
|
71
|
+
raw, _, st = kubectl.run("api-versions", attempts: 5, use_namespace: false)
|
72
|
+
# The "core" group is represented by an empty string
|
73
|
+
versions = { "" => %w(v1) }
|
74
|
+
if st.success?
|
75
|
+
rows = raw.split("\n")
|
76
|
+
rows.each do |group_version|
|
77
|
+
group, version = group_version.split("/")
|
78
|
+
versions[group] ||= []
|
79
|
+
versions[group] << version
|
80
|
+
end
|
81
|
+
end
|
82
|
+
versions
|
83
|
+
end
|
84
|
+
|
85
|
+
def version_for_kind(versions, kind)
|
86
|
+
# Override list for kinds that don't appear in the lastest version of a group
|
87
|
+
version_override = { "CronJob" => "v1beta1", "VolumeAttachment" => "v1beta1",
|
88
|
+
"CSIDriver" => "v1beta1", "Ingress" => "v1beta1", "CSINode" => "v1beta1" }
|
89
|
+
|
90
|
+
pattern = /v(?<major>\d+)(?<pre>alpha|beta)?(?<minor>\d+)?/
|
91
|
+
latest = versions.sort_by do |version|
|
92
|
+
match = version.match(pattern)
|
93
|
+
pre = { "alpha" => 0, "beta" => 1, nil => 2 }.fetch(match[:pre])
|
94
|
+
[match[:major].to_i, pre, match[:minor].to_i]
|
95
|
+
end.last
|
96
|
+
version_override.fetch(kind, latest)
|
97
|
+
end
|
98
|
+
|
99
|
+
def fetch_crds
|
100
|
+
raw_json, _, st = kubectl.run("get", "CustomResourceDefinition", output: "json", attempts: 5,
|
101
|
+
use_namespace: false)
|
102
|
+
if st.success?
|
103
|
+
JSON.parse(raw_json)["items"]
|
104
|
+
else
|
105
|
+
[]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def kubectl
|
110
|
+
@kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/krane/common.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/object/blank'
|
4
|
+
require 'active_support/core_ext/hash/reverse_merge'
|
5
|
+
require 'active_support/core_ext/hash/slice'
|
6
|
+
require 'active_support/core_ext/numeric/time'
|
7
|
+
require 'active_support/core_ext/string/inflections'
|
8
|
+
require 'active_support/core_ext/string/strip'
|
9
|
+
require 'active_support/core_ext/hash/keys'
|
10
|
+
require 'active_support/core_ext/array/conversions'
|
11
|
+
require 'colorized_string'
|
12
|
+
|
13
|
+
require 'krane/version'
|
14
|
+
require 'krane/oj'
|
15
|
+
require 'krane/errors'
|
16
|
+
require 'krane/formatted_logger'
|
17
|
+
require 'krane/statsd'
|
18
|
+
require 'krane/task_config'
|
19
|
+
require 'krane/task_config_validator'
|
20
|
+
|
21
|
+
module Krane
|
22
|
+
MIN_KUBE_VERSION = '1.11.0'
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Krane
|
4
|
+
module TemplateReporting
|
5
|
+
def record_invalid_template(logger:, err:, filename:, content: nil)
|
6
|
+
debug_msg = ColorizedString.new("Invalid template: #{filename}\n").red
|
7
|
+
debug_msg += "> Error message:\n#{Krane::FormattedLogger.indent_four(err)}"
|
8
|
+
if content
|
9
|
+
debug_msg += if content =~ /kind:\s*Secret/
|
10
|
+
"\n> Template content: Suppressed because it may contain a Secret"
|
11
|
+
else
|
12
|
+
"\n> Template content:\n#{Krane::FormattedLogger.indent_four(content)}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
logger.summary.add_paragraph(debug_msg)
|
16
|
+
end
|
17
|
+
|
18
|
+
def record_warnings(logger:, warning:, filename:)
|
19
|
+
warn_msg = "Template warning: #{filename}\n"
|
20
|
+
warn_msg += "> Warning message:\n#{Krane::FormattedLogger.indent_four(warning)}"
|
21
|
+
logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow)
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_para_from_list(logger:, action:, enum:)
|
25
|
+
logger.summary.add_action(action)
|
26
|
+
logger.summary.add_paragraph(enum.map { |e| "- #{e}" }.join("\n"))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Krane
|
3
|
+
module Concurrency
|
4
|
+
MAX_THREADS = 8
|
5
|
+
|
6
|
+
def self.split_across_threads(all_work, &block)
|
7
|
+
return if all_work.empty?
|
8
|
+
raise ArgumentError, "Block of work is required" unless block_given?
|
9
|
+
|
10
|
+
slice_size = ((all_work.length + MAX_THREADS - 1) / MAX_THREADS)
|
11
|
+
threads = []
|
12
|
+
all_work.each_slice(slice_size) do |work_group|
|
13
|
+
threads << Thread.new { work_group.each(&block) }
|
14
|
+
end
|
15
|
+
threads.each(&:join)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Krane
|
3
|
+
class ContainerLogs
|
4
|
+
attr_reader :lines, :container_name
|
5
|
+
|
6
|
+
DEFAULT_LINE_LIMIT = 250
|
7
|
+
|
8
|
+
def initialize(parent_id:, container_name:, namespace:, context:, logger:)
|
9
|
+
@parent_id = parent_id
|
10
|
+
@container_name = container_name
|
11
|
+
@namespace = namespace
|
12
|
+
@context = context
|
13
|
+
@logger = logger
|
14
|
+
@lines = []
|
15
|
+
@next_print_index = 0
|
16
|
+
@printed_latest = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def sync
|
20
|
+
new_logs = fetch_latest
|
21
|
+
return unless new_logs.present?
|
22
|
+
@lines += sort_and_deduplicate(new_logs)
|
23
|
+
end
|
24
|
+
|
25
|
+
def empty?
|
26
|
+
lines.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def print_latest(prefix: false)
|
30
|
+
prefix_str = "[#{container_name}] " if prefix
|
31
|
+
|
32
|
+
lines[@next_print_index..-1].each do |msg|
|
33
|
+
@logger.info("#{prefix_str}#{msg}")
|
34
|
+
end
|
35
|
+
|
36
|
+
@next_print_index = lines.length
|
37
|
+
@printed_latest = true
|
38
|
+
end
|
39
|
+
|
40
|
+
def print_all
|
41
|
+
lines.each { |line| @logger.info("\t#{line}") }
|
42
|
+
end
|
43
|
+
|
44
|
+
def printing_started?
|
45
|
+
@printed_latest
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def fetch_latest
|
51
|
+
cmd = ["logs", @parent_id, "--container=#{container_name}", "--timestamps"]
|
52
|
+
cmd << if @last_timestamp.present?
|
53
|
+
"--since-time=#{rfc3339_timestamp(@last_timestamp)}"
|
54
|
+
else
|
55
|
+
"--tail=#{DEFAULT_LINE_LIMIT}"
|
56
|
+
end
|
57
|
+
out, _err, _st = kubectl.run(*cmd, log_failure: false)
|
58
|
+
out.split("\n")
|
59
|
+
end
|
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
|
+
def rfc3339_timestamp(time)
|
67
|
+
time.strftime("%FT%T.%N%:z")
|
68
|
+
end
|
69
|
+
|
70
|
+
def sort_and_deduplicate(logs)
|
71
|
+
parsed_lines = logs.map { |line| split_timestamped_line(line) }
|
72
|
+
sorted_lines = parsed_lines.sort do |(timestamp1, _msg1), (timestamp2, _msg2)|
|
73
|
+
if timestamp1.nil?
|
74
|
+
-1
|
75
|
+
elsif timestamp2.nil?
|
76
|
+
1
|
77
|
+
else
|
78
|
+
timestamp1 <=> timestamp2
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
deduped = []
|
83
|
+
sorted_lines.each do |timestamp, msg|
|
84
|
+
next if likely_duplicate?(timestamp)
|
85
|
+
@last_timestamp = timestamp if timestamp
|
86
|
+
deduped << msg
|
87
|
+
end
|
88
|
+
deduped
|
89
|
+
end
|
90
|
+
|
91
|
+
def split_timestamped_line(log_line)
|
92
|
+
timestamp, message = log_line.split(" ", 2)
|
93
|
+
[Time.parse(timestamp), message]
|
94
|
+
rescue ArgumentError
|
95
|
+
# Don't fail on unparsable timestamp
|
96
|
+
[nil, log_line]
|
97
|
+
end
|
98
|
+
|
99
|
+
def likely_duplicate?(timestamp)
|
100
|
+
return false unless @last_timestamp && timestamp
|
101
|
+
# The --since-time granularity the API server supports is not adequate to prevent duplicates
|
102
|
+
# This comparison takes the fractional seconds into account
|
103
|
+
timestamp <= @last_timestamp
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|