Skip to content

Commit 69b0ec1

Browse files
authored
RUBY-3298 Add container info to handshake metadata (#2821)
* RUBY-3298 include container info in handshake metadata * make sure it works even when faas info is not present * look for .dockerenv, not Dockerfile * add a test to check the value of DOCKERENV_PATH * linter appeasement
1 parent e06e03a commit 69b0ec1

File tree

5 files changed

+219
-15
lines changed

5 files changed

+219
-15
lines changed

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ Layout/SpaceInsideArrayLiteralBrackets:
5151
Layout/SpaceInsidePercentLiteralDelimiters:
5252
Enabled: false
5353

54+
Metrics/ClassLength:
55+
Max: 200
56+
5457
Metrics/ModuleLength:
5558
Enabled: false
5659

lib/mongo/server/app_metadata.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,14 @@ def os_doc
187187
}
188188
end
189189

190-
# Returns the environment doc describing the current FaaS environment.
190+
# Returns the environment doc describing the current execution
191+
# environment.
191192
#
192-
# @return [ Hash | nil ] the environment doc (or nil if not in a FaaS
193-
# environment).
193+
# @return [ Hash | nil ] the environment doc (or nil if no relevant
194+
# environment info was detected)
194195
def env_doc
195196
env = Environment.new
196-
env.faas? ? env.to_h : nil
197+
env.present? ? env.to_h : nil
197198
end
198199

199200
def type

lib/mongo/server/app_metadata/environment.rb

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ module Mongo
1818
class Server
1919
class AppMetadata
2020
# Implements the logic from the handshake spec, for deducing and
21-
# reporting the current FaaS environment in which the program is
21+
# reporting the current environment in which the program is
2222
# executing.
2323
#
24+
# This includes FaaS environment checks, as well as checks for the
25+
# presence of a container (Docker) and/or orchestrator (Kubernetes).
26+
#
2427
# @api private
2528
class Environment
2629
# Error class for reporting that too many discriminators were found
@@ -39,6 +42,10 @@ class TypeMismatch < Mongo::Error; end
3942
# Error class for reporting that the value for a field is too long.
4043
class ValueTooLong < Mongo::Error; end
4144

45+
# The name and location of the .dockerenv file that will signal the
46+
# presence of Docker.
47+
DOCKERENV_PATH = '/.dockerenv'
48+
4249
# This value is not explicitly specified in the spec, only implied to be
4350
# less than 512.
4451
MAXIMUM_VALUE_LENGTH = 500
@@ -102,9 +109,11 @@ class ValueTooLong < Mongo::Error; end
102109
# if the environment contains invalid or contradictory state, it will
103110
# be initialized with {{name}} set to {{nil}}.
104111
def initialize
112+
@fields = {}
105113
@error = nil
106114
@name = detect_environment
107-
populate_fields
115+
populate_faas_fields
116+
detect_container
108117
rescue TooManyEnvironments => e
109118
self.error = "too many environments detected: #{e.message}"
110119
rescue MissingVariable => e
@@ -115,6 +124,23 @@ def initialize
115124
self.error = "value for #{e.message} is too long"
116125
end
117126

127+
# Queries the detected container information.
128+
#
129+
# @return [ Hash | nil ] the detected container information, or
130+
# nil if no container was detected.
131+
def container
132+
fields[:container]
133+
end
134+
135+
# Queries whether any environment information was able to be
136+
# detected.
137+
#
138+
# @return [ true | false ] if any environment information was
139+
# detected.
140+
def present?
141+
@name || fields.any?
142+
end
143+
118144
# Queries whether the current environment is a valid FaaS environment.
119145
#
120146
# @return [ true | false ] whether the environment is a FaaS
@@ -159,14 +185,11 @@ def vercel?
159185
@name == 'vercel'
160186
end
161187

