diff --git a/x-pack/elastic-agent/CHANGELOG.next.asciidoc b/x-pack/elastic-agent/CHANGELOG.next.asciidoc index 0d8a6362e29c..3bf4ce446094 100644 --- a/x-pack/elastic-agent/CHANGELOG.next.asciidoc +++ b/x-pack/elastic-agent/CHANGELOG.next.asciidoc @@ -62,6 +62,7 @@ - Don't log when upgrade capability doesn't apply {pull}25386[25386] - Fixed issue when unversioned home is set and invoked watcher failing with ENOENT {issue}25371[25371] - Fixed Elastic Agent: expecting Dict and received *transpiler.Key for '0' {issue}24453[24453] +- Fix AckBatch to do nothing when no actions passed {pull}25562[25562] ==== New features diff --git a/x-pack/elastic-agent/pkg/fleetapi/acker/fleet/fleet_acker.go b/x-pack/elastic-agent/pkg/fleetapi/acker/fleet/fleet_acker.go index cebf49b027fe..cdab68fceb7c 100644 --- a/x-pack/elastic-agent/pkg/fleetapi/acker/fleet/fleet_acker.go +++ b/x-pack/elastic-agent/pkg/fleetapi/acker/fleet/fleet_acker.go @@ -79,6 +79,11 @@ func (f *Acker) AckBatch(ctx context.Context, actions []fleetapi.Action) error { ids = append(ids, action.ID()) } + if len(events) == 0 { + // no events to send (nothing to do) + return nil + } + cmd := fleetapi.NewAckCmd(f.agentInfo, f.client) req := &fleetapi.AckRequest{ Events: events, diff --git a/x-pack/elastic-agent/pkg/fleetapi/acker/fleet/fleet_acker_test.go b/x-pack/elastic-agent/pkg/fleetapi/acker/fleet/fleet_acker_test.go index 4facc9c63ede..3f78a3424689 100644 --- a/x-pack/elastic-agent/pkg/fleetapi/acker/fleet/fleet_acker_test.go +++ b/x-pack/elastic-agent/pkg/fleetapi/acker/fleet/fleet_acker_test.go @@ -22,7 +22,7 @@ import ( "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/fleetapi" ) -func TestAcker(t *testing.T) { +func TestAcker_AckCommit(t *testing.T) { type ackRequest struct { Events []fleetapi.AckEvent `json:"events"` } @@ -69,6 +69,80 @@ func TestAcker(t *testing.T) { } } +func TestAcker_AckBatch(t *testing.T) { + type ackRequest struct { + Events []fleetapi.AckEvent `json:"events"` + } + + log, _ := logger.New("fleet_acker", false) + client := newTestingClient() + agentInfo := &testAgentInfo{} + acker, err := NewAcker(log, agentInfo, client) + if err != nil { + t.Fatal(err) + } + + if acker == nil { + t.Fatal("acker not initialized") + } + + testID1 := "ack-test-action-id-1" + testAction1 := &fleetapi.ActionUnknown{ActionID: testID1} + testID2 := "ack-test-action-id-2" + testAction2 := &fleetapi.ActionUnknown{ActionID: testID2} + + ch := client.Answer(func(headers http.Header, body io.Reader) (*http.Response, error) { + content, err := ioutil.ReadAll(body) + assert.NoError(t, err) + cr := &ackRequest{} + err = json.Unmarshal(content, &cr) + assert.NoError(t, err) + + assert.EqualValues(t, 2, len(cr.Events)) + assert.EqualValues(t, testID1, cr.Events[0].ActionID) + assert.EqualValues(t, testID2, cr.Events[1].ActionID) + + resp := wrapStrToResp(http.StatusOK, `{ "actions": [] }`) + return resp, nil + }) + + go func() { + for range ch { + } + }() + + if err := acker.AckBatch(context.Background(), []fleetapi.Action{testAction1, testAction2}); err != nil { + t.Fatal(err) + } + if err := acker.Commit(context.Background()); err != nil { + t.Fatal(err) + } +} + +func TestAcker_AckBatch_Empty(t *testing.T) { + log, _ := logger.New("fleet_acker", false) + client := newNotCalledClient() + agentInfo := &testAgentInfo{} + acker, err := NewAcker(log, agentInfo, client) + if err != nil { + t.Fatal(err) + } + + if acker == nil { + t.Fatal("acker not initialized") + } + + if err := acker.AckBatch(context.Background(), []fleetapi.Action{}); err != nil { + t.Fatal(err) + } + if err := acker.Commit(context.Background()); err != nil { + t.Fatal(err) + } + if client.called { + t.Fatal("client should not have been used") + } +} + type clientCallbackFunc func(headers http.Header, body io.Reader) (*http.Response, error) type testingClient struct { @@ -122,3 +196,30 @@ func wrapStrToResp(code int, body string) *http.Response { Header: make(http.Header), } } + +type notCalledClient struct { + sync.Mutex + called bool +} + +func (t *notCalledClient) Send( + _ context.Context, + method string, + path string, + params url.Values, + headers http.Header, + body io.Reader, +) (*http.Response, error) { + t.Lock() + defer t.Unlock() + t.called = true + return nil, fmt.Errorf("should not have been called") +} + +func (t *notCalledClient) URI() string { + return "http://localhost" +} + +func newNotCalledClient() *notCalledClient { + return ¬CalledClient{} +}