@@ -581,60 +581,82 @@ func (suite *StandAloneSuite) TestAPMInstrumentation() {
581581 cmd .Wait ()
582582}
583583
584- // TestOpAMP ensures that the OpAMP endpoint in Fleet Server works as expected by installing
585- // an OTel Collector, configuring it with the OpAMP extension, and having it connect to Fleet
586- // Server using OpAMP , and verifying that Fleet Server responds to this request with an HTTP
587- // 200 OK status response.
588- func ( suite * StandAloneSuite ) TestOpAMP () {
589- // Create a config file from a template in the test temp dir
590- dir := suite .T ().TempDir ()
584+ // startFleetServerForOpAMP creates the fleet-server config from stand-alone-opamp.tpl,
585+ // starts the fleet-server binary, waits for it to be healthy, fetches an enrollment token
586+ // for "dummy-policy" , and enrolls a dummy agent ( to ensure .fleet-agents exists before any
587+ // OpAMP collector connects). It returns the enrollment API key. Fleet-server is stopped when
588+ // ctx is cancelled; the caller owns the context lifetime.
589+ func ( suite * StandAloneSuite ) startFleetServerForOpAMP ( ctx context. Context , dir , staticTokenKey string ) string {
590+ suite .T ().Helper ()
591591 tpl , err := template .ParseFiles (filepath .Join ("testdata" , "stand-alone-opamp.tpl" ))
592592 suite .Require ().NoError (err )
593593 f , err := os .Create (filepath .Join (dir , "config.yml" ))
594594 suite .Require ().NoError (err )
595595 err = tpl .Execute (f , map [string ]interface {}{
596596 "Hosts" : suite .ESHosts ,
597597 "ServiceToken" : suite .ServiceToken ,
598- "StaticTokenKey" : "opamp-e2e-test-key" ,
598+ "StaticTokenKey" : staticTokenKey ,
599599 })
600600 f .Close ()
601601 suite .Require ().NoError (err )
602602
603- ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Minute )
604- defer cancel ()
605-
606- // Run the fleet-server binary
607603 cmd := exec .CommandContext (ctx , suite .binaryPath , "-c" , filepath .Join (dir , "config.yml" ))
608- cmd .Cancel = func () error {
609- return cmd .Process .Signal (syscall .SIGTERM )
610- }
604+ cmd .Cancel = func () error { return cmd .Process .Signal (syscall .SIGTERM ) }
611605 cmd .Env = []string {"GOCOVERDIR=" + suite .CoverPath }
612606 err = cmd .Start ()
613607 suite .Require ().NoError (err )
614- defer cmd .Wait ()
608+ suite . T (). Cleanup ( func () { cmd .Wait () } )
615609
616610 suite .FleetServerStatusOK (ctx , "http://localhost:8220" )
617611
618612 apiKey := suite .GetEnrollmentTokenForPolicyID (ctx , "dummy-policy" )
613+ tester := api_version .NewClientAPITesterCurrent (suite .Scaffold , "http://localhost:8220" , apiKey )
614+ tester .Enroll (ctx , apiKey )
615+ return apiKey
616+ }
619617
620- // Make sure the OpAMP endpoint works.
618+ // writeOpAMPCollectorConfig renders otelcol-opamp.tpl into dir/configFile and returns
619+ // the full path to the written file.
620+ func (suite * StandAloneSuite ) writeOpAMPCollectorConfig (dir , configFile , instanceUID , apiKey string ) string {
621+ suite .T ().Helper ()
622+ tpl , err := template .ParseFiles (filepath .Join ("testdata" , "otelcol-opamp.tpl" ))
623+ suite .Require ().NoError (err )
624+ path := filepath .Join (dir , configFile )
625+ f , err := os .Create (path )
626+ suite .Require ().NoError (err )
627+ err = tpl .Execute (f , map [string ]interface {}{
628+ "OpAMP" : map [string ]string {
629+ "InstanceUID" : instanceUID ,
630+ "APIKey" : apiKey ,
631+ },
632+ })
633+ f .Close ()
634+ suite .Require ().NoError (err )
635+ return path
636+ }
637+
638+ // TestOpAMP ensures that the OpAMP endpoint in Fleet Server works as expected by installing
639+ // an OTel Collector, configuring it with the OpAMP extension, and having it connect to Fleet
640+ // Server using OpAMP, and verifying that Fleet Server responds to this request with an HTTP
641+ // 200 OK status response.
642+ func (suite * StandAloneSuite ) TestOpAMPWithUpstreamCollector () {
643+ dir := suite .T ().TempDir ()
644+
645+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Minute )
646+ defer cancel ()
647+
648+ apiKey := suite .startFleetServerForOpAMP (ctx , dir , "opamp-e2e-test-key" )
649+
650+ // Make sure the OpAMP endpoint works before proceeding to build the collector.
621651 req , err := http .NewRequestWithContext (ctx , "POST" , "http://localhost:8220/v1/opamp" , nil )
622652 suite .Require ().NoError (err )
623653 req .Header .Set ("Authorization" , "ApiKey " + apiKey )
624654 req .Header .Set ("Content-Type" , "application/x-protobuf" )
625-
626655 resp , err := suite .Client .Do (req )
627656 suite .Require ().NoError (err )
628657 resp .Body .Close ()
629658 suite .Require ().Equal (http .StatusOK , resp .StatusCode )
630659
631- // Enroll a dummy agent to initialize the .fleet-agents index before the OTel Collector connects.
632- // Without this, findEnrolledAgent fails with index_not_found_exception when the OTel Collector
633- // sends its first AgentToServer message, because .fleet-agents doesn't exist yet in a fresh
634- // standalone fleet-server environment (unlike agent-managed fleet-server which self-enrolls).
635- tester := api_version .NewClientAPITesterCurrent (suite .Scaffold , "http://localhost:8220" , apiKey )
636- tester .Enroll (ctx , apiKey )
637-
638660 // Clone OTel Collector contrib repository (shallow clone of main branch)
639661 cloneDir := filepath .Join (dir , "opentelemetry-collector-contrib" )
640662 suite .T ().Logf ("Cloning opentelemetry-collector-contrib (main) to %s" , cloneDir )
@@ -670,22 +692,11 @@ func (suite *StandAloneSuite) TestOpAMP() {
670692 // Configure it with the OpAMP extension
671693 instanceUID := "019b8d7a-2da8-7657-b52d-492a9de33319"
672694 suite .T ().Logf ("Configuring OTel Collector with OpAMP extension (instanceUID=%s)" , instanceUID )
673- tpl , err = template .ParseFiles (filepath .Join ("testdata" , "otelcol-opamp.tpl" ))
674- suite .Require ().NoError (err )
675- f , err = os .Create (filepath .Join (dir , "otelcol.yml" ))
676- suite .Require ().NoError (err )
677- err = tpl .Execute (f , map [string ]interface {}{
678- "OpAMP" : map [string ]string {
679- "InstanceUID" : instanceUID ,
680- "APIKey" : apiKey ,
681- },
682- })
683- f .Close ()
684- suite .Require ().NoError (err )
695+ collectorConfig := suite .writeOpAMPCollectorConfig (dir , "otelcol.yml" , instanceUID , apiKey )
685696
686697 // Start OTel Collector
687698 suite .T ().Log ("Starting OTel Collector" )
688- otelCmd := exec .CommandContext (ctx , otelBinaryPath , "--config" , filepath . Join ( dir , "otelcol.yml" ) )
699+ otelCmd := exec .CommandContext (ctx , otelBinaryPath , "--config" , collectorConfig )
689700 otelCmd .Cancel = func () error {
690701 return otelCmd .Process .Signal (syscall .SIGTERM )
691702 }
@@ -711,105 +722,87 @@ func (suite *StandAloneSuite) TestOpAMP() {
711722 suite .Contains (agentDoc .Tags , "otelcontribcol" , "expected tags to contain otelcontribcol" )
712723}
713724
714- // TestEDOTOpAMP ensures that the EDOT (Elastic Distribution of OpenTelemetry) Collector,
715- // bundled inside the Elastic Agent package, can connect to Fleet Server over OpAMP and
716- // enroll as an agent in the .fleet-agents index.
717- func (suite * StandAloneSuite ) TestEDOTOpAMP () {
718- // Create a config file from a template in the test temp dir
725+ // TestOpAMPWithEDOTCollector ensures that the EDOT Collector can connect to Fleet Server
726+ // over OpAMP and enroll as an agent in the .fleet-agents index.
727+ func (suite * StandAloneSuite ) TestOpAMPWithEDOTCollector () {
719728 dir := suite .T ().TempDir ()
720- tpl , err := template .ParseFiles (filepath .Join ("testdata" , "stand-alone-opamp.tpl" ))
721- suite .Require ().NoError (err )
722- f , err := os .Create (filepath .Join (dir , "config.yml" ))
723- suite .Require ().NoError (err )
724- err = tpl .Execute (f , map [string ]interface {}{
725- "Hosts" : suite .ESHosts ,
726- "ServiceToken" : suite .ServiceToken ,
727- "StaticTokenKey" : "edot-opamp-e2e-test-key" ,
728- })
729- f .Close ()
730- suite .Require ().NoError (err )
731-
732- ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Minute )
733- defer cancel ()
734729
735- // Run the fleet-server binary
736- cmd := exec .CommandContext (ctx , suite .binaryPath , "-c" , filepath .Join (dir , "config.yml" ))
737- cmd .Cancel = func () error {
738- return cmd .Process .Signal (syscall .SIGTERM )
739- }
740- cmd .Env = []string {"GOCOVERDIR=" + suite .CoverPath }
741- err = cmd .Start ()
742- suite .Require ().NoError (err )
743- defer cmd .Wait ()
744-
745- suite .FleetServerStatusOK (ctx , "http://localhost:8220" )
746-
747- apiKey := suite .GetEnrollmentTokenForPolicyID (ctx , "dummy-policy" )
748-
749- // Enroll a dummy agent to initialize the .fleet-agents index before the EDOT Collector connects.
750- // Without this, WaitForAgentDoc fails with index_not_found_exception when the EDOT Collector
751- // sends its first AgentToServer message, because .fleet-agents doesn't exist yet in a fresh
752- // standalone fleet-server environment.
753- tester := api_version .NewClientAPITesterCurrent (suite .Scaffold , "http://localhost:8220" , apiKey )
754- tester .Enroll (ctx , apiKey )
755-
756- // Download and extract the Elastic Agent package to obtain the bundled EDOT Collector binary.
757- suite .T ().Log ("Downloading Elastic Agent package to extract EDOT Collector binary" )
758- downloadCtx , downloadCancel := context .WithTimeout (ctx , 5 * time .Minute )
730+ // Download and extract the full Elastic Agent package before starting the timed
731+ // portion of the test. The archive is cached on disk after the first run so this
732+ // is fast on subsequent runs; extracting everything ensures all components
733+ // (e.g. elastic-otel-collector) needed by elastic-agent otel are present.
734+ suite .T ().Log ("Downloading Elastic Agent package" )
735+ agentExtractDir := filepath .Join (dir , "elastic-agent-package" )
736+ suite .Require ().NoError (os .MkdirAll (agentExtractDir , 0755 ))
737+ downloadCtx , downloadCancel := context .WithTimeout (context .Background (), 10 * time .Minute )
759738 defer downloadCancel ()
760739 rc := downloadElasticAgent (downloadCtx , suite .T (), suite .Client )
761- defer rc .Close ()
762-
763- agentExtractDir := filepath .Join (dir , "elastic-agent-package" )
764- err = os .MkdirAll (agentExtractDir , 0755 )
765- suite .Require ().NoError (err )
766- paths := extractAgentArchive (suite .T (), rc , agentExtractDir , nil )
740+ paths := extractAgentArchive (suite .T (), rc , agentExtractDir , nil , nil )
767741 rc .Close ()
768742
769- edotBinaryPath , ok := paths [edotCollectorName ]
770- suite .Require ().Truef (ok , "EDOT Collector binary %q not found in elastic-agent package" , edotCollectorName )
771- suite .T ().Logf ("Found EDOT Collector binary at %s" , edotBinaryPath )
743+ ctx , cancel := context .WithTimeout (context .Background (), 2 * time .Minute )
744+ defer cancel ()
745+
746+ apiKey := suite .startFleetServerForOpAMP (ctx , dir , "edot-opamp-e2e-test-key" )
747+
748+ agentBinaryPath , ok := paths [agentName ]
749+ suite .Require ().Truef (ok , "elastic-agent binary %q not found in package" , agentName )
750+ suite .T ().Logf ("Found elastic-agent binary at %s" , agentBinaryPath )
772751
773- // Configure the EDOT Collector with the OpAMP extension
774752 instanceUID := "029c9e8b-3eb9-8768-c63e-593b0ef44430"
775753 suite .T ().Logf ("Configuring EDOT Collector with OpAMP extension (instanceUID=%s)" , instanceUID )
776- tpl , err = template .ParseFiles (filepath .Join ("testdata" , "otelcol-opamp.tpl" ))
777- suite .Require ().NoError (err )
778- f , err = os .Create (filepath .Join (dir , "edot-otelcol.yml" ))
779- suite .Require ().NoError (err )
780- err = tpl .Execute (f , map [string ]interface {}{
781- "OpAMP" : map [string ]string {
782- "InstanceUID" : instanceUID ,
783- "APIKey" : apiKey ,
784- },
785- })
786- f .Close ()
787- suite .Require ().NoError (err )
754+ collectorConfig := suite .writeOpAMPCollectorConfig (dir , "edot-otelcol.yml" , instanceUID , apiKey )
788755
789- // Start the EDOT Collector
790- suite .T ().Log ("Starting EDOT Collector" )
791- edotCmd := exec .CommandContext (ctx , edotBinaryPath , "--config" , filepath .Join (dir , "edot-otelcol.yml" ))
756+ // Start the EDOT Collector via `elastic-agent otel`
757+ suite .T ().Log ("Starting EDOT Collector via elastic-agent otel" )
758+ edotOutputFile , err := os .CreateTemp (dir , "edot-output-*.log" )
759+ suite .Require ().NoError (err )
760+ edotCmd := exec .CommandContext (ctx , agentBinaryPath , "otel" , "--config" , collectorConfig )
792761 edotCmd .Cancel = func () error {
793762 return edotCmd .Process .Signal (syscall .SIGTERM )
794763 }
795- edotCmd .Stdout = os . Stdout
796- edotCmd .Stderr = os . Stderr
764+ edotCmd .Stdout = edotOutputFile
765+ edotCmd .Stderr = edotOutputFile
797766 err = edotCmd .Start ()
798767 suite .Require ().NoError (err )
799- defer edotCmd .Wait ()
768+ // processExited owns the single Wait() call on edotCmd.
769+ processExited := make (chan error , 1 )
770+ go func () { processExited <- edotCmd .Wait () }()
771+ suite .T ().Cleanup (func () {
772+ // Wait for the process to exit (context cancellation will have killed it)
773+ // before closing the output file and reading it. The 30s fallback handles
774+ // the case where the early-exit path already consumed processExited.
775+ select {
776+ case <- processExited :
777+ case <- time .After (30 * time .Second ):
778+ }
779+ edotOutputFile .Close ()
780+ if out , readErr := os .ReadFile (edotOutputFile .Name ()); readErr == nil {
781+ suite .T ().Logf ("EDOT Collector output:\n %s" , string (out ))
782+ }
783+ })
784+ // Detect immediate exit — if the process dies within 5s it's a startup failure.
785+ select {
786+ case exitErr := <- processExited :
787+ edotOutputFile .Close ()
788+ if out , readErr := os .ReadFile (edotOutputFile .Name ()); readErr == nil {
789+ suite .T ().Logf ("EDOT Collector output:\n %s" , string (out ))
790+ }
791+ suite .Require ().NoError (exitErr , "EDOT Collector exited prematurely" )
792+ return
793+ case <- time .After (5 * time .Second ):
794+ // Process is still running after 5s — proceed
795+ }
800796
801797 // Verify that the EDOT Collector was enrolled in Fleet by fetching its document from
802798 // .fleet-agents and asserting on its contents.
803799 suite .T ().Logf ("Waiting for EDOT agent %s to appear in .fleet-agents" , instanceUID )
804800 agentDoc := suite .WaitForAgentDoc (ctx , instanceUID )
805801
806802 suite .Equal (instanceUID , agentDoc .Agent .ID , "expected agent.id to match instanceUID" )
807- versionOut , err := exec .Command (edotBinaryPath , "--version" ).Output ()
808- suite .Require ().NoError (err )
809- edotVersion := strings .TrimPrefix (strings .TrimSpace (string (versionOut )), "elastic-agent-otelcol version " )
810803 suite .Equal ("OPAMP" , agentDoc .Type , "expected type to be OPAMP" )
811- suite .Equal ("elastic-agent-otelcol" , agentDoc .Agent .Type , "expected agent.type to be elastic-agent-otelcol" )
812- suite .Equal (edotVersion , agentDoc .Agent .Version , "expected agent.version to match EDOT Collector binary version" )
804+ suite .Equal ("elastic-otel-collector" , agentDoc .Agent .Type , "expected agent.type to be elastic-otel-collector" )
805+ suite .NotEmpty (agentDoc .Agent .Version , "expected agent.version to be set" )
806+ suite .Contains (agentDoc .Tags , "elastic-otel-collector" , "expected tags to contain elastic-otel-collector" )
813807 suite .Equal (1 , agentDoc .Revision , "expected policy_revision_idx to be 1" )
814- suite .Contains (agentDoc .Tags , "elastic-agent-otelcol" , "expected tags to contain elastic-agent-otelcol" )
815808}
0 commit comments