162-
# Compiles the detected environment information into a Hash. It will
163-
# always include a {{name}} key, but may include other keys as well,
164-
# depending on the detected FaaS environment. (See the handshake
165-
# spec for details.)
188+
# Compiles the detected environment information into a Hash.
166189
#
167190
# @return [ Hash ] the detected environment information.
168191
def to_h
169-
fields.merge(name: name)
192+
name ? fields.merge(name: name) : fields
170193
end
171194

172195
private
@@ -192,6 +215,38 @@ def detect_environment
192215
names.first
193216
end
194217

218+
# Looks for the presence of a container. Currently can detect
219+
# Docker (by the existence of a .dockerenv file in the root
220+
# directory) and Kubernetes (by the existence of the KUBERNETES_SERVICE_HOST
221+
# environment variable).
222+
def detect_container
223+
runtime = docker_present? && 'docker'
224+
orchestrator = kubernetes_present? && 'kubernetes'
225+
226+
return unless runtime || orchestrator
227+
228+
fields[:container] = {}
229+
fields[:container][:runtime] = runtime if runtime
230+
fields[:container][:orchestrator] = orchestrator if orchestrator
231+
end
232+
233+
# Checks for the existence of a .dockerenv in the root directory.
234+
def docker_present?
235+
File.exist?(dockerenv_path)
236+
end
237+
238+
# Implementing this as a method so that it can be mocked in tests, to
239+
# test the presence or absence of Docker.
240+
def dockerenv_path
241+
DOCKERENV_PATH
242+
end
243+
244+
# Checks for the presence of a non-empty KUBERNETES_SERVICE_HOST
245+
# environment variable.
246+
def kubernetes_present?
247+
!ENV['KUBERNETES_SERVICE_HOST'].to_s.empty?
248+
end
249+
195250
# Determines whether the named environment variable exists, and (if
196251
# a pattern has been declared for that descriminator) whether the
197252
# pattern matches the value of the variable.
@@ -212,10 +267,10 @@ def discriminator_matches?(var)
212267
# Extracts environment information from the current environment
213268
# variables, based on the detected FaaS environment. Populates the
214269
# {{@fields}} instance variable.
215-
def populate_fields
270+
def populate_faas_fields
216271
return unless name
217272

218-
@fields = FIELDS[name].each_with_object({}) do |(var, defn), fields|
273+
FIELDS[name].each_with_object(@fields) do |(var, defn), fields|
219274
fields[defn[:field]] = extract_field(var, defn)
220275
end
221276
end

spec/mongo/server/app_metadata/environment_spec.rb

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,52 @@
11
# frozen_string_literal: true
2+
# rubocop:todo all
23

34
require 'spec_helper'
5+
require 'fileutils'
6+
7+
MOCKED_DOCKERENV_PATH = File.expand_path(File.join(Dir.pwd, '.dockerenv-mocked'))
8+
9+
module ContainerChecking
10+
def mock_dockerenv_path
11+
before do
12+
allow_any_instance_of(Mongo::Server::AppMetadata::Environment)
13+
.to receive(:dockerenv_path)
14+
.and_return(MOCKED_DOCKERENV_PATH)
15+
end
16+
end
17+
18+
def with_docker
19+
mock_dockerenv_path
20+
21+
around do |example|
22+
File.write(MOCKED_DOCKERENV_PATH, 'placeholder')
23+
example.run
24+
ensure
25+
File.delete(MOCKED_DOCKERENV_PATH)
26+
end
27+
end
28+
29+
def without_docker
30+
mock_dockerenv_path
31+
32+
around do |example|
33+
FileUtils.rm_f(MOCKED_DOCKERENV_PATH)
34+
example.run
35+
end
36+
end
37+
38+
def with_kubernetes
39+
local_env 'KUBERNETES_SERVICE_HOST' => 'kubernetes.default.svc.cluster.local'
40+
end
41+
42+
def without_kubernetes
43+
local_env 'KUBERNETES_SERVICE_HOST' => nil
44+
end
45+
end
446

547
describe Mongo::Server::AppMetadata::Environment do
48+
extend ContainerChecking
49+
650
let(:env) { described_class.new }
751

