Skip to content

RUBY-3298 Add container info to handshake metadata #2821

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ Layout/SpaceInsideArrayLiteralBrackets:
Layout/SpaceInsidePercentLiteralDelimiters:
Enabled: false

Metrics/ClassLength:
Max: 200

Metrics/ModuleLength:
Enabled: false

Expand Down
9 changes: 5 additions & 4 deletions lib/mongo/server/app_metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,14 @@ def os_doc
}
end

# Returns the environment doc describing the current FaaS environment.
# Returns the environment doc describing the current execution
# environment.
#
# @return [ Hash | nil ] the environment doc (or nil if not in a FaaS
# environment).
# @return [ Hash | nil ] the environment doc (or nil if no relevant
# environment info was detected)
def env_doc
env = Environment.new
env.faas? ? env.to_h : nil
env.present? ? env.to_h : nil
end

def type
Expand Down
73 changes: 64 additions & 9 deletions lib/mongo/server/app_metadata/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ module Mongo
class Server
class AppMetadata
# Implements the logic from the handshake spec, for deducing and
# reporting the current FaaS environment in which the program is
# reporting the current environment in which the program is
# executing.
#
# This includes FaaS environment checks, as well as checks for the
# presence of a container (Docker) and/or orchestrator (Kubernetes).
#
# @api private
class Environment
# Error class for reporting that too many discriminators were found
Expand All @@ -39,6 +42,10 @@ class TypeMismatch < Mongo::Error; end
# Error class for reporting that the value for a field is too long.
class ValueTooLong < Mongo::Error; end

# The name and location of the .dockerenv file that will signal the
# presence of Docker.
DOCKERENV_PATH = '/.dockerenv'

# This value is not explicitly specified in the spec, only implied to be
# less than 512.
MAXIMUM_VALUE_LENGTH = 500
Expand Down Expand Up @@ -102,9 +109,11 @@ class ValueTooLong < Mongo::Error; end
# if the environment contains invalid or contradictory state, it will
# be initialized with {{name}} set to {{nil}}.
def initialize
@fields = {}
@error = nil
@name = detect_environment
populate_fields
populate_faas_fields
detect_container
rescue TooManyEnvironments => e
self.error = "too many environments detected: #{e.message}"
rescue MissingVariable => e
Expand All @@ -115,6 +124,23 @@ def initialize
self.error = "value for #{e.message} is too long"
end

# Queries the detected container information.
#
# @return [ Hash | nil ] the detected container information, or
# nil if no container was detected.
def container
fields[:container]
end

# Queries whether any environment information was able to be
# detected.
#
# @return [ true | false ] if any environment information was
# detected.
def present?
@name || fields.any?
end

# Queries whether the current environment is a valid FaaS environment.
#
# @return [ true | false ] whether the environment is a FaaS
Expand Down Expand Up @@ -159,14 +185,11 @@ def vercel?
@name == 'vercel'
end

# Compiles the detected environment information into a Hash. It will
# always include a {{name}} key, but may include other keys as well,
# depending on the detected FaaS environment. (See the handshake
# spec for details.)
# Compiles the detected environment information into a Hash.
#
# @return [ Hash ] the detected environment information.
def to_h
fields.merge(name: name)
name ? fields.merge(name: name) : fields
end

private
Expand All @@ -192,6 +215,38 @@ def detect_environment
names.first
end

# Looks for the presence of a container. Currently can detect
# Docker (by the existence of a .dockerenv file in the root
# directory) and Kubernetes (by the existence of the KUBERNETES_SERVICE_HOST
# environment variable).
def detect_container
runtime = docker_present? && 'docker'
orchestrator = kubernetes_present? && 'kubernetes'

return unless runtime || orchestrator

fields[:container] = {}
fields[:container][:runtime] = runtime if runtime
fields[:container][:orchestrator] = orchestrator if orchestrator
end

# Checks for the existence of a .dockerenv in the root directory.
def docker_present?
File.exist?(dockerenv_path)
end

# Implementing this as a method so that it can be mocked in tests, to
# test the presence or absence of Docker.
def dockerenv_path
DOCKERENV_PATH
end

# Checks for the presence of a non-empty KUBERNETES_SERVICE_HOST
# environment variable.
def kubernetes_present?
!ENV['KUBERNETES_SERVICE_HOST'].to_s.empty?
end

# Determines whether the named environment variable exists, and (if
# a pattern has been declared for that descriminator) whether the
# pattern matches the value of the variable.
Expand All @@ -212,10 +267,10 @@ def discriminator_matches?(var)
# Extracts environment information from the current environment
# variables, based on the detected FaaS environment. Populates the
# {{@fields}} instance variable.
def populate_fields
def populate_faas_fields
return unless name

