diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index 6108b48d..4d84a413 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true +require "datadog/core/configuration/agent_settings_resolver" +require "datadog/core/remote/negotiation" + +require_relative "../ext/transport" require_relative "../test_visibility/flush" require_relative "../test_visibility/transport" +require_relative "../transport/api/builder" module Datadog module CI @@ -17,25 +22,13 @@ def initialize(settings) end def activate_ci!(settings) - agentless_transport = nil + test_visibility_transport = nil + agent_settings = Datadog::Core::Configuration::AgentSettingsResolver.call(settings) if settings.ci.agentless_mode_enabled - if settings.api_key.nil? - # agentless mode is requested but no API key is provided - - # we cannot continue and log an error - # Tests are running without CI visibility enabled - - Datadog.logger.error( - "DATADOG CONFIGURATION - CI VISIBILITY - ATTENTION - " \ - "Agentless mode was enabled but DD_API_KEY is not set: CI visibility is disabled. " \ - "Please make sure to set valid api key in DD_API_KEY environment variable" - ) - - settings.ci.enabled = false - return - else - agentless_transport = build_agentless_transport(settings) - end + test_visibility_transport = build_agentless_transport(settings) + elsif can_use_evp_proxy?(settings, agent_settings) + test_visibility_transport = build_evp_proxy_transport(settings, agent_settings) end # Deactivate telemetry @@ -51,8 +44,8 @@ def activate_ci!(settings) settings.tracing.test_mode.trace_flush = settings.ci.trace_flush || CI::TestVisibility::Flush::Finished.new writer_options = settings.ci.writer_options - if agentless_transport - writer_options[:transport] = agentless_transport + if test_visibility_transport + writer_options[:transport] = test_visibility_transport writer_options[:shutdown_timeout] = 60 settings.tracing.test_mode.async = true @@ -61,15 +54,43 @@ def activate_ci!(settings) settings.tracing.test_mode.writer_options = writer_options end + def can_use_evp_proxy?(settings, agent_settings) + Datadog::Core::Remote::Negotiation.new(settings, agent_settings).endpoint?( + Ext::Transport::EVP_PROXY_PATH_PREFIX + ) + end + def build_agentless_transport(settings) - dd_site = settings.site || "datadoghq.com" - agentless_url = settings.ci.agentless_url || - "https://#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{dd_site}:443" + if settings.api_key.nil? + # agentless mode is requested but no API key is provided - + # we cannot continue and log an error + # Tests are running without CI visibility enabled + + Datadog.logger.error( + "DATADOG CONFIGURATION - CI VISIBILITY - ATTENTION - " \ + "Agentless mode was enabled but DD_API_KEY is not set: CI visibility is disabled. " \ + "Please make sure to set valid api key in DD_API_KEY environment variable" + ) + + settings.ci.enabled = false + + nil + else + Datadog.logger.debug("CI visibility configured to use agentless transport") + + Datadog::CI::TestVisibility::Transport.new( + api: Transport::Api::Builder.build_ci_test_cycle_api(settings), + dd_env: settings.env + ) + end + end + + def build_evp_proxy_transport(settings, agent_settings) + Datadog.logger.debug("CI visibility configured to use agent transport via EVP proxy") Datadog::CI::TestVisibility::Transport.new( - api_key: settings.api_key, - url: agentless_url, - env: settings.env + api: Transport::Api::Builder.build_evp_proxy_api(agent_settings), + dd_env: settings.env ) end end diff --git a/lib/datadog/ci/ext/transport.rb b/lib/datadog/ci/ext/transport.rb index ece118eb..791f6085 100644 --- a/lib/datadog/ci/ext/transport.rb +++ b/lib/datadog/ci/ext/transport.rb @@ -4,10 +4,15 @@ module Datadog module CI module Ext module Transport + DEFAULT_DD_SITE = "datadoghq.com" + HEADER_DD_API_KEY = "DD-API-KEY" HEADER_CONTENT_TYPE = "Content-Type" HEADER_CONTENT_ENCODING = "Content-Encoding" + HEADER_EVP_SUBDOMAIN = "X-Datadog-EVP-Subdomain" + HEADER_CONTAINER_ID = "Datadog-Container-ID" + EVP_PROXY_PATH_PREFIX = "/evp_proxy/v2/" TEST_VISIBILITY_INTAKE_HOST_PREFIX = "citestcycle-intake" TEST_VISIBILITY_INTAKE_PATH = "/api/v2/citestcycle" diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 472280b8..03b12fec 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -9,7 +9,6 @@ require_relative "serializers/factories/test_level" require_relative "../ext/transport" -require_relative "../transport/http" module Datadog module CI @@ -20,33 +19,20 @@ class Transport DEFAULT_MAX_PAYLOAD_SIZE = 5 * 1024 * 1024 attr_reader :serializers_factory, - :api_key, + :api, :max_payload_size, - :http, - :env + :dd_env def initialize( - api_key:, - url:, - env: nil, + api:, + dd_env: nil, serializers_factory: Datadog::CI::TestVisibility::Serializers::Factories::TestLevel, max_payload_size: DEFAULT_MAX_PAYLOAD_SIZE ) @serializers_factory = serializers_factory - @api_key = api_key @max_payload_size = max_payload_size - @env = env - - uri = URI.parse(url) - - raise "Invalid agentless mode URL: #{url}" if uri.host.nil? - - @http = Datadog::CI::Transport::HTTP.new( - host: uri.host, - port: uri.port, - ssl: uri.scheme == "https" || uri.port == 443, - compress: true - ) + @dd_env = dd_env + @api = api end def send_traces(traces) @@ -82,13 +68,9 @@ def send_traces(traces) private def send_payload(encoded_payload) - http.request( + api.request( path: Datadog::CI::Ext::Transport::TEST_VISIBILITY_INTAKE_PATH, - payload: encoded_payload, - headers: { - Ext::Transport::HEADER_DD_API_KEY => api_key, - Ext::Transport::HEADER_CONTENT_TYPE => Ext::Transport::CONTENT_TYPE_MESSAGEPACK - } + payload: encoded_payload ) end @@ -141,12 +123,12 @@ def pack_events(encoded_events) packer.write_map_header(1) packer.write("*") - metadata_fields_count = env ? 4 : 3 + metadata_fields_count = dd_env ? 4 : 3 packer.write_map_header(metadata_fields_count) - if env + if dd_env packer.write("env") - packer.write(env) + packer.write(dd_env) end packer.write("runtime-id") diff --git a/lib/datadog/ci/transport/api/base.rb b/lib/datadog/ci/transport/api/base.rb new file mode 100644 index 00000000..6230b887 --- /dev/null +++ b/lib/datadog/ci/transport/api/base.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative "../../ext/transport" + +module Datadog + module CI + module Transport + module Api + class Base + attr_reader :http + + def initialize(http:) + @http = http + end + + def request(path:, payload:, verb: "post") + http.request( + path: path, + payload: payload, + verb: verb, + headers: headers + ) + end + + private + + def headers + { + Ext::Transport::HEADER_CONTENT_TYPE => Ext::Transport::CONTENT_TYPE_MESSAGEPACK + } + end + end + end + end + end +end diff --git a/lib/datadog/ci/transport/api/builder.rb b/lib/datadog/ci/transport/api/builder.rb new file mode 100644 index 00000000..cf65674d --- /dev/null +++ b/lib/datadog/ci/transport/api/builder.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require_relative "ci_test_cycle" +require_relative "evp_proxy" +require_relative "../http" +require_relative "../../ext/transport" + +module Datadog + module CI + module Transport + module Api + module Builder + def self.build_ci_test_cycle_api(settings) + dd_site = settings.site || Ext::Transport::DEFAULT_DD_SITE + url = settings.ci.agentless_url || + "https://#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{dd_site}:443" + + uri = URI.parse(url) + raise "Invalid agentless mode URL: #{url}" if uri.host.nil? + + http = Datadog::CI::Transport::HTTP.new( + host: uri.host, + port: uri.port, + ssl: uri.scheme == "https" || uri.port == 443, + compress: true + ) + + CiTestCycle.new(api_key: settings.api_key, http: http) + end + + def self.build_evp_proxy_api(agent_settings) + http = Datadog::CI::Transport::HTTP.new( + host: agent_settings.hostname, + port: agent_settings.port, + ssl: agent_settings.ssl, + timeout: agent_settings.timeout_seconds, + compress: false + ) + + EvpProxy.new(http: http) + end + end + end + end + end +end diff --git a/lib/datadog/ci/transport/api/ci_test_cycle.rb b/lib/datadog/ci/transport/api/ci_test_cycle.rb new file mode 100644 index 00000000..b3cec84d --- /dev/null +++ b/lib/datadog/ci/transport/api/ci_test_cycle.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative "base" +require_relative "../../ext/transport" + +module Datadog + module CI + module Transport + module Api + class CiTestCycle < Base + attr_reader :api_key + + def initialize(api_key:, http:) + @api_key = api_key + + super(http: http) + end + + private + + def headers + headers = super + headers[Ext::Transport::HEADER_DD_API_KEY] = api_key + headers + end + end + end + end + end +end diff --git a/lib/datadog/ci/transport/api/evp_proxy.rb b/lib/datadog/ci/transport/api/evp_proxy.rb new file mode 100644 index 00000000..87ccd79c --- /dev/null +++ b/lib/datadog/ci/transport/api/evp_proxy.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "datadog/core/environment/container" + +require_relative "base" +require_relative "../../ext/transport" + +module Datadog + module CI + module Transport + module Api + class EvpProxy < Base + def request(path:, payload:, verb: "post") + path = "#{Ext::Transport::EVP_PROXY_PATH_PREFIX}#{path.sub(/^\//, "")}" + + super( + path: path, + payload: payload, + verb: verb + ) + end + + private + + def container_id + return @container_id if defined?(@container_id) + + @container_id = Datadog::Core::Environment::Container.container_id + end + + def headers + headers = super + headers[Ext::Transport::HEADER_EVP_SUBDOMAIN] = Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX + + c_id = container_id + headers[Ext::Transport::HEADER_CONTAINER_ID] = c_id unless c_id.nil? + + headers + end + end + end + end + end +end diff --git a/sig/datadog/ci/configuration/components.rbs b/sig/datadog/ci/configuration/components.rbs index 6d7569e6..62304a3e 100644 --- a/sig/datadog/ci/configuration/components.rbs +++ b/sig/datadog/ci/configuration/components.rbs @@ -6,7 +6,9 @@ module Datadog def activate_ci!: (untyped settings) -> untyped - def build_agentless_transport: (untyped settings) -> Datadog::CI::TestVisibility::Transport + def build_agentless_transport: (untyped settings) -> Datadog::CI::TestVisibility::Transport? + def build_evp_proxy_transport: (untyped settings, untyped agent_settings) -> Datadog::CI::TestVisibility::Transport + def can_use_evp_proxy?: (untyped settings, untyped agent_settings) -> bool end end end diff --git a/sig/datadog/ci/ext/transport.rbs b/sig/datadog/ci/ext/transport.rbs index 6c40b3b6..8ca2b5a7 100644 --- a/sig/datadog/ci/ext/transport.rbs +++ b/sig/datadog/ci/ext/transport.rbs @@ -2,12 +2,20 @@ module Datadog module CI module Ext module Transport + DEFAULT_DD_SITE: "datadoghq.com" + HEADER_DD_API_KEY: "DD-API-KEY" HEADER_CONTENT_TYPE: "Content-Type" HEADER_CONTENT_ENCODING: "Content-Encoding" + HEADER_EVP_SUBDOMAIN: "X-Datadog-EVP-Subdomain" + + HEADER_CONTAINER_ID: "Datadog-Container-ID" + + EVP_PROXY_PATH_PREFIX: "/evp_proxy/v2/" + TEST_VISIBILITY_INTAKE_HOST_PREFIX: "citestcycle-intake" TEST_VISIBILITY_INTAKE_PATH: "/api/v2/citestcycle" diff --git a/sig/datadog/ci/test_visibility/transport.rbs b/sig/datadog/ci/test_visibility/transport.rbs index 774564c2..f94a68bf 100644 --- a/sig/datadog/ci/test_visibility/transport.rbs +++ b/sig/datadog/ci/test_visibility/transport.rbs @@ -5,21 +5,17 @@ module Datadog DEFAULT_MAX_PAYLOAD_SIZE: Integer attr_reader serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel) - attr_reader api_key: String - attr_reader env: String? - attr_reader http: Datadog::CI::Transport::HTTP + attr_reader dd_env: String? + attr_reader api: Datadog::CI::Transport::Api::Base attr_reader max_payload_size: Integer - @api_key: String - @env: String? - @http: Datadog::CI::Transport::HTTP + @dd_env: String? @serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel) @max_payload_size: Integer def initialize: ( - api_key: String, - url: ::String, - ?env: ::String?, + api: Datadog::CI::Transport::Api::Base, + ?dd_env: ::String?, ?serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel), ?max_payload_size: Integer ) -> void diff --git a/sig/datadog/ci/transport/api/base.rbs b/sig/datadog/ci/transport/api/base.rbs new file mode 100644 index 00000000..6fd14d70 --- /dev/null +++ b/sig/datadog/ci/transport/api/base.rbs @@ -0,0 +1,21 @@ +module Datadog + module CI + module Transport + module Api + class Base + attr_reader http: Datadog::CI::Transport::HTTP + + @http: Datadog::CI::Transport::HTTP + + def initialize: (http: Datadog::CI::Transport::HTTP) -> void + + def request: (path: String, payload: String, ?verb: ::String) -> untyped + + private + + def headers: () -> Hash[String, String] + end + end + end + end +end diff --git a/sig/datadog/ci/transport/api/builder.rbs b/sig/datadog/ci/transport/api/builder.rbs new file mode 100644 index 00000000..52217995 --- /dev/null +++ b/sig/datadog/ci/transport/api/builder.rbs @@ -0,0 +1,12 @@ +module Datadog + module CI + module Transport + module Api + module Builder + def self.build_ci_test_cycle_api: (untyped settings) -> Datadog::CI::Transport::Api::CiTestCycle + def self.build_evp_proxy_api: (untyped agent_settings) -> Datadog::CI::Transport::Api::EvpProxy + end + end + end + end +end diff --git a/sig/datadog/ci/transport/api/ci_test_cycle.rbs b/sig/datadog/ci/transport/api/ci_test_cycle.rbs new file mode 100644 index 00000000..ef48bd45 --- /dev/null +++ b/sig/datadog/ci/transport/api/ci_test_cycle.rbs @@ -0,0 +1,21 @@ +module Datadog + module CI + module Transport + module Api + class CiTestCycle < Base + attr_reader api_key: String + + @api_key: String + + def initialize: (api_key: String, http: Datadog::CI::Transport::HTTP) -> void + + def request: (path: String, payload: String, ?verb: ::String) -> Datadog::CI::Transport::HTTP::ResponseDecorator + + private + + def headers: () -> Hash[String, String] + end + end + end + end +end diff --git a/sig/datadog/ci/transport/api/evp_proxy.rbs b/sig/datadog/ci/transport/api/evp_proxy.rbs new file mode 100644 index 00000000..7f989a19 --- /dev/null +++ b/sig/datadog/ci/transport/api/evp_proxy.rbs @@ -0,0 +1,19 @@ +module Datadog + module CI + module Transport + module Api + class EvpProxy < Base + @container_id: String? + + def request: (path: String, payload: String, ?verb: ::String) -> Datadog::CI::Transport::HTTP::ResponseDecorator + + private + + def container_id: () -> String? + + def headers: () -> Hash[String, String] + end + end + end + end +end diff --git a/spec/datadog/ci/configuration/components_spec.rb b/spec/datadog/ci/configuration/components_spec.rb index 9f5283d2..426baf99 100644 --- a/spec/datadog/ci/configuration/components_spec.rb +++ b/spec/datadog/ci/configuration/components_spec.rb @@ -54,6 +54,16 @@ .to receive(:api_key) .and_return(api_key) + negotiation = double(:negotiation) + + allow(Datadog::Core::Remote::Negotiation) + .to receive(:new) + .and_return(negotiation) + + allow(negotiation) + .to receive(:endpoint?).with("/evp_proxy/v2/") + .and_return(evp_proxy_supported) + # Spy on test mode behavior allow(settings.tracing.test_mode) .to receive(:enabled=) @@ -79,6 +89,7 @@ let(:api_key) { nil } let(:agentless_url) { nil } let(:dd_site) { nil } + let(:evp_proxy_supported) { false } context "is enabled" do let(:enabled) { true } @@ -87,22 +98,38 @@ context "is disabled" do let(:agentless_enabled) { false } - it do - expect(settings.tracing.test_mode) - .to have_received(:enabled=) - .with(true) - end + context "and when agent supports EVP proxy" do + let(:evp_proxy_supported) { true } + + it "sets async for test mode and constructs transport with EVP proxy API" do + expect(settings.tracing.test_mode) + .to have_received(:async=) + .with(true) - it do - expect(settings.tracing.test_mode) - .to have_received(:trace_flush=) - .with(settings.ci.trace_flush || kind_of(Datadog::CI::TestVisibility::Flush::Finished)) + expect(settings.tracing.test_mode).to have_received(:writer_options=) do |options| + expect(options[:transport]).to be_kind_of(Datadog::CI::TestVisibility::Transport) + expect(options[:transport].api).to be_kind_of(Datadog::CI::Transport::Api::EvpProxy) + expect(options[:shutdown_timeout]).to eq(60) + end + end end - it do - expect(settings.tracing.test_mode) - .to have_received(:writer_options=) - .with(settings.ci.writer_options) + context "and when agent does not support EVP proxy" do + let(:evp_proxy_supported) { false } + + it "falls back to default transport" do + expect(settings.tracing.test_mode) + .to have_received(:enabled=) + .with(true) + + expect(settings.tracing.test_mode) + .to have_received(:trace_flush=) + .with(settings.ci.trace_flush || kind_of(Datadog::CI::TestVisibility::Flush::Finished)) + + expect(settings.tracing.test_mode).to have_received(:writer_options=) do |options| + expect(options[:transport]).to be_nil + end + end end end @@ -112,45 +139,15 @@ context "when api key is set" do let(:api_key) { "api_key" } - it "sets async for test mode and provides transport and shutdown timeout to the write" do + it "sets async for test mode and constructs transport with CI intake API" do expect(settings.tracing.test_mode) .to have_received(:async=) .with(true) expect(settings.tracing.test_mode).to have_received(:writer_options=) do |options| expect(options[:transport]).to be_kind_of(Datadog::CI::TestVisibility::Transport) + expect(options[:transport].api).to be_kind_of(Datadog::CI::Transport::Api::CiTestCycle) expect(options[:shutdown_timeout]).to eq(60) - - http_client = options[:transport].http - expect(http_client.host).to eq("citestcycle-intake.datadoghq.com") - expect(http_client.port).to eq(443) - expect(http_client.ssl).to eq(true) - end - end - - context "when agentless_url is provided" do - let(:agentless_url) { "http://localhost:5555" } - - it "configures transport to use intake URL from settings" do - expect(settings.tracing.test_mode).to have_received(:writer_options=) do |options| - http_client = options[:transport].http - expect(http_client.host).to eq("localhost") - expect(http_client.port).to eq(5555) - expect(http_client.ssl).to eq(false) - end - end - end - - context "when dd_site is provided" do - let(:dd_site) { "eu.datadoghq.com" } - - it "construct intake url using provided host" do - expect(settings.tracing.test_mode).to have_received(:writer_options=) do |options| - http_client = options[:transport].http - expect(http_client.host).to eq("citestcycle-intake.eu.datadoghq.com") - expect(http_client.port).to eq(443) - expect(http_client.ssl).to eq(true) - end end end end diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb index a775da46..b137a338 100644 --- a/spec/datadog/ci/test_visibility/transport_spec.rb +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -7,30 +7,18 @@ subject do described_class.new( - api_key: api_key, - env: env, - url: url, + api: api, + dd_env: dd_env, serializers_factory: serializers_factory, max_payload_size: max_payload_size ) end - let(:api_key) { "api_key" } - let(:env) { nil } - let(:url) { "https://citestcycle-intake.datad0ghq.com:443" } + let(:dd_env) { nil } let(:serializers_factory) { Datadog::CI::TestVisibility::Serializers::Factories::TestLevel } let(:max_payload_size) { 4 * 1024 * 1024 } - let(:http) { spy(:http) } - - before do - expect(Datadog::CI::Transport::HTTP).to receive(:new).with( - host: "citestcycle-intake.datad0ghq.com", - port: 443, - ssl: true, - compress: true - ).and_return(http) - end + let(:api) { spy(:api) } describe "#send_traces" do context "with a single trace and a single span" do @@ -41,12 +29,8 @@ it "sends correct payload" do subject.send_traces([trace]) - expect(http).to have_received(:request) do |args| + expect(api).to have_received(:request) do |args| expect(args[:path]).to eq("/api/v2/citestcycle") - expect(args[:headers]).to eq({ - "DD-API-KEY" => "api_key", - "Content-Type" => "application/msgpack" - }) payload = MessagePack.unpack(args[:payload]) expect(payload["version"]).to eq(1) @@ -62,8 +46,8 @@ end end - context "with env defined" do - let(:env) { "ci" } + context "with dd_env defined" do + let(:dd_env) { "ci" } before do produce_test_trace end @@ -71,7 +55,7 @@ it "sends correct payload including env" do subject.send_traces([trace]) - expect(http).to have_received(:request) do |args| + expect(api).to have_received(:request) do |args| payload = MessagePack.unpack(args[:payload]) metadata = payload["metadata"]["*"] @@ -91,7 +75,7 @@ it "sends event for each of spans" do subject.send_traces(traces) - expect(http).to have_received(:request) do |args| + expect(api).to have_received(:request) do |args| payload = MessagePack.unpack(args[:payload]) events = payload["events"] expect(events.count).to eq(expected_events_count) @@ -109,7 +93,7 @@ it "filters out invalid events" do subject.send_traces(traces) - expect(http).to have_received(:request) do |args| + expect(api).to have_received(:request) do |args| payload = MessagePack.unpack(args[:payload]) events = payload["events"] @@ -129,7 +113,7 @@ it "filters out invalid events" do responses = subject.send_traces(traces) - expect(http).to have_received(:request).twice + expect(api).to have_received(:request).twice expect(responses.count).to eq(2) end end @@ -142,7 +126,7 @@ it "does not send events that are larger than max size" do subject.send_traces(traces) - expect(http).not_to have_received(:request) + expect(api).not_to have_received(:request) end end end @@ -157,7 +141,7 @@ it "does not send anything" do subject.send_traces(traces) - expect(http).not_to have_received(:request) + expect(api).not_to have_received(:request) end end end diff --git a/spec/datadog/ci/transport/api/builder_spec.rb b/spec/datadog/ci/transport/api/builder_spec.rb new file mode 100644 index 00000000..7331f37b --- /dev/null +++ b/spec/datadog/ci/transport/api/builder_spec.rb @@ -0,0 +1,125 @@ +require_relative "../../../../../lib/datadog/ci/transport/api/builder" + +RSpec.describe Datadog::CI::Transport::Api::Builder do + let(:settings) do + # When 'datadog/ci' is required, it automatically extends Settings. + if Datadog::Core::Configuration::Settings <= Datadog::CI::Configuration::Settings + Datadog::Core::Configuration::Settings.new + else + Datadog::Core::Configuration::Settings.new.tap do |settings| + settings.extend(Datadog::CI::Configuration::Settings) + end + end + end + + describe ".build_ci_test_cycle_api" do + subject { described_class.build_ci_test_cycle_api(settings) } + + let(:api) { double(:api) } + let(:http) { double(:http) } + let(:agentless_url) { nil } + let(:dd_site) { nil } + let(:api_key) { "api_key" } + + before do + # Stub CI mode behavior + allow(settings.ci) + .to receive(:agentless_url) + .and_return(agentless_url) + + allow(settings) + .to receive(:site) + .and_return(dd_site) + + allow(settings) + .to receive(:api_key) + .and_return(api_key) + end + + it "creates and configures http client and CiTestCycle" do + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: "citestcycle-intake.datadoghq.com", + port: 443, + ssl: true, + compress: true + ).and_return(http) + + expect(Datadog::CI::Transport::Api::CiTestCycle).to receive(:new).with( + api_key: "api_key", http: http + ).and_return(api) + + expect(subject).to eq(api) + end + + context "when agentless_url is provided" do + let(:agentless_url) { "http://localhost:5555" } + + it "configures transport to use intake URL from settings" do + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: "localhost", + port: 5555, + ssl: false, + compress: true + ).and_return(http) + + expect(Datadog::CI::Transport::Api::CiTestCycle).to receive(:new).with( + api_key: "api_key", http: http + ).and_return(api) + + expect(subject).to eq(api) + end + end + + context "when dd_site is provided" do + let(:dd_site) { "datadoghq.eu" } + + it "construct intake url using provided host" do + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: "citestcycle-intake.datadoghq.eu", + port: 443, + ssl: true, + compress: true + ).and_return(http) + + expect(Datadog::CI::Transport::Api::CiTestCycle).to receive(:new).with( + api_key: "api_key", http: http + ).and_return(api) + + expect(subject).to eq(api) + end + end + end + + describe ".build_evp_proxy_api" do + subject { described_class.build_evp_proxy_api(agent_settings) } + + let(:api) { double(:api) } + let(:http) { double(:http) } + + let(:agent_settings) do + Datadog::Core::Configuration::AgentSettingsResolver::AgentSettings.new( + adapter: nil, + ssl: false, + hostname: "localhost", + port: 5555, + uds_path: nil, + timeout_seconds: 42, + deprecated_for_removal_transport_configuration_proc: nil + ) + end + + it "creates and configures http client and EvpProxy" do + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: "localhost", + port: 5555, + ssl: false, + timeout: 42, + compress: false + ).and_return(http) + + expect(Datadog::CI::Transport::Api::EvpProxy).to receive(:new).with(http: http).and_return(api) + + expect(subject).to eq(api) + end + end +end diff --git a/spec/datadog/ci/transport/api/ci_test_cycle_spec.rb b/spec/datadog/ci/transport/api/ci_test_cycle_spec.rb new file mode 100644 index 00000000..4c961133 --- /dev/null +++ b/spec/datadog/ci/transport/api/ci_test_cycle_spec.rb @@ -0,0 +1,33 @@ +require_relative "../../../../../lib/datadog/ci/transport/api/ci_test_cycle" + +RSpec.describe Datadog::CI::Transport::Api::CiTestCycle do + subject do + described_class.new( + api_key: api_key, + http: http + ) + end + + let(:api_key) { "api_key" } + let(:http) { double(:http) } + + describe "#request" do + before do + allow(Datadog::CI::Transport::HTTP).to receive(:new).and_return(http) + end + + it "produces correct headers and forwards request to HTTP layer" do + expect(http).to receive(:request).with( + path: "path", + payload: "payload", + verb: "post", + headers: { + "DD-API-KEY" => "api_key", + "Content-Type" => "application/msgpack" + } + ) + + subject.request(path: "path", payload: "payload") + end + end +end diff --git a/spec/datadog/ci/transport/api/evp_proxy_spec.rb b/spec/datadog/ci/transport/api/evp_proxy_spec.rb new file mode 100644 index 00000000..dbd64519 --- /dev/null +++ b/spec/datadog/ci/transport/api/evp_proxy_spec.rb @@ -0,0 +1,71 @@ +require_relative "../../../../../lib/datadog/ci/transport/api/evp_proxy" + +RSpec.describe Datadog::CI::Transport::Api::EvpProxy do + subject do + described_class.new(http: http) + end + + let(:http) { double(:http) } + + describe "#request" do + before do + allow(Datadog::CI::Transport::HTTP).to receive(:new).and_return(http) + expect(Datadog::Core::Environment::Container).to receive(:container_id).and_return(container_id) + end + + context "without container id" do + let(:container_id) { nil } + + context "with path starting from / character" do + it "produces correct headers and forwards request to HTTP layer prepending path with evp_proxy" do + expect(http).to receive(:request).with( + path: "/evp_proxy/v2/path", + payload: "payload", + verb: "post", + headers: { + "Content-Type" => "application/msgpack", + "X-Datadog-EVP-Subdomain" => "citestcycle-intake" + } + ) + + subject.request(path: "/path", payload: "payload") + end + end + + context "with path without / in the beginning" do + it "constructs evp proxy path correctly" do + expect(http).to receive(:request).with( + path: "/evp_proxy/v2/path", + payload: "payload", + verb: "post", + headers: { + "Content-Type" => "application/msgpack", + "X-Datadog-EVP-Subdomain" => "citestcycle-intake" + } + ) + + subject.request(path: "path", payload: "payload") + end + end + end + + context "with container id" do + let(:container_id) { "container-id" } + + it "adds an additional Datadog-Container-ID header" do + expect(http).to receive(:request).with( + path: "/evp_proxy/v2/path", + payload: "payload", + verb: "post", + headers: { + "Content-Type" => "application/msgpack", + "X-Datadog-EVP-Subdomain" => "citestcycle-intake", + "Datadog-Container-ID" => "container-id" + } + ) + + subject.request(path: "/path", payload: "payload") + end + end + end +end diff --git a/vendor/rbs/ddtrace/0/datadog/core/environment/container.rbs b/vendor/rbs/ddtrace/0/datadog/core/environment/container.rbs new file mode 100644 index 00000000..35f02b3d --- /dev/null +++ b/vendor/rbs/ddtrace/0/datadog/core/environment/container.rbs @@ -0,0 +1,29 @@ +module Datadog + module Core + module Environment + module Container + UUID_PATTERN: untyped + + CONTAINER_PATTERN: untyped + + PLATFORM_REGEX: untyped + + POD_REGEX: untyped + + CONTAINER_REGEX: untyped + + FARGATE_14_CONTAINER_REGEX: untyped + + Descriptor: untyped + + def self?.platform: () -> untyped + + def self?.container_id: () -> untyped + + def self?.task_uid: () -> untyped + + def self?.descriptor: () -> untyped + end + end + end +end \ No newline at end of file diff --git a/vendor/rbs/ddtrace/0/datadog/core/remote/negotiation.rbs b/vendor/rbs/ddtrace/0/datadog/core/remote/negotiation.rbs new file mode 100644 index 00000000..2beb34ac --- /dev/null +++ b/vendor/rbs/ddtrace/0/datadog/core/remote/negotiation.rbs @@ -0,0 +1,18 @@ +module Datadog + module Core + module Remote + class Negotiation + @transport_root: Datadog::Core::Remote::Transport::Negotiation::Transport + @logged: ::Hash[::Symbol, bool] + + def initialize: (Datadog::Core::Configuration::Settings _settings, Datadog::Core::Configuration::AgentSettingsResolver::AgentSettings agent_settings) -> void + + def endpoint?: (::String path) -> bool + + private + + def network_error?: (::Exception error) -> bool + end + end + end +end diff --git a/vendor/rbs/ddtrace/0/datadog/core/remote/transport/negotiation.rbs b/vendor/rbs/ddtrace/0/datadog/core/remote/transport/negotiation.rbs new file mode 100644 index 00000000..5a70cd70 --- /dev/null +++ b/vendor/rbs/ddtrace/0/datadog/core/remote/transport/negotiation.rbs @@ -0,0 +1,36 @@ +module Datadog + module Core + module Remote + module Transport + module Negotiation + class Request < Datadog::Core::Transport::Request + end + + module Response + attr_reader version: untyped + + attr_reader endpoints: untyped + + attr_reader config: untyped + end + + class Transport + attr_reader client: untyped + + attr_reader apis: untyped + + attr_reader default_api: untyped + + attr_reader current_api_id: untyped + + def initialize: (untyped apis, untyped default_api) -> void + + def send_info: () -> untyped + + def current_api: () -> untyped + end + end + end + end + end +end \ No newline at end of file