852
shared_examples_for 'running in a FaaS environment' do
@@ -17,6 +61,36 @@
1761
end
1862
end
1963

64+
shared_examples_for 'not running in a Docker container' do
65+
it 'does not detect Docker' do
66+
expect(env.container || {}).not_to include :runtime
67+
end
68+
end
69+
70+
shared_examples_for 'not running under Kubernetes' do
71+
it 'does not detect Kubernetes' do
72+
expect(env.container || {}).not_to include :orchestrator
73+
end
74+
end
75+
76+
shared_examples_for 'running under Kubernetes' do
77+
it 'detects that Kubernetes is present' do
78+
expect(env.container[:orchestrator]).to be == 'kubernetes'
79+
end
80+
end
81+
82+
shared_examples_for 'running in a Docker container' do
83+
it 'detects that Docker is present' do
84+
expect(env.container[:runtime]).to be == 'docker'
85+
end
86+
end
87+
88+
shared_examples_for 'running under Kerbenetes' do
89+
it 'detects that kubernetes is present' do
90+
expect(env.container['orchestrator']).to be == 'kubernetes'
91+
end
92+
end
93+
2094
context 'when run outside of a FaaS environment' do
2195
it_behaves_like 'running outside a FaaS environment'
2296
end
@@ -204,6 +278,67 @@
204278
timeout_sec: 60, region: 'us-central1',
205279
}
206280
end
281+
282+
context 'when a container is present' do
283+
with_kubernetes
284+
with_docker
285+
286+
it 'includes a container key' do
287+
expect(env.to_h[:container]).to be == {
288+
runtime: 'docker',
289+
orchestrator: 'kubernetes'
290+
}
291+
end
292+
end
293+
294+
context 'when no container is present' do
295+
without_kubernetes
296+
without_docker
297+
298+
it 'does not include a container key' do
299+
expect(env.to_h).not_to include(:container)
300+
end
301+
end
302+
end
303+
end
304+
305+
# have a specific test for this, since the tests that check
306+
# for Docker use a mocked value for the .dockerenv path.
307+
it 'should look for dockerenv in root directory' do
308+
expect(described_class::DOCKERENV_PATH).to be == '/.dockerenv'
309+
end
310+
311+
context 'when no container is present' do
312+
without_kubernetes
313+
without_docker
314+
315+
it_behaves_like 'not running in a Docker container'
316+
it_behaves_like 'not running under Kubernetes'
317+
end
318+
319+
context 'when container is present' do
320+
context 'when kubernetes is present' do
321+
without_docker
322+
with_kubernetes
323+
324+
it_behaves_like 'not running in a Docker container'
325+
it_behaves_like 'running under Kubernetes'
326+
end
327+
328+
context 'when docker is present' do
329+
with_docker
330+
without_kubernetes
331+
332+
it_behaves_like 'running in a Docker container'
333+
it_behaves_like 'not running under Kubernetes'
334+
end
335+
336+
context 'when both kubernetes and docker are present' do
337+
with_docker
338+
with_kubernetes
339+
340+
it_behaves_like 'running in a Docker container'
341+
it_behaves_like 'running under Kubernetes'
207342
end
208343
end
209344
end

spec/mongo/server/app_metadata_spec.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,18 @@
8787
end
8888

8989
context 'when run outside of a FaaS environment' do
90-
it 'excludes the :env key from the client document' do
91-
expect(app_metadata.client_document.key?(:env)).to be false
90+
context 'when a container is present' do
91+
local_env 'KUBERNETES_SERVICE_HOST' => 'something'
92+
93+
it 'includes the :env key in the client document' do
94+
expect(app_metadata.client_document.key?(:env)).to be true
95+
end
96+
end
97+
98+
context 'when no container is present' do
99+
it 'excludes the :env key from the client document' do
100+
expect(app_metadata.client_document.key?(:env)).to be false
101+
end
92102
end
93103
end
94104

0 commit comments

Comments
 (0)