Skip to content
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

First pass at shared validation #533

Merged
merged 7 commits into from
Aug 30, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/kubernetes-deploy/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
require 'kubernetes-deploy/errors'
require 'kubernetes-deploy/formatted_logger'
require 'kubernetes-deploy/statsd'
require 'kubernetes-deploy/task_config'
require 'kubernetes-deploy/task_config_validator'

module KubernetesDeploy
MIN_KUBE_VERSION = '1.10.0'
Expand Down
7 changes: 3 additions & 4 deletions lib/kubernetes-deploy/deploy_task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,13 @@ def server_version

def initialize(namespace:, context:, current_sha:, template_dir:, logger: nil, kubectl_instance: nil, bindings: {},
max_watch_seconds: nil, selector: nil)
@logger = logger || KubernetesDeploy::FormattedLogger.build(namespace, context)
@task_config = KubernetesDeploy::TaskConfig.new(context, namespace, @logger)
@namespace = namespace
@namespace_tags = []
@context = context
@current_sha = current_sha
@template_dir = File.expand_path(template_dir)
@logger = logger || KubernetesDeploy::FormattedLogger.build(namespace, context)
@kubectl = kubectl_instance
@max_watch_seconds = max_watch_seconds
@renderer = KubernetesDeploy::Renderer.new(
Expand Down Expand Up @@ -551,9 +552,7 @@ def confirm_cluster_reachable
end
end
raise FatalDeploymentError, "Failed to reach server for #{@context}" unless success
if kubectl.server_version < Gem::Version.new(MIN_KUBE_VERSION)
@logger.warn(KubernetesDeploy::Errors.server_version_warning(server_version))
end
TaskConfigValidator.new(@task_config, kubectl, kubeclient_builder, only: [:validate_server_version]).valid?
end

def confirm_namespace_exists
Expand Down
8 changes: 0 additions & 8 deletions lib/kubernetes-deploy/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,4 @@ def initialize
"kubernetes-deploy will not continue since it is extremely unlikely that this secret should be pruned.")
end
end

module Errors
extend self
def server_version_warning(server_version)
"Minimum cluster version requirement of #{MIN_KUBE_VERSION} not met. "\
"Using #{server_version} could result in unexpected behavior as it is no longer tested against"
end
end
end
25 changes: 13 additions & 12 deletions lib/kubernetes-deploy/restart_task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ def initialize(deployment_name, response)
ANNOTATION = "shipit.shopify.io/restart"

def initialize(context:, namespace:, logger: nil, max_watch_seconds: nil)
@logger = logger || KubernetesDeploy::FormattedLogger.build(namespace, context)
@task_config = KubernetesDeploy::TaskConfig.new(context, namespace, @logger)
@context = context
@namespace = namespace
@logger = logger || KubernetesDeploy::FormattedLogger.build(namespace, context)
@max_watch_seconds = max_watch_seconds
end

Expand All @@ -40,11 +41,9 @@ def perform!(deployments_names = nil, selector: nil)
@logger.reset

@logger.phase_heading("Initializing restart")
verify_namespace
verify_config!
deployments = identify_target_deployments(deployments_names, selector: selector)
if kubectl.server_version < Gem::Version.new(MIN_KUBE_VERSION)
@logger.warn(KubernetesDeploy::Errors.server_version_warning(kubectl.server_version))
end

@logger.phase_heading("Triggering restart by touching ENV[RESTARTED_AT]")
patch_kubeclient_deployments(deployments)

Expand Down Expand Up @@ -120,13 +119,6 @@ def build_watchables(kubeclient_resources, started)
end
end

def verify_namespace
kubeclient.get_namespace(@namespace)
@logger.info("Namespace #{@namespace} found in context #{@context}")
rescue Kubeclient::ResourceNotFoundError
raise NamespaceNotFoundError.new(@namespace, @context)
end

def patch_deployment_with_restart(record)
v1beta1_kubeclient.patch_deployment(
record.metadata.name,
Expand Down Expand Up @@ -176,6 +168,15 @@ def build_patch_payload(deployment)
}
end

def verify_config!
task_config_validator = TaskConfigValidator.new(@task_config, kubectl, kubeclient_builder)
unless task_config_validator.valid?
@logger.summary.add_action("Configuration invalid")
@logger.summary.add_paragraph(task_config_validator.errors.map { |err| "- #{err}" }.join("\n"))
raise KubernetesDeploy::TaskConfigurationError
end
end

