-
Notifications
You must be signed in to change notification settings - Fork 115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Batch dry run but like for real this time #943
Changes from 18 commits
d319915
358f91c
e5fe76b
ac10cce
415ff19
1d15998
d58b46a
94d5c77
4abbac6
2e386ed
95f2a6d
924f6c0
40c4d46
3b8cfff
98818e7
7fd0e3f
2c7ad38
7c30468
2bf697d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -142,7 +142,6 @@ def run(**args) | |
def run!(verify_result: true, prune: true) | ||
start = Time.now.utc | ||
@logger.reset | ||
|
||
@logger.phase_heading("Initializing deploy") | ||
validate_configuration(prune: prune) | ||
resources = discover_resources | ||
|
@@ -285,26 +284,29 @@ def validate_configuration(prune:) | |
measure_method(:validate_configuration) | ||
|
||
def validate_resources(resources) | ||
validate_globals(resources) | ||
batch_dry_run_success = validate_dry_run(resources) | ||
resources.select! { |r| r.selected?(@selector) } if @selector_as_filter | ||
validate_globals(resources) | ||
applyables, _ = resources.partition { |r| r.deploy_method == :apply } | ||
batch_dry_run_success = validate_dry_run(applyables) | ||
if batch_dry_run_success | ||
applyables.map { |r| r.server_dry_run_validated = true } | ||
end | ||
Krane::Concurrency.split_across_threads(resources) do |r| | ||
# No need to pass in kubectl (and do per-resource dry run apply) if batch dry run succeeded | ||
if batch_dry_run_success | ||
r.validate_definition(kubectl: nil, selector: @selector, dry_run: false) | ||
else | ||
r.validate_definition(kubectl: kubectl, selector: @selector, dry_run: true) | ||
end | ||
# No need to pass in kubectl as we batch dry run server-side apply above | ||
r.validate_definition(kubectl: nil, selector: @selector, dry_run: false) | ||
Comment on lines
294
to
+296
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We still validate ALL resources, because there are additional |
||
end | ||
|
||
failed_resources = resources.select(&:validation_failed?) | ||
if failed_resources.present? | ||
if failed_resources.present? || !batch_dry_run_success | ||
failed_resources.each do |r| | ||
content = File.read(r.file_path) if File.file?(r.file_path) && !r.sensitive_template_content? | ||
record_invalid_template(logger: @logger, err: r.validation_error_msg, | ||
filename: File.basename(r.file_path), content: content) | ||
end | ||
raise FatalDeploymentError, "Template validation failed" | ||
raise FatalDeploymentError | ||
end | ||
rescue FatalDeploymentError => err | ||
raise FatalDeploymentError, "Template validation failed" | ||
end | ||
measure_method(:validate_resources) | ||
|
||
|
@@ -315,13 +317,15 @@ def validate_globals(resources) | |
end | ||
global_names = FormattedLogger.indent_four(global_names.join("\n")) | ||
|
||
message = "This command is namespaced and cannot be used to deploy global resources. "\ | ||
"Use GlobalDeployTask instead." | ||
@logger.summary.add_paragraph(ColorizedString.new(message).yellow) | ||
@logger.summary.add_paragraph(ColorizedString.new("Global resources:\n#{global_names}").yellow) | ||
raise FatalDeploymentError, "This command is namespaced and cannot be used to deploy global resources. "\ | ||
"Use GlobalDeployTask instead." | ||
raise FatalDeploymentError | ||
Comment on lines
+320
to
+324
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moves around log lines to make the |
||
end | ||
|
||
def validate_dry_run(resources) | ||
resource_deployer.dry_run(resources) | ||
resource_deployer.dry_run!(resources) | ||
end | ||
|
||
def namespace_definition | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,9 +20,13 @@ def initialize(task_config:, prune_allowlist:, global_timeout:, current_sha: nil | |
@statsd_tags = statsd_tags | ||
end | ||
|
||
def dry_run(resources) | ||
def dry_run!(resources) | ||
apply_all(resources, true, dry_run: true) | ||
true | ||
end | ||
|
||
def dry_run(resources) | ||
dry_run!(resources) | ||
rescue FatalDeploymentError | ||
false | ||
end | ||
|
@@ -163,13 +167,15 @@ def apply_all(resources, prune, dry_run: false) | |
global_mode = resources.all?(&:global?) | ||
out, err, st = kubectl.run(*command, log_failure: false, output_is_sensitive: output_is_sensitive, | ||
attempts: 2, use_namespace: !global_mode) | ||
|
||
tags = statsd_tags + (dry_run ? ['dry_run:true'] : ['dry_run:false']) | ||
Krane::StatsD.client.distribution('apply_all.duration', Krane::StatsD.duration(start), tags: tags) | ||
if st.success? | ||
log_pruning(out) if prune | ||
elsif dry_run | ||
record_dry_run_apply_failure(err, resources: resources) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logging is just different enough to require separate methods for the dry-run and actual-run failures |
||
raise FatalDeploymentError, "Command failed: #{Shellwords.join(command)}" | ||
else | ||
record_apply_failure(err, resources: resources) unless dry_run | ||
record_apply_failure(err, resources: resources) | ||
raise FatalDeploymentError, "Command failed: #{Shellwords.join(command)}" | ||
end | ||
end | ||
|
@@ -183,11 +189,52 @@ def log_pruning(kubectl_output) | |
logger.summary.add_action("pruned #{pruned.length} #{'resource'.pluralize(pruned.length)}") | ||
end | ||
|
||
def record_dry_run_apply_failure(err, resources: []) | ||
if err == "error: no objects passed to apply" | ||
heading = ColorizedString.new(err).red | ||
msg = FormattedLogger.indent_four("No resources could be applied: please ensure your label selectors select at least one resource") | ||
logger.summary.add_paragraph("#{heading}\n#{msg}") | ||
return | ||
end | ||
unidentified_errors = [] | ||
filenames_with_sensitive_content = resources | ||
.select(&:sensitive_template_content?) | ||
.map { |r| File.basename(r.file_path) } | ||
|
||
err.each_line do |line| | ||
bad_files = find_bad_files_from_kubectl_output(line) | ||
unless bad_files.present? | ||
unidentified_errors << line | ||
next | ||
end | ||
|
||
bad_files.each do |f| | ||
err_msg = f[:err] | ||
if filenames_with_sensitive_content.include?(f[:filename]) | ||
# Hide the error and template contents in case it has sensitive information | ||
record_invalid_template(logger: logger, err: "SUPPRESSED FOR SECURITY", filename: f[:filename], | ||
content: nil) | ||
else | ||
record_invalid_template(logger: logger, err: err_msg, filename: f[:filename], content: f[:content]) | ||
end | ||
end | ||
end | ||
return unless unidentified_errors.any? | ||
|
||
if resources.none?(&:sensitive_template_content?) | ||
template_contents = resources.map(&:file_path).map { |path| File.read(path) } | ||
record_invalid_template(logger: logger, err: unidentified_errors.join("\n"), filename: "", content: template_contents.join("\n")) | ||
else | ||
warn_msg = "WARNING: There was an error applying some or all resources. The raw output may be sensitive and " \ | ||
"so cannot be displayed." | ||
logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow) | ||
end | ||
end | ||
|
||
def record_apply_failure(err, resources: []) | ||
warn_msg = "WARNING: Any resources not mentioned in the error(s) below were likely created/updated. " \ | ||
"You may wish to roll back this deploy." | ||
logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow) | ||
|
||
unidentified_errors = [] | ||
filenames_with_sensitive_content = resources | ||
.select(&:sensitive_template_content?) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -75,10 +75,9 @@ def test_apply_failure_with_sensitive_resources_hides_template_content | |
refute_logs_match(%r{Kubectl err:.*something/invalid}) | ||
|
||
assert_logs_match_all([ | ||
"Command failed: apply -f", | ||
"Template validation failed", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Previously these errors would occur during the deploy step (since we threw away batch dry-run apply and fell back to client-side validations). Now that we trust batch dry-run, the failure occurs earlier |
||
/Invalid template: Deployment-web.*\.yml/, | ||
]) | ||
|
||
refute_logs_match("kind: Deployment") # content of the sensitive template | ||
end | ||
|
||
|
@@ -526,16 +525,6 @@ def test_resource_discovery_stops_deploys_when_fetch_crds_kubectl_errs | |
], in_order: true) | ||
end | ||
|
||
def test_batch_dry_run_apply_failure_falls_back_to_individual_resource_dry_run_validation | ||
Krane::KubernetesResource.any_instance.expects(:validate_definition).with do |kwargs| | ||
kwargs[:kubectl].is_a?(Krane::Kubectl) && kwargs[:dry_run] | ||
end | ||
deploy_fixtures("hello-cloud", subset: %w(secret.yml)) do |fixtures| | ||
secret = fixtures["secret.yml"]["Secret"].first | ||
secret["bad_field"] = "bad_key" | ||
end | ||
end | ||
|
||
def test_batch_dry_run_apply_success_precludes_individual_resource_dry_run_validation | ||
Krane::KubernetesResource.any_instance.expects(:validate_definition).with { |params| params[:dry_run] == false } | ||
result = deploy_fixtures("hello-cloud", subset: %w(secret.yml)) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
integration tests are flaky and take a long time to run. Adding this extra dimension so we don't have to sit through unit/cli/serial tests each time integration fails