/ee/spec/models/ee/clusters/platforms/kubernetes_spec.rb
Ruby | 392 lines | 307 code | 84 blank | 1 comment | 0 complexity | 7941de476bc88610673a554a87675032 MD5 | raw file
1# frozen_string_literal: true
2
3require 'spec_helper'
4
5describe Clusters::Platforms::Kubernetes do
6 include KubernetesHelpers
7 include ReactiveCachingHelpers
8
9 shared_examples 'resource not found error' do |message|
10 it 'raises error' do
11 result = subject
12
13 expect(result[:error]).to eq(message)
14 expect(result[:status]).to eq(:error)
15 end
16 end
17
18 shared_examples 'kubernetes API error' do |error_code|
19 it 'raises error' do
20 result = subject
21
22 expect(result[:error]).to eq("Kubernetes API returned status code: #{error_code}")
23 expect(result[:status]).to eq(:error)
24 end
25 end
26
27 describe '#rollout_status' do
28 let(:deployments) { [] }
29 let(:pods) { [] }
30 let(:service) { create(:cluster_platform_kubernetes, :configured) }
31 let!(:cluster) { create(:cluster, :project, enabled: true, platform_kubernetes: service) }
32 let(:project) { cluster.project }
33 let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
34 let(:cache_data) { Hash(deployments: deployments, pods: pods) }
35
36 subject(:rollout_status) { service.rollout_status(environment, cache_data) }
37
38 context 'legacy deployments based on app label' do
39 let(:legacy_deployment) do
40 kube_deployment(name: 'legacy-deployment').tap do |deployment|
41 deployment['metadata']['annotations'].delete('app.gitlab.com/env')
42 deployment['metadata']['annotations'].delete('app.gitlab.com/app')
43 deployment['metadata']['labels']['app'] = environment.slug
44 end
45 end
46
47 let(:legacy_pod) do
48 kube_pod(name: 'legacy-pod').tap do |pod|
49 pod['metadata']['annotations'].delete('app.gitlab.com/env')
50 pod['metadata']['annotations'].delete('app.gitlab.com/app')
51 pod['metadata']['labels']['app'] = environment.slug
52 end
53 end
54
55 context 'only legacy deployments' do
56 let(:deployments) { [legacy_deployment] }
57 let(:pods) { [legacy_pod] }
58
59 it 'contains nothing' do
60 expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
61
62 expect(rollout_status.deployments).to eq([])
63 end
64
65 it 'has the has_legacy_app_label flag' do
66 expect(rollout_status).to be_has_legacy_app_label
67 end
68 end
69
70 context 'new deployment based on annotations' do
71 let(:matched_deployment) { kube_deployment(name: 'matched-deployment', environment_slug: environment.slug, project_slug: project.full_path_slug) }
72 let(:matched_pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug) }
73 let(:deployments) { [matched_deployment, legacy_deployment] }
74 let(:pods) { [matched_pod, legacy_pod] }
75
76 it 'contains only matching deployments' do
77 expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
78
79 expect(rollout_status.deployments.map(&:name)).to contain_exactly('matched-deployment')
80 end
81
82 it 'does have the has_legacy_app_label flag' do
83 expect(rollout_status).to be_has_legacy_app_label
84 end
85 end
86
87 context 'deployment with app label not matching the environment' do
88 let(:other_deployment) do
89 kube_deployment(name: 'other-deployment').tap do |deployment|
90 deployment['metadata']['annotations'].delete('app.gitlab.com/env')
91 deployment['metadata']['annotations'].delete('app.gitlab.com/app')
92 deployment['metadata']['labels']['app'] = 'helm-app-label'
93 end
94 end
95
96 let(:other_pod) do
97 kube_pod(name: 'other-pod').tap do |pod|
98 pod['metadata']['annotations'].delete('app.gitlab.com/env')
99 pod['metadata']['annotations'].delete('app.gitlab.com/app')
100 pod['metadata']['labels']['app'] = environment.slug
101 end
102 end
103
104 let(:deployments) { [other_deployment] }
105 let(:pods) { [other_pod] }
106
107 it 'does not have the has_legacy_app_label flag' do
108 expect(rollout_status).not_to be_has_legacy_app_label
109 end
110 end
111 end
112
113 context 'with valid deployments' do
114 let(:matched_deployment) { kube_deployment(environment_slug: environment.slug, project_slug: project.full_path_slug) }
115 let(:unmatched_deployment) { kube_deployment }
116 let(:matched_pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug) }
117 let(:unmatched_pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: 'Pending') }
118 let(:deployments) { [matched_deployment, unmatched_deployment] }
119 let(:pods) { [matched_pod, unmatched_pod] }
120
121 it 'creates a matching RolloutStatus' do
122 expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
123 expect(rollout_status.deployments.map(&:annotations)).to eq([
124 { 'app.gitlab.com/app' => project.full_path_slug, 'app.gitlab.com/env' => 'env-000000' }
125 ])
126 end
127 end
128
129 context 'with empty list of deployments' do
130 it 'creates a matching RolloutStatus' do
131 expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
132 expect(rollout_status).to be_not_found
133 end
134 end
135 end
136
137 describe '#read_pod_logs' do
138 let(:environment) { create(:environment) }
139 let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
140 let(:service) { create(:cluster_platform_kubernetes, :configured) }
141 let(:pod_name) { 'pod-1' }
142 let(:namespace) { 'app' }
143 let(:container) { 'some-container' }
144
145 subject { service.read_pod_logs(environment.id, pod_name, namespace, container: container) }
146
147 shared_examples 'successful log request' do
148 it do
149 expect(subject[:logs]).to eq(["Log 1", "Log 2", "Log 3"])
150 expect(subject[:status]).to eq(:success)
151 expect(subject[:pod_name]).to eq(pod_name)
152 expect(subject[:container_name]).to eq(container)
153 end
154 end
155
156 shared_examples 'returns pod_name and container_name' do
157 it do
158 expect(subject[:pod_name]).to eq(pod_name)
159 expect(subject[:container_name]).to eq(container)
160 end
161 end
162
163 context 'with reactive cache' do
164 before do
165 synchronous_reactive_cache(service)
166 end
167
168 context 'when ElasticSearch is enabled' do
169 let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
170 let!(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: cluster) }
171
172 before do
173 expect_any_instance_of(::Clusters::Applications::ElasticStack).to receive(:elasticsearch_client).at_least(:once).and_return(Elasticsearch::Transport::Client.new)
174 expect_any_instance_of(::Gitlab::Elasticsearch::Logs).to receive(:pod_logs).and_return(["Log 1", "Log 2", "Log 3"])
175 stub_feature_flags(enable_cluster_application_elastic_stack: true)
176 end
177
178 include_examples 'successful log request'
179 end
180
181 context 'when kubernetes responds with valid logs' do
182 before do
183 stub_kubeclient_logs(pod_name, namespace, container: container)
184 end
185
186 context 'on a project level cluster' do
187 let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
188
189 include_examples 'successful log request'
190 end
191
192 context 'on a group level cluster' do
193 let(:cluster) { create(:cluster, :group, platform_kubernetes: service) }
194
195 include_examples 'successful log request'
196 end
197
198 context 'on an instance level cluster' do
199 let(:cluster) { create(:cluster, :instance, platform_kubernetes: service) }
200
201 include_examples 'successful log request'
202 end
203 end
204
205 context 'when kubernetes responds with 500s' do
206 before do
207 stub_kubeclient_logs(pod_name, namespace, container: 'some-container', status: 500)
208 end
209
210 it_behaves_like 'kubernetes API error', 500
211
212 it_behaves_like 'returns pod_name and container_name'
213 end
214
215 context 'when container does not exist' do
216 before do
217 container = 'some-container'
218
219 stub_kubeclient_logs(pod_name, namespace, container: container,
220 status: 400, message: "container #{container} is not valid for pod #{pod_name}")
221 end
222
223 it_behaves_like 'kubernetes API error', 400
224
225 it_behaves_like 'returns pod_name and container_name'
226 end
227
228 context 'when kubernetes responds with 404s' do
229 before do
230 stub_kubeclient_logs(pod_name, namespace, container: 'some-container', status: 404)
231 end
232
233 it_behaves_like 'resource not found error', 'Pod not found'
234
235 it_behaves_like 'returns pod_name and container_name'
236 end
237
238 context 'when container name is not specified' do
239 let(:container) { 'container-0' }
240
241 subject { service.read_pod_logs(environment.id, pod_name, namespace) }
242
243 before do
244 stub_kubeclient_pod_details(pod_name, namespace)
245 stub_kubeclient_logs(pod_name, namespace, container: container)
246 end
247
248 include_examples 'successful log request'
249 end
250 end
251
252 context 'with caching', :use_clean_rails_memory_store_caching do
253 let(:opts) do
254 [
255 'get_pod_log',
256 {
257 'environment_id' => environment.id,
258 'pod_name' => pod_name,
259 'namespace' => namespace,
260 'container' => container
261 }
262 ]
263 end
264
265 context 'result is cacheable' do
266 before do
267 stub_kubeclient_logs(pod_name, namespace, container: container)
268 end
269
270 it do
271 result = subject
272
273 expect { stub_reactive_cache(service, result, opts) }.not_to raise_error
274 end
275 end
276
277 context 'when value present in cache' do
278 let(:return_value) { { 'status' => :success, 'logs' => 'logs' } }
279
280 before do
281 stub_reactive_cache(service, return_value, opts)
282 end
283
284 it 'returns cached value' do
285 result = subject
286
287 expect(result).to eq(return_value)
288 end
289 end
290
291 context 'when value not present in cache' do
292 it 'returns nil' do
293 expect(ReactiveCachingWorker)
294 .to receive(:perform_async)
295 .with(service.class, service.id, *opts)
296
297 result = subject
298
299 expect(result).to eq(nil)
300 end
301 end
302 end
303
304 context '#reactive_cache_updated' do
305 let(:opts) do
306 {
307 'environment_id' => environment.id,
308 'pod_name' => pod_name,
309 'namespace' => namespace,
310 'container' => container
311 }
312 end
313
314 subject { service.reactive_cache_updated('get_pod_log', opts) }
315
316 it 'expires k8s_pod_logs etag cache' do
317 expect_next_instance_of(Gitlab::EtagCaching::Store) do |store|
318 expect(store).to receive(:touch)
319 .with(
320 ::Gitlab::Routing.url_helpers.k8s_pod_logs_project_environment_path(
321 environment.project,
322 environment,
323 opts['pod_name'],
324 opts['container_name'],
325 format: :json
326 )
327 )
328 .and_call_original
329 end
330
331 subject
332 end
333 end
334 end
335
336 describe '#calculate_reactive_cache_for' do
337 let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
338 let(:service) { create(:cluster_platform_kubernetes, :configured) }
339 let(:namespace) { 'app' }
340 let(:environment) { instance_double(Environment, deployment_namespace: namespace) }
341
342 subject { service.calculate_reactive_cache_for(environment) }
343
344 before do
345 allow(service).to receive(:read_pods).and_return([])
346 end
347
348 context 'when kubernetes responds with valid deployments' do
349 before do
350 stub_kubeclient_deployments(namespace)
351 end
352
353 shared_examples 'successful deployment request' do
354 it { is_expected.to include(deployments: [kube_deployment]) }
355 end
356
357 context 'on a project level cluster' do
358 let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
359
360 include_examples 'successful deployment request'
361 end
362
363 context 'on a group level cluster' do
364 let(:cluster) { create(:cluster, :group, platform_kubernetes: service) }
365
366 include_examples 'successful deployment request'
367 end
368
369 context 'on an instance level cluster' do
370 let(:cluster) { create(:cluster, :instance, platform_kubernetes: service) }
371
372 include_examples 'successful deployment request'
373 end
374 end
375
376 context 'when kubernetes responds with 500s' do
377 before do
378 stub_kubeclient_deployments(namespace, status: 500)
379 end
380
381 it { expect { subject }.to raise_error(::Kubeclient::HttpError) }
382 end
383
384 context 'when kubernetes responds with 404s' do
385 before do
386 stub_kubeclient_deployments(namespace, status: 404)
387 end
388
389 it { is_expected.to include(deployments: []) }
390 end
391 end
392end