Skip to content

Commit b90f730

Browse files
ycombinatorclaude
andcommitted
e2e: add TestOpAMPWithEDOTCollector and refactor shared OpAMP setup
- Extract shared download/extract helpers into agent_download.go with caching (sha512 comparison), FileReplacer, ExtractFilter, and correct chmod after extraction - Extract startFleetServerForOpAMP and writeOpAMPCollectorConfig helpers shared by both OpAMP tests - Rename TestOpAMP → TestOpAMPWithUpstreamCollector - Add TestOpAMPWithEDOTCollector: downloads elastic-agent package, runs elastic-agent otel subcommand, verifies EDOT Collector enrolls in Fleet Server over OpAMP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c55c71e commit b90f730

File tree

3 files changed

+108
-117
lines changed

3 files changed

+108
-117
lines changed

testing/e2e/const.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@ package e2e
77
const binaryName = "fleet-server" //nolint:unused // work around to get platform specific binary name for tests
88
const agentName = "elastic-agent" //nolint:unused // work around to get platform specific binary name for tests
99
const agentDevName = "elastic-development-agent" //nolint:unused // work around to get platform specific binary name for tests
10-
const edotCollectorName = "elastic-agent-otelcol" //nolint:unused // EDOT collector binary name bundled inside elastic-agent package

testing/e2e/const_windows.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@ package e2e
77
const binaryName = "fleet-server.exe"
88
const agentName = "elastic-agent.exe"
99
const agentDevName = "elastic-development-agent.exe"
10-
const edotCollectorName = "elastic-agent-otelcol.exe"

testing/e2e/stand_alone_test.go

Lines changed: 108 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)