Skip to content

Commit

Permalink
Runtime resource discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanmb committed Oct 17, 2017
1 parent 0c9eb79 commit 33111fe
Show file tree
Hide file tree
Showing 19 changed files with 402 additions and 104 deletions.
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ gem 'timecop'
gem 'byebug'
gem 'codecov', require: false
gem 'ruby-prof', require: false
gem 'ruby-prof-flamegraph', require: false
gem 'ruby-prof-flamegraph', require: false
3 changes: 2 additions & 1 deletion kubernetes-deploy.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ Gem::Specification.new do |spec|

spec.required_ruby_version = '>= 2.3.0'
spec.add_dependency "activesupport", ">= 4.2"
spec.add_dependency "kubeclient", "~> 2.4"
spec.add_dependency "kubeclient", "~> 2.5.1"
spec.add_dependency "googleauth", ">= 0.5"
spec.add_dependency "ejson", "1.0.1"
spec.add_dependency "colorize", "~> 0.8"
spec.add_dependency "statsd-instrument", "~> 2.1"
spec.add_dependency "jsonpath", "0.8.8"

spec.add_development_dependency "bundler"
spec.add_development_dependency "rake", "~> 10.0"
Expand Down
173 changes: 173 additions & 0 deletions lib/kubernetes-deploy/discoverable_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# frozen_string_literal: true
require 'kubernetes-deploy/kubernetes_resource'
require 'kubernetes-deploy/kubeclient_builder'
require "pry"
require 'jsonpath'

module KubernetesDeploy
class DiscoverableResource < KubernetesResource
extend KubernetesDeploy::KubeclientBuilder

class << self
attr_accessor :version, :group, :type
end

STATUS_FIELD_ANNOTATION = 'kubernetes-deploy.shopify.io/status-field'
STATUS_SUCCESS_ANNOTATION = 'kubernetes-deploy.shopify.io/status-success'
TIMEOUT_ANNOTATION = 'kubernetes-deploy.shopify.io/timeout'
PREDEPLOY_ANNOTATION = 'kubernetes-deploy.shopify.io/predeploy'
PRUNABLE_ANNOTATION = 'kubernetes-deploy.shopify.io/prunable'

def type
self.class.type
end

def self.inherited(child_class)
child_classes.add child_class
end

def self.child_classes
@child_classes ||= Set.new
end

def self.prunable?
return @prunable if defined?(@prunable)
false
end

def self.predeploy?
return @predeploy if defined?(@predeploy)
false
end

def self.timeout
return @timeout if defined?(@timeout)
super
end

def self.identity
"#{group}/#{version}/#{type}"
end

def self.all
child_classes.dup
end

def self.discover(context:, logger:)
logger.info("Discovering custom resources:")
@resources = nil
@child_classes = nil
discover_tpr(v1beta1_kubeclient(context))
discover_crd(v1beta1_crd_kubeclient(context))
end

def self.build(namespace:, context:, definition:, logger:)
# We only discover once per kubernetes-deploy invocation
discover(context: context, logger: logger) unless @resources

type = definition["kind"]
group, _, version = definition['apiVersion'].rpartition('/')

resource_class = @resources.dig(group, type, version) if @resources
return super unless resource_class

opts = { namespace: namespace, context: context, definition: definition, logger: logger }
resource_class.new(**opts)
end

private

def self.discover_tpr(client)
return unless client.respond_to? :get_third_party_resources
resources = client.get_third_party_resources
resources.each do |res|
type, _, group = res.metadata.name.partition('.')
# TPR API supports multiple versions for a single resource :(
res.versions.each do |version|
discovered(group: group,
type: type,
version: version.name,
annotations: res.metadata.annotations)
end
end
end

def self.discover_crd(client)
return unless client.respond_to? :get_custom_resource_definitions
resources = client.get_custom_resource_definitions
resources.each do |res|
discovered(group: res.spec.group,
type: res.spec.names.kind,
version: res.spec.version,
annotations: res.metadata.annotations)
end
end

def self.discovered(group:, type:, version:, annotations:)
resource_class = Class.new(self) do
type = type.capitalize # TPRs are inconsistent about capitalization
@group = group
@type = type
@version = version
@prunable = DiscoverableResource.parse_bool(annotations[PRUNABLE_ANNOTATION])
@predeploy = DiscoverableResource.parse_bool(annotations[PREDEPLOY_ANNOTATION])
@timeout = DiscoverableResource.parse_timeout(type, annotations[TIMEOUT_ANNOTATION])

status_field = annotations[STATUS_FIELD_ANNOTATION]
success_status = annotations[STATUS_SUCCESS_ANNOTATION]

define_method 'deploy_succeeded?' do
getter = "get_#{type.downcase}"
@client ||= DiscoverableResource.kubeclient(context: @context, resource_class: self.class)
raw_json = @client.send(getter, @name, @namespace, as: :raw)
query_path = JsonPath.new(status_field)
current_status = query_path.first(raw_json)
current_status == success_status
end if status_field && success_status
end