def kubeclient
@kubeclient ||= kubeclient_builder.build_v1_kubeclient(@context)
end
Expand Down
5 changes: 2 additions & 3 deletions lib/kubernetes-deploy/runner_task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class TaskTemplateMissingError < TaskConfigurationError; end

def initialize(namespace:, context:, logger: nil, max_watch_seconds: nil)
@logger = logger || KubernetesDeploy::FormattedLogger.build(namespace, context)
@task_config = KubernetesDeploy::TaskConfig.new(context, namespace, @logger)
@namespace = namespace
@context = context
@max_watch_seconds = max_watch_seconds
Expand Down Expand Up @@ -140,9 +141,7 @@ def validate_configuration(task_template, args)
raise TaskConfigurationError, "Configuration invalid: #{errors.join(', ')}"
end

if kubectl.server_version < Gem::Version.new(MIN_KUBE_VERSION)
@logger.warn(KubernetesDeploy::Errors.server_version_warning(kubectl.server_version))
end
TaskConfigValidator.new(@task_config, kubectl, kubeclient_builder, only: [:validate_server_version]).valid?
end

def get_template(template_name)
Expand Down
16 changes: 16 additions & 0 deletions lib/kubernetes-deploy/task_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true
module KubernetesDeploy
class TaskConfig
attr_reader :context, :namespace

def initialize(context, namespace, logger = nil)
@context = context
@namespace = namespace
@logger = logger
end

def logger
@logger ||= KubernetesDeploy::FormattedLogger.build(@namespace, @context)
end
end
end
96 changes: 96 additions & 0 deletions lib/kubernetes-deploy/task_config_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# frozen_string_literal: true
module KubernetesDeploy
class TaskConfigValidator
DEFAULT_VALIDATIONS = %i(
validate_kubeconfig
validate_context_exists_in_kubeconfig
validate_context_reachable
validate_server_version
dturn marked this conversation as resolved.
Show resolved Hide resolved
validate_namespace_exists
).freeze

delegate :context, :namespace, :logger, to: :@task_config

def initialize(task_config, kubectl, kubeclient_builder, only: nil)
@task_config = task_config
@kubectl = kubectl
@kubeclient_builder = kubeclient_builder
@errors = nil
@validations = only || DEFAULT_VALIDATIONS
end

def valid?
@errors = []
@validations.each do |validator_name|
break if @errors.present?
send(validator_name)
end
@errors.empty?
end

def errors
valid?
@errors
end

private

def validate_kubeconfig
@errors += @kubeclient_builder.validate_config_files
end

def validate_context_exists_in_kubeconfig
unless context.present?
return @errors << "Context can not be blank"
end

_, err, st = @kubectl.run("config", "get-contexts", context, "-o", "name",
use_namespace: false, use_context: false, log_failure: false)

unless st.success?
@errors << if err.match("error: context #{context} not found")
"Context #{context} missing from your kubeconfig file(s)"
else
"Something went wrong. #{err} "
end
end
end

def validate_context_reachable
_, err, st = @kubectl.run("get", "namespaces", "-o", "name",
use_namespace: false, log_failure: false)

unless st.success?
@errors << "Something went wrong connectting to #{context}. #{err} "
dturn marked this conversation as resolved.
Show resolved Hide resolved
end
end

def validate_namespace_exists
unless namespace.present?
return @errors << "Namespace can not be blank"
end

_, err, st = @kubectl.run("get", "namespace", "-o", "name", namespace,
use_namespace: false, log_failure: false)

unless st.success?
@errors << if err.match("Error from server [(]NotFound[)]: namespace")
"Cloud not find Namespace: #{namespace} in Context: #{context}"
dturn marked this conversation as resolved.
Show resolved Hide resolved
else
"Could not connect to kubernetes cluster. #{err}"
end
end
end

def validate_server_version
if @kubectl.server_version < Gem::Version.new(MIN_KUBE_VERSION)
logger.warn(server_version_warning(@kubectl.server_version))
end
end

