From 8a1aba5d76af4075aa89b704e9cd8eab1576ce68 Mon Sep 17 00:00:00 2001 From: Stefan Budeanu Date: Tue, 3 Oct 2017 10:19:30 -0400 Subject: [PATCH] Runtime resource discovery --- bin/setup | 5 + bin/test | 7 +- dev.yml | 1 + kubernetes-deploy.gemspec | 1 + lib/kubernetes-deploy/deploy_task.rb | 94 ++++--- .../discoverable_resource.rb | 234 ++++++++++++++++++ lib/kubernetes-deploy/kubeclient_builder.rb | 12 +- lib/kubernetes-deploy/kubernetes_resource.rb | 91 ++++--- .../kubernetes_resource/bucket.rb | 2 +- .../kubernetes_resource/cloudsql.rb | 3 + .../kubernetes_resource/config_map.rb | 2 + .../kubernetes_resource/cron_job.rb | 1 + .../customresourcedefinition.rb | 19 ++ .../kubernetes_resource/daemon_set.rb | 1 + .../kubernetes_resource/deployment.rb | 3 +- .../kubernetes_resource/elasticsearch.rb | 2 + .../horizontalpodautoscaler.rb | 19 ++ .../kubernetes_resource/ingress.rb | 1 + .../kubernetes_resource/job.rb | 19 ++ .../kubernetes_resource/memcached.rb | 3 + .../persistent_volume_claim.rb | 1 + .../kubernetes_resource/pod.rb | 4 +- .../kubernetes_resource/pod_set_base.rb | 2 +- .../kubernetes_resource/redis.rb | 3 + .../kubernetes_resource/resource_quota.rb | 2 + .../kubernetes_resource/service.rb | 1 + .../kubernetes_resource/service_account.rb | 1 + .../kubernetes_resource/stateful_set.rb | 1 + .../kubernetes_resource/statefulservice.rb | 3 + lib/kubernetes-deploy/sync_mediator.rb | 2 +- .../resource-discovery/definitions/crd.yml | 21 ++ .../definitions/crd_invalid_metadata.yml | 16 ++ .../definitions/crd_invalid_query.yml | 17 ++ .../crd_non_prunable_no_predeploy.yml | 17 ++ .../resource-discovery/instances/crd.yml | 6 + test/helpers/kubeclient_helper.rb | 4 + .../resource_discovery_test.rb | 103 ++++++++ .../kubernetes-deploy/deploy_task_test.rb | 75 +++++- .../resource_watcher_test.rb | 2 +- test/unit/sync_mediator_test.rb | 10 +- 40 files changed, 721 insertions(+), 90 deletions(-) create mode 100644 lib/kubernetes-deploy/discoverable_resource.rb create mode 100644 lib/kubernetes-deploy/kubernetes_resource/customresourcedefinition.rb create mode 100644 lib/kubernetes-deploy/kubernetes_resource/horizontalpodautoscaler.rb create mode 100644 lib/kubernetes-deploy/kubernetes_resource/job.rb create mode 100644 test/fixtures/resource-discovery/definitions/crd.yml create mode 100644 test/fixtures/resource-discovery/definitions/crd_invalid_metadata.yml create mode 100644 test/fixtures/resource-discovery/definitions/crd_invalid_query.yml create mode 100644 test/fixtures/resource-discovery/definitions/crd_non_prunable_no_predeploy.yml create mode 100644 test/fixtures/resource-discovery/instances/crd.yml create mode 100644 test/integration-serial/resource_discovery_test.rb diff --git a/bin/setup b/bin/setup index 39941ba79..9cd1b79d2 100755 --- a/bin/setup +++ b/bin/setup @@ -4,6 +4,11 @@ IFS=$'\n\t' bundle install +if [ ! -x "$(which jq)" ]; then + echo -e "\n\033[0;33mPlease install jq: https://stedolan.github.io/jq/download/\033[0m" + exit 1 +fi + if [ ! -x "$(which minikube)" ]; then echo -e "\n\033[0;33mIf you're going to run the tests, please follow the minikube setup instructions for your operating system:\nhttps://kubernetes.io/docs/getting-started-guides/minikube/#installation\033[0m" fi diff --git a/bin/test b/bin/test index d7ac8c4bb..4c7a7c2fc 100755 --- a/bin/test +++ b/bin/test @@ -27,9 +27,14 @@ if [[ ${PARALLELISM:=0} -lt 1 ]]; then fi fi +if [[ ${CI:="0"} == "1" ]]; then + echo "--- :linux: Install jq" + wget -qP /usr/local/bin http://stedolan.github.io/jq/download/linux64/jq && chmod a+x /usr/local/bin/jq +fi + if [[ ${CI:="0"} == "1" ]]; then echo "--- :ruby: Bundle Install" - bundle install --jobs 4 + bundle install --jobs 4 || exit 1 fi print_header "Run Unit Tests" diff --git a/dev.yml b/dev.yml index 57a5c2abc..89266d93b 100644 --- a/dev.yml +++ b/dev.yml @@ -5,6 +5,7 @@ up: - bundler - homebrew: - Caskroom/cask/minikube + - jq - custom: name: Minikube Cluster met?: test $(minikube status | grep Running | wc -l) -eq 2 && $(minikube status | grep -q 'Correctly Configured') diff --git a/kubernetes-deploy.gemspec b/kubernetes-deploy.gemspec index 4e7672f3f..8828cc3dc 100644 --- a/kubernetes-deploy.gemspec +++ b/kubernetes-deploy.gemspec @@ -29,6 +29,7 @@ Gem::Specification.new do |spec| spec.add_dependency "ejson", "1.0.1" spec.add_dependency "colorize", "~> 0.8" spec.add_dependency "statsd-instrument", "~> 2.1" + spec.add_dependency "rgl", "0.5.3" spec.add_development_dependency "bundler" spec.add_development_dependency "rake", "~> 10.0" diff --git a/lib/kubernetes-deploy/deploy_task.rb b/lib/kubernetes-deploy/deploy_task.rb index 67fc6e07c..5b5ebf05f 100644 --- a/lib/kubernetes-deploy/deploy_task.rb +++ b/lib/kubernetes-deploy/deploy_task.rb @@ -4,7 +4,11 @@ require 'shellwords' require 'tempfile' require 'fileutils' +require 'rgl/adjacency' +require 'rgl/topsort' +require 'kubernetes-deploy/discoverable_resource' require 'kubernetes-deploy/kubernetes_resource' + %w( cloudsql config_map @@ -27,6 +31,7 @@ bucket stateful_set cron_job + customresourcedefinition ).each do |subresource| require "kubernetes-deploy/kubernetes_resource/#{subresource}" end @@ -40,16 +45,6 @@ module KubernetesDeploy class DeployTask include KubeclientBuilder - PREDEPLOY_SEQUENCE = %w( - ResourceQuota - Cloudsql - Redis - Memcached - ConfigMap - PersistentVolumeClaim - ServiceAccount - Pod - ) PROTECTED_NAMESPACES = %w( default kube-system @@ -64,23 +59,48 @@ class DeployTask # extensions/v1beta1/ReplicaSet -- managed by deployments # core/v1/Secret -- should not committed / managed by shipit def prune_whitelist - wl = %w( - core/v1/ConfigMap - core/v1/Pod - core/v1/Service - core/v1/ResourceQuota - batch/v1/Job - extensions/v1beta1/DaemonSet - extensions/v1beta1/Deployment - apps/v1beta1/Deployment - extensions/v1beta1/Ingress - apps/v1beta1/StatefulSet - autoscaling/v1/HorizontalPodAutoscaler - ) - if server_version >= Gem::Version.new('1.8.0') - wl << "batch/v1beta1/CronJob" + @prune_whitelist ||= _build_prune_whitelist + end + + def _build_prune_whitelist + prunable_resources = all_resources.select(&:prunable?) + prunable_resources.map(&:qualified_kind) + end + + def predeploy_sequence + @predeploy_sequence ||= _build_predeploy_sequence + end + + def _build_predeploy_sequence + # Express dependencies as DAG + graph = RGL::DirectedAdjacencyGraph.new + graph.add_vertex(:ROOT_NODE) + + # This is a partially ordered set, so we must make sure everything is reachable: + predeploy_resources = all_resources.select(&:predeploy?) + predeploy_resources.each { |res| graph.add_edge(:ROOT_NODE, res.kind) } + + # Find resources that have explicit predeploy (inter-)dependencies: + predeploy_res_with_deps = all_resources.select(&:predeploy_dependencies) + predeploy_res_with_deps.each do |res| + # Edge [A,B] means B requires A to be deployed first + res.predeploy_dependencies.each { |dep| graph.add_edge(dep, res.kind) } + end + + raise FatalDeploymentError, "Cyclic predeploy requirements: #{graph.cycles.flatten}" unless graph.cycles.empty? + + # Topological sort is not unique, but will respect the requirements + predeploy_order = graph.topsort_iterator.to_a + predeploy_order.delete(:ROOT_NODE) + predeploy_order + end + + def all_resources + resources = DiscoverableResource.all + KubernetesResource.all + # Omit unqualified kinds (they are unsupported on this cluster, or discovery hasn't been performed yet) + resources.select do |res| + res.constants.include?(:GROUP) && res.constants.include?(:VERSION) end - wl end def server_version @@ -124,7 +144,8 @@ def run!(verify_result: true, allow_protected_ns: false, prune: true) confirm_context_exists confirm_namespace_exists @namespace_tags |= tags_from_namespace_labels - resources = discover_resources + discover_resources + resources = load_resource_from_file validate_definitions(resources) @logger.phase_heading("Checking initial resource statuses") @@ -190,12 +211,12 @@ def run!(verify_result: true, allow_protected_ns: false, prune: true) private def deploy_has_priority_resources?(resources) - resources.any? { |r| PREDEPLOY_SEQUENCE.include?(r.type) } + resources.any? { |r| predeploy_sequence.include?(r.kind) } end def predeploy_priority_resources(resource_list) - PREDEPLOY_SEQUENCE.each do |resource_type| - matching_resources = resource_list.select { |r| r.type == resource_type } + predeploy_sequence.each do |resource_kind| + matching_resources = resource_list.select { |r| r.kind == resource_kind } next if matching_resources.empty? deploy_resources(matching_resources, verify: true, record_summary: false) @@ -225,6 +246,13 @@ def validate_definitions(resources) end def discover_resources + # (Lazily) rebuild these lists after discovery if they were present. + @predeploy_sequence = nil + @prune_whitelist = nil + DiscoverableResource.discover(context: @context, logger: @logger, server_version: server_version) + end + + def load_resource_from_file resources = [] @logger.info("Discovering templates:") @@ -232,8 +260,8 @@ def discover_resources next unless filename.end_with?(".yml.erb", ".yml", ".yaml", ".yaml.erb") split_templates(filename) do |r_def| - r = KubernetesResource.build(namespace: @namespace, context: @context, logger: @logger, - definition: r_def, statsd_tags: @namespace_tags) + r = DiscoverableResource.build(namespace: @namespace, context: @context, logger: @logger, + definition: r_def, statsd_tags: @namespace_tags) resources << r @logger.info " - #{r.id}" end @@ -385,7 +413,7 @@ def apply_all(resources, prune) if prune command.push("--prune", "--all") - prune_whitelist.each { |type| command.push("--prune-whitelist=#{type}") } + prune_whitelist.each { |kind| command.push("--prune-whitelist=#{kind}") } end out, err, st = kubectl.run(*command, log_failure: false) diff --git a/lib/kubernetes-deploy/discoverable_resource.rb b/lib/kubernetes-deploy/discoverable_resource.rb new file mode 100644 index 000000000..0fe42f16f --- /dev/null +++ b/lib/kubernetes-deploy/discoverable_resource.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true +require 'kubernetes-deploy/kubernetes_resource' +require 'kubernetes-deploy/kubeclient_builder' +require 'erb' +require 'json' +require 'open3' + +module KubernetesDeploy + class DiscoverableResource < KubernetesResource + extend KubernetesDeploy::KubeclientBuilder + + TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set + DEPLOY_METADATA_ANNOTATION = 'kubernetes-deploy.shopify.io/metadata' + + def self.inherited(child_class) + DiscoverableResource.child_classes.add(child_class) + end + + def self.discover(context:, logger:, server_version:) + logger.info("Discovering custom resources:") + with_retries { discover_groups(context) } + if server_version >= Gem::Version.new('1.7.0') + kube_client = v1beta1_crd_kubeclient(context) + with_retries { discover_crd(logger: logger, client: kube_client) } + end + end + + def self.discover_groups(context) + kinds = discover_kinds(context) + kinds.each_pair do |key, val| + klass = get_static_class(kind: key) + next unless klass + klass.const_set(:GROUP, val[:group]) unless klass.constants.include?(:GROUP) + klass.const_set(:VERSION, val[:version]) unless klass.constants.include?(:VERSION) + end + end + + def self.discover_kinds(context) + kinds = {} + + # At the top level there is the core group (everything below /api/v1), + rest_client = v1_kubeclient(context).create_rest_client + raw_json = rest_client['v1'].get(rest_client.headers) + resource_list = JSON.parse(raw_json) + v1_group_version = { group: 'core', version: 'v1' } + resource_list['resources'].map do |res| + kind = res['kind'] + kinds[kind] = v1_group_version + end + + # ...and the named groups (at path /apis/$NAME/$VERSION) + rest_client = apis_kubeclient(context).create_rest_client + raw_json = rest_client.get(rest_client.headers) + group_list = JSON.parse(raw_json) + group_list = group_list['groups'] + + # Map out all detected kinds to their (preferred) group version + group_list.each do |group| + preferred_version = group['preferredVersion']['groupVersion'] + all_versions = group['versions'].map { |version| version['groupVersion'] } + # Make sure the preferred version gets checked first. + all_versions.delete(preferred_version) + all_versions.unshift(preferred_version) + + # Grab kinds from all versions + all_versions.each do |group_version| + raw_response = rest_client[group_version].get(rest_client.headers) + json_response = JSON.parse(raw_response) + resources = json_response['resources'] + resources.each do |res| + kind = res['kind'] + next if kinds.key?(kind) # Respect the preferred version + group, _, version = group_version.rpartition('/') + kinds[kind] = { group: group, version: version } + end + end + end + + kinds + end + + def self.build(namespace:, context:, definition:, logger:, statsd_tags:) + opts = { namespace: namespace, context: context, definition: definition, logger: logger, statsd_tags: statsd_tags } + kind = definition["kind"] + group, _, version = definition['apiVersion'].rpartition('/') + + raise InvalidTemplateError.new("Template missing 'Kind'", content: definition.to_yaml) if definition["kind"].blank? + + klass = get_static_class(kind: kind) + klass = get_dynamic_class(group: group, version: version, kind: kind) unless klass + klass.new(**opts) + end + + def self.get_static_class(kind:) + KubernetesDeploy.const_get(kind) if KubernetesDeploy.const_defined?(kind) + end + + def self.get_dynamic_class(group:, version:, kind:) + unless DiscoverableResource.const_defined?(kind) + generate_resource(group: group, version: version, kind: kind, annotations: {}) + end + DiscoverableResource.const_get(kind) + end + + def self.with_retries(retries = 3, backoff = 10) + yield + rescue KubeException => err + if (retries -= 1) > 0 + logger.warn("Retrying to discover CustomResourceDefinitions: #{err}") + sleep(backoff) + retry + else + logger.warn("Unable to discover CustomResourceDefinitions: #{err}") + end + end + + def self.discover_crd(logger:, client:) + @child_classes = Set.new + resources = client.get_custom_resource_definitions + resources.each do |res| + kind = res.spec.names.kind + # Remove and redefine the class if it already exists so we can be up to date. + if DiscoverableResource.const_defined?(kind) + klass = DiscoverableResource.const_get(kind) + DiscoverableResource.send(:remove_const, kind) + child_classes.delete(klass) + end + generate_resource(group: res.spec.group, + version: res.spec.version, + kind: kind, + annotations: res.metadata.annotations) + logger.info(" - #{res.spec.group}/#{res.spec.version}/#{kind}") + end + end + + def self.generate_resource(group:, version:, kind:, annotations:) + deploy_metadata = annotations[DEPLOY_METADATA_ANNOTATION] || '{}' + metadata = JSON.parse(deploy_metadata) + unless metadata.is_a?(Hash) + raise FatalDeploymentError, "Invalid metadata for #{kind} #{metadata.inspect}" + end + + predeploy_dependencies = [] << metadata['predeploy-dependencies'] + predeploy_dependencies.flatten! + predeploy_dependencies.compact! + + prunable = parse_bool(metadata['prunable']) + predeploy = parse_bool(metadata['predeploy']) + status_query = metadata['status-query'] + success_status = metadata['status-success'] + + resource_template = ERB.new <<-CLASS + class #{kind.capitalize} < DiscoverableResource + GROUP = '#{group}' + VERSION = '#{version}' + PREDEPLOY = #{predeploy} + PRUNABLE = #{prunable} + + <% unless predeploy_dependencies.empty? %> + PREDEPLOY_DEPENDENCIES = #{predeploy_dependencies} + <% end %> + + <% if status_query && success_status %> + def deploy_succeeded? + getter = "get_#{kind.downcase}" + @client ||= DiscoverableResource.kubeclient(context: @context, resource_class: self.class) + raw_json = @client.send(getter, @name, @namespace, as: :raw) + result = query_json(query: STATUS_QUERY, json: raw_json) + result == SUCCESS_STATUS + end + <% end %> + + self + end + CLASS + + rendered_template = resource_template.result(binding) + klass = class_eval(rendered_template) + klass.const_set(:STATUS_QUERY, status_query) + klass.const_set(:SUCCESS_STATUS, success_status) + klass + rescue JSON::ParserError => e + raise FatalDeploymentError, "Invalid metadata for #{group}/#{version}/#{kind}: #{e}" + end + + def self.parse_bool(value) + return true if TRUE_VALUES.include?(value) + false + end + + def query_json(query:, json:) + safe_query = Shellwords.escape(query) + out, err, status = Open3.capture3("jq -r #{safe_query}", stdin_data: json) + unless status.success? + str = "Invalid status query '#{query}' for #{self.class.qualified_kind}: #{err}" + raise FatalDeploymentError, str + end + out.strip # Trailing newlines + end + + def self.kubeclient(context:, resource_class:) + _build_kubeclient( + api_version: resource_class.version, + context: context, + endpoint_path: "/apis/#{resource_class.group}" + ) + end + + def self.apis_kubeclient(context) + @apis_kubeclient ||= _build_kubeclient( + api_version: '', # The apis endpoint is not versioned + context: context, + endpoint_path: "/apis", + discover: false # Will fail on apis endpoint + ) + end + + def self.v1_kubeclient(context) + @v1_kubeclient ||= build_v1_kubeclient(context) + end + + def self.v1beta1_kubeclient(context) + @v1beta1_kubeclient ||= build_v1beta1_kubeclient(context) + end + + def self.v1beta1_crd_kubeclient(context) + @v1beta1_kubeclient_crd ||= _build_kubeclient( + api_version: "v1beta1", + context: context, + endpoint_path: "/apis/apiextensions.k8s.io/" + ) + end + end +end diff --git a/lib/kubernetes-deploy/kubeclient_builder.rb b/lib/kubernetes-deploy/kubeclient_builder.rb index b5d3693fd..0aed6366e 100644 --- a/lib/kubernetes-deploy/kubeclient_builder.rb +++ b/lib/kubernetes-deploy/kubeclient_builder.rb @@ -52,7 +52,15 @@ def build_apps_v1beta1_kubeclient(context) ) end - def _build_kubeclient(api_version:, context:, endpoint_path: nil) + def build_apiextensions_v1beta1_kubeclient(context) + _build_kubeclient( + api_version: "v1beta1", + context: context, + endpoint_path: "/apis/apiextensions.k8s.io" + ) + end + + def _build_kubeclient(api_version:, context:, endpoint_path: nil, discover: true) # Find a context defined in kube conf files that matches the input context by name friendly_configs = config_files.map { |f| GoogleFriendlyConfig.read(f) } config = friendly_configs.find { |c| c.contexts.include?(context) } @@ -67,7 +75,7 @@ def _build_kubeclient(api_version:, context:, endpoint_path: nil) ssl_options: kube_context.ssl_options, auth_options: kube_context.auth_options ) - client.discover + client.discover if discover client end diff --git a/lib/kubernetes-deploy/kubernetes_resource.rb b/lib/kubernetes-deploy/kubernetes_resource.rb index 1bcefa45d..f74950c2e 100644 --- a/lib/kubernetes-deploy/kubernetes_resource.rb +++ b/lib/kubernetes-deploy/kubernetes_resource.rb @@ -6,7 +6,7 @@ module KubernetesDeploy class KubernetesResource attr_reader :name, :namespace, :context - attr_writer :type, :deploy_started_at + attr_writer :deploy_started_at TIMEOUT = 5.minutes LOG_LINE_COUNT = 250 @@ -27,29 +27,56 @@ class KubernetesResource TIMEOUT_OVERRIDE_ANNOTATION = "kubernetes-deploy.shopify.io/timeout-override" - class << self - def build(namespace:, context:, definition:, logger:, statsd_tags:) - opts = { namespace: namespace, context: context, definition: definition, logger: logger, - statsd_tags: statsd_tags } - if definition["kind"].blank? - raise InvalidTemplateError.new("Template missing 'Kind'", content: definition.to_yaml) - elsif KubernetesDeploy.const_defined?(definition["kind"]) - klass = KubernetesDeploy.const_get(definition["kind"]) - klass.new(**opts) - else - inst = new(**opts) - inst.type = definition["kind"] - inst - end - end + def self.inherited(child_class) + KubernetesResource.child_classes.add(child_class) + end - def timeout - self::TIMEOUT - end + def self.child_classes + @child_classes ||= Set.new + end - def kind - name.demodulize - end + def self.all + child_classes.dup + end + + def self.prunable? + self::PRUNABLE if defined? self::PRUNABLE + end + + def self.predeploy? + self::PREDEPLOY if defined? self::PREDEPLOY + end + + def self.predeploy_dependencies + self::PREDEPLOY_DEPENDENCIES if defined? self::PREDEPLOY_DEPENDENCIES + end + + def self.group + self::GROUP + end + + def self.version + self::VERSION + end + + def self.kind + name.demodulize + end + + def self.qualified_kind + "#{group}/#{version}/#{kind}" + end + + def kind + self.class.kind + end + + def id + "#{kind}/#{name}" + end + + def self.timeout + self::TIMEOUT end def timeout @@ -105,16 +132,12 @@ def validation_failed? @validation_errors.present? end - def id - "#{type}/#{name}" - end - def file_path file.path end def sync(mediator) - @instance_data = mediator.get_instance(type, name) + @instance_data = mediator.get_instance(kind, name) end def deploy_failed? @@ -128,7 +151,7 @@ def deploy_started? def deploy_succeeded? return false unless deploy_started? unless @success_assumption_warning_shown - @logger.warn("Don't know how to monitor resources of type #{type}. Assuming #{id} deployed successfully.") + @logger.warn("Don't know how to monitor resources of type #{kind}. Assuming #{id} deployed successfully.") @success_assumption_warning_shown = true end true @@ -142,10 +165,6 @@ def status exists? ? "Exists" : "Unknown" end - def type - @type || self.class.kind - end - def deploy_timed_out? return false unless deploy_started? !deploy_succeeded? && !deploy_failed? && (Time.now.utc - @deploy_started_at > timeout) @@ -226,7 +245,7 @@ def debug_message(cause = nil, info_hash = {}) # } def fetch_events(kubectl) return {} unless exists? - out, _err, st = kubectl.run("get", "events", "--output=go-template=#{Event.go_template_for(type, name)}", + out, _err, st = kubectl.run("get", "events", "--output=go-template=#{Event.go_template_for(kind, name)}", log_failure: false) return {} unless st.success? @@ -339,7 +358,7 @@ def file end def create_definition_tempfile - file = Tempfile.new(["#{type}-#{name}", ".yml"]) + file = Tempfile.new(["#{kind}-#{name}", ".yml"]) file.write(YAML.dump(@definition)) file ensure @@ -360,8 +379,8 @@ def statsd_tags else "unknown" end - tags = %W(context:#{context} namespace:#{namespace} resource:#{id} - type:#{type} sha:#{ENV['REVISION']} status:#{status}) + tags = %W(context:#{context} namespace:#{namespace} resource:#{id} type:#{kind} sha:#{ENV['REVISION']} status:#{status} + sha:#{ENV['REVISION']} status:#{status}) tags | @optional_statsd_tags end end diff --git a/lib/kubernetes-deploy/kubernetes_resource/bucket.rb b/lib/kubernetes-deploy/kubernetes_resource/bucket.rb index 9dd2abdcd..710c6e6bc 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/bucket.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/bucket.rb @@ -5,7 +5,7 @@ def deploy_succeeded? return false unless deploy_started? unless @success_assumption_warning_shown - @logger.warn("Don't know how to monitor resources of type #{type}. Assuming #{id} deployed successfully.") + @logger.warn("Don't know how to monitor resources of type #{kind}. Assuming #{id} deployed successfully.") @success_assumption_warning_shown = true end true diff --git a/lib/kubernetes-deploy/kubernetes_resource/cloudsql.rb b/lib/kubernetes-deploy/kubernetes_resource/cloudsql.rb index 4b92d4073..aadae8fc9 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/cloudsql.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/cloudsql.rb @@ -2,6 +2,9 @@ module KubernetesDeploy class Cloudsql < KubernetesResource TIMEOUT = 10.minutes + PREDEPLOY = true + GROUP = 'stable.shopify.io' + VERSION = 'v1' SYNC_DEPENDENCIES = %w(Deployment Service) def sync(mediator) diff --git a/lib/kubernetes-deploy/kubernetes_resource/config_map.rb b/lib/kubernetes-deploy/kubernetes_resource/config_map.rb index 8aabec341..9c759a6a8 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/config_map.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/config_map.rb @@ -2,6 +2,8 @@ module KubernetesDeploy class ConfigMap < KubernetesResource TIMEOUT = 30.seconds + PREDEPLOY = true + PRUNABLE = true def deploy_succeeded? exists? diff --git a/lib/kubernetes-deploy/kubernetes_resource/cron_job.rb b/lib/kubernetes-deploy/kubernetes_resource/cron_job.rb index 3d8aeb210..d217f2668 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/cron_job.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/cron_job.rb @@ -2,6 +2,7 @@ module KubernetesDeploy class CronJob < KubernetesResource TIMEOUT = 30.seconds + PRUNABLE = true def deploy_succeeded? exists? diff --git a/lib/kubernetes-deploy/kubernetes_resource/customresourcedefinition.rb b/lib/kubernetes-deploy/kubernetes_resource/customresourcedefinition.rb new file mode 100644 index 000000000..cdf16abab --- /dev/null +++ b/lib/kubernetes-deploy/kubernetes_resource/customresourcedefinition.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +module KubernetesDeploy + class CustomResourceDefinition < KubernetesResource + TIMEOUT = 10.seconds + PREDEPLOY = true + + def deploy_succeeded? + exists? + end + + def deploy_failed? + false + end + + def timeout_message + UNUSUAL_FAILURE_MESSAGE + end + end +end diff --git a/lib/kubernetes-deploy/kubernetes_resource/daemon_set.rb b/lib/kubernetes-deploy/kubernetes_resource/daemon_set.rb index c1f302af6..fc611a0e3 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/daemon_set.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/daemon_set.rb @@ -3,6 +3,7 @@ module KubernetesDeploy class DaemonSet < PodSetBase TIMEOUT = 5.minutes + PRUNABLE = true attr_reader :pods SYNC_DEPENDENCIES = %w(Pod) diff --git a/lib/kubernetes-deploy/kubernetes_resource/deployment.rb b/lib/kubernetes-deploy/kubernetes_resource/deployment.rb index e845a1c86..fe0c7fcb1 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/deployment.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/deployment.rb @@ -5,6 +5,7 @@ class Deployment < KubernetesResource REQUIRED_ROLLOUT_ANNOTATION = 'kubernetes-deploy.shopify.io/required-rollout' REQUIRED_ROLLOUT_TYPES = %w(maxUnavailable full none).freeze DEFAULT_REQUIRED_ROLLOUT = 'full' + PRUNABLE = true SYNC_DEPENDENCIES = %w(Pod ReplicaSet) def sync(mediator) @@ -63,7 +64,7 @@ def timeout_message reason_msg = if progress_condition.present? "Timeout reason: #{progress_condition['reason']}" else - "Timeout reason: hard deadline for #{type}" + "Timeout reason: hard deadline for #{kind}" end return reason_msg unless @latest_rs.present? "#{reason_msg}\nLatest ReplicaSet: #{@latest_rs.name}\n\n#{@latest_rs.timeout_message}" diff --git a/lib/kubernetes-deploy/kubernetes_resource/elasticsearch.rb b/lib/kubernetes-deploy/kubernetes_resource/elasticsearch.rb index 52caa9df3..12b8081ba 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/elasticsearch.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/elasticsearch.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module KubernetesDeploy class Elasticsearch < KubernetesResource + GROUP = 'stable.shopify.io' + VERSION = 'v1' def deploy_succeeded? super # success assumption, with warning end diff --git a/lib/kubernetes-deploy/kubernetes_resource/horizontalpodautoscaler.rb b/lib/kubernetes-deploy/kubernetes_resource/horizontalpodautoscaler.rb new file mode 100644 index 000000000..525289f15 --- /dev/null +++ b/lib/kubernetes-deploy/kubernetes_resource/horizontalpodautoscaler.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +module KubernetesDeploy + class HorizontalPodAutoscaler < KubernetesResource + PRUNABLE = true + TIMEOUT = 30.seconds + + def deploy_succeeded? + exists? + end + + def deploy_failed? + !exists? + end + + def timeout_message + UNUSUAL_FAILURE_MESSAGE + end + end +end diff --git a/lib/kubernetes-deploy/kubernetes_resource/ingress.rb b/lib/kubernetes-deploy/kubernetes_resource/ingress.rb index 7cf9d2d83..7c2036883 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/ingress.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/ingress.rb @@ -2,6 +2,7 @@ module KubernetesDeploy class Ingress < KubernetesResource TIMEOUT = 30.seconds + PRUNABLE = true def status exists? ? "Created" : "Unknown" diff --git a/lib/kubernetes-deploy/kubernetes_resource/job.rb b/lib/kubernetes-deploy/kubernetes_resource/job.rb new file mode 100644 index 000000000..b8b53c04f --- /dev/null +++ b/lib/kubernetes-deploy/kubernetes_resource/job.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +module KubernetesDeploy + class Job < KubernetesResource + PRUNABLE = true + TIMEOUT = 30.seconds + + def deploy_succeeded? + exists? + end + + def deploy_failed? + !exists? + end + + def timeout_message + UNUSUAL_FAILURE_MESSAGE + end + end +end diff --git a/lib/kubernetes-deploy/kubernetes_resource/memcached.rb b/lib/kubernetes-deploy/kubernetes_resource/memcached.rb index 2a10ad1b5..129c1058d 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/memcached.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/memcached.rb @@ -2,7 +2,10 @@ module KubernetesDeploy class Memcached < KubernetesResource TIMEOUT = 5.minutes + PREDEPLOY = true CONFIGMAP_NAME = "memcached-url" + GROUP = 'stable.shopify.io' + VERSION = 'v1' SYNC_DEPENDENCIES = %w(Deployment Service ConfigMap) def sync(mediator) diff --git a/lib/kubernetes-deploy/kubernetes_resource/persistent_volume_claim.rb b/lib/kubernetes-deploy/kubernetes_resource/persistent_volume_claim.rb index 9593f3b9b..c1530b1cd 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/persistent_volume_claim.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/persistent_volume_claim.rb @@ -2,6 +2,7 @@ module KubernetesDeploy class PersistentVolumeClaim < KubernetesResource TIMEOUT = 5.minutes + PREDEPLOY = true def status exists? ? @instance_data["status"]["phase"] : "Unknown" diff --git a/lib/kubernetes-deploy/kubernetes_resource/pod.rb b/lib/kubernetes-deploy/kubernetes_resource/pod.rb index 529206249..2fe21cebc 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/pod.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/pod.rb @@ -2,7 +2,9 @@ module KubernetesDeploy class Pod < KubernetesResource TIMEOUT = 10.minutes - + PREDEPLOY = true + PREDEPLOY_DEPENDENCIES = %w(ResourceQuota ServiceAccount ConfigMap PersistentVolumeClaim) + PRUNABLE = true FAILED_PHASE_NAME = "Failed" def initialize(namespace:, context:, definition:, logger:, diff --git a/lib/kubernetes-deploy/kubernetes_resource/pod_set_base.rb b/lib/kubernetes-deploy/kubernetes_resource/pod_set_base.rb index b103183ba..74114b756 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/pod_set_base.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/pod_set_base.rb @@ -57,7 +57,7 @@ def find_pods(mediator) context: context, definition: pod_data, logger: @logger, - parent: "#{name.capitalize} #{type}", + parent: "#{name.capitalize} #{kind}", deploy_started_at: @deploy_started_at ) pod.sync(mediator) diff --git a/lib/kubernetes-deploy/kubernetes_resource/redis.rb b/lib/kubernetes-deploy/kubernetes_resource/redis.rb index 446981988..fa512be9a 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/redis.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/redis.rb @@ -2,7 +2,10 @@ module KubernetesDeploy class Redis < KubernetesResource TIMEOUT = 5.minutes + PREDEPLOY = true UUID_ANNOTATION = "redis.stable.shopify.io/owner_uid" + GROUP = 'stable.shopify.io' + VERSION = 'v1' SYNC_DEPENDENCIES = %w(Deployment Service) def sync(mediator) diff --git a/lib/kubernetes-deploy/kubernetes_resource/resource_quota.rb b/lib/kubernetes-deploy/kubernetes_resource/resource_quota.rb index d8aebb5c8..f69510436 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/resource_quota.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/resource_quota.rb @@ -2,6 +2,8 @@ module KubernetesDeploy class ResourceQuota < KubernetesResource TIMEOUT = 30.seconds + PREDEPLOY = true + PRUNABLE = true def status exists? ? "In effect" : "Unknown" diff --git a/lib/kubernetes-deploy/kubernetes_resource/service.rb b/lib/kubernetes-deploy/kubernetes_resource/service.rb index 7bde0f038..dc709223c 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/service.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/service.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module KubernetesDeploy class Service < KubernetesResource + PRUNABLE = true TIMEOUT = 7.minutes SYNC_DEPENDENCIES = %w(Pod Deployment) diff --git a/lib/kubernetes-deploy/kubernetes_resource/service_account.rb b/lib/kubernetes-deploy/kubernetes_resource/service_account.rb index 2b4548afa..877c5b562 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/service_account.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/service_account.rb @@ -2,6 +2,7 @@ module KubernetesDeploy class ServiceAccount < KubernetesResource TIMEOUT = 30.seconds + PREDEPLOY = true def status exists? ? "Created" : "Unknown" diff --git a/lib/kubernetes-deploy/kubernetes_resource/stateful_set.rb b/lib/kubernetes-deploy/kubernetes_resource/stateful_set.rb index b489233f6..5dce5a603 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/stateful_set.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/stateful_set.rb @@ -3,6 +3,7 @@ module KubernetesDeploy class StatefulSet < PodSetBase TIMEOUT = 10.minutes + PRUNABLE = true ONDELETE = 'OnDelete' attr_reader :pods diff --git a/lib/kubernetes-deploy/kubernetes_resource/statefulservice.rb b/lib/kubernetes-deploy/kubernetes_resource/statefulservice.rb index 4971a175c..54cb7bd5f 100644 --- a/lib/kubernetes-deploy/kubernetes_resource/statefulservice.rb +++ b/lib/kubernetes-deploy/kubernetes_resource/statefulservice.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true module KubernetesDeploy class Statefulservice < KubernetesResource + GROUP = 'stable.shopify.io' + VERSION = 'v1' + def deploy_succeeded? super # success assumption, with warning end diff --git a/lib/kubernetes-deploy/sync_mediator.rb b/lib/kubernetes-deploy/sync_mediator.rb index f8ad95afe..02f5dcb34 100644 --- a/lib/kubernetes-deploy/sync_mediator.rb +++ b/lib/kubernetes-deploy/sync_mediator.rb @@ -32,7 +32,7 @@ def sync(resources) dependencies = resources.map(&:class).uniq.flat_map do |c| c::SYNC_DEPENDENCIES if c.const_defined?('SYNC_DEPENDENCIES') end - kinds = (resources.map(&:type) + dependencies).compact.uniq + kinds = (resources.map(&:kind) + dependencies).compact.uniq kinds.each { |kind| fetch_by_kind(kind) } KubernetesDeploy::Concurrency.split_across_threads(resources) do |r| diff --git a/test/fixtures/resource-discovery/definitions/crd.yml b/test/fixtures/resource-discovery/definitions/crd.yml new file mode 100644 index 000000000..cad21d01a --- /dev/null +++ b/test/fixtures/resource-discovery/definitions/crd.yml @@ -0,0 +1,21 @@ +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: widgets.api.foobar.com + annotations: + kubernetes-deploy.shopify.io/metadata: '{ + "prunable": "true", + "predeploy": "true", + "predeploy-dependencies": ["ServiceAccount"], + "timeout": "PT9M", + "status-query": ".status", + "status-success": "ok" + }' +spec: + group: api.foobar.com + version: v1 + names: + kind: Widget + plural: widgets + scope: Namespaced diff --git a/test/fixtures/resource-discovery/definitions/crd_invalid_metadata.yml b/test/fixtures/resource-discovery/definitions/crd_invalid_metadata.yml new file mode 100644 index 000000000..ec9725cd5 --- /dev/null +++ b/test/fixtures/resource-discovery/definitions/crd_invalid_metadata.yml @@ -0,0 +1,16 @@ +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: widgets.api.foobar.com + annotations: + kubernetes-deploy.shopify.io/metadata: '{ + "status-success": + }' +spec: + group: api.foobar.com + version: v1 + names: + kind: Widget + plural: widgets + scope: Namespaced diff --git a/test/fixtures/resource-discovery/definitions/crd_invalid_query.yml b/test/fixtures/resource-discovery/definitions/crd_invalid_query.yml new file mode 100644 index 000000000..ce3f433c3 --- /dev/null +++ b/test/fixtures/resource-discovery/definitions/crd_invalid_query.yml @@ -0,0 +1,17 @@ +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: widgets.api.foobar.com + annotations: + kubernetes-deploy.shopify.io/metadata: '{ + "status-query": "*", + "status-success": "ok" + }' +spec: + group: api.foobar.com + version: v1 + names: + kind: Widget + plural: widgets + scope: Namespaced diff --git a/test/fixtures/resource-discovery/definitions/crd_non_prunable_no_predeploy.yml b/test/fixtures/resource-discovery/definitions/crd_non_prunable_no_predeploy.yml new file mode 100644 index 000000000..60319d660 --- /dev/null +++ b/test/fixtures/resource-discovery/definitions/crd_non_prunable_no_predeploy.yml @@ -0,0 +1,17 @@ +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: widgets.api.foobar.com + annotations: + kubernetes-deploy.shopify.io/metadata: '{ + "status-query": ".status", + "status-success": "ok" + }' +spec: + group: api.foobar.com + version: v1 + names: + kind: Widget + plural: widgets + scope: Namespaced diff --git a/test/fixtures/resource-discovery/instances/crd.yml b/test/fixtures/resource-discovery/instances/crd.yml new file mode 100644 index 000000000..e08049694 --- /dev/null +++ b/test/fixtures/resource-discovery/instances/crd.yml @@ -0,0 +1,6 @@ +--- +apiVersion: "api.foobar.com/v1" +kind: Widget +metadata: + name: my-first-widget +status: "ok" diff --git a/test/helpers/kubeclient_helper.rb b/test/helpers/kubeclient_helper.rb index 3e5b3a040..5cbe06755 100644 --- a/test/helpers/kubeclient_helper.rb +++ b/test/helpers/kubeclient_helper.rb @@ -18,6 +18,10 @@ def policy_v1beta1_kubeclient @policy_v1beta1_kubeclient ||= build_policy_v1beta1_kubeclient(MINIKUBE_CONTEXT) end + def apiextensions_v1beta1_kubeclient + @apiextensions_v1beta1_kubeclient ||= build_apiextensions_v1beta1_kubeclient(MINIKUBE_CONTEXT) + end + def apps_v1beta1_kubeclient @apps_v1beta1_kubeclient ||= build_apps_v1beta1_kubeclient(MINIKUBE_CONTEXT) end diff --git a/test/integration-serial/resource_discovery_test.rb b/test/integration-serial/resource_discovery_test.rb new file mode 100644 index 000000000..5f36f7817 --- /dev/null +++ b/test/integration-serial/resource_discovery_test.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true +require 'test_helper' + +class ResourceDiscoveryTest < KubernetesDeploy::IntegrationTest + def cleanup + crd_list = apiextensions_v1beta1_kubeclient.get_custom_resource_definitions + crd_list.each do |res| + apiextensions_v1beta1_kubeclient.delete_custom_resource_definition res.metadata.name + end + end + + def test_invalid_crd_query + skip if KUBE_SERVER_VERSION < Gem::Version.new('1.7.0') + begin + assert_deploy_success(deploy_fixtures("resource-discovery/definitions", subset: ["crd_invalid_query.yml"])) + # Deploy any other resource to trigger discovery + assert_deploy_failure(deploy_fixtures("resource-discovery/instances", subset: ["crd.yml"])) + assert_logs_match_all([ + "Invalid status query '*' for api.foobar.com/v1/widget", + "syntax error" + ], in_order: true) + ensure + cleanup + end + end + + def test_invalid_crd_metadata + skip if KUBE_SERVER_VERSION < Gem::Version.new('1.7.0') + begin + assert_deploy_success(deploy_fixtures("resource-discovery/definitions", subset: ["crd_invalid_metadata.yml"])) + # Deploy any other resource to trigger discovery + assert_deploy_failure(deploy_fixtures("hello-cloud", subset: ["configmap-data.yml",])) + assert_logs_match_all([ + "Invalid metadata for api.foobar.com/v1/widget", + "unexpected token at '{ \"status-success\": }'" + ], in_order: true) + ensure + cleanup + end + end + + def test_non_prunable_crd_no_predeploy + skip if KUBE_SERVER_VERSION < Gem::Version.new('1.7.0') + begin + assert_deploy_success(deploy_fixtures("resource-discovery/definitions", + subset: ["crd_non_prunable_no_predeploy.yml"])) + assert_deploy_success(deploy_fixtures("resource-discovery/instances", subset: ["crd.yml"])) + # Deploy any other non-priority (predeployable) resource to trigger pruning + assert_deploy_success(deploy_fixtures("hello-cloud", subset: ["daemon_set.yml",])) + + refute_logs_match("The following resources were pruned: widget \"my-first-widget\"") + refute_logs_match("Don't know how to monitor resources of type Widget. " \ + "Assuming Widget/my-first-widget deployed successfully") + # Should predeploy the CR definition, but *not* the instance. + assert_logs_match(%r{ + Predeploying\spriority\sresources + \-+\s+\[INFO\] # Line header + \[(\d|\-|\s|\:|\+)+\]\s+ # Timestamp + Deploying\sCustomResourceDefinition + \/widgets\.api\.foobar\.com + }x) + refute_logs_match(%r{ + Predeploying\spriority\sresources + \-+\s+\[INFO\] # Line header + \[(\d|\-|\s|\:|\+)+\]\s+ # Timestamp + Deploying\sWidget + \/my\-first\-widget + }x) + ensure + cleanup + end + end + + def test_prunable_crd_with_predeploy + skip if KUBE_SERVER_VERSION < Gem::Version.new('1.7.0') + begin + assert_deploy_success(deploy_fixtures("resource-discovery/definitions", subset: ["crd.yml"])) + assert_deploy_success(deploy_fixtures("resource-discovery/instances", subset: ["crd.yml"])) + # Deploy any other resource to trigger pruning + assert_deploy_success(deploy_fixtures("hello-cloud", subset: ["configmap-data.yml",])) + # Should predeploy the CR definition, *and* the instance. + assert_logs_match(%r{ + Predeploying\spriority\sresources + \-+\s+\[INFO\] # Line header + \[(\d|\-|\s|\:|\+)+\]\s+ # Timestamp + Deploying\sCustomResourceDefinition + \/widgets\.api\.foobar\.com + }x) + assert_logs_match(%r{ + Predeploying\spriority\sresources + \-+\s+\[INFO\] # Line header + \[(\d|\-|\s|\:|\+)+\]\s+ # Timestamp + Deploying\sWidget + \/my\-first\-widget + }x) + assert_logs_match("The following resources were pruned: widget \"my-first-widget\"") + refute_logs_match("Don't know how to monitor resources of type Widget. " \ + "Assuming Widget/my-first-widget deployed successfully") + ensure + cleanup + end + end +end diff --git a/test/unit/kubernetes-deploy/deploy_task_test.rb b/test/unit/kubernetes-deploy/deploy_task_test.rb index 3b43a3215..08f55585a 100644 --- a/test/unit/kubernetes-deploy/deploy_task_test.rb +++ b/test/unit/kubernetes-deploy/deploy_task_test.rb @@ -26,22 +26,83 @@ def test_initializer assert_logs_match(/Template directory (\S+) doesn't exist/) end + def test_resource_deploy_order_cycle + task = deploy_task + + list = [] + list << resource_class(kind: 'a', deps: %w(b)) + list << resource_class(kind: 'b', deps: %w(c)) + list << resource_class(kind: 'c', deps: %w(a)) + + task.stubs(:all_resources).returns(list) + + assert_raises(KubernetesDeploy::FatalDeploymentError) do + task._build_predeploy_sequence + end + end + + def test_resource_deploy_order_correcntess + task = deploy_task + + list = [] + list << resource_class(kind: 'a', deps: %w(b k)) + list << resource_class(kind: 'b', deps: []) + list << resource_class(kind: 'c', deps: %w(h i d)) + list << resource_class(kind: 'd', deps: %w(j h)) + list << resource_class(kind: 'e', deps: []) + list << resource_class(kind: 'f', deps: %w(e)) + list << resource_class(kind: 'g', deps: []) + list << resource_class(kind: 'h', deps: []) + list << resource_class(kind: 'i', deps: %w(j)) + list << resource_class(kind: 'j', deps: %w(g)) + list << resource_class(kind: 'k', deps: %w(i)) + + task.stubs(:all_resources).returns(list) + order = task._build_predeploy_sequence + + # We have the right number of resources + assert_equal list.count, order.count + + # All resource should be in the list + list.each { |r| assert_includes order, r.kind } + + # All preconditions should be respected + order.each_with_index do |r, idx| + klass = list.detect { |e| e.kind == r } + klass.predeploy_dependencies.each do |dep| + pos = order.map.with_index { |e, i| e == dep ? i : nil }.compact.first + # The current resource is deployed *after* its deps + assert_operator idx, :>, pos, "#{r} requires #{dep} but got #{order}" + end + end + end + private - def runner_with_env(value) - # TODO: Switch to --kubeconfig for kubectl shell out and pass env var as arg to DeployTask init - # Then fix this crappy env manipulation - original_env = ENV["KUBECONFIG"] - ENV["KUBECONFIG"] = value + def resource_class(kind:, deps:) + klass = Class.new(KubernetesDeploy::KubernetesResource) + klass.const_set(:PREDEPLOY_DEPENDENCIES, deps) + klass.const_set(:PREDEPLOY, !deps.empty?) + klass.stubs(:kind).returns(kind) + klass + end - deploy = KubernetesDeploy::DeployTask.new( + def deploy_task + KubernetesDeploy::DeployTask.new( namespace: "", context: "", logger: logger, current_sha: "", template_dir: "unknown", ) - deploy.run + end + + def runner_with_env(value) + # TODO: Switch to --kubeconfig for kubectl shell out and pass env var as arg to DeployTask init + # Then fix this crappy env manipulation + original_env = ENV["KUBECONFIG"] + ENV["KUBECONFIG"] = value + deploy_task.run ensure ENV["KUBECONFIG"] = original_env end diff --git a/test/unit/kubernetes-deploy/resource_watcher_test.rb b/test/unit/kubernetes-deploy/resource_watcher_test.rb index 290a42b43..b5b7e1575 100644 --- a/test/unit/kubernetes-deploy/resource_watcher_test.rb +++ b/test/unit/kubernetes-deploy/resource_watcher_test.rb @@ -133,7 +133,7 @@ def sync(_mediator) @hits += 1 end - def type + def kind "MockResource" end diff --git a/test/unit/sync_mediator_test.rb b/test/unit/sync_mediator_test.rb index 809d0ce7c..a67387bd0 100644 --- a/test/unit/sync_mediator_test.rb +++ b/test/unit/sync_mediator_test.rb @@ -56,7 +56,7 @@ def test_get_all_does_not_cache_error_result_from_kubectl # Neither the main code path nor the selector-based code path should cause error results to be cached assert_equal [], mediator.get_all('FakeConfigMap') - assert_equal [], mediator.get_all('FakeConfigMap', "fake" => "false", "type" => "fakeconfigmap") + assert_equal [], mediator.get_all('FakeConfigMap', "fake" => "false", "kind" => "fakeconfigmap") assert_equal @fake_cm.kubectl_response, mediator.get_instance('FakeConfigMap', @fake_cm.name) end @@ -68,10 +68,10 @@ def test_get_all_with_selector_populates_full_cache_and_filters_results_returned assert_equal 1, maps.length assert_equal @fake_cm2.kubectl_response, maps.first - maps = mediator.get_all('FakeConfigMap', "fake" => "true", "type" => "fakeconfigmap") + maps = mediator.get_all('FakeConfigMap', "fake" => "true", "kind" => "fakeconfigmap") assert_equal 2, maps.length - maps = mediator.get_all('FakeConfigMap', "fake" => "false", "type" => "fakeconfigmap") + maps = mediator.get_all('FakeConfigMap', "fake" => "false", "kind" => "fakeconfigmap") assert_equal 0, maps.length end @@ -151,7 +151,7 @@ def initialize(name) def sync(*) end - def type + def kind self.class.name.demodulize end @@ -162,7 +162,7 @@ def kubectl_response "labels" => { "name" => @name, "fake" => "true", - "type" => type.downcase + "kind" => kind.downcase } } }