|
5 | 5 | package runtime |
6 | 6 |
|
7 | 7 | import ( |
| 8 | + "context" |
| 9 | + "fmt" |
| 10 | + "net" |
8 | 11 | "net/url" |
| 12 | + "os" |
| 13 | + "os/exec" |
| 14 | + "path/filepath" |
| 15 | + "runtime" |
| 16 | + "strconv" |
| 17 | + "strings" |
9 | 18 | "testing" |
| 19 | + "time" |
| 20 | + |
| 21 | + "github.com/elastic/elastic-agent-libs/api/npipe" |
| 22 | + |
| 23 | + "go.uber.org/zap/zapcore" |
| 24 | + |
| 25 | + "github.com/stretchr/testify/require" |
10 | 26 |
|
11 | 27 | "github.com/elastic/elastic-agent-client/v7/pkg/client" |
12 | 28 | "github.com/elastic/elastic-agent-client/v7/pkg/proto" |
13 | 29 | "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" |
14 | 30 | "github.com/elastic/elastic-agent/pkg/component" |
| 31 | + "github.com/elastic/elastic-agent/pkg/core/logger/loggertest" |
15 | 32 |
|
16 | 33 | "github.com/google/go-cmp/cmp" |
17 | 34 | "github.com/google/go-cmp/cmp/cmpopts" |
@@ -255,3 +272,122 @@ func TestGetConnInfoServerAddress(t *testing.T) { |
255 | 272 | }) |
256 | 273 | } |
257 | 274 | } |
| 275 | + |
| 276 | +// TestCISKeepsRunningOnNonFatalExitCodeFromStart tests that the connection info |
| 277 | +// server keeps running when starting a service component results in a non-fatal |
| 278 | +// exit code. |
| 279 | +func TestCISKeepsRunningOnNonFatalExitCodeFromStart(t *testing.T) { |
| 280 | + log, logObs := loggertest.New("test") |
| 281 | + const nonFatalExitCode = 99 |
| 282 | + const cisPort = 9999 |
| 283 | + const cisSocket = ".teaci.sock" |
| 284 | + |
| 285 | + // Make an Endpoint component for testing |
| 286 | + endpoint := makeEndpointComponent(t, map[string]interface{}{}) |
| 287 | + endpoint.InputSpec.Spec.Service = &component.ServiceSpec{ |
| 288 | + CPort: cisPort, |
| 289 | + CSocket: cisSocket, |
| 290 | + Log: nil, |
| 291 | + Operations: component.ServiceOperationsSpec{ |
| 292 | + Check: &component.ServiceOperationsCommandSpec{}, |
| 293 | + Install: &component.ServiceOperationsCommandSpec{ |
| 294 | + NonFatalExitCodes: []int{nonFatalExitCode}, |
| 295 | + }, |
| 296 | + }, |
| 297 | + Timeouts: component.ServiceTimeoutSpec{}, |
| 298 | + } |
| 299 | + |
| 300 | + // Create binary mocking Endpoint such that executing it will return |
| 301 | + // the non-fatal exit code from the spec above. |
| 302 | + endpoint.InputSpec.BinaryPath = mockEndpointBinary(t, nonFatalExitCode) |
| 303 | + endpoint.InputSpec.BinaryName = "endpoint" |
| 304 | + |
| 305 | + t.Logf("mock binary path: %s\n", endpoint.InputSpec.BinaryPath) |
| 306 | + |
| 307 | + // Create new service runtime with component |
| 308 | + service, err := newServiceRuntime(endpoint, log, true) |
| 309 | + require.NoError(t, err) |
| 310 | + |
| 311 | + ctx, cancel := context.WithCancel(context.Background()) |
| 312 | + defer cancel() |
| 313 | + comm := newMockCommunicator("") |
| 314 | + |
| 315 | + // Observe component state |
| 316 | + go func() { |
| 317 | + for { |
| 318 | + select { |
| 319 | + case <-ctx.Done(): |
| 320 | + return |
| 321 | + case <-service.ch: |
| 322 | + } |
| 323 | + } |
| 324 | + }() |
| 325 | + |
| 326 | + // Run the service |
| 327 | + go func() { |
| 328 | + err := service.Run(ctx, comm) |
| 329 | + require.EqualError(t, err, context.Canceled.Error()) |
| 330 | + }() |
| 331 | + |
| 332 | + service.actionCh <- actionModeSigned{ |
| 333 | + actionMode: actionStart, |
| 334 | + } |
| 335 | + |
| 336 | + // Check that connection info server is still running and that we see the |
| 337 | + // warning log message about Endpoint's install operation failing with a non-fatal exit |
| 338 | + // code but the service runtime continuing to run. |
| 339 | + cisAddr, err := getConnInfoServerAddress(runtime.GOOS, true, cisPort, cisSocket) |
| 340 | + require.NoError(t, err) |
| 341 | + |
| 342 | + parsedCISAddr, err := url.Parse(cisAddr) |
| 343 | + require.NoError(t, err) |
| 344 | + |
| 345 | + expectedWarnLogMsg := fmt.Sprintf("exit code %d is non-fatal, continuing to run...", nonFatalExitCode) |
| 346 | + require.Eventually(t, func() bool { |
| 347 | + if runtime.GOOS != "windows" { |
| 348 | + _, err = net.Dial("unix", parsedCISAddr.Host+parsedCISAddr.Path) |
| 349 | + } else { |
| 350 | + if strings.HasPrefix(cisAddr, "npipe:///") { |
| 351 | + path := strings.TrimPrefix(cisAddr, "npipe:///") |
| 352 | + cisAddr = `\\.\pipe\` + path |
| 353 | + } |
| 354 | + _, err = npipe.Dial(cisAddr)("", "") |
| 355 | + } |
| 356 | + |
| 357 | + if err != nil { |
| 358 | + t.Logf("Connection info server is not running: %v", err) |
| 359 | + return false |
| 360 | + } |
| 361 | + |
| 362 | + logs := logObs.TakeAll() |
| 363 | + for _, l := range logs { |
| 364 | + t.Logf("[%s] %s", l.Level, l.Message) |
| 365 | + if l.Level == zapcore.WarnLevel && l.Message == expectedWarnLogMsg { |
| 366 | + return true |
| 367 | + } |
| 368 | + } |
| 369 | + |
| 370 | + return false |
| 371 | + }, 2*time.Second, 200*time.Millisecond) |
| 372 | +} |
| 373 | + |
| 374 | +func mockEndpointBinary(t *testing.T, exitCode int) string { |
| 375 | + // Build a mock Endpoint binary that can return a specific exit code. |
| 376 | + outPath := filepath.Join(t.TempDir(), "mock_endpoint") |
| 377 | + if runtime.GOOS == "windows" { |
| 378 | + outPath += ".exe" |
| 379 | + } |
| 380 | + |
| 381 | + cmd := exec.Command( |
| 382 | + "go", "build", |
| 383 | + "-o", outPath, |
| 384 | + "-ldflags", "-X 'main.ExitCode="+strconv.Itoa(exitCode)+"'", |
| 385 | + "testdata/exitcode/main.go", |
| 386 | + ) |
| 387 | + cmd.Stdout = os.Stdout |
| 388 | + cmd.Stderr = os.Stderr |
| 389 | + err := cmd.Run() |
| 390 | + require.NoError(t, err) |
| 391 | + |
| 392 | + return outPath |
| 393 | +} |
0 commit comments