def server_version_warning(server_version)
"Minimum cluster version requirement of #{MIN_KUBE_VERSION} not met. "\
"Using #{server_version} could result in unexpected behavior as it is no longer tested against"
end
end
end
4 changes: 2 additions & 2 deletions test/integration/restart_task_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def test_restart_not_existing_context
assert_restart_failure(restart.perform(%w(web)))
assert_logs_match_all([
"Result: FAILURE",
"`walrus` context must be configured in your kubeconfig file(s)",
/- Context walrus missing from your kubeconfig file\(s\)/,
],
in_order: true)
end
Expand All @@ -188,7 +188,7 @@ def test_restart_not_existing_namespace
assert_restart_failure(restart.perform(%w(web)))
assert_logs_match_all([
"Result: FAILURE",
"Namespace `walrus` not found in context `#{TEST_CONTEXT}`",
"- Cloud not find Namespace: walrus in Context: #{KubeclientHelper::TEST_CONTEXT}",
dturn marked this conversation as resolved.
Show resolved Hide resolved
],
in_order: true)
end
Expand Down
60 changes: 60 additions & 0 deletions test/integration/task_config_validator_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true
require 'integration_test_helper'

class TaskConfigValidatorTest < KubernetesDeploy::IntegrationTest
def test_valid_configuration
assert_predicate(validator(context: KubeclientHelper::TEST_CONTEXT, namespace: 'default'), :valid?)
end

def test_only_is_respected
assert_predicate(validator(only: []), :valid?)
end

def test_invalid_kubeconfig
bad_file = "/IM_NOT_A_REAL_FILE.yml"
builder = KubernetesDeploy::KubeclientBuilder.new(kubeconfig: bad_file)
assert_match("Kube config not found at #{bad_file}",
dturn marked this conversation as resolved.
Show resolved Hide resolved
validator(kubeclient_builder: builder, only: [:validate_kubeconfig]).errors.join("\n"))
end

def test_context_does_not_exists_in_kubeconfig
assert_match(/Context #{task_config.context} missing from your kubeconfig file/,
validator.errors.join("\n"))
end

def test_context_not_reachable
assert_match(/Something went wrong connectting to #{task_config.context}/,
dturn marked this conversation as resolved.
Show resolved Hide resolved
validator(only: [:validate_context_reachable]).errors.join("\n"))
end

def test_namespace_does_not_exists
assert_match(/Cloud not find Namespace: test-namespace in Context: #{KubeclientHelper::TEST_CONTEXT}/,
dturn marked this conversation as resolved.
Show resolved Hide resolved
validator(context: KubeclientHelper::TEST_CONTEXT).errors.join("\n"))
end

def test_invalid_server_version
old_min_version = KubernetesDeploy::MIN_KUBE_VERSION
new_min_version = "99999"
KubernetesDeploy.const_set(:MIN_KUBE_VERSION, new_min_version)
validator(context: KubeclientHelper::TEST_CONTEXT, namespace: 'default', logger: @logger).valid?
assert_logs_match_all([
"Minimum cluster version requirement of #{new_min_version} not met.",
])
ensure
KubernetesDeploy.const_set(:MIN_KUBE_VERSION, old_min_version)
end

private

def validator(context: nil, namespace: nil, logger: nil, kubeclient_builder: nil, only: nil)
config = task_config(context: context, namespace: namespace, logger: logger)
kubectl = KubernetesDeploy::Kubectl.new(namespace: config.namespace,
context: config.context, logger: config.logger, log_failure_by_default: true)
kubeclient_builder ||= KubernetesDeploy::KubeclientBuilder.new
KubernetesDeploy::TaskConfigValidator.new(config, kubectl, kubeclient_builder, only: only)
end

def task_config(context: nil, namespace: nil, logger: nil)
KubernetesDeploy::TaskConfig.new(context || "test-context", namespace || "test-namespace", logger)
end
end
27 changes: 27 additions & 0 deletions test/unit/task_config_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'test_helper'

class TaskConfigTest < KubernetesDeploy::TestCase
def test_responds_to_namespace
assert_equal(task_config.namespace, "test-namespace")
end

def test_responds_to_context
assert_equal(task_config.context, "test-context")
end

def test_builds_a_logger_if_none_provided
assert_equal(task_config.logger.class, KubernetesDeploy::FormattedLogger)
end

def test_uses_provided_logger
logger = KubernetesDeploy::FormattedLogger.build(nil, nil)
assert_equal(task_config(logger: logger).logger, logger)
end

private

def task_config(context: nil, namespace: nil, logger: nil)
dturn marked this conversation as resolved.
Show resolved Hide resolved
KubernetesDeploy::TaskConfig.new(context || "test-context", namespace || "test-namespace", logger)
end
end