@fields = FIELDS[name].each_with_object({}) do |(var, defn), fields|
FIELDS[name].each_with_object(@fields) do |(var, defn), fields|
fields[defn[:field]] = extract_field(var, defn)
end
end
Expand Down
135 changes: 135 additions & 0 deletions spec/mongo/server/app_metadata/environment_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,52 @@
# frozen_string_literal: true
# rubocop:todo all

require 'spec_helper'
require 'fileutils'

MOCKED_DOCKERENV_PATH = File.expand_path(File.join(Dir.pwd, '.dockerenv-mocked'))

module ContainerChecking
def mock_dockerenv_path
before do
allow_any_instance_of(Mongo::Server::AppMetadata::Environment)
.to receive(:dockerenv_path)
.and_return(MOCKED_DOCKERENV_PATH)
end
end

def with_docker
mock_dockerenv_path

around do |example|
File.write(MOCKED_DOCKERENV_PATH, 'placeholder')
example.run
ensure
File.delete(MOCKED_DOCKERENV_PATH)
end
end

def without_docker
mock_dockerenv_path

around do |example|
FileUtils.rm_f(MOCKED_DOCKERENV_PATH)
example.run
end
end

def with_kubernetes
local_env 'KUBERNETES_SERVICE_HOST' => 'kubernetes.default.svc.cluster.local'
end

def without_kubernetes
local_env 'KUBERNETES_SERVICE_HOST' => nil
end
end

describe Mongo::Server::AppMetadata::Environment do
extend ContainerChecking

let(:env) { described_class.new }

shared_examples_for 'running in a FaaS environment' do
Expand All @@ -17,6 +61,36 @@
end
end

shared_examples_for 'not running in a Docker container' do
it 'does not detect Docker' do
expect(env.container || {}).not_to include :runtime
end
end

shared_examples_for 'not running under Kubernetes' do
it 'does not detect Kubernetes' do
expect(env.container || {}).not_to include :orchestrator
end
end

shared_examples_for 'running under Kubernetes' do
it 'detects that Kubernetes is present' do
expect(env.container[:orchestrator]).to be == 'kubernetes'
end
end

shared_examples_for 'running in a Docker container' do
it 'detects that Docker is present' do
expect(env.container[:runtime]).to be == 'docker'
end
end

shared_examples_for 'running under Kerbenetes' do
it 'detects that kubernetes is present' do
expect(env.container['orchestrator']).to be == 'kubernetes'
end
end

context 'when run outside of a FaaS environment' do
it_behaves_like 'running outside a FaaS environment'
end
Expand Down Expand Up @@ -204,6 +278,67 @@
timeout_sec: 60, region: 'us-central1',
}
end

context 'when a container is present' do
with_kubernetes
with_docker

it 'includes a container key' do
expect(env.to_h[:container]).to be == {
runtime: 'docker',
orchestrator: 'kubernetes'
}
end
end

context 'when no container is present' do
without_kubernetes
without_docker

it 'does not include a container key' do
expect(env.to_h).not_to include(:container)
end
end
end
end

# have a specific test for this, since the tests that check
# for Docker use a mocked value for the .dockerenv path.
it 'should look for dockerenv in root directory' do
expect(described_class::DOCKERENV_PATH).to be == '/.dockerenv'
end

context 'when no container is present' do
without_kubernetes
without_docker

it_behaves_like 'not running in a Docker container'
it_behaves_like 'not running under Kubernetes'
end

context 'when container is present' do
context 'when kubernetes is present' do
without_docker
with_kubernetes

it_behaves_like 'not running in a Docker container'
it_behaves_like 'running under Kubernetes'
end

context 'when docker is present' do
with_docker
without_kubernetes

it_behaves_like 'running in a Docker container'
it_behaves_like 'not running under Kubernetes'
end

context 'when both kubernetes and docker are present' do
with_docker
with_kubernetes

it_behaves_like 'running in a Docker container'
it_behaves_like 'running under Kubernetes'
end
end
end
14 changes: 12 additions & 2 deletions spec/mongo/server/app_metadata_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,18 @@
end

context 'when run outside of a FaaS environment' do
it 'excludes the :env key from the client document' do
expect(app_metadata.client_document.key?(:env)).to be false
context 'when a container is present' do
local_env 'KUBERNETES_SERVICE_HOST' => 'something'

it 'includes the :env key in the client document' do
expect(app_metadata.client_document.key?(:env)).to be true
end
end

context 'when no container is present' do
it 'excludes the :env key from the client document' do
expect(app_metadata.client_document.key?(:env)).to be false
end
end
end

Expand Down