kube-platform 3.3.1.gk.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +31 -0
  3. data/README.md +192 -0
  4. data/bin/kube-platform +37 -0
  5. data/lib/kube-platform/application.rb +203 -0
  6. data/lib/kube-platform/cli.rb +114 -0
  7. data/lib/kube-platform/client.rb +217 -0
  8. data/lib/kube-platform/cluster.rb +224 -0
  9. data/lib/kube-platform/cluster_definition.rb +115 -0
  10. data/lib/kube-platform/configuration.rb +145 -0
  11. data/lib/kube-platform/exceptions.rb +9 -0
  12. data/lib/kube-platform/handlers/dockerhub_secret_copy.rb +52 -0
  13. data/lib/kube-platform/handlers/ebs_from_snapshot.rb +108 -0
  14. data/lib/kube-platform/handlers/handler.rb +36 -0
  15. data/lib/kube-platform/handlers/recreate_resource.rb +11 -0
  16. data/lib/kube-platform/handlers/secret_copy.rb +43 -0
  17. data/lib/kube-platform/handlers/wait_for_job_completion.rb +69 -0
  18. data/lib/kube-platform/handlers/wait_for_termination.rb +47 -0
  19. data/lib/kube-platform/health_check.rb +19 -0
  20. data/lib/kube-platform/health_checks/pods_ready.rb +188 -0
  21. data/lib/kube-platform/health_checks/r53_records.rb +82 -0
  22. data/lib/kube-platform/helpers/retry.rb +20 -0
  23. data/lib/kube-platform/images/descriptor.rb +49 -0
  24. data/lib/kube-platform/images/docker_hub_image.rb +49 -0
  25. data/lib/kube-platform/images/dockerhub_image_factory.rb +64 -0
  26. data/lib/kube-platform/images/kubernetes_docker_hub_secret_provider.rb +44 -0
  27. data/lib/kube-platform/images/repository.rb +77 -0
  28. data/lib/kube-platform/images/tag_associator.rb +80 -0
  29. data/lib/kube-platform/images/tagged_dockerhub_image.rb +36 -0
  30. data/lib/kube-platform/logger.rb +32 -0
  31. data/lib/kube-platform/manifest.rb +61 -0
  32. data/lib/kube-platform/pre_checks/r53_records.rb +66 -0
  33. data/lib/kube-platform/pre_checks/valid_platform_dependencies.rb +52 -0
  34. data/lib/kube-platform/pre_checks.rb +19 -0
  35. data/lib/kube-platform/resource.rb +152 -0
  36. data/lib/kube-platform/resource_repository.rb +73 -0
  37. data/lib/kube-platform/thor/descriptor_to_option_adapter.rb +33 -0
  38. data/lib/kube-platform/update_checker.rb +39 -0
  39. data/lib/kube-platform/version.rb +5 -0
  40. data/lib/kube-platform.rb +40 -0
  41. metadata +179 -0
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/indifferent_access"
4
+
5
+ module KubePlatform
6
+ module Images
7
+ class Descriptor
8
+ DEFAULT_IMAGE_CONFIG = "../../../config/images.yaml"
9
+
10
+ attr_reader :name, :default_tag, :default_tag_from_key, :key, :description, :key_alias
11
+
12
+ def initialize(name:, default_tag:, default_tag_from_key:, key:, description:, key_alias: nil)
13
+ @name = name
14
+ @default_tag = default_tag
15
+ @default_tag_from_key = default_tag_from_key
16
+ @key = key.to_sym
17
+ @description = description
18
+ @key_alias = key_alias&.to_sym
19
+ end
20
+
21
+ class << self
22
+ def with_image_defaults
23
+ images_path = File.expand_path(File.join(__dir__, DEFAULT_IMAGE_CONFIG))
24
+ array_from_yaml_file(filename: images_path)
25
+ end
26
+
27
+ def array_from_yaml_file(filename:)
28
+ yaml_to_hash(filename).map do |item|
29
+ new(
30
+ name: item[:name],
31
+ default_tag: item[:default_tag],
32
+ default_tag_from_key: item[:default_tag_from_key],
33
+ key: item[:key],
34
+ description: item[:description],
35
+ key_alias: item[:key_alias]
36
+ )
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def yaml_to_hash(filename)
43
+ YAML.load_file(filename).with_indifferent_access[:images] or # TODO: get rid of indifferent access
44
+ raise ConfigException, "#{filename} does not contain a set of valid image descriptors"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module KubePlatform
6
+ module Images
7
+ class DockerHubImage
8
+ include Logger
9
+
10
+ attr_reader :image_name, :secret_provider
11
+
12
+ def initialize(image_name:, secret_provider:)
13
+ @image_name = image_name
14
+ @secret_provider = secret_provider
15
+ end
16
+
17
+ def tag_exist?(tag)
18
+ response = RestClient.get(
19
+ "https://index.docker.io/v2/#{image_name}/manifests/#{tag}",
20
+ Authorization: "Bearer #{dockerhub_token}", Accept: "application/json"
21
+ )
22
+ response.code == 200
23
+ rescue RestClient::RequestFailed
24
+ false
25
+ end
26
+
27
+ private
28
+
29
+ def basic_auth_secret
30
+ secret_provider.secret
31
+ end
32
+
33
+ def dockerhub_token
34
+ @dockerhub_token ||= read_dockerhub_token
35
+ end
36
+
37
+ def read_dockerhub_token
38
+ response = RestClient.get(
39
+ "https://auth.docker.io/token?service=registry.docker.io&scope=repository:#{image_name}:pull",
40
+ Authorization: "Basic #{basic_auth_secret}", Accept: "application/json"
41
+ )
42
+
43
+ JSON.parse(response.body)["token"]
44
+ rescue RestClient::RequestFailed => e
45
+ raise KubePlatformException, "Could not retrieve Docker Hub token: #{e.message}"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubePlatform
4
+ module Images
5
+ class DockerHubImageFactory
6
+ def initialize(secret_provider:)
7
+ @secret_provider = secret_provider
8
+ @image_cache = {}
9
+ @tagged_image_cache = {}
10
+ end
11
+
12
+ def build(image_name:, tag:)
13
+ if fully_qualified_docker_image?(tag)
14
+ parsed_tag = parse_tag_for_full_docker_image(tag)
15
+ tagged_dockerhub_image(parsed_tag[:image_name], parsed_tag[:image_tag])
16
+ else
17
+ tagged_dockerhub_image(image_name, tag)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def fully_qualified_docker_image?(image_tag)
24
+ image_tag.split(":").count > 1
25
+ end
26
+
27
+ def parse_tag_for_full_docker_image(image_tag)
28
+ split_tag = image_tag.split(":")
29
+ { image_name: split_tag[0], image_tag: split_tag[1] }
30
+ end
31
+
32
+ def tagged_dockerhub_image(image_name, image_tag)
33
+ retrieve_tagged_image(image_name, image_tag) || create_tagged_dockerhub_image(image_name, image_tag)
34
+ end
35
+
36
+ def retrieve_tagged_image(image_name, image_tag)
37
+ @tagged_image_cache[tagged_image_hash_key(image_name, image_tag)]
38
+ end
39
+
40
+ def tagged_image_hash_key(image_name, image_tag)
41
+ [image_name, image_tag]
42
+ end
43
+
44
+ def create_tagged_dockerhub_image(image_name, image_tag)
45
+ dockerhub_image = dockerhub_image(image_name)
46
+ @tagged_image_cache[tagged_image_hash_key(image_name, image_tag)] = KubePlatform::Images::TaggedDockerHubImage.new(
47
+ image: dockerhub_image, tag: image_tag
48
+ )
49
+ end
50
+
51
+ def dockerhub_image(image_name)
52
+ retrieve_cached_image(image_name) || create_dockerhub_image(image_name)
53
+ end
54
+
55
+ def retrieve_cached_image(image_name)
56
+ @image_cache[image_name]
57
+ end
58
+
59
+ def create_dockerhub_image(image_name)
60
+ @image_cache[image_name] = KubePlatform::Images::DockerHubImage.new(image_name: image_name, secret_provider: @secret_provider)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubePlatform
4
+ module Images
5
+ class KubernetesDockerHubSecretProvider
6
+ include Logger
7
+
8
+ attr_reader :client, :secret_namespace, :secret_name
9
+
10
+ def initialize(client:, secret_name:, secret_namespace:)
11
+ @client = client
12
+ @secret_name = secret_name
13
+ @secret_namespace = secret_namespace
14
+ end
15
+
16
+ def secret
17
+ basic_auth_secret
18
+ end
19
+
20
+ private
21
+
22
+ def basic_auth_secret
23
+ resource = client.get(secret_resource)
24
+ if resource.nil?
25
+ message = "Could not retrieve Docker Hub secret from #{secret_namespace}/#{secret_name}"
26
+ logger.error(message) # TODO: don't double-log this
27
+ raise KubePlatform::KubePlatformException, message
28
+ end
29
+
30
+ decode_token(resource.unwrap.data[".dockercfg"])
31
+ end
32
+
33
+ def secret_resource
34
+ @secret_resource ||= KubePlatform::Resource.from_spec(apiVersion: "v1", kind: "Secret",
35
+ metadata: { name: secret_name, namespace: secret_namespace })
36
+ end
37
+
38
+ def decode_token(data)
39
+ decoded = Base64.decode64(data)
40
+ JSON.parse(decoded).dig("https://index.docker.io/v1/", "auth") or raise TokenNotFound, "Token not found in #{decoded}"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubePlatform
4
+ module Images
5
+ class Repository
6
+ def initialize(image_descriptors:, image_tags:, image_factory:)
7
+ @image_descriptors = image_descriptors
8
+ @image_tags = image_tags.compact
9
+ @image_factory = image_factory
10
+ end
11
+
12
+ def get(key)
13
+ registry[key.to_sym]
14
+ end
15
+
16
+ def missing_images
17
+ registry.values.reject(&:exist?)
18
+ end
19
+
20
+ def key_tag_pairs
21
+ registry.each_with_object({}) { |(key, image), pairs| pairs[key] = image.tag }
22
+ end
23
+
24
+ def key_tag_pairs_without_defaults
25
+ @image_tags
26
+ end
27
+
28
+ def add_key_tag_pairs(additional_pairs)
29
+ @image_tags.merge!(additional_pairs) { |_key, old, _new| old }
30
+ rebuild_registry
31
+ end
32
+
33
+ def update_image_tag_defaults(key_tag_pairs)
34
+ (keys_with_default_tags & key_tag_pairs.keys).each do |key|
35
+ descriptor = descriptor_for_key(key)
36
+ registry[key] = @image_factory.build(image_name: descriptor.name, tag: key_tag_pairs[key])
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def registry
43
+ @registry ||= build_registry
44
+ end
45
+
46
+ def build_registry
47
+ image_key_tag_map = associate_tags(@image_descriptors, @image_tags)
48
+
49
+ @image_descriptors.each_with_object({}) do |descriptor, registry|
50
+ key = descriptor.key
51
+ registry[key] = @image_factory.build(image_name: descriptor.name, tag: image_key_tag_map[key])
52
+ end
53
+ end
54
+
55
+ def rebuild_registry
56
+ @registry = nil
57
+ registry
58
+ end
59
+
60
+ def associate_tags(image_descriptors, image_tags)
61
+ TagAssociator.new(image_descriptors: image_descriptors, image_tags: image_tags).run
62
+ end
63
+
64
+ def keys_with_default_tags
65
+ registry.keys - @image_tags.keys
66
+ end
67
+
68
+ def descriptor_for_key(key)
69
+ key_descriptor_map[key]
70
+ end
71
+
72
+ def key_descriptor_map
73
+ @key_descriptor_map ||= @image_descriptors.each_with_object({}) { |descriptor, hash| hash[descriptor.key] = descriptor }
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubePlatform
4
+ module Images
5
+ class TagAssociator
6
+ DescriptorTagPair = Struct.new(:descriptor, :tag)
7
+
8
+ def initialize(image_descriptors:, image_tags:)
9
+ @image_descriptors = image_descriptors
10
+ @image_tags = image_tags
11
+ @associated = {}
12
+ end
13
+
14
+ def run
15
+ descriptors_with_image_tags, descriptors_requiring_defaults = @image_descriptors.partition { |d| @image_tags.key?(d.key) }
16
+ associate_descriptors_with_image_tags(descriptors_with_image_tags)
17
+ associate_descriptors_with_defaults(descriptors_requiring_defaults)
18
+ transform_descriptor_tag_map_to_image_key_tag_map
19
+ end
20
+
21
+ private
22
+
23
+ def associate_descriptors_with_image_tags(descriptors)
24
+ descriptors.each { |d| associate_tag_to_descriptor(d, @image_tags[d.key]) }
25
+ end
26
+
27
+ def associate_tag_to_descriptor(descriptor, tag)
28
+ @associated[descriptor.key] = DescriptorTagPair.new(descriptor, tag)
29
+ end
30
+
31
+ def associate_descriptors_with_defaults(descriptors)
32
+ remaining_untagged = associate_tags_to_descriptors_containing_default_tag_attributes(descriptors)
33
+ remaining_untagged = associate_tags_to_descriptors_containing_references_to_other_descriptors(remaining_untagged)
34
+ remaining_untagged.empty? or
35
+ raise ConfigException, "No tag could be found for image with key '#{remaining_untagged.first.key}'"
36
+ end
37
+
38
+ def associate_tags_to_descriptors_containing_default_tag_attributes(descriptors)
39
+ descriptors.reject do |d|
40
+ if (tag = d.default_tag)
41
+ associate_tag_to_descriptor(d, tag)
42
+ true
43
+ else
44
+ false
45
+ end
46
+ end
47
+ end
48
+
49
+ def associate_tags_to_descriptors_containing_references_to_other_descriptors(descriptors)
50
+ descriptors.reject do |d|
51
+ if (ref = d.default_tag_from_key)
52
+ resolve_descriptor_reference(d, ref)
53
+ true
54
+ else
55
+ false
56
+ end
57
+ end
58
+ end
59
+
60
+ def resolve_descriptor_reference(descriptor, ref)
61
+ descriptor_tag_pair = @associated[ref.to_sym] or
62
+ raise ConfigException,
63
+ "#{descriptor.key} wants to use the image tag from the image with key '#{ref}', but that image does not have a tag"
64
+
65
+ if descriptor.name != descriptor_tag_pair.descriptor.name &&
66
+ ([descriptor.name, descriptor_tag_pair.descriptor.name] - ['invocaops/web_app', 'invocaops/ringswitch']).any?
67
+ raise ConfigException,
68
+ "#{descriptor.key} is trying to use the default_tag_from_key directive against #{ref}, "\
69
+ "but the image names don't match"
70
+ end
71
+
72
+ associate_tag_to_descriptor(descriptor, descriptor_tag_pair.tag)
73
+ end
74
+
75
+ def transform_descriptor_tag_map_to_image_key_tag_map
76
+ @associated.each_with_object({}) { |(key, descriptor_tag_pair), hash| hash[key] = descriptor_tag_pair.tag }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubePlatform
4
+ module Images
5
+ class TaggedDockerHubImage
6
+ include Logger
7
+
8
+ attr_reader :tag
9
+
10
+ def initialize(image:, tag:)
11
+ @image = image
12
+ @tag = tag
13
+ end
14
+
15
+ def name
16
+ "#{@image.image_name}:#{@tag}"
17
+ end
18
+
19
+ def exist?
20
+ defined?(@exists) or @exists = log_success_fail(@image.tag_exist?(@tag))
21
+ end
22
+
23
+ private
24
+
25
+ def log_success_fail(success)
26
+ if success
27
+ logger.info("Successfully located Docker Hub image for #{name}")
28
+ else
29
+ logger.info("Could not locate Docker Hub image for #{name}")
30
+ end
31
+
32
+ success
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module KubePlatform
6
+ module Logger
7
+ ANSI_ESCAPE = "\x1B"
8
+ ANSI_GOTO_START_OF_LINE = ANSI_ESCAPE + "[0F"
9
+ ANSI_CLEAR_TO_END_OF_LINE = ANSI_ESCAPE + "[J"
10
+
11
+ class << self
12
+ attr_accessor :verbose
13
+ attr_writer :logger
14
+
15
+ def logger
16
+ @logger ||= ::Logger.new(STDOUT).tap do |logger|
17
+ logger.formatter = -> (severity, time, programname, msg) do
18
+ if STDOUT.isatty && !verbose
19
+ ANSI_GOTO_START_OF_LINE + ANSI_CLEAR_TO_END_OF_LINE + "%5s %s\n" % [severity, msg]
20
+ else
21
+ ::Logger::Formatter.new.call(severity, time, programname, msg)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def logger
29
+ Logger.logger
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash"
4
+ require "active_support/core_ext/string"
5
+ require "jsonnet"
6
+
7
+ module KubePlatform
8
+ class Manifest
9
+ attr_reader :resource_dir, :filename, :config
10
+
11
+ FILENAME_ANNOTATION = "invoca.com/resource_filename"
12
+
13
+ def initialize(filename:, resource_dir:, config:)
14
+ @filename = filename
15
+ @resource_dir = resource_dir
16
+ @config = config
17
+ end
18
+
19
+ def cluster_definition_by_name(name)
20
+ raise_if_definition_not_found(name)
21
+ definitions[name]
22
+ end
23
+
24
+ def version
25
+ manifest[:version] or raise ManifestException, "The manifest does not contain a 'version' key"
26
+ end
27
+
28
+ private
29
+
30
+ def raise_if_definition_not_found(definition_name)
31
+ definitions[definition_name] or raise ManifestException, "A platform definition named #{definition_name} was not found in #{filename}"
32
+ end
33
+
34
+ def definitions
35
+ @definitions ||= manifest[:definitions].each_with_object({}) do |(name, definition), clusters|
36
+ clusters[name] = ClusterDefinition.new(name: name, definition: definition, resource_dir: resource_dir, config: config)
37
+ end
38
+ end
39
+
40
+ def manifest
41
+ @manifest ||= case filename
42
+ when /\.jsonnet$/
43
+ load_jsonnet_manifest
44
+ else
45
+ load_yaml_manifest
46
+ end.with_indifferent_access
47
+ end
48
+
49
+ def load_yaml_manifest
50
+ template = File.read(filename)
51
+ renderer = ERB.new(template)
52
+ YAML.safe_load(renderer.result(config.binding), aliases: true)
53
+ end
54
+
55
+ def load_jsonnet_manifest
56
+ vm = Jsonnet::VM.new
57
+ vm.tla_code("vars", config.to_json)
58
+ JSON.parse(vm.evaluate_file(filename))
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aws-sdk-route53"
4
+ require_relative "../logger"
5
+ require_relative "../helpers/retry"
6
+
7
+ module KubePlatform
8
+ module PreChecks
9
+ class R53Records < PreCheck
10
+ include Logger
11
+
12
+ DEFAULT_CONFIG = { }.freeze
13
+
14
+ def initialize(name, config)
15
+ super(name, config.apply_defaults(DEFAULT_CONFIG))
16
+ end
17
+
18
+ def run(_client, _cluster_definition)
19
+ logger.info("Checking for Route53 DNS records")
20
+
21
+ exists = []
22
+
23
+ fully_qualified_names.each do |record|
24
+ if record_exists?(record)
25
+ exists << record
26
+ logger.error("Route53 record #{record} already exists!")
27
+ end
28
+ end
29
+
30
+ exists.length > 0 ? false : true
31
+ end
32
+
33
+ private
34
+
35
+ def fully_qualified_names
36
+ @fully_qualified_names ||= r53_records.map { |record| record.end_with?(".") ? record : "#{record}." }
37
+ end
38
+
39
+ def record_exists?(record)
40
+ response = r53_client.list_resource_record_sets(
41
+ hosted_zone_id: r53_zone_id,
42
+ start_record_name: record,
43
+ start_record_type: "A",
44
+ max_items: 1
45
+ )
46
+ response.resource_record_sets.first&.name == record
47
+ end
48
+
49
+ def r53_client
50
+ @r53_client ||= Aws::Route53::Client.new(region: region)
51
+ end
52
+
53
+ def region
54
+ config[:region]
55
+ end
56
+
57
+ def r53_zone_id
58
+ config[:r53_zone_id]
59
+ end
60
+
61
+ def r53_records
62
+ @r53_records ||= config[:r53_records]
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../health_check"
4
+ require_relative "../logger"
5
+
6
+ module KubePlatform
7
+ module PreChecks
8
+ class ValidPlatformDependencies < PreCheck
9
+ PlatformDependencyException = Class.new(KubePlatformException)
10
+
11
+ include Logger
12
+
13
+ def run(_client, cluster_definition)
14
+ pod_names = Set.new(cluster_definition.resources.map(&:name))
15
+ pods_with_invalid_dependencies = invalid_platform_startup_dependencies(cluster_definition, pod_names)
16
+ pods_with_invalid_dependencies.empty? or raise PlatformDependencyException, invalid_dependency_details(pods_with_invalid_dependencies)
17
+ logger.info("Platform startup dependencies are valid")
18
+ true
19
+ end
20
+
21
+ private
22
+
23
+ def invalid_platform_startup_dependencies(cluster_definition, pod_names)
24
+ cluster_definition.resources.map do |resource|
25
+ if (dependencies_csv = resource.pod_annotation(:platform_startup_dependencies))
26
+ unless valid_dependency_list?(dependencies_csv, pod_names)
27
+ { name: resource.name, dependencies: dependencies_csv }
28
+ end
29
+ end
30
+ end.compact
31
+ end
32
+
33
+ def valid_dependency_list?(dependencies_csv, expected_pod_names)
34
+ dependencies_csv.split(/, */).all? { |dependency| expected_pod_names.include?(dependency) }
35
+ end
36
+
37
+ def invalid_dependency_details(pods_with_invalid_dependencies)
38
+ if pods_with_invalid_dependencies.size > 5
39
+ <<~EOS.chomp
40
+ Found #{pods_with_invalid_dependencies.size} pod templates with startup dependencies that don't match existing pod names. Showing the first 5.
41
+ #{pods_with_invalid_dependencies.first(5).join("\n")}
42
+ EOS
43
+ else
44
+ <<~EOS.chomp
45
+ Found pod templates with startup dependencies that don't match existing pod names.
46
+ #{pods_with_invalid_dependencies.join("\n")}
47
+ EOS
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubePlatform
4
+ class PreCheck
5
+ attr_reader :name, :config
6
+
7
+ def initialize(name, config)
8
+ @name = name
9
+ @config = config
10
+ end
11
+
12
+ class << self
13
+ def load(class_name:, name:, config:)
14
+ klass = "KubePlatform::PreChecks::#{class_name}".constantize
15
+ klass.new(name, config)
16
+ end
17
+ end
18
+ end
19
+ end