tobsch-krane 1.0.0
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 +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
|