Skip to content

Commit 8e685fc

Browse files
ci: add integration tests for scheduled upgrade details
1 parent 180ee49 commit 8e685fc

File tree

4 files changed

+201
-5
lines changed

4 files changed

+201
-5
lines changed

testing/fleetservertest/ackableactions.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import (
1111
)
1212

1313
type ActionTmpl struct {
14-
AgentID string
15-
ActionID string
16-
Data string
17-
Type string
14+
AgentID string
15+
ActionID string
16+
Data string
17+
Type string
18+
StartTime string
1819
}
1920

2021
type CheckinData struct {

testing/fleetservertest/checkin.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ const actionTemplate = `{
8787
"data": {{.Data}},
8888
"id": "{{.ActionID}}",
8989
"input_type": "",
90-
"type": "{{.Type}}"
90+
"type": "{{.Type}}",
91+
"start_time": "{{.StartTime}}"
9192
}`
9293

9394
func NewAction(data ActionTmpl) (AckableAction, error) {

testing/fleetservertest/models.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"encoding/json"
99
"fmt"
1010
"net/http"
11+
12+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
1113
)
1214

1315
// =============================================================================
@@ -117,6 +119,8 @@ type CheckinRequest struct {
117119

118120
// An optional timeout value that informs fleet-server of when a client will time out on it's checkin request. If not specified fleet-server will use the timeout values specified in the config (defaults to 5m polling and a 10m write timeout). The value, if specified is expected to be a string that is parsable by [time.ParseDuration](https://pkg.go.dev/time#ParseDuration). If specified fleet-server will set its poll timeout to `max(1m, poll_timeout-2m)` and its write timeout to `max(2m, poll_timout-1m)`.
119121
PollTimeout string `json:"poll_timeout,omitempty"`
122+
123+
UpgradeDetails *details.Details `json:"upgrade_details,omitempty"`
120124
}
121125
type CheckinResponse struct {
122126

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
// you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
//go:build integration
6+
7+
package ess
8+
9+
import (
10+
"context"
11+
"encoding/json"
12+
"fmt"
13+
"net/http"
14+
"sync"
15+
"testing"
16+
"time"
17+
18+
"github.com/google/uuid"
19+
"github.com/stretchr/testify/assert"
20+
"github.com/stretchr/testify/require"
21+
22+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
23+
"github.com/elastic/elastic-agent/internal/pkg/fleetapi"
24+
integrationtest "github.com/elastic/elastic-agent/pkg/testing"
25+
"github.com/elastic/elastic-agent/pkg/testing/define"
26+
"github.com/elastic/elastic-agent/pkg/testing/tools/check"
27+
"github.com/elastic/elastic-agent/pkg/testing/tools/testcontext"
28+
"github.com/elastic/elastic-agent/testing/fleetservertest"
29+
"github.com/elastic/elastic-agent/testing/integration"
30+
)
31+
32+
func TestFleetScheduledUpgrade(t *testing.T) {
33+
_ = define.Require(t, define.Requirements{
34+
Group: integration.Fleet,
35+
Stack: &define.Stack{},
36+
Local: false, // requires Agent installation
37+
Sudo: true, // requires Agent installation
38+
})
39+
40+
ctx, cancel := testcontext.WithTimeout(t, t.Context(), time.Minute*10)
41+
defer cancel()
42+
43+
apiKey, policy := createBasicFleetPolicyData(t, "http://fleet-server:8221")
44+
checkinWithAcker := fleetservertest.NewCheckinActionsWithAcker()
45+
nextActionGenerator := checkinWithAcker.ActionsGenerator()
46+
47+
var checkInRequest struct {
48+
sync.Mutex
49+
updatedTime time.Time
50+
UpgradeDetails *details.Details
51+
}
52+
53+
handlers := &fleetservertest.Handlers{
54+
APIKey: apiKey.Key,
55+
EnrollmentToken: "enrollmentToken",
56+
AgentID: policy.AgentID, // as there is no enroll, the agentID needs to be manually set
57+
CheckinFn: func(ctx context.Context, h *fleetservertest.Handlers, id string, userAgent string,
58+
acceptEncoding string, checkinRequest fleetservertest.CheckinRequest,
59+
) (*fleetservertest.CheckinResponse, *fleetservertest.HTTPError) {
60+
if id != policy.AgentID {
61+
return nil, &fleetservertest.HTTPError{
62+
StatusCode: http.StatusNotFound,
63+
Message: fmt.Sprintf("agent %q not found", id),
64+
}
65+
}
66+
67+
checkInRequest.Lock()
68+
checkInRequest.updatedTime = time.Now()
69+
checkInRequest.UpgradeDetails = checkinRequest.UpgradeDetails
70+
checkInRequest.Unlock()
71+
72+
data, hErr := nextActionGenerator()
73+
if hErr != nil {
74+
return nil, hErr
75+
}
76+
77+
respStr := fleetservertest.NewCheckinResponse(data.AckToken, data.Actions...)
78+
resp := fleetservertest.CheckinResponse{}
79+
err := json.Unmarshal(
80+
[]byte(respStr),
81+
&resp)
82+
if err != nil {
83+
return nil, &fleetservertest.HTTPError{
84+
StatusCode: http.StatusInternalServerError,
85+
Message: fmt.Sprintf("failed to CheckinResponse: %v", err),
86+
}
87+
}
88+
89+
// simulate long poll
90+
time.Sleep(data.Delay)
91+
92+
return &resp, nil
93+
},
94+
EnrollFn: fleetservertest.NewHandlerEnroll(policy.AgentID, policy.PolicyID, apiKey),
95+
AckFn: fleetservertest.NewHandlerAckWithAcker(checkinWithAcker.Acker()),
96+
StatusFn: fleetservertest.NewHandlerStatusHealthy(),
97+
}
98+
99+
fleetServer := fleetservertest.NewServer(handlers, fleetservertest.WithRequestLog(t.Logf))
100+
defer fleetServer.Close()
101+
102+
fixture, err := define.NewFixtureFromLocalBuild(t,
103+
define.Version(),
104+
integrationtest.WithAllowErrors(),
105+
integrationtest.WithLogOutput())
106+
require.NoError(t, err, "SetupTest: NewFixtureFromLocalBuild failed")
107+
err = fixture.EnsurePrepared(ctx)
108+
require.NoError(t, err, "SetupTest: fixture.Prepare failed")
109+
110+
out, err := fixture.Install(
111+
ctx,
112+
&integrationtest.InstallOpts{
113+
Force: true,
114+
NonInteractive: true,
115+
Insecure: true,
116+
Privileged: false,
117+
EnrollOpts: integrationtest.EnrollOpts{
118+
URL: fleetServer.LocalhostURL,
119+
EnrollmentToken: "anythingWillDO",
120+
}})
121+
require.NoErrorf(t, err, "Error when installing agent, output: %s", out)
122+
123+
// Wait for the agent to connect to Fleet and report HEALTHY
124+
check.ConnectedToFleet(ctx, t, fixture, 5*time.Minute)
125+
126+
// Simulate a scheduled upgrade action
127+
targetVersion := "255.0.0"
128+
scheduledActionUUID := uuid.New().String()
129+
scheduledUpgradeAction, err := fleetservertest.NewAction(fleetservertest.ActionTmpl{
130+
AgentID: policy.AgentID,
131+
ActionID: scheduledActionUUID,
132+
Type: fleetapi.ActionTypeUpgrade,
133+
Data: fmt.Sprintf(`{"version": "%s"}`, targetVersion),
134+
StartTime: time.Now().Add(time.Hour).Format(time.RFC3339),
135+
})
136+
require.NoError(t, err, "failed to create scheduled upgrade action")
137+
checkinWithAcker.AddCheckin("token", 1*time.Second, scheduledUpgradeAction)
138+
139+
// Wait and check that elastic-agent has reported the scheduled upgrade
140+
// in the upgrade details
141+
require.EventuallyWithT(t, func(collect *assert.CollectT) {
142+
checkInRequest.Lock()
143+
defer checkInRequest.Unlock()
144+
if !assert.NotNil(collect, checkInRequest.UpgradeDetails) {
145+
return
146+
}
147+
assert.Equal(collect, targetVersion, checkInRequest.UpgradeDetails.TargetVersion)
148+
assert.EqualValues(collect, details.StateScheduled, checkInRequest.UpgradeDetails.State)
149+
assert.Equal(collect, scheduledActionUUID, checkInRequest.UpgradeDetails.ActionID)
150+
}, 5*time.Minute, 500*time.Millisecond, "agent did not report scheduled upgrade")
151+
152+
// Deliberately restart elastic-agent to check that it still reports
153+
// correctly the scheduled upgrade details
154+
restartAgentNTimes(t, 1, 0)
155+
156+
// Wait and check that elastic-agent has a more recent checkin with
157+
// the correct upgrade details
158+
timeSnapshot := time.Now()
159+
require.EventuallyWithT(t, func(collect *assert.CollectT) {
160+
checkInRequest.Lock()
161+
defer checkInRequest.Unlock()
162+
if !assert.NotNil(collect, checkInRequest.UpgradeDetails) {
163+
return
164+
}
165+
assert.Less(collect, timeSnapshot, checkInRequest.updatedTime)
166+
assert.Equal(collect, targetVersion, checkInRequest.UpgradeDetails.TargetVersion)
167+
assert.EqualValues(collect, details.StateScheduled, checkInRequest.UpgradeDetails.State)
168+
assert.Equal(collect, scheduledActionUUID, checkInRequest.UpgradeDetails.ActionID)
169+
}, 5*time.Minute, 500*time.Millisecond, "agent did not report scheduled upgrade after restart")
170+
171+
// Simulate a cancel action of the scheduled upgrade
172+
cancelActionUUID := uuid.New().String()
173+
cancelAction, err := fleetservertest.NewAction(fleetservertest.ActionTmpl{
174+
AgentID: policy.AgentID,
175+
Type: fleetapi.ActionTypeCancel,
176+
ActionID: cancelActionUUID,
177+
Data: fmt.Sprintf(`{"target_id": "%s"}`, scheduledActionUUID),
178+
})
179+
checkinWithAcker.AddCheckin("token", 1*time.Second, cancelAction)
180+
181+
// Wait and check that elastic-agent has reported a more recent checkin
182+
// with empty upgrade details
183+
timeSnapshot = time.Now()
184+
require.EventuallyWithT(t, func(collect *assert.CollectT) {
185+
checkInRequest.Lock()
186+
defer checkInRequest.Unlock()
187+
assert.Less(collect, timeSnapshot, checkInRequest.updatedTime)
188+
assert.Nil(collect, checkInRequest.UpgradeDetails)
189+
}, 5*time.Minute, 500*time.Millisecond, "agent did not report empty upgrade details after cancel")
190+
}

0 commit comments

Comments
 (0)