Skip to content

Commit 5ece964

Browse files
ycombinatorclaude
andauthored
[OpAMP] Add TestOpAMPWithEDOTCollector E2E test (#6612)
* test(e2e): add EDOT Collector OpAMP E2E test - Add TestEDOTOpAMP to verify that the EDOT Collector bundled inside the Elastic Agent package can connect to Fleet Server over OpAMP and enroll in .fleet-agents - Extract shared agent download/extract helpers (downloadElasticAgent, extractAgentArchive) into agent_download.go, refactoring the duplicated code from AgentInstallSuite - Fix TestOpAMP: pre-create the bin/ directory before running make otelcontribcol, which requires it to exist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 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> * chore(e2e): clean up OpAMP test comments and simplify writeOpAMPCollectorConfig Update inline comments, simplify writeOpAMPCollectorConfig to take a single configFilePath parameter, and add newlines after Require() statements for readability. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(e2e): improve EDOT test readability Elaborate on the processExited comment and move the Cleanup block below the early-exit check so the flow reads top-to-bottom. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(e2e): fix stale comments in EDOT test Rename "immediate exit" to "early exit" for consistency and update the Cleanup comment now that it is registered after the early-exit check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(e2e): extract standalone extractTar/extractZip into agent_download.go Move archive extraction out of AgentInstallSuite into standalone functions so both AgentInstallSuite and StandAloneSuite can reuse them. AgentInstallSuite now overwrites the fleet-server binary after extraction instead of during. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(e2e): remove implicit parent directory creation in extractTar/extractZip Remove MkdirAll(filepath.Dir(path)) calls for regular files and symlinks so that missing directory entries in archives surface as test failures rather than being silently papered over (ref: PR #4985). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(e2e): replace static instance UIDs with generated UUIDv7s Use uuid.Must(uuid.NewV7()) instead of hardcoded UUIDs for OpAMP test instance UIDs, as suggested in review. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d26e448 commit 5ece964

File tree

3 files changed

+306
-139
lines changed

3 files changed

+306
-139
lines changed

testing/e2e/agent_download.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77
package e2e
88

99
import (
10+
"archive/tar"
11+
"archive/zip"
12+
"bytes"
13+
"compress/gzip"
1014
"context"
1115
"crypto/sha512"
1216
"encoding/hex"
1317
"encoding/json"
18+
"errors"
1419
"fmt"
1520
"io"
1621
"net/http"
@@ -190,6 +195,131 @@ func fetchRemoteSHA512(ctx context.Context, t *testing.T, client *http.Client, u
190195
return strings.Fields(string(data))[0]
191196
}
192197

198+
// extractedPaths holds paths to notable binaries found during archive extraction.
199+
type extractedPaths struct {
200+
agentBinary string // path to the elastic-agent binary
201+
fleetServerBinary string // path to the fleet-server binary
202+
}
203+
204+
// extractTar extracts a tar.gz archive from r into destDir, returning the
205+
// paths to notable binaries found during extraction.
206+
func extractTar(t *testing.T, r io.Reader, destDir string) extractedPaths {
207+
t.Helper()
208+
gs, err := gzip.NewReader(r)
209+
if err != nil {
210+
t.Fatalf("failed to create gzip reader: %v", err)
211+
}
212+
213+
var paths extractedPaths
214+
tarReader := tar.NewReader(gs)
215+
for {
216+
header, err := tarReader.Next()
217+
if errors.Is(err, io.EOF) {
218+
break
219+
}
220+
if err != nil {
221+
t.Fatalf("failed to read tar entry: %v", err)
222+
}
223+
224+
path := filepath.Join(destDir, header.Name)
225+
mode := header.FileInfo().Mode()
226+
switch {
227+
case mode.IsDir():
228+
if err := os.MkdirAll(path, 0755); err != nil {
229+
t.Fatalf("failed to create directory %s: %v", path, err)
230+
}
231+
case mode.IsRegular():
232+
// Ensure parent directories exist — some upstream tar archives (e.g. the
233+
// Linux elastic-agent snapshot) omit explicit directory entries.
234+
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
235+
t.Fatalf("failed to create parent directory for %s: %v", path, err)
236+
}
237+
w, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
238+
if err != nil {
239+
t.Fatalf("failed to create file %s: %v", path, err)
240+
}
241+
if _, err := io.Copy(w, tarReader); err != nil {
242+
w.Close()
243+
t.Fatalf("failed to write file %s: %v", path, err)
244+
}
245+
w.Close()
246+
if strings.HasSuffix(header.Name, agentName) {
247+
paths.agentBinary = path
248+
}
249+
if strings.HasSuffix(header.Name, binaryName) {
250+
paths.fleetServerBinary = path
251+
}
252+
case mode.Type()&os.ModeSymlink == os.ModeSymlink:
253+
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
254+
t.Fatalf("failed to create parent directory for symlink %s: %v", path, err)
255+
}
256+
if err := os.Symlink(header.Linkname, path); err != nil {
257+
t.Fatalf("failed to create symlink %s: %v", path, err)
258+
}
259+
if strings.HasSuffix(header.Linkname, agentName) {
260+
paths.agentBinary = path
261+
}
262+
default:
263+
t.Logf("Unable to untar type=%c in file=%s", header.Typeflag, path)
264+
}
265+
}
266+
return paths
267+
}
268+
269+
// extractZip extracts a zip archive from r into destDir, returning the
270+
// paths to notable binaries found during extraction.
271+
func extractZip(t *testing.T, r io.Reader, destDir string) extractedPaths {
272+
t.Helper()
273+
var b bytes.Buffer
274+
n, err := io.Copy(&b, r)
275+
if err != nil {
276+
t.Fatalf("failed to buffer zip archive: %v", err)
277+
}
278+
279+
zipReader, err := zip.NewReader(bytes.NewReader(b.Bytes()), n)
280+
if err != nil {
281+
t.Fatalf("failed to create zip reader: %v", err)
282+
}
283+
284+
var paths extractedPaths
285+
for _, file := range zipReader.File {
286+
path := filepath.Join(destDir, file.Name)
287+
mode := file.FileInfo().Mode()
288+
switch {
289+
case mode.IsDir():
290+
if err := os.MkdirAll(path, 0755); err != nil {
291+
t.Fatalf("failed to create directory %s: %v", path, err)
292+
}
293+
case mode.IsRegular():
294+
w, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
295+
if err != nil {
296+
t.Fatalf("failed to create file %s: %v", path, err)
297+
}
298+
f, err := file.Open()
299+
if err != nil {
300+
w.Close()
301+
t.Fatalf("failed to open zip entry %s: %v", file.Name, err)
302+
}
303+
if _, err := io.Copy(w, f); err != nil {
304+
f.Close()
305+
w.Close()
306+
t.Fatalf("failed to write file %s: %v", path, err)
307+
}
308+
f.Close()
309+
w.Close()
310+
if strings.HasSuffix(file.Name, agentName) {
311+
paths.agentBinary = path
312+
}
313+
if strings.HasSuffix(file.Name, binaryName) {
314+
paths.fleetServerBinary = path
315+
}
316+
default:
317+
t.Logf("Unable to unzip type=%+v in file=%s", mode, path)
318+
}
319+
}
320+
return paths
321+
}
322+
193323
// sha512OfFile returns the hex-encoded SHA-512 checksum of the file at path.
194324
func sha512OfFile(path string) (string, error) {
195325
f, err := os.Open(path)

testing/e2e/agent_install_test.go

Lines changed: 15 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,14 @@
77
package e2e
88

99
import (
10-
"archive/tar"
11-
"archive/zip"
12-
"bytes"
13-
"compress/gzip"
1410
"context"
15-
"errors"
1611
"fmt"
1712
"html/template"
1813
"io"
1914
"os"
2015
"os/exec"
2116
"path/filepath"
2217
"runtime"
23-
"strings"
2418
"testing"
2519
"time"
2620

@@ -86,121 +80,41 @@ func (suite *AgentInstallSuite) SetupSuite() {
8680
suite.downloadPath = filepath.Join(os.TempDir(), "e2e-agent_install_test")
8781
err = os.MkdirAll(suite.downloadPath, 0755)
8882
suite.Require().NoError(err)
83+
var paths extractedPaths
8984
switch runtime.GOOS {
9085
case "windows":
91-
suite.extractZip(rc)
86+
paths = extractZip(suite.T(), rc, suite.downloadPath)
9287
case "darwin", "linux":
93-
suite.extractTar(rc)
88+
paths = extractTar(suite.T(), rc, suite.downloadPath)
9489
default:
9590
suite.Require().Failf("Unsupported OS", "OS %s is unsupported for tests", runtime.GOOS)
9691
}
92+
93+
// Replace the fleet-server binary from the archive with the locally compiled version.
94+
if paths.fleetServerBinary != "" {
95+
suite.copyFleetServer(paths.fleetServerBinary)
96+
}
97+
98+
suite.agentPath = paths.agentBinary
9799
_, err = os.Stat(suite.agentPath)
98100
suite.Require().NoError(err)
99101
suite.T().Log("Setup complete.")
100102
}
101103

102-
// extractZip treats the passed Reader as a zip stream and unarchives it to a temp dir
103-
// fleet-server binary in archive is replaced by a locally compiled version
104-
func (suite *AgentInstallSuite) extractZip(r io.Reader) {
104+
func (suite *AgentInstallSuite) copyFleetServer(destPath string) {
105105
suite.T().Helper()
106-
// Extract zip stream
107-
var b bytes.Buffer
108-
n, err := io.Copy(&b, r)
109-
suite.Require().NoError(err)
110-
zipReader, err := zip.NewReader(bytes.NewReader(b.Bytes()), n)
106+
src, err := os.Open(suite.binaryPath)
111107
suite.Require().NoError(err)
112-
for _, file := range zipReader.File {
113-
path := filepath.Join(suite.downloadPath, file.Name)
114-
mode := file.FileInfo().Mode()
115-
switch {
116-
case mode.IsDir():
117-
err := os.MkdirAll(path, 0755)
118-
suite.Require().NoError(err)
119-
case mode.IsRegular():
120-
err := os.MkdirAll(filepath.Dir(path), 0755)
121-
suite.Require().NoError(err)
122-
w, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
123-
suite.Require().NoError(err)
124-
if strings.HasSuffix(file.Name, binaryName) {
125-
suite.copyFleetServer(w)
126-
continue
127-
}
128-
if strings.HasSuffix(file.Name, agentName) {
129-
suite.agentPath = path
130-
}
131-
f, err := file.Open()
132-
suite.Require().NoError(err)
133-
_, err = io.Copy(w, f)
134-
suite.Require().NoError(err)
135-
err = w.Close()
136-
suite.Require().NoError(err)
137-
err = f.Close()
138-
suite.Require().NoError(err)
139-
default:
140-
suite.T().Logf("Unable to unzip type=%+v in file=%s", mode, path)
141-
}
142-
}
143-
}
108+
defer src.Close()
144109

145-
// extractTar treats the passed Reader as a tar.gz stream and unarchives it to the suite.downloadPath
146-
// fleet-server binary in archive is replaced by a locally compiled version
147-
func (suite *AgentInstallSuite) extractTar(r io.Reader) {
148-
suite.T().Helper()
149-
// Extract tar.gz stream
150-
gs, err := gzip.NewReader(r)
110+
w, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
151111
suite.Require().NoError(err)
152-
tarReader := tar.NewReader(gs)
153-
for {
154-
header, err := tarReader.Next()
155-
if errors.Is(err, io.EOF) {
156-
break
157-
}
158-
suite.Require().NoError(err)
159-
160-
path := filepath.Join(suite.downloadPath, header.Name)
161-
mode := header.FileInfo().Mode()
162-
switch {
163-
case mode.IsDir():
164-
err := os.MkdirAll(path, 0755)
165-
suite.Require().NoError(err)
166-
case mode.IsRegular():
167-
err := os.MkdirAll(filepath.Dir(path), 0755)
168-
suite.Require().NoError(err)
169-
w, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
170-
suite.Require().NoError(err)
171-
// Use local fleet-server instead of the one from the archive
172-
if strings.HasSuffix(header.Name, binaryName) {
173-
suite.copyFleetServer(w)
174-
continue
175-
}
176-
_, err = io.Copy(w, tarReader)
177-
suite.Require().NoError(err)
178-
err = w.Close()
179-
suite.Require().NoError(err)
180-
case mode.Type()&os.ModeSymlink == os.ModeSymlink:
181-
err := os.MkdirAll(filepath.Dir(path), 0755)
182-
suite.Require().NoError(err)
183-
err = os.Symlink(header.Linkname, path)
184-
suite.Require().NoError(err)
185-
if strings.HasSuffix(header.Linkname, agentName) {
186-
suite.agentPath = path
187-
}
188-
default:
189-
suite.T().Logf("Unable to untar type=%c in file=%s", header.Typeflag, path)
190-
}
191-
}
192-
}
193112

194-
func (suite *AgentInstallSuite) copyFleetServer(w io.WriteCloser) {
195-
suite.T().Helper()
196-
src, err := os.Open(suite.binaryPath)
197-
suite.Require().NoError(err)
198113
_, err = io.Copy(w, src)
199114
suite.Require().NoError(err)
115+
200116
err = w.Close()
201117
suite.Require().NoError(err)
202-
err = src.Close()
203-
suite.Require().NoError(err)
204118
}
205119

206120
func (suite *AgentInstallSuite) TearDownSuite() {

0 commit comments

Comments
 (0)