kube-platform 3.3.1.gk.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +31 -0
- data/README.md +192 -0
- data/bin/kube-platform +37 -0
- data/lib/kube-platform/application.rb +203 -0
- data/lib/kube-platform/cli.rb +114 -0
- data/lib/kube-platform/client.rb +217 -0
- data/lib/kube-platform/cluster.rb +224 -0
- data/lib/kube-platform/cluster_definition.rb +115 -0
- data/lib/kube-platform/configuration.rb +145 -0
- data/lib/kube-platform/exceptions.rb +9 -0
- data/lib/kube-platform/handlers/dockerhub_secret_copy.rb +52 -0
- data/lib/kube-platform/handlers/ebs_from_snapshot.rb +108 -0
- data/lib/kube-platform/handlers/handler.rb +36 -0
- data/lib/kube-platform/handlers/recreate_resource.rb +11 -0
- data/lib/kube-platform/handlers/secret_copy.rb +43 -0
- data/lib/kube-platform/handlers/wait_for_job_completion.rb +69 -0
- data/lib/kube-platform/handlers/wait_for_termination.rb +47 -0
- data/lib/kube-platform/health_check.rb +19 -0
- data/lib/kube-platform/health_checks/pods_ready.rb +188 -0
- data/lib/kube-platform/health_checks/r53_records.rb +82 -0
- data/lib/kube-platform/helpers/retry.rb +20 -0
- data/lib/kube-platform/images/descriptor.rb +49 -0
- data/lib/kube-platform/images/docker_hub_image.rb +49 -0
- data/lib/kube-platform/images/dockerhub_image_factory.rb +64 -0
- data/lib/kube-platform/images/kubernetes_docker_hub_secret_provider.rb +44 -0
- data/lib/kube-platform/images/repository.rb +77 -0
- data/lib/kube-platform/images/tag_associator.rb +80 -0
- data/lib/kube-platform/images/tagged_dockerhub_image.rb +36 -0
- data/lib/kube-platform/logger.rb +32 -0
- data/lib/kube-platform/manifest.rb +61 -0
- data/lib/kube-platform/pre_checks/r53_records.rb +66 -0
- data/lib/kube-platform/pre_checks/valid_platform_dependencies.rb +52 -0
- data/lib/kube-platform/pre_checks.rb +19 -0
- data/lib/kube-platform/resource.rb +152 -0
- data/lib/kube-platform/resource_repository.rb +73 -0
- data/lib/kube-platform/thor/descriptor_to_option_adapter.rb +33 -0
- data/lib/kube-platform/update_checker.rb +39 -0
- data/lib/kube-platform/version.rb +5 -0
- data/lib/kube-platform.rb +40 -0
- metadata +179 -0
@@ -0,0 +1,217 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "logger"
|
4
|
+
|
5
|
+
module KubePlatform
|
6
|
+
class Client
|
7
|
+
include Logger
|
8
|
+
|
9
|
+
attr_reader :context
|
10
|
+
|
11
|
+
CONFIG_PATH = "#{ENV['HOME']}/.kube/config"
|
12
|
+
HTTP_ERROR_RETRIES = 5
|
13
|
+
BEARER_TOKEN_FILE_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/token'
|
14
|
+
CERT_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
|
15
|
+
|
16
|
+
def initialize(context_name: nil, config_path: CONFIG_PATH)
|
17
|
+
unless in_cluster?
|
18
|
+
@context = load_context(context_name, config_path)
|
19
|
+
end
|
20
|
+
@client_map = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def create(resource)
|
24
|
+
lifecycle_hook(resource, :pre_create)
|
25
|
+
log_action(:creating, resource)
|
26
|
+
create_resource(resource)
|
27
|
+
lifecycle_hook(resource, :post_create)
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(resource)
|
31
|
+
lifecycle_hook(resource, :pre_update)
|
32
|
+
log_action(:updating, resource)
|
33
|
+
update_resource(resource)
|
34
|
+
lifecycle_hook(resource, :post_update)
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete(resource, continue_on_missing: true)
|
38
|
+
lifecycle_hook(resource, :pre_delete)
|
39
|
+
log_action(:deleting, resource)
|
40
|
+
delete_resource(resource, continue_on_missing: continue_on_missing)
|
41
|
+
lifecycle_hook(resource, :post_delete)
|
42
|
+
end
|
43
|
+
|
44
|
+
def exist?(resource)
|
45
|
+
get(resource) != nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def client_for_api(api = "v1")
|
49
|
+
@client_map[api] ||= build_client(api)
|
50
|
+
end
|
51
|
+
|
52
|
+
def api_endpoint_from_context
|
53
|
+
@context&.api_endpoint
|
54
|
+
end
|
55
|
+
|
56
|
+
def get(resource)
|
57
|
+
method = method_for_resource_action(:get, resource)
|
58
|
+
client = client_for_resource(resource)
|
59
|
+
|
60
|
+
raw = resource.namespaced? ? client.send(method, resource.name, resource.namespace) : client.send(method, resource.name)
|
61
|
+
KubePlatform::Resource.from_spec(**(raw.to_h))
|
62
|
+
rescue Kubeclient::ResourceNotFoundError
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
# This modifies the API response slightly to appease the Resource wrapper
|
67
|
+
def pods_matching_selector(selector:, namespace:)
|
68
|
+
client = client_for_api("v1")
|
69
|
+
client.get_pods(label_selector: selector, namespace: namespace).map do |pod|
|
70
|
+
pod.apiVersion = "v1"
|
71
|
+
pod.kind = "Pod"
|
72
|
+
KubePlatform::Resource.from_spec(**(pod.to_h))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def log_action(action, resource)
|
79
|
+
logger.info("#{action.capitalize} #{resource.kind} #{resource.name}")
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_resource(resource)
|
83
|
+
method = method_for_resource_action(:create, resource)
|
84
|
+
client = client_for_resource(resource)
|
85
|
+
|
86
|
+
with_http_error_retries do
|
87
|
+
client.send(method, resource.unwrap)
|
88
|
+
end
|
89
|
+
rescue StandardError # TODO: Fix this double log. Either do nothing here, or wrap a new exception around the rescued one.
|
90
|
+
log_exception(:create, resource)
|
91
|
+
raise
|
92
|
+
end
|
93
|
+
|
94
|
+
def update_resource(resource)
|
95
|
+
method = method_for_resource_action(:update, resource)
|
96
|
+
client = client_for_resource(resource)
|
97
|
+
|
98
|
+
with_http_error_retries do
|
99
|
+
client.send(method, resource.unwrap)
|
100
|
+
end
|
101
|
+
rescue StandardError # TODO: ditto
|
102
|
+
log_exception(:update, resource)
|
103
|
+
raise
|
104
|
+
end
|
105
|
+
|
106
|
+
def delete_resource(resource, continue_on_missing:)
|
107
|
+
execute_delete(resource)
|
108
|
+
rescue Kubeclient::ResourceNotFoundError
|
109
|
+
resource_not_found!(resource, continue_on_missing)
|
110
|
+
rescue StandardError # TODO: ditto
|
111
|
+
log_exception(:delete, resource)
|
112
|
+
raise
|
113
|
+
end
|
114
|
+
|
115
|
+
def execute_delete(resource)
|
116
|
+
method = method_for_resource_action(:delete, resource)
|
117
|
+
client = client_for_resource(resource)
|
118
|
+
|
119
|
+
with_http_error_retries do
|
120
|
+
if resource.namespaced?
|
121
|
+
client.send(method, resource.name, resource.namespace)
|
122
|
+
else
|
123
|
+
client.send(method, resource.name)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def with_http_error_retries
|
129
|
+
attempt_number = 0
|
130
|
+
backoff_randomization = rand(1.0...1.5)
|
131
|
+
begin
|
132
|
+
yield
|
133
|
+
rescue Kubeclient::ResourceNotFoundError
|
134
|
+
# This is not recoverable, so re-raise
|
135
|
+
raise
|
136
|
+
rescue Kubeclient::HttpError
|
137
|
+
if attempt_number >= HTTP_ERROR_RETRIES # TODO: ditto
|
138
|
+
logger.error("Giving up, retry limit of #{HTTP_ERROR_RETRIES} exceeded")
|
139
|
+
raise
|
140
|
+
end
|
141
|
+
|
142
|
+
sleep_time = backoff_randomization * 2**attempt_number
|
143
|
+
logger.warn("Encountered an HTTP error. Sleeping #{sleep_time}s before retrying (#{attempt_number + 1}/#{HTTP_ERROR_RETRIES})")
|
144
|
+
sleep(sleep_time)
|
145
|
+
attempt_number += 1
|
146
|
+
retry
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def resource_not_found!(resource, continue_on_missing) # TODO: ditto
|
151
|
+
if continue_on_missing
|
152
|
+
logger.debug("Failed to delete #{resource.kind} #{resource.name}, it does not exist")
|
153
|
+
else
|
154
|
+
logger.error("Failed to delete #{resource.kind} #{resource.name}, it does not exist")
|
155
|
+
raise
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def log_exception(action, resource)
|
160
|
+
logger.error("Exception encountered trying to #{action} #{resource.kind} '#{resource.name}'")
|
161
|
+
end
|
162
|
+
|
163
|
+
def method_for_resource_action(action, resource)
|
164
|
+
"#{action}_#{Kubeclient::ClientMixin.underscore_entity(resource.kind)}".to_sym
|
165
|
+
end
|
166
|
+
|
167
|
+
def load_context(context_name, config_path)
|
168
|
+
config = Kubeclient::Config.read(config_path)
|
169
|
+
config.context(context_name)
|
170
|
+
end
|
171
|
+
|
172
|
+
def client_for_resource(resource)
|
173
|
+
client_for_api(resource.api_version)
|
174
|
+
end
|
175
|
+
|
176
|
+
def build_client(api)
|
177
|
+
case api
|
178
|
+
when "v1"
|
179
|
+
path = "api"
|
180
|
+
version = "v1"
|
181
|
+
else
|
182
|
+
api_group, _, version = api.rpartition("/")
|
183
|
+
path = "apis/#{api_group}"
|
184
|
+
end
|
185
|
+
|
186
|
+
if in_cluster?
|
187
|
+
build_client_in_cluster(path, version)
|
188
|
+
else
|
189
|
+
Kubeclient::Client.new("#{context.api_endpoint}/#{path}", version, ssl_options: context.ssl_options, auth_options: context.auth_options)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def build_client_in_cluster(path, version)
|
194
|
+
auth_options = {
|
195
|
+
bearer_token_file: BEARER_TOKEN_FILE_PATH
|
196
|
+
}
|
197
|
+
ssl_options = {}
|
198
|
+
if File.exist?(CERT_PATH)
|
199
|
+
ssl_options[:ca_file] = CERT_PATH
|
200
|
+
end
|
201
|
+
Kubeclient::Client.new(
|
202
|
+
"https://#{ENV['KUBERNETES_SERVICE_HOST']}:#{ENV['KUBERNETES_SERVICE_PORT']}/#{path}",
|
203
|
+
version,
|
204
|
+
auth_options: auth_options,
|
205
|
+
ssl_options: ssl_options
|
206
|
+
)
|
207
|
+
end
|
208
|
+
|
209
|
+
def lifecycle_hook(resource, hook)
|
210
|
+
resource.send(hook, self) if resource.respond_to?(hook)
|
211
|
+
end
|
212
|
+
|
213
|
+
def in_cluster?
|
214
|
+
ENV['KUBERNETES_SERVICE_HOST'] && ENV['KUBERNETES_SERVICE_PORT']
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "logger"
|
4
|
+
require "active_support/time"
|
5
|
+
|
6
|
+
module KubePlatform
|
7
|
+
class Cluster
|
8
|
+
ClusterDeprecation = ActiveSupport::Deprecation.new('2.0', 'KubePlatform')
|
9
|
+
|
10
|
+
class PlatformDefinitionMismatchException < KubePlatformException
|
11
|
+
attr_reader :received_definition, :expected_definition
|
12
|
+
|
13
|
+
def initialize(message, expected_definition:, received_definition:)
|
14
|
+
@expected_definition = expected_definition
|
15
|
+
@received_definition = received_definition
|
16
|
+
super(message)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
PlatformExistsException = Class.new(KubePlatformException)
|
21
|
+
PlatformNotFoundException = Class.new(KubePlatformException)
|
22
|
+
HealthCheckFailedException = Class.new(KubePlatformException)
|
23
|
+
NamespaceCreationException = Class.new(KubePlatformException)
|
24
|
+
PreCheckFailedException = Class.new(KubePlatformException)
|
25
|
+
|
26
|
+
include Logger
|
27
|
+
|
28
|
+
attr_reader :name, :cluster_definition, :client, :config
|
29
|
+
|
30
|
+
# Resources that exist outside of a namespace
|
31
|
+
GLOBAL_RESOURCES = ["Namespace", "PersistentVolume"].freeze
|
32
|
+
DEFINITION_ANNOTATION = "invoca.com/cluster_definition"
|
33
|
+
IMAGE_TAG_ANNOTATION = "invoca.com/image_tags"
|
34
|
+
|
35
|
+
NAMESPACE_CREATION_TIMEOUT = 5.minutes
|
36
|
+
|
37
|
+
def initialize(name:, cluster_definition:, client:, config:)
|
38
|
+
@name = name
|
39
|
+
@cluster_definition = cluster_definition
|
40
|
+
@client = client
|
41
|
+
@config = config
|
42
|
+
end
|
43
|
+
|
44
|
+
def create
|
45
|
+
exist? and raise PlatformExistsException, "Platform #{name} already exists"
|
46
|
+
|
47
|
+
run_pre_checks
|
48
|
+
annotate_namespace
|
49
|
+
create_namespace
|
50
|
+
create_cluster_resources
|
51
|
+
run_health_checks
|
52
|
+
logger.info "Successfully created platform #{name}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def update
|
56
|
+
exist? or raise PlatformNotFoundException, "Platform #{name} does not exist"
|
57
|
+
|
58
|
+
raise_if_definition_mismatch!
|
59
|
+
copy_namespace_annotations
|
60
|
+
annotate_namespace
|
61
|
+
update_namespace
|
62
|
+
actioned_pre_update_resources? ? pre_update_actions : pre_updates
|
63
|
+
update_pods
|
64
|
+
run_health_checks
|
65
|
+
logger.info "Successfully updated platform #{name}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete
|
69
|
+
if exist?
|
70
|
+
raise_if_definition_mismatch!
|
71
|
+
delete_cluster_resources
|
72
|
+
delete_namespace
|
73
|
+
logger.info "Successfully deleted platform #{name}"
|
74
|
+
elsif @config.force_api_calls
|
75
|
+
logger.info "Namespace #{name} does not exist. Returning."
|
76
|
+
else
|
77
|
+
raise PlatformNotFoundException, "Platform #{name} does not exist"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def image_key_tag_pairs
|
82
|
+
exist? or raise PlatformNotFoundException, "Platform #{name} does not exist"
|
83
|
+
|
84
|
+
JSON.parse(existing_namespace.annotation(IMAGE_TAG_ANNOTATION)).symbolize_keys
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def exist?
|
90
|
+
!existing_namespace.nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
def actioned_pre_update_resources?
|
94
|
+
cluster_definition.actioned_pre_update_resources?
|
95
|
+
end
|
96
|
+
|
97
|
+
def raise_if_definition_mismatch!
|
98
|
+
cluster_launch_definition.nil? and raise KubePlatformException, "Platform #{name} is missing the #{DEFINITION_ANNOTATION} annotation."
|
99
|
+
if cluster_launch_definition != definition_name
|
100
|
+
raise PlatformDefinitionMismatchException.new(
|
101
|
+
"Platform was launched with definition #{cluster_launch_definition} but current operation is for #{definition_name}",
|
102
|
+
expected_definition: cluster_launch_definition,
|
103
|
+
received_definition: definition_name
|
104
|
+
)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def cluster_launch_definition
|
109
|
+
@cluster_launch_definition ||= existing_namespace.annotation(DEFINITION_ANNOTATION)
|
110
|
+
end
|
111
|
+
|
112
|
+
def existing_namespace
|
113
|
+
@existing_namespace ||= client.get(namespace)
|
114
|
+
end
|
115
|
+
|
116
|
+
def definition_name
|
117
|
+
cluster_definition.name
|
118
|
+
end
|
119
|
+
|
120
|
+
def copy_namespace_annotations
|
121
|
+
existing_namespace&.annotations&.each_pair { |name, value| namespace.annotate(name, value) }
|
122
|
+
end
|
123
|
+
|
124
|
+
def annotate_namespace
|
125
|
+
namespace.annotate(DEFINITION_ANNOTATION, definition_name)
|
126
|
+
namespace.annotate(IMAGE_TAG_ANNOTATION, config.image_key_tag_pairs_without_defaults.to_json)
|
127
|
+
end
|
128
|
+
|
129
|
+
def namespace_resources(resources)
|
130
|
+
resources.each { |r| r.namespace = namespace.name unless GLOBAL_RESOURCES.include?(r.kind) }
|
131
|
+
end
|
132
|
+
|
133
|
+
def namespace
|
134
|
+
cluster_definition.namespace
|
135
|
+
end
|
136
|
+
|
137
|
+
def create_namespace
|
138
|
+
client.create(namespace)
|
139
|
+
create_time = Time.now
|
140
|
+
until client.exist?(namespace) # TODO: Use MonotonicTickCount?
|
141
|
+
if (Time.now - create_time) > NAMESPACE_CREATION_TIMEOUT
|
142
|
+
raise NamespaceCreationException, "Platform namespace #{name} failed to create in under #{NAMESPACE_CREATION_TIMEOUT} seconds"
|
143
|
+
end
|
144
|
+
|
145
|
+
logger.info("Waiting for namespace to exist... sleeping 10 seconds")
|
146
|
+
sleep(10.seconds)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def delete_namespace
|
151
|
+
client.delete(namespace)
|
152
|
+
end
|
153
|
+
|
154
|
+
def resources
|
155
|
+
@resources ||= namespace_resources(cluster_definition.resources)
|
156
|
+
end
|
157
|
+
|
158
|
+
def create_cluster_resources
|
159
|
+
resources.each { |r| client.create(r) }
|
160
|
+
end
|
161
|
+
|
162
|
+
def update_namespace
|
163
|
+
client.update(namespace)
|
164
|
+
end
|
165
|
+
|
166
|
+
def pre_update_actioned_resources
|
167
|
+
@pre_update_actioned_resources ||= cluster_definition.pre_update_actioned_resources.each { |_a, r| namespace_resources(r) }
|
168
|
+
end
|
169
|
+
|
170
|
+
def pre_update_actions
|
171
|
+
pre_update_actioned_resources.each do |action, resources|
|
172
|
+
resources.each { |r| client.send(action, r) }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def pre_update_resources
|
177
|
+
@pre_update_resources ||= namespace_resources(cluster_definition.pre_update_resources)
|
178
|
+
end
|
179
|
+
|
180
|
+
def pre_updates
|
181
|
+
pre_update_resources.each { |r| client.create(r) }
|
182
|
+
end
|
183
|
+
|
184
|
+
def update_pods
|
185
|
+
pods_to_update.each { |r| client.update(r) }
|
186
|
+
end
|
187
|
+
|
188
|
+
def pods_to_update
|
189
|
+
client.pods_matching_selector(selector: cluster_definition.update_pod_selector, namespace: namespace.name).map do |pod|
|
190
|
+
resources.find do |resource|
|
191
|
+
resource.pod_annotation(Manifest::FILENAME_ANNOTATION) == pod.annotation(Manifest::FILENAME_ANNOTATION)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def delete_cluster_resources
|
197
|
+
resources.reverse_each { |r| client.delete(r) }
|
198
|
+
end
|
199
|
+
|
200
|
+
# Stops at first failing health check, raising HealthCheckFailedException
|
201
|
+
# Or raises nothing if all pass.
|
202
|
+
def run_health_checks
|
203
|
+
cluster_definition.health_checks.each do |check|
|
204
|
+
logger.info("Running #{check.name} health check")
|
205
|
+
check.run(client, cluster_definition) or raise HealthCheckFailedException, "Health check for #{check.name} failed"
|
206
|
+
logger.info("Health check for #{check.name} was successful")
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Dont create cluster if failing pre check, raising HealthCheckFailedException
|
211
|
+
# Or raises nothing if all pass.
|
212
|
+
def run_pre_checks
|
213
|
+
cluster_definition.pre_checks.each do |check|
|
214
|
+
logger.info("Running #{check.name} pre check")
|
215
|
+
check.run(client, cluster_definition) or raise PreCheckFailedException, "Pre check for #{check.name} failed"
|
216
|
+
logger.info("Pre check for #{check.name} was successful")
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
deprecate :pre_updates,
|
221
|
+
:pre_update_resources,
|
222
|
+
deprecator: ClusterDeprecation
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KubePlatform
|
4
|
+
class ClusterDefinition
|
5
|
+
ClusterDefinitionDeprecation = ActiveSupport::Deprecation.new('2.0', 'KubePlatform')
|
6
|
+
|
7
|
+
attr_reader :name, :config, :resource_dir
|
8
|
+
|
9
|
+
def initialize(name:, definition:, resource_dir:, config:)
|
10
|
+
@name = name
|
11
|
+
@definition = definition
|
12
|
+
@resource_dir = resource_dir
|
13
|
+
@config = config
|
14
|
+
end
|
15
|
+
|
16
|
+
def namespace
|
17
|
+
@namespace ||= load_namespace
|
18
|
+
end
|
19
|
+
|
20
|
+
def resources
|
21
|
+
@resources ||= @definition[:resources].map { |r| load_resource(r) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def actioned_pre_update_resources?
|
25
|
+
@definition.dig(:update, :resources).is_a?(Hash)
|
26
|
+
end
|
27
|
+
|
28
|
+
def pre_update_actioned_resources
|
29
|
+
@pre_update_actioned_resources ||= (@definition.dig(:update, :resources) || {}).reduce({}) do |hash, (action, resources)|
|
30
|
+
hash.merge(action => resources.map { |r| load_resource(r) })
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def pre_update_resources
|
35
|
+
@pre_update_resources ||= (@definition.dig(:update, :resources) || []).map { |r| load_resource(r) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def health_checks
|
39
|
+
@health_checks ||= (@definition[:health_checks] || []).map { |hc| load_health_check(hc) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def update_pod_selector
|
43
|
+
@definition.dig(:update, :pod_selector)
|
44
|
+
end
|
45
|
+
|
46
|
+
def pre_checks
|
47
|
+
@pre_checks ||= (@definition[:pre_checks] || []).map { |pc| load_pre_check(pc) }
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def handlers
|
53
|
+
@definition[:handlers] || {}
|
54
|
+
end
|
55
|
+
|
56
|
+
def load_namespace
|
57
|
+
resource_file = @definition[:namespace]
|
58
|
+
namespace = resource_file && load_resource(resource_file) or raise ManifestException, "Platform definitions must contain a 'namespace' key that references a namespace resource"
|
59
|
+
|
60
|
+
namespace.kind == "Namespace" or raise ManifestException, "#{resource_file} does not contain a Namespace resource"
|
61
|
+
|
62
|
+
namespace
|
63
|
+
end
|
64
|
+
|
65
|
+
def load_resource(resource_name)
|
66
|
+
Resource.from_yaml(File.join(resource_dir, resource_name), handlers: handlers_for_resource(resource_name), config: config, jsonnet_library_path: "#{Dir.pwd}/vendor-jb")
|
67
|
+
end
|
68
|
+
|
69
|
+
def load_health_check(check)
|
70
|
+
HealthCheck.load(class_name: check[:class], name: check[:name], config: config_for_health_check(check))
|
71
|
+
end
|
72
|
+
|
73
|
+
def load_pre_check(check)
|
74
|
+
PreCheck.load(class_name: check[:class], name: check[:name], config: config_for_pre_check(check))
|
75
|
+
end
|
76
|
+
|
77
|
+
def resource_has_handlers?(resource_name)
|
78
|
+
handlers[resource_name] && !handlers[resource_name].empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
def handlers_for_resource(resource_name)
|
82
|
+
if resource_has_handlers?(resource_name)
|
83
|
+
handlers[resource_name].map do |handler_spec|
|
84
|
+
handler_class = handler_spec[:class]
|
85
|
+
config = config_for_handler(handler_spec)
|
86
|
+
build_handler(handler_class, config)
|
87
|
+
end
|
88
|
+
else
|
89
|
+
[]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def config_for_health_check(check_spec)
|
94
|
+
check_config = check_spec[:config] || {}
|
95
|
+
config.merge(check_config)
|
96
|
+
end
|
97
|
+
|
98
|
+
def config_for_handler(handler_spec)
|
99
|
+
handler_config = handler_spec[:config] || {}
|
100
|
+
config.merge(handler_config)
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_handler(handler_class, config)
|
104
|
+
klass = "KubePlatform::Handlers::#{handler_class}".constantize
|
105
|
+
klass.new(config)
|
106
|
+
end
|
107
|
+
|
108
|
+
def config_for_pre_check(check_spec)
|
109
|
+
check_config = check_spec[:config] || {}
|
110
|
+
config.merge(check_config)
|
111
|
+
end
|
112
|
+
|
113
|
+
deprecate :pre_update_resources, deprecator: ClusterDefinitionDeprecation
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hashie"
|
4
|
+
|
5
|
+
module KubePlatform
|
6
|
+
class Configuration < Hashie::Dash
|
7
|
+
include Hashie::Extensions::Dash::IndifferentAccess
|
8
|
+
|
9
|
+
property :cluster_name
|
10
|
+
property :domain_name
|
11
|
+
property :cluster_definition
|
12
|
+
property :manifest_path
|
13
|
+
property :image_repository
|
14
|
+
property :git_url
|
15
|
+
property :git_branch
|
16
|
+
property :git_resource_path
|
17
|
+
property :user
|
18
|
+
property :dockerhub_secret_name
|
19
|
+
property :dockerhub_secret_namespace
|
20
|
+
property :kubectl_context
|
21
|
+
property :dev_mode
|
22
|
+
property :ct_mode
|
23
|
+
property :faketime
|
24
|
+
property :force_api_calls
|
25
|
+
property :api_endpoint
|
26
|
+
|
27
|
+
NO_DEFAULT_TAG = Object.new
|
28
|
+
MASTER_TAG = "master"
|
29
|
+
MAIN_TAG = "main"
|
30
|
+
PRODUCTION_TAG = "production"
|
31
|
+
ALWAYS_PULL_TAGS = [MASTER_TAG, MAIN_TAG, PRODUCTION_TAG].freeze
|
32
|
+
CONTEXT_REGEX = %r{^https://api\.([^.]+)\.([^.]+)\.([^.]+)\.k8s\.dev\.invocaops\.com$}.freeze
|
33
|
+
OPS_CONTEXT_REGEX = %r{^[^.]+\.ops\.[^.]+$}
|
34
|
+
|
35
|
+
def initialize(attributes = {})
|
36
|
+
super(attributes)
|
37
|
+
yield self if block_given? # TODO: yield self is a code smell as it enables circular dependencies.
|
38
|
+
end
|
39
|
+
|
40
|
+
def cluster_fqdn
|
41
|
+
"#{cluster_name}.#{domain_name}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def kubernetes_context
|
45
|
+
context_from_api_endpoint || kubectl_context
|
46
|
+
end
|
47
|
+
|
48
|
+
def service_tag_filters
|
49
|
+
filters = [cluster_name]
|
50
|
+
|
51
|
+
if kubernetes_context =~ OPS_CONTEXT_REGEX
|
52
|
+
filters << 'staging-ops'
|
53
|
+
else
|
54
|
+
filters << 'staging'
|
55
|
+
end
|
56
|
+
|
57
|
+
filters.compact.join(",")
|
58
|
+
end
|
59
|
+
|
60
|
+
# TODO: Remove the default option. All images in the image repository will be tagged.
|
61
|
+
def tag_for_image(image, default: NO_DEFAULT_TAG)
|
62
|
+
if default == NO_DEFAULT_TAG
|
63
|
+
tag_for_image_key(image) or
|
64
|
+
raise KubePlatform::ConfigException, "Could not locate an image tag for '#{image}' and no default was provided"
|
65
|
+
else
|
66
|
+
tag_for_image_key(image) || tag_for_image_key(default) or
|
67
|
+
raise KubePlatform::ConfigException, "Could not locate an image tag for '#{image}' or the requested default '#{default}'"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def image_for_key(key)
|
72
|
+
image_repository.get(key)&.name or
|
73
|
+
raise KubePlatform::ConfigException, "No image found for '#{key}' in #{image_repository.inspect}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def master_tag?(tag)
|
77
|
+
[MASTER_TAG, MAIN_TAG].include?(tag)
|
78
|
+
end
|
79
|
+
|
80
|
+
def production_tag?(tag)
|
81
|
+
tag == PRODUCTION_TAG
|
82
|
+
end
|
83
|
+
|
84
|
+
def always_pull_tag?(tag)
|
85
|
+
ALWAYS_PULL_TAGS.include?(tag)
|
86
|
+
end
|
87
|
+
|
88
|
+
def image_key_tag_pairs
|
89
|
+
image_repository.key_tag_pairs
|
90
|
+
end
|
91
|
+
|
92
|
+
def image_key_tag_pairs_without_defaults
|
93
|
+
image_repository.key_tag_pairs_without_defaults
|
94
|
+
end
|
95
|
+
|
96
|
+
def apply_defaults(defaults)
|
97
|
+
obj = new_instance_with_additional_properties(defaults.keys)
|
98
|
+
|
99
|
+
defaults.each do |key, value|
|
100
|
+
if obj[key].nil?
|
101
|
+
obj[key] = value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
obj
|
106
|
+
end
|
107
|
+
|
108
|
+
def binding
|
109
|
+
super
|
110
|
+
end
|
111
|
+
|
112
|
+
alias orig_merge merge
|
113
|
+
|
114
|
+
def merge(other)
|
115
|
+
new_instance_with_additional_properties(other.keys).orig_merge(other)
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def context_from_api_endpoint
|
121
|
+
if (match = CONTEXT_REGEX.match(api_endpoint))
|
122
|
+
match.captures.join(".")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def tag_for_image_key(key)
|
127
|
+
image_repository.get(key)&.tag
|
128
|
+
end
|
129
|
+
|
130
|
+
def new_instance_with_additional_properties(properties)
|
131
|
+
klass = add_additional_properties(properties)
|
132
|
+
klass.new(self)
|
133
|
+
end
|
134
|
+
|
135
|
+
def add_additional_properties(properties)
|
136
|
+
dash_class = Class.new(self.class)
|
137
|
+
properties.each do |p|
|
138
|
+
dash_class.class_exec do
|
139
|
+
property p.to_sym
|
140
|
+
end
|
141
|
+
end
|
142
|
+
dash_class
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KubePlatform
|
4
|
+
KubePlatformException = Class.new(StandardError)
|
5
|
+
ConfigException = Class.new(KubePlatformException)
|
6
|
+
ManifestException = Class.new(KubePlatformException)
|
7
|
+
WaitTimeoutException = Class.new(KubePlatformException)
|
8
|
+
TokenNotFound = Class.new(KubePlatformException)
|
9
|
+
end
|