add_resource(resource_class)
end

def self.add_resource(resource_class)
group = resource_class.group
type = resource_class.type
version = resource_class.version
@resources ||= {}
@resources[group] ||= {}
@resources[group][type] ||= {}
@resources[group][type][version] = resource_class
end

def self.parse_bool(value)
value.to_s == "true" # value could be nil
end

def self.parse_timeout(type, timeout)
begin
ActiveSupport::Duration.parse(timeout) if timeout
rescue ActiveSupport::Duration::ISO8601Parser::ParsingError
raise FatalDeploymentError, "Resource #{type} specified invalid timeout value '#{timeout}', must use ISO8601 duration."
end
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.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
24 changes: 1 addition & 23 deletions lib/kubernetes-deploy/kubernetes_resource/config_map.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,6 @@
# frozen_string_literal: true
module KubernetesDeploy
class ConfigMap < KubernetesResource
class ConfigMap < GenericResource
TIMEOUT = 30.seconds

def sync
_, _err, st = kubectl.run("get", type, @name)
@status = st.success? ? "Available" : "Unknown"
@found = st.success?
end

def deploy_succeeded?
exists?
end

def deploy_failed?
false
end

def timeout_message
UNUSUAL_FAILURE_MESSAGE
end

def exists?
@found
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true
module KubernetesDeploy
class CustomResourceDefinition < GenericResource
TIMEOUT = 10.seconds
end
end
26 changes: 26 additions & 0 deletions lib/kubernetes-deploy/kubernetes_resource/genericresource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true
module KubernetesDeploy
class GenericResource < KubernetesResource
def sync
_, _err, st = kubectl.run("get", type, @name)
@status = st.success? ? "Available" : "Unknown"
@found = st.success?
end

def deploy_succeeded?
exists?
end

def deploy_failed?
false
end

def timeout_message
UNUSUAL_FAILURE_MESSAGE
end

def exists?
@found
end
end
end
20 changes: 1 addition & 19 deletions lib/kubernetes-deploy/kubernetes_resource/ingress.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
# frozen_string_literal: true
module KubernetesDeploy
class Ingress < KubernetesResource
class Ingress < GenericResource
TIMEOUT = 30.seconds

def sync
_, _err, st = kubectl.run("get", type, @name)
@status = st.success? ? "Created" : "Unknown"
@found = st.success?
end

def deploy_succeeded?
exists?
end

def deploy_failed?
false
end

def exists?
@found
end
end
end
Original file line number Diff line number Diff line change
@@ -1,29 +1,9 @@
# frozen_string_literal: true
module KubernetesDeploy
class PodDisruptionBudget < KubernetesResource
TIMEOUT = 10.seconds

def sync
_, _err, st = kubectl.run("get", type, @name)
@found = st.success?
@status = @found ? "Available" : "Unknown"
end

def deploy_succeeded?
exists?
end

class PodDisruptionBudget < GenericResource
def deploy_method
# Required until https://github.com/kubernetes/kubernetes/issues/45398 changes
:replace_force
end

def timeout_message
UNUSUAL_FAILURE_MESSAGE
end

def exists?
@found
end
end
end
23 changes: 1 addition & 22 deletions lib/kubernetes-deploy/kubernetes_resource/pod_template.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,5 @@
# frozen_string_literal: true
module KubernetesDeploy
class PodTemplate < KubernetesResource
def sync
_, _err, st = kubectl.run("get", type, @name)
@status = st.success? ? "Available" : "Unknown"
@found = st.success?
end

def deploy_succeeded?
exists?
end

def deploy_failed?
false
end

def timeout_message
UNUSUAL_FAILURE_MESSAGE
end

def exists?
@found
end
class PodTemplate < GenericResource
end
end
14 changes: 1 addition & 13 deletions lib/kubernetes-deploy/kubernetes_resource/resource_quota.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true
module KubernetesDeploy
class ResourceQuota < KubernetesResource
class ResourceQuota < GenericResource
TIMEOUT = 30.seconds

def sync
Expand All @@ -17,17 +17,5 @@ def sync
def deploy_succeeded?
@rollout_data.dig("spec", "hard") == @rollout_data.dig("status", "hard")
end

def deploy_failed?
false
end

def timeout_message
UNUSUAL_FAILURE_MESSAGE
end

def exists?
@found
end
end
end
12 changes: 12 additions & 0 deletions lib/kubernetes-deploy/kubernetes_resource/thirdpartyresource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true
module KubernetesDeploy
class ThirdPartyResource < GenericResource
TIMEOUT = 30.seconds

def exists?
# TPRs take time to become available.
_, _err, st = kubectl.run("get", @name)
st.success?
end
end
end
Loading

0 comments on commit 33111fe

Please sign in to comment.