From 3ece59debb99ce7b1e481d209cbbc2cb2b215dd1 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Tue, 30 May 2023 14:38:33 -0600 Subject: [PATCH 1/6] add environment detection logic --- lib/mongo/server/app_metadata.rb | 55 +++--- lib/mongo/server/app_metadata/environment.rb | 153 +++++++++++++++ spec/mongo/client_construction_spec.rb | 6 +- spec/mongo/cluster_spec.rb | 4 +- .../server/app_metadata/environment_spec.rb | 183 ++++++++++++++++++ spec/mongo/server/app_metadata_spec.rb | 31 ++- spec/mongo/socket/ssl_spec.rb | 10 +- 7 files changed, 404 insertions(+), 38 deletions(-) create mode 100644 lib/mongo/server/app_metadata/environment.rb create mode 100644 spec/mongo/server/app_metadata/environment_spec.rb diff --git a/lib/mongo/server/app_metadata.rb b/lib/mongo/server/app_metadata.rb index 5d47a1d438..bdcaab3365 100644 --- a/lib/mongo/server/app_metadata.rb +++ b/lib/mongo/server/app_metadata.rb @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'mongo/server/app_metadata/environment' + module Mongo class Server # Application metadata that is sent to the server during a handshake, @@ -139,32 +141,34 @@ def validated_document document end - private - - # Check whether it is possible to build a valid app metadata document - # with params provided on intialization. - # - # @raise [ Error::InvalidApplicationName ] When the metadata are invalid. - def validate! - if @app_name && @app_name.bytesize > MAX_APP_NAME_SIZE - raise Error::InvalidApplicationName.new(@app_name, MAX_APP_NAME_SIZE) - end - true - end - # Get BSON::Document to be used as value for `client` key in # handshake document. # # @return [BSON::Document] Document describing client for handshake. - def full_client_document + # + # @api private + def client_document BSON::Document.new.tap do |doc| doc[:application] = { name: @app_name } if @app_name doc[:driver] = driver_doc doc[:os] = os_doc doc[:platform] = platform + env_doc.tap { |env| doc[:env] = env if env } end end + private + + # Check whether it is possible to build a valid app metadata document + # with params provided on intialization. + # + # @raise [ Error::InvalidApplicationName ] When the metadata are invalid. + def validate! + if @app_name && @app_name.bytesize > MAX_APP_NAME_SIZE + raise Error::InvalidApplicationName.new(@app_name, MAX_APP_NAME_SIZE) + end + true + end # Get the metadata as BSON::Document to be sent to # as part of the handshake. The document should @@ -173,21 +177,21 @@ def full_client_document # @return [BSON::Document] Document for connection's handshake. def document @document ||= begin - client_document = full_client_document - while client_document.to_bson.to_s.size > MAX_DOCUMENT_SIZE do - if client_document[:os][:name] || client_document[:os][:architecture] - client_document[:os].delete(:name) - client_document[:os].delete(:architecture) - elsif client_document[:platform] - client_document.delete(:platform) + client = client_document + while client.to_bson.to_s.size > MAX_DOCUMENT_SIZE do + if client[:os][:name] || client[:os][:architecture] + client[:os].delete(:name) + client[:os].delete(:architecture) + elsif client[:platform] + client.delete(:platform) else - client_document = nil + client = nil end end document = BSON::Document.new( { compression: @compressors, - client: client_document, + client: client, } ) document[:saslSupportedMechs] = @request_auth_mech if @request_auth_mech @@ -223,6 +227,11 @@ def os_doc } end + def env_doc + env = Environment.new + env.faas? ? env.to_h : nil + end + def type (RbConfig::CONFIG && RbConfig::CONFIG['host_os']) ? RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase : 'unknown' diff --git a/lib/mongo/server/app_metadata/environment.rb b/lib/mongo/server/app_metadata/environment.rb new file mode 100644 index 0000000000..e2d4a5ba51 --- /dev/null +++ b/lib/mongo/server/app_metadata/environment.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +# Copyright (C) 2016-2020 MongoDB Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'mongo/server/app_metadata/environment' + +module Mongo + class Server + class AppMetadata + # @api private + class Environment + # Error class for reporting that too many discriminators were found + # in the environment. (E.g. if the environment reports that it is + # running under both AWS and Azure.) + class TooManyEnvironments < Mongo::Error; end + + # Error class for reporting that a required environment variable is + # missing. + class MissingVariable < Mongo::Error; end + + # Error class for reporting that the wrong type was given for a + # field. + class TypeMismatch < Mongo::Error; end + + # Error class for reporting that the value for a field is too long. + class ValueTooLong < Mongo::Error; end + + # This value is not explicitly specified in the spec, only implied to be + # less than 512. + MAXIMUM_VALUE_LENGTH = 500 + + DISCRIMINATORS = { + 'AWS_EXECUTION_ENV' => 'aws.lambda', + 'AWS_LAMBDA_RUNTIME_API' => 'aws.lambda', + 'FUNCTIONS_WORKER_RUNTIME' => 'azure.func', + 'K_SERVICE' => 'gcp.func', + 'FUNCTION_NAME' => 'gcp.func', + 'VERCEL' => 'vercel', + }.freeze + + COERCIONS = { + string: -> (v) { String(v) }, + integer: -> (v) { Integer(v) } + } + + FIELDS = { + 'aws.lambda' => { + 'AWS_REGION' => { field: :region, type: :string }, + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => { field: :memory_mb, type: :integer }, + }, + + 'azure.func' => {}, + + 'gcp.func' => { + 'FUNCTION_MEMORY_MB' => { field: :memory_mb, type: :integer }, + 'FUNCTION_TIMEOUT_SEC' => { field: :timeout_sec, type: :integer }, + 'FUNCTION_REGION' => { field: :region, type: :string }, + }, + + 'vercel' => { + 'VERCEL_URL' => { field: :url, type: :string }, + 'VERCEL_REGION' => { field: :region, type: :string }, + }, + } + + attr_reader :name + attr_reader :fields + attr_reader :error + + def initialize + @error = nil + @name = detect_environment + populate_fields + rescue TooManyEnvironments => e + set_error "too many environments detected: #{e.message}" + rescue MissingVariable => e + set_error "missing environment variable: #{e.message}" + rescue TypeMismatch => e + set_error e.message + rescue ValueTooLong => e + set_error "value for #{e.message} is too long" + end + + def faas? + @name != nil + end + + def aws? + @name == 'aws.lambda' + end + + def azure? + @name == 'azure.func' + end + + def gcp? + @name == 'gcp.func' + end + + def vercel? + @name == 'vercel' + end + + def to_h + fields.merge(name: name) + end + + private + + def detect_environment + matches = DISCRIMINATORS.keys.select { |k| ENV[k] } + names = matches.map { |m| DISCRIMINATORS[m] }.uniq + + raise TooManyEnvironments, names.join(", ") if names.length > 1 + + names.first + end + + def populate_fields + return unless name + + @fields = FIELDS[name].each_with_object({}) do |(var, defn), fields| + raise MissingVariable, var unless ENV[var] + + raise ValueTooLong, var if ENV[var].length > MAXIMUM_VALUE_LENGTH + coerced = COERCIONS[defn[:type]].call(ENV[var]) + + fields[defn[:field]] = coerced + rescue ArgumentError + raise TypeMismatch, "#{var} must be #{defn[:type]} (got #{ENV[var].inspect})" + end + end + + def set_error(msg) + @name = nil + @error = msg + end + end + end + end +end diff --git a/spec/mongo/client_construction_spec.rb b/spec/mongo/client_construction_spec.rb index 254cc9001a..ec70621d92 100644 --- a/spec/mongo/client_construction_spec.rb +++ b/spec/mongo/client_construction_spec.rb @@ -955,7 +955,7 @@ end it 'includes the platform info in the app metadata' do - expect(app_metadata.send(:full_client_document)[:platform]).to match(/mongoid-6\.0\.2/) + expect(app_metadata.client_document[:platform]).to match(/mongoid-6\.0\.2/) end end @@ -982,7 +982,7 @@ end it 'does not include the platform info in the app metadata' do - expect(app_metadata.send(:full_client_document)[:platform]).to eq(platform_string) + expect(app_metadata.client_document[:platform]).to eq(platform_string) end end @@ -1001,7 +1001,7 @@ end it 'does not include the platform info in the app metadata' do - expect(app_metadata.send(:full_client_document)[:platform]).to eq(platform_string) + expect(app_metadata.client_document[:platform]).to eq(platform_string) end end end diff --git a/spec/mongo/cluster_spec.rb b/spec/mongo/cluster_spec.rb index f51436c253..6db47aff5a 100644 --- a/spec/mongo/cluster_spec.rb +++ b/spec/mongo/cluster_spec.rb @@ -480,7 +480,7 @@ end it 'constructs an AppMetadata object with the app_name' do - expect(cluster.app_metadata.send(:full_client_document)[:application]).to eq('name' => 'cluster_test') + expect(cluster.app_metadata.client_document[:application]).to eq('name' => 'cluster_test') end end @@ -491,7 +491,7 @@ end it 'constructs an AppMetadata object with no app_name' do - expect(cluster.app_metadata.send(:full_client_document)[:application]).to be_nil + expect(cluster.app_metadata.client_document[:application]).to be_nil end end end diff --git a/spec/mongo/server/app_metadata/environment_spec.rb b/spec/mongo/server/app_metadata/environment_spec.rb new file mode 100644 index 0000000000..9fa1ed9948 --- /dev/null +++ b/spec/mongo/server/app_metadata/environment_spec.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mongo::Server::AppMetadata::Environment do + let(:env) { described_class.new } + + shared_examples_for 'running in a FaaS environment' do + it 'reports that a FaaS environment is detected' do + expect(env.faas?).to be true + end + end + + shared_examples_for 'running outside a FaaS environment' do + it 'reports that no FaaS environment is detected' do + expect(env.faas?).to be false + end + end + + context 'when run outside of a FaaS environment' do + it_behaves_like 'running outside a FaaS environment' + end + + context 'when run in a FaaS environment' do + context 'when environment is invalid due to type mismatch' do + local_env( + 'AWS_EXECUTION_ENV' => 'AWS_Lambda_ruby2.7', + 'AWS_REGION' => 'us-east-2', + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => 'big' + ) + + it_behaves_like 'running outside a FaaS environment' + + it 'fails due to type mismatch' do + expect(env.error).to match(/AWS_LAMBDA_FUNCTION_MEMORY_SIZE must be integer/) + end + end + + context 'when environment is invalid due to long string' do + local_env( + 'AWS_EXECUTION_ENV' => 'AWS_Lambda_ruby2.7', + 'AWS_REGION' => 'a' * 512, + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => '1024' + ) + + it_behaves_like 'running outside a FaaS environment' + + it 'fails due to long string' do + expect(env.error).to match(/too long/) + end + end + + context 'when environment is invalid due to multiple providers' do + local_env( + 'AWS_EXECUTION_ENV' => 'AWS_Lambda_ruby2.7', + 'AWS_REGION' => 'us-east-2', + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => '1024', + 'FUNCTIONS_WORKER_RUNTIME' => 'ruby' + ) + + it_behaves_like 'running outside a FaaS environment' + + it 'fails due to multiple providers' do + expect(env.error).to match(/too many environments/) + end + end + + context 'when environment is invalid due to missing variable' do + local_env( + 'AWS_EXECUTION_ENV' => 'AWS_Lambda_ruby2.7', + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => '1024' + ) + + it_behaves_like 'running outside a FaaS environment' + + it 'fails due to missing variable' do + expect(env.error).to match(/missing environment variable/) + end + end + + context 'when FaaS environment is AWS' do + shared_examples_for 'running in an AWS environment' do + context 'when environment is valid' do + local_env( + 'AWS_REGION' => 'us-east-2', + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => '1024' + ) + + it_behaves_like 'running in a FaaS environment' + + it 'recognizes AWS' do + expect(env.name).to be == 'aws.lambda' + expect(env.fields[:region]).to be == 'us-east-2' + expect(env.fields[:memory_mb]).to be == 1024 + end + end + end + + context 'when AWS_EXECUTION_ENV is detected' do + local_env('AWS_EXECUTION_ENV' => 'AWS_Lambda_ruby2.7') + it_behaves_like 'running in an AWS environment' + end + + context 'when AWS_LAMBDA_RUNTIME_API is detected' do + local_env('AWS_LAMBDA_RUNTIME_API' => 'lambda.aws.amazon.com/api') + it_behaves_like 'running in an AWS environment' + end + end + + context 'when FaaS environment is Azure' do + local_env('FUNCTIONS_WORKER_RUNTIME' => 'ruby') + + it_behaves_like 'running in a FaaS environment' + + it 'recognizes Azure' do + expect(env.name).to be == 'azure.func' + end + end + + context 'when FaaS environment is GCP' do + local_env( + 'FUNCTION_MEMORY_MB' => '1024', + 'FUNCTION_TIMEOUT_SEC' => '60', + 'FUNCTION_REGION' => 'us-central1' + ) + + shared_examples_for 'running in a GCP environment' do + it_behaves_like 'running in a FaaS environment' + + it 'recognizes GCP' do + expect(env.name).to be == 'gcp.func' + expect(env.fields[:region]).to be == 'us-central1' + expect(env.fields[:memory_mb]).to be == 1024 + expect(env.fields[:timeout_sec]).to be == 60 + end + end + + context 'when K_SERVICE is present' do + local_env('K_SERVICE' => 'servicename') + it_behaves_like 'running in a GCP environment' + end + + context 'when FUNCTION_NAME is present' do + local_env('FUNCTION_NAME' => 'functionName') + it_behaves_like 'running in a GCP environment' + end + end + + context 'when FaaS environment is Vercel' do + local_env( + 'VERCEL' => '1', + 'VERCEL_URL' => '*.vercel.app', + 'VERCEL_REGION' => 'cdg1' + ) + + it_behaves_like 'running in a FaaS environment' + + it 'recognizes Vercel' do + expect(env.name).to be == 'vercel' + expect(env.fields[:url]).to be == '*.vercel.app' + expect(env.fields[:region]).to be == 'cdg1' + end + end + + context 'when converting environment to a hash' do + local_env( + 'K_SERVICE' => 'servicename', + 'FUNCTION_MEMORY_MB' => '1024', + 'FUNCTION_TIMEOUT_SEC' => '60', + 'FUNCTION_REGION' => 'us-central1' + ) + + it 'includes name and all fields' do + expect(env.to_h).to be == { + name: 'gcp.func', + memory_mb: 1024, + timeout_sec: 60, + region: 'us-central1', + } + end + end + end +end diff --git a/spec/mongo/server/app_metadata_spec.rb b/spec/mongo/server/app_metadata_spec.rb index 80b9c6cdca..1dc6e26e87 100644 --- a/spec/mongo/server/app_metadata_spec.rb +++ b/spec/mongo/server/app_metadata_spec.rb @@ -34,7 +34,7 @@ end it 'sets the app name' do - expect(app_metadata.send(:full_client_document)[:application][:name]).to eq('app_metadata_test') + expect(app_metadata.client_document[:application][:name]).to eq('app_metadata_test') end context 'when the app name exceeds the max length of 128' do @@ -58,7 +58,7 @@ context 'when the cluster does not have an app name option set' do it 'does not set the app name' do - expect(app_metadata.send(:full_client_document)[:application]).to be(nil) + expect(app_metadata.client_document[:application]).to be(nil) end end @@ -132,6 +132,33 @@ end end end + + context 'when run outside of a FaaS environment' do + it 'should exclude the :env key from the client document' do + expect(app_metadata.client_document.key?(:env)).to be false + end + end + + context 'when run inside of a FaaS environment' do + context 'when the environment is invalid' do + # invalid, because it is missing the other required fields + local_env('AWS_EXECUTION_ENV' => 'AWS_Lambda_ruby2.7') + + it 'should exclude the :env key from the client document' do + expect(app_metadata.client_document.key?(:env)).to be false + end + end + + context 'when the environment is valid' do + # valid, because Azure requires only the one field + local_env('FUNCTIONS_WORKER_RUNTIME' => 'ruby') + + it 'should include the :env key in the client document' do + expect(app_metadata.client_document.key?(:env)).to be true + expect(app_metadata.client_document[:env][:name]).to be == "azure.func" + end + end + end end describe '#document' do diff --git a/spec/mongo/socket/ssl_spec.rb b/spec/mongo/socket/ssl_spec.rb index d3d7b5fa9d..ecdb74e06a 100644 --- a/spec/mongo/socket/ssl_spec.rb +++ b/spec/mongo/socket/ssl_spec.rb @@ -592,14 +592,8 @@ ) end - around do |example| - saved = ENV['SSL_CERT_FILE'] - ENV['SSL_CERT_FILE'] = SpecConfig.instance.local_ca_cert_path - begin - example.run - ensure - ENV['SSL_CERT_FILE'] = saved - end + local_env do + { 'SSL_CERT_FILE' => SpecConfig.instance.local_ca_cert_path } end it 'uses the default cert store' do From bd2aa6f6a6180c8ee66de6e4365e0eaef07fbe9a Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 31 May 2023 09:29:00 -0600 Subject: [PATCH 2/6] add environment info, and updated truncation logic --- lib/mongo/server/app_metadata.rb | 50 +--- lib/mongo/server/app_metadata/environment.rb | 2 - lib/mongo/server/app_metadata/truncator.rb | 122 +++++++++ .../server/app_metadata/truncator_spec.rb | 254 ++++++++++++++++++ spec/mongo/server/app_metadata_spec.rb | 59 ++-- 5 files changed, 407 insertions(+), 80 deletions(-) create mode 100644 lib/mongo/server/app_metadata/truncator.rb create mode 100644 spec/mongo/server/app_metadata/truncator_spec.rb diff --git a/lib/mongo/server/app_metadata.rb b/lib/mongo/server/app_metadata.rb index bdcaab3365..1e80518765 100644 --- a/lib/mongo/server/app_metadata.rb +++ b/lib/mongo/server/app_metadata.rb @@ -16,6 +16,7 @@ # limitations under the License. require 'mongo/server/app_metadata/environment' +require 'mongo/server/app_metadata/truncator' module Mongo class Server @@ -28,11 +29,6 @@ class Server class AppMetadata extend Forwardable - # The max application metadata document byte size. - # - # @since 2.4.0 - MAX_DOCUMENT_SIZE = 512.freeze - # The max application name byte size. # # @since 2.4.0 @@ -148,18 +144,19 @@ def validated_document # # @api private def client_document - BSON::Document.new.tap do |doc| - doc[:application] = { name: @app_name } if @app_name - doc[:driver] = driver_doc - doc[:os] = os_doc - doc[:platform] = platform - env_doc.tap { |env| doc[:env] = env if env } - end + @client_document ||= + BSON::Document.new.tap do |doc| + doc[:application] = { name: @app_name } if @app_name + doc[:driver] = driver_doc + doc[:os] = os_doc + doc[:platform] = platform + env_doc.tap { |env| doc[:env] = env if env } + end end private - # Check whether it is possible to build a valid app metadata document + # Check whether it is possible to build a valid app metadata document # with params provided on intialization. # # @raise [ Error::InvalidApplicationName ] When the metadata are invalid. @@ -177,30 +174,11 @@ def validate! # @return [BSON::Document] Document for connection's handshake. def document @document ||= begin - client = client_document - while client.to_bson.to_s.size > MAX_DOCUMENT_SIZE do - if client[:os][:name] || client[:os][:architecture] - client[:os].delete(:name) - client[:os].delete(:architecture) - elsif client[:platform] - client.delete(:platform) - else - client = nil - end - end - document = BSON::Document.new( - { - compression: @compressors, - client: client, - } - ) - document[:saslSupportedMechs] = @request_auth_mech if @request_auth_mech - if @server_api - document.update( - Utils.transform_server_api(@server_api) - ) + client = Truncator.new(client_document).document + BSON::Document.new(compression: @compressors, client: client).tap do |doc| + doc[:saslSupportedMechs] = @request_auth_mech if @request_auth_mech + doc.update(Utils.transform_server_api(@server_api)) if @server_api end - document end end diff --git a/lib/mongo/server/app_metadata/environment.rb b/lib/mongo/server/app_metadata/environment.rb index e2d4a5ba51..18230784b4 100644 --- a/lib/mongo/server/app_metadata/environment.rb +++ b/lib/mongo/server/app_metadata/environment.rb @@ -14,8 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'mongo/server/app_metadata/environment' - module Mongo class Server class AppMetadata diff --git a/lib/mongo/server/app_metadata/truncator.rb b/lib/mongo/server/app_metadata/truncator.rb new file mode 100644 index 0000000000..cd70beef4c --- /dev/null +++ b/lib/mongo/server/app_metadata/truncator.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +# Copyright (C) 2016-2020 MongoDB Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'mongo/server/app_metadata/environment' + +module Mongo + class Server + class AppMetadata + # @api private + class Truncator + attr_reader :document + + # The max application metadata document byte size. + MAX_DOCUMENT_SIZE = 512 + + def initialize(document) + @document = document + try_truncate! + end + + def size + @document.to_bson.to_s.length + end + + def ok? + size <= MAX_DOCUMENT_SIZE + end + + private + + def excess + size - MAX_DOCUMENT_SIZE + end + + def try_truncate! + %i[ platform env os env_name os_type driver app_name ].each do |target| + break if ok? + send(:"try_truncate_#{target}!") + end + end + + def try_truncate_platform! + unless try_truncate_string(@document[:platform]) + @document.delete(:platform) + end + end + + def try_truncate_env! + try_truncate_hash(@document[:env], reserved: %w[ name ]) + end + + def try_truncate_os! + try_truncate_hash(@document[:os], reserved: %w[ type ]) + end + + def try_truncate_env_name! + return unless @document[:env] + + unless try_truncate_string(@document[:env][:name]) + @document.delete(:env) + end + end + + def try_truncate_os_type! + return unless @document[:os] + + unless try_truncate_string(@document[:os][:type]) + @document.delete(:os) + end + end + + def try_truncate_driver! + unless try_truncate_hash(@document[:driver]) + @document.delete(:driver) + end + end + + def try_truncate_app_name! + unless try_truncate_string(@document[:application][:name]) + @document.delete(:application) + end + end + + def try_truncate_string(string) + length = string&.length || 0 + + return false if excess > length + + string[(length - excess)..] = "" + end + + def try_truncate_hash(hash, reserved: []) + return false unless hash + + keys = hash.keys - reserved + keys.each do |key| + unless try_truncate_string(hash[key].to_s) + hash.delete(key) + end + + return true if ok? + end + + false + end + end + end + end +end diff --git a/spec/mongo/server/app_metadata/truncator_spec.rb b/spec/mongo/server/app_metadata/truncator_spec.rb new file mode 100644 index 0000000000..10a87e6223 --- /dev/null +++ b/spec/mongo/server/app_metadata/truncator_spec.rb @@ -0,0 +1,254 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Quoted from specifications/source/mongodb-handshake/handshake.rst: +# +# Drivers MUST validate these values and truncate or omit driver provided +# values if necessary. Implementors SHOULD prioritize fields to preserve in +# this order: +# +# 1. application.name +# 2. driver.* +# 3. os.type +# 4. env.name +# 5. os.* (except type) +# 6. env.* (except name) +# 7. platform + +describe Mongo::Server::AppMetadata::Truncator do + let(:truncator) { described_class.new(Marshal.load(Marshal.dump(metadata))) } + + let(:app_name) { 'application' } + let(:driver) { { name: 'driver', version: '1.2.3' } } + let(:os) { { type: 'Darwin', name: 'macOS', architecture: 'arm64', version: '13.4' } } + let(:platform) { { platform: 'platform' } } + let(:env) { { name: 'aws.lambda', region: 'region', memory_mb: 1024 } } + let(:extra) { nil } + + let(:metadata) do + BSON::Document.new.tap do |doc| + doc[:application] = { name: app_name } if app_name + doc[:driver] = driver if driver + doc[:os] = os if os + doc[:platform] = platform if platform + doc[:env] = env if env + doc[:__extra__] = extra if extra + end + end + + let(:untruncated_length) { metadata.to_bson.to_s.length } + let(:truncated_length) { truncator.document.to_bson.to_s.length } + + shared_examples_for 'a truncated document' do + it 'should be shorter' do + expect(truncated_length).to be < untruncated_length + end + + it 'should not be longer than the maximum document size' do + expect(truncated_length).to be <= described_class::MAX_DOCUMENT_SIZE + end + end + + describe 'MAX_DOCUMENT_SIZE' do + it 'should be 512 bytes' do + # This test is an additional check that MAX_DOCUMENT_SIZE + # has not been accidentially changed. + expect(described_class::MAX_DOCUMENT_SIZE).to be == 512 + end + end + + context 'when document does not need truncating' do + it 'should not truncate anything' do + expect(truncated_length).to be == untruncated_length + end + end + + context 'when modifying the platform is sufficient' do + context 'when truncating the platform is sufficient' do + let(:platform) { "a" * 1000 } + + it 'should truncate the platform' do + expect(truncator.document[:platform].length).to be < 1000 + end + + it_behaves_like 'a truncated document' + end + + context 'when the platform must be removed' do + # env is higher priority than platform, and will require platform to + # be resolved before truncating or removing anything in env. + let(:env) { { name: 'abc', a: 'a' * 1000 } } + + it 'should remove the platform' do + expect(truncator.document.key?(:platform)).to be false + end + + it_behaves_like 'a truncated document' + end + end + + context 'when modifying env is required' do + context 'when truncating a single key is sufficient' do + let(:env) { { name: 'abc', a: 'a' * 1000, b: '123' } } + + it 'should truncate that key' do + expect(truncator.document[:env].keys.sort).to be == %w[ a b name ] + expect(truncator.document[:env][:a].length).to be < 1000 + expect(truncator.document[:env][:name]).to be == 'abc' + expect(truncator.document[:env][:b]).to be == '123' + end + + it_behaves_like 'a truncated document' + end + + context 'when removing a key is required' do + let(:env) { { name: 'abc', a: 'a' * 1000, b: 'b' * 1000 } } + + it 'should remove the key' do + expect(truncator.document[:env].keys.sort).to be == %w[ b name ] + expect(truncator.document[:env][:b].length).to be < 1000 + expect(truncator.document[:env][:name]).to be == 'abc' + end + + it_behaves_like 'a truncated document' + end + end + + context 'when modifying os is required' do + context 'when env is problematic' do + let(:env) { { name: 'abc', a: 'a' * 1000, b: 'b' * 1000 } } + let(:os) { { type: 'abc', a: 'a' * 1000 } } + + it 'should modify env first' do + expect(truncator.document[:env].keys.sort).to be == %w[ name ] + expect(truncator.document[:os].keys.sort).to be == %w[ a type ] + end + + it_behaves_like 'a truncated document' + end + + context 'when truncating a single value is sufficient' do + let(:os) { { type: 'abc', a: 'a' * 1000, b: '123' } } + + it 'should truncate that key' do + expect(truncator.document[:os].keys.sort).to be == %w[ a b type ] + expect(truncator.document[:os][:a].length).to be < 1000 + expect(truncator.document[:os][:type]).to be == 'abc' + expect(truncator.document[:os][:b]).to be == '123' + end + + it_behaves_like 'a truncated document' + end + + context 'when removing a key is required' do + let(:os) { { type: 'abc', a: 'a' * 1000, b: 'b' * 1000 } } + + it 'should remove the key' do + expect(truncator.document[:os].keys.sort).to be == %w[ b type ] + expect(truncator.document[:os][:b].length).to be < 1000 + expect(truncator.document[:os][:type]).to be == 'abc' + end + + it_behaves_like 'a truncated document' + end + end + + context 'when modifying env.name is required' do + let(:env) { { name: 'n' * 1000, a: 'a' * 1000, b: 'b' * 1000 } } + let(:os) { { type: '123', a: 'a' * 1000, b: 'b' * 1000 } } + + context 'when truncating env.name is sufficient' do + it 'should truncate env.name' do + expect(truncator.document[:env].keys.sort).to be == %w[ name ] + expect(truncator.document[:os].keys.sort).to be == %w[ type ] + expect(truncator.document[:env][:name].length).to be < 1000 + end + + it_behaves_like 'a truncated document' + end + + context 'when removing env.name is required' do + let(:os) { { type: 'n' * 1000 } } + + it 'should remove env' do + expect(truncator.document.key?(:env)).to be false + end + + it_behaves_like 'a truncated document' + end + end + + context 'when modifying os.type is required' do + let(:os) { { type: 'n' * 1000, a: 'a' * 1000, b: 'b' * 1000 } } + + context 'when truncating os.type is sufficient' do + it 'should truncate os.type' do + expect(truncator.document.key?(:env)).to be false + expect(truncator.document[:os].keys.sort).to be == %w[ type ] + expect(truncator.document[:os][:type].length).to be < 1000 + end + + it_behaves_like 'a truncated document' + end + + context 'when removing os.type is required' do + let(:app_name) { 'n' * 1000 } + + it 'should remove os' do + expect(truncator.document.key?(:os)).to be false + end + + it_behaves_like 'a truncated document' + end + end + + context 'when modifying driver is required' do + context 'when truncating a single key is sufficient' do + let(:driver) { { name: 'd' * 1000, version: '1.2.3' } } + + it 'should truncate that key' do + expect(truncator.document.key?(:os)).to be false + expect(truncator.document[:driver].keys.sort).to be == %w[ name version ] + expect(truncator.document[:driver][:version].length).to be < 1000 + end + + it_behaves_like 'a truncated document' + end + + context 'when removing a key is required' do + let(:driver) { { name: 'd' * 1000, version: 'v' * 1000 } } + + it 'should remove the key' do + expect(truncator.document.key?(:os)).to be false + expect(truncator.document[:driver].keys.sort).to be == %w[ version ] + expect(truncator.document[:driver][:version].length).to be < 1000 + end + + it_behaves_like 'a truncated document' + end + end + + context 'when modifying application.name is required' do + context 'when truncating application.name is sufficient' do + let(:app_name) { 'n' * 1000 } + + it 'should truncate the name' do + expect(truncator.document.key?(:driver)).to be false + expect(truncator.document[:application][:name].length).to be < 1000 + end + + it_behaves_like 'a truncated document' + end + + context 'when removing application.name is required' do + let(:app_name) { 'n' * 1000 } + let(:extra) { 'n' * described_class::MAX_DOCUMENT_SIZE } + + it 'should remove the application key' do + expect(truncator.document.key?(:driver)).to be false + expect(truncator.document.key?(:application)).to be false + end + end + end +end diff --git a/spec/mongo/server/app_metadata_spec.rb b/spec/mongo/server/app_metadata_spec.rb index 1dc6e26e87..ba19b08484 100644 --- a/spec/mongo/server/app_metadata_spec.rb +++ b/spec/mongo/server/app_metadata_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe Mongo::Server::AppMetadata do + let(:max_size) { described_class::Truncator::MAX_DOCUMENT_SIZE } let(:app_metadata) do described_class.new(cluster.options) @@ -13,14 +14,6 @@ authorized_client.cluster end - describe 'MAX_DOCUMENT_SIZE' do - it 'should be 512 bytes' do - # This test is an additional check that MAX_DOCUMENT_SIZE - # has not been accidentially changed. - expect(described_class::MAX_DOCUMENT_SIZE).to eq(512) - end - end - describe '#initialize' do context 'when the cluster has an app name option set' do @@ -63,73 +56,55 @@ end context 'when the client document exceeds the max of 512 bytes' do - # Server api parameters change metadata length - require_no_required_api_version + shared_examples_for 'a truncated document' do + it 'is too long before validation' do + expect(app_metadata.client_document.to_bson.to_s.size).to be > max_size + end - context 'when the os.type length is too long' do + it 'is acceptable after validation' do + app_metadata.validated_document # force validation + expect(app_metadata.client_document.to_bson.to_s.size).to be <= max_size + end + end + context 'when the os.type length is too long' do before do allow(app_metadata).to receive(:type).and_return('x'*500) end - it 'truncates the document' do - expect( - app_metadata.validated_document.to_bson.to_s.size - ).to be < described_class::MAX_DOCUMENT_SIZE - end + it_behaves_like 'a truncated document' end context 'when the os.name length is too long' do - before do allow(app_metadata).to receive(:name).and_return('x'*500) end - it 'truncates the document' do - expect( - app_metadata.validated_document.to_bson.to_s.size - ).to be < described_class::MAX_DOCUMENT_SIZE - end + it_behaves_like 'a truncated document' end context 'when the os.architecture length is too long' do - before do allow(app_metadata).to receive(:architecture).and_return('x'*500) end - it 'truncates the document' do - expect( - app_metadata.validated_document.to_bson.to_s.size - ).to be < described_class::MAX_DOCUMENT_SIZE - end + it_behaves_like 'a truncated document' end context 'when the platform length is too long' do - before do allow(app_metadata).to receive(:platform).and_return('x'*500) end - it 'truncates the document' do - expect( - app_metadata.validated_document.to_bson.to_s.size - ).to be < described_class::MAX_DOCUMENT_SIZE - end + it_behaves_like 'a truncated document' end context 'when the driver info is too long' do - require_no_compression - before do - allow(app_metadata).to receive(:driver_doc).and_return('x'*500) + allow(app_metadata).to receive(:driver_doc).and_return({ name: 'x'*500 }) end - it 'truncates the document' do - expect( - app_metadata.validated_document.to_bson.to_s.size - ).to be < described_class::MAX_DOCUMENT_SIZE - end + it_behaves_like 'a truncated document' end end From a1b583ab4f95e46693d0a242499c2915487f560a Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 31 May 2023 10:08:08 -0600 Subject: [PATCH 3/6] rubocop pass for new files --- .rubocop.yml | 3 + lib/mongo/server/app_metadata/environment.rb | 133 +++++++++++++++--- lib/mongo/server/app_metadata/truncator.rb | 84 ++++++++--- .../server/app_metadata/environment_spec.rb | 8 +- .../server/app_metadata/truncator_spec.rb | 40 +++--- 5 files changed, 201 insertions(+), 67 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 3f63fb451b..2b7a7f90f7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -64,6 +64,9 @@ RSpec/BeforeAfterAll: RSpec/DescribeClass: Enabled: false +Style/FetchEnvVar: + Enabled: false + RSpec/ImplicitExpect: EnforcedStyle: is_expected diff --git a/lib/mongo/server/app_metadata/environment.rb b/lib/mongo/server/app_metadata/environment.rb index 18230784b4..526c4fc41a 100644 --- a/lib/mongo/server/app_metadata/environment.rb +++ b/lib/mongo/server/app_metadata/environment.rb @@ -17,6 +17,10 @@ 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 + # executing. + # # @api private class Environment # Error class for reporting that too many discriminators were found @@ -39,20 +43,26 @@ class ValueTooLong < Mongo::Error; end # less than 512. MAXIMUM_VALUE_LENGTH = 500 + # The mapping that determines which FaaS environment is active, based + # on which environment variable(s) are present. DISCRIMINATORS = { - 'AWS_EXECUTION_ENV' => 'aws.lambda', - 'AWS_LAMBDA_RUNTIME_API' => 'aws.lambda', + 'AWS_EXECUTION_ENV' => 'aws.lambda', + 'AWS_LAMBDA_RUNTIME_API' => 'aws.lambda', 'FUNCTIONS_WORKER_RUNTIME' => 'azure.func', - 'K_SERVICE' => 'gcp.func', - 'FUNCTION_NAME' => 'gcp.func', - 'VERCEL' => 'vercel', + 'K_SERVICE' => 'gcp.func', + 'FUNCTION_NAME' => 'gcp.func', + 'VERCEL' => 'vercel', }.freeze + # Describes how to coerce values of the specified type. COERCIONS = { - string: -> (v) { String(v) }, - integer: -> (v) { Integer(v) } - } + string: ->(v) { String(v) }, + integer: ->(v) { Integer(v) } + }.freeze + # Describes which fields are required for each FaaS environment, + # along with their expected types, and how they should be named in + # the handshake document. FIELDS = { 'aws.lambda' => { 'AWS_REGION' => { field: :region, type: :string }, @@ -71,77 +81,154 @@ class ValueTooLong < Mongo::Error; end 'VERCEL_URL' => { field: :url, type: :string }, 'VERCEL_REGION' => { field: :region, type: :string }, }, - } + }.freeze + # @return [ String | nil ] the name of the FaaS environment that was + # detected, or nil if no valid FaaS environment was detected. attr_reader :name + + # @return [ Hash | nil ] the fields describing detected FaaS + # environment. attr_reader :fields + + # @return [ String | nil ] the error message explaining why a valid + # FaaS environment was not detected, or nil if no error occurred. + # + # @note These error messagess are not to be propogated to the + # user; they are intended only for troubleshooting and debugging.) attr_reader :error + # Create a new AppMetadata::Environment object, initializing it from + # the current ENV variables. If no FaaS environment is detected, or + # if the environment contains invalid or contradictory state, it will + # be initialized with {{name}} set to {{nil}}. def initialize @error = nil @name = detect_environment populate_fields rescue TooManyEnvironments => e - set_error "too many environments detected: #{e.message}" + self.error = "too many environments detected: #{e.message}" rescue MissingVariable => e - set_error "missing environment variable: #{e.message}" + self.error = "missing environment variable: #{e.message}" rescue TypeMismatch => e - set_error e.message + self.error = e.message rescue ValueTooLong => e - set_error "value for #{e.message} is too long" + self.error = "value for #{e.message} is too long" end + # Queries whether the current environment is a valid FaaS environment. + # + # @return [ true | false ] whether the environment is a FaaS + # environment or not. def faas? @name != nil end + # Queries whether the current environment is a valid AWS Lambda + # environment. + # + # @return [ true | false ] whether the environment is a AWS Lambda + # environment or not. def aws? @name == 'aws.lambda' end + # Queries whether the current environment is a valid Azure + # environment. + # + # @return [ true | false ] whether the environment is a Azure + # environment or not. def azure? @name == 'azure.func' end + # Queries whether the current environment is a valid GCP + # environment. + # + # @return [ true | false ] whether the environment is a GCP + # environment or not. def gcp? @name == 'gcp.func' end + # Queries whether the current environment is a valid Vercel + # environment. + # + # @return [ true | false ] whether the environment is a Vercel + # environment or not. 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.) + # + # @return [ Hash ] the detected environment information. def to_h fields.merge(name: name) end private + # Searches the DESCRIMINATORS list to see which (if any) apply to + # the current environment. + # + # @return [ String | nil ] the name of the detected FaaS provider. + # + # @raise [ TooManyEnvironments ] if the environment contains + # discriminating variables for more than one FaaS provider. def detect_environment matches = DISCRIMINATORS.keys.select { |k| ENV[k] } names = matches.map { |m| DISCRIMINATORS[m] }.uniq - raise TooManyEnvironments, names.join(", ") if names.length > 1 + raise TooManyEnvironments, names.join(', ') if names.length > 1 names.first end + # Extracts environment information from the current environment + # variables, based on the detected FaaS environment. Populates the + # {{@fields}} instance variable. def populate_fields return unless name @fields = FIELDS[name].each_with_object({}) do |(var, defn), fields| - raise MissingVariable, var unless ENV[var] - - raise ValueTooLong, var if ENV[var].length > MAXIMUM_VALUE_LENGTH - coerced = COERCIONS[defn[:type]].call(ENV[var]) - - fields[defn[:field]] = coerced - rescue ArgumentError - raise TypeMismatch, "#{var} must be #{defn[:type]} (got #{ENV[var].inspect})" + fields[defn[:field]] = extract_field(var, defn) end end - def set_error(msg) + # Extracts the named variable from the environment and validates it + # against its declared definition. + # + # @param [ String ] var The name of the environment variable to look + # for. + # @param [ Hash ] definition The definition of the field that applies + # to the named variable. + # + # @return [ Integer | String ] the validated and coerced value of the + # given environment variable. + # + # @raise [ MissingVariable ] if the environment does not include a + # variable required by the current FaaS provider. + # @raise [ ValueTooLong ] if a required variable is too long. + # @raise [ TypeMismatch ] if a required variable cannot be coerced to + # the expected type. + def extract_field(var, definition) + raise MissingVariable, var unless ENV[var] + raise ValueTooLong, var if ENV[var].length > MAXIMUM_VALUE_LENGTH + + COERCIONS[definition[:type]].call(ENV[var]) + rescue ArgumentError + raise TypeMismatch, + "#{var} must be #{definition[:type]} (got #{ENV[var].inspect})" + end + + # Sets the error message to the given value and sets the name to nil. + # + # @param [ String ] msg The error message to store. + def error=(msg) @name = nil @error = msg end diff --git a/lib/mongo/server/app_metadata/truncator.rb b/lib/mongo/server/app_metadata/truncator.rb index cd70beef4c..dcd0d558c7 100644 --- a/lib/mongo/server/app_metadata/truncator.rb +++ b/lib/mongo/server/app_metadata/truncator.rb @@ -19,97 +19,143 @@ module Mongo class Server class AppMetadata + # Implements the metadata truncation logic described in the handshake + # spec. + # # @api private class Truncator + # @return [ BSON::Document ] the document being truncated. attr_reader :document # The max application metadata document byte size. MAX_DOCUMENT_SIZE = 512 + # Creates a new Truncator instance and tries enforcing the maximum + # document size on the given document. + # + # @param [ BSON::Document] document The document to (potentially) + # truncate. + # + # @note The document is modified in-place; if you wish to keep the + # original unchanged, you must deep-clone it prior to sending it to + # a truncator. def initialize(document) @document = document try_truncate! end + # The current size of the document, in bytes, as a serialized BSON + # document. + # + # @return [ Integer ] the size of the document def size @document.to_bson.to_s.length end + # Whether the document fits within the required maximum document size. + # + # @return [ true | false ] if the document is okay or not. def ok? size <= MAX_DOCUMENT_SIZE end private + # How many extra bytes must be trimmed before the document may be + # considered #ok?. + # + # @return [ Integer ] how many bytes larger the document is than the + # maximum document size. def excess size - MAX_DOCUMENT_SIZE end + # Attempt to truncate the document using the documented metadata + # priorities (see the handshake specification). def try_truncate! %i[ platform env os env_name os_type driver app_name ].each do |target| break if ok? + send(:"try_truncate_#{target}!") end end + # Attempt to truncate or remove the {{:platform}} key from the + # document. def try_truncate_platform! - unless try_truncate_string(@document[:platform]) - @document.delete(:platform) - end + @document.delete(:platform) unless try_truncate_string(@document[:platform]) end + # Attempt to truncate the keys in the {{:env}} subdocument. def try_truncate_env! try_truncate_hash(@document[:env], reserved: %w[ name ]) end + # Attempt to truncate the keys in the {{:os}} subdocument. def try_truncate_os! try_truncate_hash(@document[:os], reserved: %w[ type ]) end + # Attempt to truncate {{env[:name]}} key. If that does not suffice, + # remove the {{:env}} key entirely. def try_truncate_env_name! return unless @document[:env] - unless try_truncate_string(@document[:env][:name]) - @document.delete(:env) - end + @document.delete(:env) unless try_truncate_string(@document[:env][:name]) end + # Attempt to truncate {{os[:type]}} key. If that does not suffice, + # remove the {{:os}} key entirely. def try_truncate_os_type! return unless @document[:os] - unless try_truncate_string(@document[:os][:type]) - @document.delete(:os) - end + @document.delete(:os) unless try_truncate_string(@document[:os][:type]) end + # Attempt to truncate the keys in the {{:driver}} subdocument. If + # that does not suffice, remove the {{:driver}} key entirely. def try_truncate_driver! - unless try_truncate_hash(@document[:driver]) - @document.delete(:driver) - end + @document.delete(:driver) unless try_truncate_hash(@document[:driver]) end + # Attempt to truncate {{application[:name]}} key. If that does not + # suffice, remove the {{:application}} key entirely. def try_truncate_app_name! - unless try_truncate_string(@document[:application][:name]) - @document.delete(:application) - end + @document.delete(:application) unless try_truncate_string(@document[:application][:name]) end + # A helper method for truncating a string (in-place) by whatever + # {{#excess}} is required. + # + # @param [ String ] string the string value to truncate. + # + # @note the parameter is modified in-place. def try_truncate_string(string) length = string&.length || 0 return false if excess > length - string[(length - excess)..] = "" + string[(length - excess)..-1] = '' end + # A helper method for truncating the keys of a Hash (in-place) by + # whatever {{#excess}} is required. The keys are considered in order + # (using the Hash's native key ordering), and each will be either + # truncated (if that suffices) or removed from the hash (if truncating + # is not sufficient). + # + # Any keys in the {{reserved}} list will be ignored. + # + # @param [ Hash | nil ] hash the Hash instance to consider. + # @param [ Array ] reserved the list of keys to ignore in the hash. + # + # @note the hash parameter is modified in-place. def try_truncate_hash(hash, reserved: []) return false unless hash keys = hash.keys - reserved keys.each do |key| - unless try_truncate_string(hash[key].to_s) - hash.delete(key) - end + hash.delete(key) unless try_truncate_string(hash[key].to_s) return true if ok? end diff --git a/spec/mongo/server/app_metadata/environment_spec.rb b/spec/mongo/server/app_metadata/environment_spec.rb index 9fa1ed9948..c63209160c 100644 --- a/spec/mongo/server/app_metadata/environment_spec.rb +++ b/spec/mongo/server/app_metadata/environment_spec.rb @@ -82,7 +82,7 @@ shared_examples_for 'running in an AWS environment' do context 'when environment is valid' do local_env( - 'AWS_REGION' => 'us-east-2', + 'AWS_REGION' => 'us-east-2', 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => '1024' ) @@ -172,10 +172,8 @@ it 'includes name and all fields' do expect(env.to_h).to be == { - name: 'gcp.func', - memory_mb: 1024, - timeout_sec: 60, - region: 'us-central1', + name: 'gcp.func', memory_mb: 1024, + timeout_sec: 60, region: 'us-central1', } end end diff --git a/spec/mongo/server/app_metadata/truncator_spec.rb b/spec/mongo/server/app_metadata/truncator_spec.rb index 10a87e6223..06bc414b10 100644 --- a/spec/mongo/server/app_metadata/truncator_spec.rb +++ b/spec/mongo/server/app_metadata/truncator_spec.rb @@ -41,17 +41,17 @@ let(:truncated_length) { truncator.document.to_bson.to_s.length } shared_examples_for 'a truncated document' do - it 'should be shorter' do + it 'is shorter' do expect(truncated_length).to be < untruncated_length end - it 'should not be longer than the maximum document size' do + it 'is not be longer than the maximum document size' do expect(truncated_length).to be <= described_class::MAX_DOCUMENT_SIZE end end describe 'MAX_DOCUMENT_SIZE' do - it 'should be 512 bytes' do + it 'is 512 bytes' do # This test is an additional check that MAX_DOCUMENT_SIZE # has not been accidentially changed. expect(described_class::MAX_DOCUMENT_SIZE).to be == 512 @@ -59,16 +59,16 @@ end context 'when document does not need truncating' do - it 'should not truncate anything' do + it 'does not truncate anything' do expect(truncated_length).to be == untruncated_length end end context 'when modifying the platform is sufficient' do context 'when truncating the platform is sufficient' do - let(:platform) { "a" * 1000 } + let(:platform) { 'a' * 1000 } - it 'should truncate the platform' do + it 'truncates the platform' do expect(truncator.document[:platform].length).to be < 1000 end @@ -80,7 +80,7 @@ # be resolved before truncating or removing anything in env. let(:env) { { name: 'abc', a: 'a' * 1000 } } - it 'should remove the platform' do + it 'removes the platform' do expect(truncator.document.key?(:platform)).to be false end @@ -92,7 +92,7 @@ context 'when truncating a single key is sufficient' do let(:env) { { name: 'abc', a: 'a' * 1000, b: '123' } } - it 'should truncate that key' do + it 'truncates that key' do expect(truncator.document[:env].keys.sort).to be == %w[ a b name ] expect(truncator.document[:env][:a].length).to be < 1000 expect(truncator.document[:env][:name]).to be == 'abc' @@ -105,7 +105,7 @@ context 'when removing a key is required' do let(:env) { { name: 'abc', a: 'a' * 1000, b: 'b' * 1000 } } - it 'should remove the key' do + it 'removes the key' do expect(truncator.document[:env].keys.sort).to be == %w[ b name ] expect(truncator.document[:env][:b].length).to be < 1000 expect(truncator.document[:env][:name]).to be == 'abc' @@ -120,7 +120,7 @@ let(:env) { { name: 'abc', a: 'a' * 1000, b: 'b' * 1000 } } let(:os) { { type: 'abc', a: 'a' * 1000 } } - it 'should modify env first' do + it 'modifies env first' do expect(truncator.document[:env].keys.sort).to be == %w[ name ] expect(truncator.document[:os].keys.sort).to be == %w[ a type ] end @@ -131,7 +131,7 @@ context 'when truncating a single value is sufficient' do let(:os) { { type: 'abc', a: 'a' * 1000, b: '123' } } - it 'should truncate that key' do + it 'truncates that key' do expect(truncator.document[:os].keys.sort).to be == %w[ a b type ] expect(truncator.document[:os][:a].length).to be < 1000 expect(truncator.document[:os][:type]).to be == 'abc' @@ -144,7 +144,7 @@ context 'when removing a key is required' do let(:os) { { type: 'abc', a: 'a' * 1000, b: 'b' * 1000 } } - it 'should remove the key' do + it 'removes the key' do expect(truncator.document[:os].keys.sort).to be == %w[ b type ] expect(truncator.document[:os][:b].length).to be < 1000 expect(truncator.document[:os][:type]).to be == 'abc' @@ -159,7 +159,7 @@ let(:os) { { type: '123', a: 'a' * 1000, b: 'b' * 1000 } } context 'when truncating env.name is sufficient' do - it 'should truncate env.name' do + it 'truncates env.name' do expect(truncator.document[:env].keys.sort).to be == %w[ name ] expect(truncator.document[:os].keys.sort).to be == %w[ type ] expect(truncator.document[:env][:name].length).to be < 1000 @@ -171,7 +171,7 @@ context 'when removing env.name is required' do let(:os) { { type: 'n' * 1000 } } - it 'should remove env' do + it 'removes env' do expect(truncator.document.key?(:env)).to be false end @@ -183,7 +183,7 @@ let(:os) { { type: 'n' * 1000, a: 'a' * 1000, b: 'b' * 1000 } } context 'when truncating os.type is sufficient' do - it 'should truncate os.type' do + it 'truncates os.type' do expect(truncator.document.key?(:env)).to be false expect(truncator.document[:os].keys.sort).to be == %w[ type ] expect(truncator.document[:os][:type].length).to be < 1000 @@ -195,7 +195,7 @@ context 'when removing os.type is required' do let(:app_name) { 'n' * 1000 } - it 'should remove os' do + it 'removes os' do expect(truncator.document.key?(:os)).to be false end @@ -207,7 +207,7 @@ context 'when truncating a single key is sufficient' do let(:driver) { { name: 'd' * 1000, version: '1.2.3' } } - it 'should truncate that key' do + it 'truncates that key' do expect(truncator.document.key?(:os)).to be false expect(truncator.document[:driver].keys.sort).to be == %w[ name version ] expect(truncator.document[:driver][:version].length).to be < 1000 @@ -219,7 +219,7 @@ context 'when removing a key is required' do let(:driver) { { name: 'd' * 1000, version: 'v' * 1000 } } - it 'should remove the key' do + it 'removes the key' do expect(truncator.document.key?(:os)).to be false expect(truncator.document[:driver].keys.sort).to be == %w[ version ] expect(truncator.document[:driver][:version].length).to be < 1000 @@ -233,7 +233,7 @@ context 'when truncating application.name is sufficient' do let(:app_name) { 'n' * 1000 } - it 'should truncate the name' do + it 'truncates the name' do expect(truncator.document.key?(:driver)).to be false expect(truncator.document[:application][:name].length).to be < 1000 end @@ -245,7 +245,7 @@ let(:app_name) { 'n' * 1000 } let(:extra) { 'n' * described_class::MAX_DOCUMENT_SIZE } - it 'should remove the application key' do + it 'removes the application key' do expect(truncator.document.key?(:driver)).to be false expect(truncator.document.key?(:application)).to be false end From 4c96e907525a1cc2c0413814bee7a5388bea7f60 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 31 May 2023 10:36:40 -0600 Subject: [PATCH 4/6] prose tests from handshake spec --- spec/integration/connection/faas_env_spec.rb | 63 ++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 spec/integration/connection/faas_env_spec.rb diff --git a/spec/integration/connection/faas_env_spec.rb b/spec/integration/connection/faas_env_spec.rb new file mode 100644 index 0000000000..08f2b55867 --- /dev/null +++ b/spec/integration/connection/faas_env_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Test Plan scenarios from the handshake spec +SCENARIOS = { + 'Valid AWS' => { + 'AWS_EXECUTION_ENV' => 'AWS_Lambda_ruby2.7', + 'AWS_REGION' => 'us-east-2', + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => '1024', + }, + + 'Valid Azure' => { + 'FUNCTIONS_WORKER_RUNTIME' => 'ruby', + }, + + 'Valid GCP' => { + 'K_SERVICE' => 'servicename', + 'FUNCTION_MEMORY_MB' => '1024', + 'FUNCTION_TIMEOUT_SEC' => '60', + 'FUNCTION_REGION' => 'us-central1', + }, + + 'Valid Vercel' => { + 'VERCEL' => '1', + 'VERCEL_URL' => '*.vercel.app', + 'VERCEL_REGION' => 'cdg1', + }, + + 'Invalid - multiple providers' => { + 'AWS_EXECUTION_ENV' => 'AWS_Lambda_ruby2.7', + 'AWS_REGION' => 'us-east-2', + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => '1024', + 'FUNCTIONS_WORKER_RUNTIME' => 'ruby', + }, + + 'Invalid - long string' => { + 'AWS_EXECUTION_ENV' => 'AWS_Lambda_ruby2.7', + 'AWS_REGION' => 'a' * 512, + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => '1024', + }, + + 'Invalid - wrong types' => { + 'AWS_EXECUTION_ENV' => 'AWS_Lambda_ruby2.7', + 'AWS_REGION' => 'us-east-2', + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => 'big', + }, +}.freeze + +describe 'Connect under FaaS Env' do + clean_slate + + SCENARIOS.each do |name, env| + context "when given #{name}" do + local_env(env) + + it 'connects successfully' do + resp = authorized_client.database.command(ping: 1) + expect(resp).to be_a(Mongo::Operation::Result) + end + end + end +end From e02b6ef247521c3d2d9c1498fb0ba7973dc5fde0 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 31 May 2023 11:12:51 -0600 Subject: [PATCH 5/6] updated truncation logic, and AWS detection logic --- lib/mongo/server/app_metadata/environment.rb | 33 ++- lib/mongo/server/app_metadata/truncator.rb | 44 +--- .../server/app_metadata/environment_spec.rb | 12 + .../server/app_metadata/truncator_spec.rb | 212 +++++------------- spec/mongo/server/app_metadata_spec.rb | 16 -- 5 files changed, 106 insertions(+), 211 deletions(-) diff --git a/lib/mongo/server/app_metadata/environment.rb b/lib/mongo/server/app_metadata/environment.rb index 526c4fc41a..22e358bc4f 100644 --- a/lib/mongo/server/app_metadata/environment.rb +++ b/lib/mongo/server/app_metadata/environment.rb @@ -46,12 +46,12 @@ class ValueTooLong < Mongo::Error; end # The mapping that determines which FaaS environment is active, based # on which environment variable(s) are present. DISCRIMINATORS = { - 'AWS_EXECUTION_ENV' => 'aws.lambda', - 'AWS_LAMBDA_RUNTIME_API' => 'aws.lambda', - 'FUNCTIONS_WORKER_RUNTIME' => 'azure.func', - 'K_SERVICE' => 'gcp.func', - 'FUNCTION_NAME' => 'gcp.func', - 'VERCEL' => 'vercel', + 'AWS_EXECUTION_ENV' => { pattern: /^AWS_Lambda_/, name: 'aws.lambda' }, + 'AWS_LAMBDA_RUNTIME_API' => { name: 'aws.lambda' }, + 'FUNCTIONS_WORKER_RUNTIME' => { name: 'azure.func' }, + 'K_SERVICE' => { name: 'gcp.func' }, + 'FUNCTION_NAME' => { name: 'gcp.func' }, + 'VERCEL' => { name: 'vercel' }, }.freeze # Describes how to coerce values of the specified type. @@ -180,14 +180,31 @@ def to_h # @raise [ TooManyEnvironments ] if the environment contains # discriminating variables for more than one FaaS provider. def detect_environment - matches = DISCRIMINATORS.keys.select { |k| ENV[k] } - names = matches.map { |m| DISCRIMINATORS[m] }.uniq + matches = DISCRIMINATORS.keys.select { |k| discriminator_matches?(k) } + names = matches.map { |m| DISCRIMINATORS[m][:name] }.uniq raise TooManyEnvironments, names.join(', ') if names.length > 1 names.first 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. + # + # @param [ String ] var the name of the environment variable + # + # @return [ true | false ] if the variable describes the current + # environment or not. + def discriminator_matches?(var) + return false unless ENV[var] + + disc = DISCRIMINATORS[var] + return true unless disc[:pattern] + + disc[:pattern].match?(ENV[var]) + end + # Extracts environment information from the current environment # variables, based on the detected FaaS environment. Populates the # {{@fields}} instance variable. diff --git a/lib/mongo/server/app_metadata/truncator.rb b/lib/mongo/server/app_metadata/truncator.rb index dcd0d558c7..d6a380b665 100644 --- a/lib/mongo/server/app_metadata/truncator.rb +++ b/lib/mongo/server/app_metadata/truncator.rb @@ -73,7 +73,7 @@ def excess # Attempt to truncate the document using the documented metadata # priorities (see the handshake specification). def try_truncate! - %i[ platform env os env_name os_type driver app_name ].each do |target| + %i[ env_fields os_fields env platform ].each do |target| break if ok? send(:"try_truncate_#{target}!") @@ -87,41 +87,20 @@ def try_truncate_platform! end # Attempt to truncate the keys in the {{:env}} subdocument. - def try_truncate_env! + def try_truncate_env_fields! try_truncate_hash(@document[:env], reserved: %w[ name ]) end # Attempt to truncate the keys in the {{:os}} subdocument. - def try_truncate_os! + def try_truncate_os_fields! try_truncate_hash(@document[:os], reserved: %w[ type ]) end - # Attempt to truncate {{env[:name]}} key. If that does not suffice, - # remove the {{:env}} key entirely. - def try_truncate_env_name! + # Remove the {{:env}} key from the document. + def try_truncate_env! return unless @document[:env] - @document.delete(:env) unless try_truncate_string(@document[:env][:name]) - end - - # Attempt to truncate {{os[:type]}} key. If that does not suffice, - # remove the {{:os}} key entirely. - def try_truncate_os_type! - return unless @document[:os] - - @document.delete(:os) unless try_truncate_string(@document[:os][:type]) - end - - # Attempt to truncate the keys in the {{:driver}} subdocument. If - # that does not suffice, remove the {{:driver}} key entirely. - def try_truncate_driver! - @document.delete(:driver) unless try_truncate_hash(@document[:driver]) - end - - # Attempt to truncate {{application[:name]}} key. If that does not - # suffice, remove the {{:application}} key entirely. - def try_truncate_app_name! - @document.delete(:application) unless try_truncate_string(@document[:application][:name]) + @document.delete(:env) end # A helper method for truncating a string (in-place) by whatever @@ -138,11 +117,10 @@ def try_truncate_string(string) string[(length - excess)..-1] = '' end - # A helper method for truncating the keys of a Hash (in-place) by - # whatever {{#excess}} is required. The keys are considered in order - # (using the Hash's native key ordering), and each will be either - # truncated (if that suffices) or removed from the hash (if truncating - # is not sufficient). + # A helper method for removing the keys of a Hash (in-place) until + # the document is the necessary size. The keys are considered in order + # (using the Hash's native key ordering), and each will be removed from + # the hash in turn, until the document is the necessary size. # # Any keys in the {{reserved}} list will be ignored. # @@ -155,7 +133,7 @@ def try_truncate_hash(hash, reserved: []) keys = hash.keys - reserved keys.each do |key| - hash.delete(key) unless try_truncate_string(hash[key].to_s) + hash.delete(key) return true if ok? end diff --git a/spec/mongo/server/app_metadata/environment_spec.rb b/spec/mongo/server/app_metadata/environment_spec.rb index c63209160c..3bf88acca9 100644 --- a/spec/mongo/server/app_metadata/environment_spec.rb +++ b/spec/mongo/server/app_metadata/environment_spec.rb @@ -96,6 +96,18 @@ end end + # per DRIVERS-2623, AWS_EXECUTION_ENV must be prefixed + # with 'AWS_Lambda_'. + context 'when AWS_EXECUTION_ENV is invalid' do + local_env( + 'AWS_EXECUTION_ENV' => 'EC2', + 'AWS_REGION' => 'us-east-2', + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE' => '1024' + ) + + it_behaves_like 'running outside a FaaS environment' + end + context 'when AWS_EXECUTION_ENV is detected' do local_env('AWS_EXECUTION_ENV' => 'AWS_Lambda_ruby2.7') it_behaves_like 'running in an AWS environment' diff --git a/spec/mongo/server/app_metadata/truncator_spec.rb b/spec/mongo/server/app_metadata/truncator_spec.rb index 06bc414b10..52fcad3240 100644 --- a/spec/mongo/server/app_metadata/truncator_spec.rb +++ b/spec/mongo/server/app_metadata/truncator_spec.rb @@ -4,17 +4,13 @@ # Quoted from specifications/source/mongodb-handshake/handshake.rst: # -# Drivers MUST validate these values and truncate or omit driver provided -# values if necessary. Implementors SHOULD prioritize fields to preserve in -# this order: +# Implementors SHOULD cumulatively update fields in the following order +# until the document is under the size limit: # -# 1. application.name -# 2. driver.* -# 3. os.type -# 4. env.name -# 5. os.* (except type) -# 6. env.* (except name) -# 7. platform +# 1. Omit fields from env except env.name. +# 2. Omit fields from os except os.type. +# 3. Omit the env document entirely. +# 4. Truncate platform. describe Mongo::Server::AppMetadata::Truncator do let(:truncator) { described_class.new(Marshal.load(Marshal.dump(metadata))) } @@ -24,16 +20,14 @@ let(:os) { { type: 'Darwin', name: 'macOS', architecture: 'arm64', version: '13.4' } } let(:platform) { { platform: 'platform' } } let(:env) { { name: 'aws.lambda', region: 'region', memory_mb: 1024 } } - let(:extra) { nil } let(:metadata) do BSON::Document.new.tap do |doc| - doc[:application] = { name: app_name } if app_name - doc[:driver] = driver if driver - doc[:os] = os if os - doc[:platform] = platform if platform - doc[:env] = env if env - doc[:__extra__] = extra if extra + doc[:application] = { name: app_name } + doc[:driver] = driver + doc[:os] = os + doc[:platform] = platform + doc[:env] = env end end @@ -64,191 +58,101 @@ end end - context 'when modifying the platform is sufficient' do - context 'when truncating the platform is sufficient' do - let(:platform) { 'a' * 1000 } + context 'when modifying env is sufficient' do + context 'when a single value is too long' do + let(:env) { { name: 'name', a: 'a' * 1000, b: 'b' } } - it 'truncates the platform' do - expect(truncator.document[:platform].length).to be < 1000 + it 'preserves name' do + expect(truncator.document[:env][:name]).to be == 'name' end - it_behaves_like 'a truncated document' - end - - context 'when the platform must be removed' do - # env is higher priority than platform, and will require platform to - # be resolved before truncating or removing anything in env. - let(:env) { { name: 'abc', a: 'a' * 1000 } } - - it 'removes the platform' do - expect(truncator.document.key?(:platform)).to be false + it 'removes the too-long entry and keeps name' do + expect(truncator.document[:env].keys).to be == %w[ name b ] end it_behaves_like 'a truncated document' end - end - context 'when modifying env is required' do - context 'when truncating a single key is sufficient' do - let(:env) { { name: 'abc', a: 'a' * 1000, b: '123' } } + context 'when multiple values are too long' do + let(:env) { { name: 'name', a: 'a' * 1000, b: 'b', c: 'c' * 1000, d: 'd' } } - it 'truncates that key' do - expect(truncator.document[:env].keys.sort).to be == %w[ a b name ] - expect(truncator.document[:env][:a].length).to be < 1000 - expect(truncator.document[:env][:name]).to be == 'abc' - expect(truncator.document[:env][:b]).to be == '123' + it 'preserves name' do + expect(truncator.document[:env][:name]).to be == 'name' end - it_behaves_like 'a truncated document' - end - - context 'when removing a key is required' do - let(:env) { { name: 'abc', a: 'a' * 1000, b: 'b' * 1000 } } - - it 'removes the key' do - expect(truncator.document[:env].keys.sort).to be == %w[ b name ] - expect(truncator.document[:env][:b].length).to be < 1000 - expect(truncator.document[:env][:name]).to be == 'abc' + it 'removes all other entries until size is satisifed' do + expect(truncator.document[:env].keys).to be == %w[ name d ] end it_behaves_like 'a truncated document' end end - context 'when modifying os is required' do - context 'when env is problematic' do - let(:env) { { name: 'abc', a: 'a' * 1000, b: 'b' * 1000 } } - let(:os) { { type: 'abc', a: 'a' * 1000 } } + context 'when modifying os is sufficient' do + context 'when a single value is too long' do + let(:os) { { type: 'type', a: 'a' * 1000, b: 'b' } } - it 'modifies env first' do - expect(truncator.document[:env].keys.sort).to be == %w[ name ] - expect(truncator.document[:os].keys.sort).to be == %w[ a type ] + it 'truncates env' do + expect(truncator.document[:env].keys).to be == %w[ name ] end - it_behaves_like 'a truncated document' - end - - context 'when truncating a single value is sufficient' do - let(:os) { { type: 'abc', a: 'a' * 1000, b: '123' } } - - it 'truncates that key' do - expect(truncator.document[:os].keys.sort).to be == %w[ a b type ] - expect(truncator.document[:os][:a].length).to be < 1000 - expect(truncator.document[:os][:type]).to be == 'abc' - expect(truncator.document[:os][:b]).to be == '123' + it 'preserves type' do + expect(truncator.document[:os][:type]).to be == 'type' end - it_behaves_like 'a truncated document' - end - - context 'when removing a key is required' do - let(:os) { { type: 'abc', a: 'a' * 1000, b: 'b' * 1000 } } - - it 'removes the key' do - expect(truncator.document[:os].keys.sort).to be == %w[ b type ] - expect(truncator.document[:os][:b].length).to be < 1000 - expect(truncator.document[:os][:type]).to be == 'abc' - end - - it_behaves_like 'a truncated document' - end - end - - context 'when modifying env.name is required' do - let(:env) { { name: 'n' * 1000, a: 'a' * 1000, b: 'b' * 1000 } } - let(:os) { { type: '123', a: 'a' * 1000, b: 'b' * 1000 } } - - context 'when truncating env.name is sufficient' do - it 'truncates env.name' do - expect(truncator.document[:env].keys.sort).to be == %w[ name ] - expect(truncator.document[:os].keys.sort).to be == %w[ type ] - expect(truncator.document[:env][:name].length).to be < 1000 + it 'removes the too-long entry and keeps type' do + expect(truncator.document[:os].keys).to be == %w[ type b ] end it_behaves_like 'a truncated document' end - context 'when removing env.name is required' do - let(:os) { { type: 'n' * 1000 } } + context 'when multiple values are too long' do + let(:os) { { type: 'type', a: 'a' * 1000, b: 'b', c: 'c' * 1000, d: 'd' } } - it 'removes env' do - expect(truncator.document.key?(:env)).to be false + it 'truncates env' do + expect(truncator.document[:env].keys).to be == %w[ name ] end - it_behaves_like 'a truncated document' - end - end - - context 'when modifying os.type is required' do - let(:os) { { type: 'n' * 1000, a: 'a' * 1000, b: 'b' * 1000 } } - - context 'when truncating os.type is sufficient' do - it 'truncates os.type' do - expect(truncator.document.key?(:env)).to be false - expect(truncator.document[:os].keys.sort).to be == %w[ type ] - expect(truncator.document[:os][:type].length).to be < 1000 + it 'preserves type' do + expect(truncator.document[:os][:type]).to be == 'type' end - it_behaves_like 'a truncated document' - end - - context 'when removing os.type is required' do - let(:app_name) { 'n' * 1000 } - - it 'removes os' do - expect(truncator.document.key?(:os)).to be false + it 'removes all other entries until size is satisifed' do + expect(truncator.document[:os].keys).to be == %w[ type d ] end it_behaves_like 'a truncated document' end end - context 'when modifying driver is required' do - context 'when truncating a single key is sufficient' do - let(:driver) { { name: 'd' * 1000, version: '1.2.3' } } - - it 'truncates that key' do - expect(truncator.document.key?(:os)).to be false - expect(truncator.document[:driver].keys.sort).to be == %w[ name version ] - expect(truncator.document[:driver][:version].length).to be < 1000 - end + context 'when truncating os is insufficient' do + let(:env) { { name: 'n' * 1000 } } - it_behaves_like 'a truncated document' + it 'truncates os' do + expect(truncator.document[:os].keys).to be == %w[ type ] end - context 'when removing a key is required' do - let(:driver) { { name: 'd' * 1000, version: 'v' * 1000 } } - - it 'removes the key' do - expect(truncator.document.key?(:os)).to be false - expect(truncator.document[:driver].keys.sort).to be == %w[ version ] - expect(truncator.document[:driver][:version].length).to be < 1000 - end - - it_behaves_like 'a truncated document' + it 'removes env' do + expect(truncator.document.key?(:env)).to be false end - end - context 'when modifying application.name is required' do - context 'when truncating application.name is sufficient' do - let(:app_name) { 'n' * 1000 } + it_behaves_like 'a truncated document' + end - it 'truncates the name' do - expect(truncator.document.key?(:driver)).to be false - expect(truncator.document[:application][:name].length).to be < 1000 - end + context 'when platform is too long' do + let(:platform) { 'n' * 1000 } - it_behaves_like 'a truncated document' + it 'truncates os' do + expect(truncator.document[:os].keys).to be == %w[ type ] end - context 'when removing application.name is required' do - let(:app_name) { 'n' * 1000 } - let(:extra) { 'n' * described_class::MAX_DOCUMENT_SIZE } + it 'removes env' do + expect(truncator.document.key?(:env)).to be false + end - it 'removes the application key' do - expect(truncator.document.key?(:driver)).to be false - expect(truncator.document.key?(:application)).to be false - end + it 'truncates platform' do + expect(truncator.document[:platform].length).to be < 1000 end end end diff --git a/spec/mongo/server/app_metadata_spec.rb b/spec/mongo/server/app_metadata_spec.rb index ba19b08484..c8819c67ce 100644 --- a/spec/mongo/server/app_metadata_spec.rb +++ b/spec/mongo/server/app_metadata_spec.rb @@ -67,14 +67,6 @@ end end - context 'when the os.type length is too long' do - before do - allow(app_metadata).to receive(:type).and_return('x'*500) - end - - it_behaves_like 'a truncated document' - end - context 'when the os.name length is too long' do before do allow(app_metadata).to receive(:name).and_return('x'*500) @@ -98,14 +90,6 @@ it_behaves_like 'a truncated document' end - - context 'when the driver info is too long' do - before do - allow(app_metadata).to receive(:driver_doc).and_return({ name: 'x'*500 }) - end - - it_behaves_like 'a truncated document' - end end context 'when run outside of a FaaS environment' do From e5d9ecd2b01afbfadd5ebbcbfe749a3bff4ff46a Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 31 May 2023 11:28:43 -0600 Subject: [PATCH 6/6] minor cleanups --- lib/mongo/server/app_metadata.rb | 2 +- lib/mongo/server/app_metadata/environment.rb | 4 ++-- lib/mongo/server/app_metadata/truncator.rb | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/mongo/server/app_metadata.rb b/lib/mongo/server/app_metadata.rb index 1e80518765..88a75032c2 100644 --- a/lib/mongo/server/app_metadata.rb +++ b/lib/mongo/server/app_metadata.rb @@ -156,7 +156,7 @@ def client_document private - # Check whether it is possible to build a valid app metadata document + # Check whether it is possible to build a valid app metadata document # with params provided on intialization. # # @raise [ Error::InvalidApplicationName ] When the metadata are invalid. diff --git a/lib/mongo/server/app_metadata/environment.rb b/lib/mongo/server/app_metadata/environment.rb index 22e358bc4f..5e9c556f7b 100644 --- a/lib/mongo/server/app_metadata/environment.rb +++ b/lib/mongo/server/app_metadata/environment.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (C) 2016-2020 MongoDB Inc. +# Copyright (C) 2016-2023 MongoDB Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -87,7 +87,7 @@ class ValueTooLong < Mongo::Error; end # detected, or nil if no valid FaaS environment was detected. attr_reader :name - # @return [ Hash | nil ] the fields describing detected FaaS + # @return [ Hash | nil ] the fields describing the detected FaaS # environment. attr_reader :fields diff --git a/lib/mongo/server/app_metadata/truncator.rb b/lib/mongo/server/app_metadata/truncator.rb index d6a380b665..125b8f1e2e 100644 --- a/lib/mongo/server/app_metadata/truncator.rb +++ b/lib/mongo/server/app_metadata/truncator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (C) 2016-2020 MongoDB Inc. +# Copyright (C) 2016-2023 MongoDB Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,8 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'mongo/server/app_metadata/environment' - module Mongo class Server class AppMetadata @@ -98,8 +96,6 @@ def try_truncate_os_fields! # Remove the {{:env}} key from the document. def try_truncate_env! - return unless @document[:env] - @document.delete(:env) end