Skip to content

Commit e660098

Browse files
authoredNov 3, 2023
Include upgrade details in output of elastic-agent status during ongoing upgrade (elastic#3615)
* Remove context and handle cancellation internally instead * More optimizations * Add back context * Adding FSM for upgrades * Implementing TODO * WIP * WIP * Reorganizing imports * Running go mod tidy * Add unit tests * Remove Fleet changes * Fixing booboos introduced during conflict resolution * Add nil guard * Setting logger in test * Adding upgrade details to V2 control protocol * Regenerating protobuf implementation files * Including upgrade details in Agent state * Adding test case * Adding CHANGELOG entry * Newline fixes * Fix data types * Generating protobuf implementations * Running mage update * Add generated protobuf code files to sonar exclusions list * Try removing comments * Include upgrade details in elastic-agent status output --full * Increase test coverage
1 parent 38bdc91 commit e660098

File tree

13 files changed

+740
-268
lines changed

13 files changed

+740
-268
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Kind can be one of:
2+
# - breaking-change: a change to previously-documented behavior
3+
# - deprecation: functionality that is being removed in a later release
4+
# - bug-fix: fixes a problem in a previous version
5+
# - enhancement: extends functionality but does not break or fix existing behavior
6+
# - feature: new functionality
7+
# - known-issue: problems that we are aware of in a given version
8+
# - security: impacts on the security of a product or a user’s deployment.
9+
# - upgrade: important information for someone upgrading from a prior version
10+
# - other: does not fit into any of the other categories
11+
kind: enhancement
12+
13+
# Change summary; a 80ish characters long description of the change.
14+
summary: Include upgrade details in output of `elastic-agent status`.
15+
16+
# Long description; in case the summary is not enough to describe the change
17+
# this field accommodate a description without length limits.
18+
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment.
19+
#description:
20+
21+
# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
22+
component: elastic-agent
23+
24+
# PR URL; optional; the PR number that added the changeset.
25+
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
26+
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
27+
# Please provide it if you are adding a fragment for a different PR.
28+
pr: https://github.com/elastic/elastic-agent/pull/3615
29+
30+
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
31+
# If not present is automatically filled by the tooling with the issue linked to the PR number.
32+
#issue: https://github.com/owner/repo/1234

‎control_v2.proto

+38-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ message StateAgentInfo {
171171
}
172172

173173
// StateResponse is the current state of Elastic Agent.
174-
// Next unused id: 7
174+
// Next unused id: 8
175175
message StateResponse {
176176
// Overall information of Elastic Agent.
177177
StateAgentInfo info = 1;
@@ -188,6 +188,43 @@ message StateResponse {
188188

189189
// State of each component in Elastic Agent.
190190
repeated ComponentState components = 4;
191+
192+
// Upgrade details
193+
UpgradeDetails upgrade_details = 7;
194+
}
195+
196+
// UpgradeDetails captures the details of an ongoing Agent upgrade.
197+
message UpgradeDetails {
198+
// Version the Agent is being upgraded to.
199+
string target_version = 1;
200+
201+
// Current state of the upgrade process.
202+
string state = 2;
203+
204+
// Fleet Action ID that initiated the upgrade, if in managed mode.
205+
string action_id = 3;
206+
207+
// Metadata about the upgrade process.
208+
UpgradeDetailsMetadata metadata = 4;
209+
}
210+
211+
// UpgradeDetailsMetadata has additional information about an Agent's
212+
// ongoing upgrade.
213+
message UpgradeDetailsMetadata {
214+
// If the upgrade is a scheduled upgrade, the timestamp of when the
215+
// upgrade is expected to start.
216+
google.protobuf.Timestamp scheduled_at = 1;
217+
218+
// If the upgrade is in the UPG_DOWNLOADING state, the percentage of
219+
// the Elastic Agent artifact that has already been downloaded, to
220+
// serve as an indicator of download progress.
221+
float download_percent = 2;
222+
223+
// If the upgrade has failed, what upgrade state failed.
224+
string failed_state = 3;
225+
226+
// Any error encountered during the upgrade process.
227+
string error_msg = 4;
191228
}
192229

193230
// DiagnosticFileResult is a file result from a diagnostic result.

‎internal/pkg/agent/application/upgrade/details/details_test.go

-1
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,4 @@ func TestDetailsDownloadRateJSON(t *testing.T) {
9191
require.Equal(t, math.Inf(1), float64(unmarshalledDetails.Metadata.DownloadRate))
9292
require.Equal(t, 0.99, unmarshalledDetails.Metadata.DownloadPercent)
9393
})
94-
9594
}

‎internal/pkg/agent/cmd/status.go

+39
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import (
1313
"sort"
1414
"time"
1515

16+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
1617
"github.com/elastic/elastic-agent/pkg/control/v2/client"
18+
"github.com/elastic/elastic-agent/pkg/control/v2/cproto"
1719

1820
"gopkg.in/yaml.v2"
1921

@@ -145,6 +147,43 @@ func listAgentState(l list.Writer, state *client.AgentState, all bool) {
145147
}
146148
l.UnIndent()
147149
listComponentState(l, state.Components, all)
150+
151+
// Upgrade details
152+
listUpgradeDetails(l, state.UpgradeDetails)
153+
}
154+
155+
func listUpgradeDetails(l list.Writer, upgradeDetails *cproto.UpgradeDetails) {
156+
if upgradeDetails == nil {
157+
return
158+
}
159+
160+
l.AppendItem("upgrade_details")
161+
l.Indent()
162+
l.AppendItem("target_version: " + upgradeDetails.TargetVersion)
163+
l.AppendItem("state: " + upgradeDetails.State)
164+
if upgradeDetails.ActionId != "" {
165+
l.AppendItem("action_id: " + upgradeDetails.ActionId)
166+
}
167+
168+
if upgradeDetails.Metadata != nil {
169+
l.AppendItem("metadata")
170+
l.Indent()
171+
if upgradeDetails.Metadata.ScheduledAt != nil && !upgradeDetails.Metadata.ScheduledAt.AsTime().IsZero() {
172+
l.AppendItem("scheduled_at: " + upgradeDetails.Metadata.ScheduledAt.AsTime().UTC().Format(time.RFC3339))
173+
}
174+
if upgradeDetails.Metadata.FailedState != "" {
175+
l.AppendItem("failed_state: " + upgradeDetails.Metadata.FailedState)
176+
}
177+
if upgradeDetails.Metadata.ErrorMsg != "" {
178+
l.AppendItem("error_msg: " + upgradeDetails.Metadata.ErrorMsg)
179+
}
180+
if upgradeDetails.State == string(details.StateDownloading) {
181+
l.AppendItem(fmt.Sprintf("download_percent: %.2f%%", upgradeDetails.Metadata.DownloadPercent*100))
182+
}
183+
l.UnIndent()
184+
}
185+
186+
l.UnIndent()
148187
}
149188

150189
func listFleetState(l list.Writer, state *client.AgentState, all bool) {

‎internal/pkg/agent/cmd/status_test.go

+83
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@ package cmd
66

77
import (
88
"bytes"
9+
"fmt"
910
"os"
1011
"path/filepath"
1112
"testing"
13+
"time"
14+
15+
"google.golang.org/protobuf/types/known/timestamppb"
16+
17+
"github.com/jedib0t/go-pretty/v6/list"
1218

1319
"github.com/stretchr/testify/require"
1420

1521
"github.com/elastic/elastic-agent/pkg/control/v2/client"
22+
"github.com/elastic/elastic-agent/pkg/control/v2/cproto"
1623
)
1724

1825
func TestHumanOutput(t *testing.T) {
@@ -148,3 +155,79 @@ func TestHumanOutput(t *testing.T) {
148155
require.Equalf(t, string(expected), b.String(), "unexpected input with output: %s, state: %s", test.output, test.state_name)
149156
}
150157
}
158+
159+
func TestListUpgradeDetails(t *testing.T) {
160+
now := time.Now().UTC()
161+
cases := map[string]struct {
162+
upgradeDetails *cproto.UpgradeDetails
163+
expectedOutput string
164+
}{
165+
"no_details": {
166+
upgradeDetails: nil,
167+
expectedOutput: "",
168+
},
169+
"no_metadata": {
170+
upgradeDetails: &cproto.UpgradeDetails{
171+
TargetVersion: "8.12.0",
172+
State: "UPG_REQUESTED",
173+
ActionId: "foobar",
174+
},
175+
expectedOutput: `── upgrade_details
176+
├─ target_version: 8.12.0
177+
├─ state: UPG_REQUESTED
178+
└─ action_id: foobar`,
179+
},
180+
"no_action_id": {
181+
upgradeDetails: &cproto.UpgradeDetails{
182+
TargetVersion: "8.12.0",
183+
State: "UPG_REQUESTED",
184+
},
185+
expectedOutput: `── upgrade_details
186+
├─ target_version: 8.12.0
187+
└─ state: UPG_REQUESTED`,
188+
},
189+
"no_scheduled_at": {
190+
upgradeDetails: &cproto.UpgradeDetails{
191+
TargetVersion: "8.12.0",
192+
State: "UPG_FAILED",
193+
Metadata: &cproto.UpgradeDetailsMetadata{
194+
FailedState: "UPG_DOWNLOADING",
195+
ErrorMsg: "error downloading",
196+
DownloadPercent: 0.104,
197+
},
198+
},
199+
expectedOutput: `── upgrade_details
200+
├─ target_version: 8.12.0
201+
├─ state: UPG_FAILED
202+
└─ metadata
203+
├─ failed_state: UPG_DOWNLOADING
204+
└─ error_msg: error downloading`,
205+
},
206+
"no_failed_state": {
207+
upgradeDetails: &cproto.UpgradeDetails{
208+
TargetVersion: "8.12.0",
209+
State: "UPG_DOWNLOADING",
210+
Metadata: &cproto.UpgradeDetailsMetadata{
211+
ScheduledAt: timestamppb.New(now),
212+
DownloadPercent: 0.17679,
213+
},
214+
},
215+
expectedOutput: fmt.Sprintf(`── upgrade_details
216+
├─ target_version: 8.12.0
217+
├─ state: UPG_DOWNLOADING
218+
└─ metadata
219+
├─ scheduled_at: %s
220+
└─ download_percent: 17.68%%`, now.Format(time.RFC3339)),
221+
}}
222+
223+
for name, test := range cases {
224+
t.Run(name, func(t *testing.T) {
225+
l := list.NewWriter()
226+
l.SetStyle(list.StyleConnectedLight)
227+
228+
listUpgradeDetails(l, test.upgradeDetails)
229+
actualOutput := l.Render()
230+
require.Equal(t, test.expectedOutput, actualOutput)
231+
})
232+
}
233+
}

‎pkg/control/v1/proto/control_v1.pb.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pkg/control/v1/proto/control_v1_grpc.pb.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pkg/control/v2/client/client.go

+12-10
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,13 @@ type AgentStateInfo struct {
111111

112112
// AgentState is the current state of the Elastic Agent.
113113
type AgentState struct {
114-
Info AgentStateInfo `json:"info" yaml:"info"`
115-
State State `json:"state" yaml:"state"`
116-
Message string `json:"message" yaml:"message"`
117-
Components []ComponentState `json:"components" yaml:"components"`
118-
FleetState State `yaml:"fleet_state"`
119-
FleetMessage string `yaml:"fleet_message"`
114+
Info AgentStateInfo `json:"info" yaml:"info"`
115+
State State `json:"state" yaml:"state"`
116+
Message string `json:"message" yaml:"message"`
117+
Components []ComponentState `json:"components" yaml:"components"`
118+
FleetState State `yaml:"fleet_state"`
119+
FleetMessage string `yaml:"fleet_message"`
120+
UpgradeDetails *cproto.UpgradeDetails `json:"upgrade_details,omitempty" yaml:"upgrade_details,omitempty"`
120121
}
121122

122123
// DiagnosticFileResult is a diagnostic file result.
@@ -475,10 +476,11 @@ func toState(res *cproto.StateResponse) (*AgentState, error) {
475476
Snapshot: res.Info.Snapshot,
476477
PID: res.Info.Pid,
477478
},
478-
State: res.State,
479-
Message: res.Message,
480-
FleetState: res.FleetState,
481-
FleetMessage: res.FleetMessage,
479+
State: res.State,
480+
Message: res.Message,
481+
FleetState: res.FleetState,
482+
FleetMessage: res.FleetMessage,
483+
UpgradeDetails: res.UpgradeDetails,
482484

483485
Components: make([]ComponentState, 0, len(res.Components)),
484486
}

‎pkg/control/v2/cproto/control_v2.pb.go

+462-241
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pkg/control/v2/cproto/control_v2_grpc.pb.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pkg/control/v2/server/server.go

+22-5
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,22 @@ func stateToProto(state *coordinator.State, agentInfo *info.AgentInfo) (*cproto.
362362
},
363363
})
364364
}
365+
366+
var upgradeDetails *cproto.UpgradeDetails
367+
if state.UpgradeDetails != nil {
368+
upgradeDetails = &cproto.UpgradeDetails{
369+
TargetVersion: state.UpgradeDetails.TargetVersion,
370+
State: string(state.UpgradeDetails.State),
371+
ActionId: state.UpgradeDetails.ActionID,
372+
Metadata: &cproto.UpgradeDetailsMetadata{
373+
ScheduledAt: timestamppb.New(state.UpgradeDetails.Metadata.ScheduledAt),
374+
DownloadPercent: float32(state.UpgradeDetails.Metadata.DownloadPercent),
375+
FailedState: string(state.UpgradeDetails.Metadata.FailedState),
376+
ErrorMsg: state.UpgradeDetails.Metadata.ErrorMsg,
377+
},
378+
}
379+
}
380+
365381
return &cproto.StateResponse{
366382
Info: &cproto.StateAgentInfo{
367383
Id: agentInfo.AgentID(),
@@ -371,10 +387,11 @@ func stateToProto(state *coordinator.State, agentInfo *info.AgentInfo) (*cproto.
371387
Snapshot: release.Snapshot(),
372388
Pid: int32(os.Getpid()),
373389
},
374-
State: state.State,
375-
Message: state.Message,
376-
FleetState: state.FleetState,
377-
FleetMessage: state.FleetMessage,
378-
Components: components,
390+
State: state.State,
391+
Message: state.Message,
392+
FleetState: state.FleetState,
393+
FleetMessage: state.FleetMessage,
394+
Components: components,
395+
UpgradeDetails: upgradeDetails,
379396
}, nil
380397
}

‎pkg/control/v2/server/server_test.go

+45-6
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ package server
77
import (
88
"testing"
99

10+
"google.golang.org/protobuf/types/known/timestamppb"
11+
1012
"github.com/stretchr/testify/assert"
1113
"github.com/stretchr/testify/require"
1214

1315
"github.com/elastic/elastic-agent-client/v7/pkg/client"
1416
"github.com/elastic/elastic-agent-libs/logp"
1517
"github.com/elastic/elastic-agent/internal/pkg/agent/application/coordinator"
1618
"github.com/elastic/elastic-agent/internal/pkg/agent/application/info"
19+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
1720
"github.com/elastic/elastic-agent/pkg/component"
1821
"github.com/elastic/elastic-agent/pkg/component/runtime"
1922
"github.com/elastic/elastic-agent/pkg/control/v2/cproto"
@@ -22,11 +25,12 @@ import (
2225
func TestStateMapping(t *testing.T) {
2326

2427
testcases := []struct {
25-
name string
26-
agentState cproto.State
27-
agentMessage string
28-
fleetState cproto.State
29-
fleetMessage string
28+
name string
29+
agentState cproto.State
30+
agentMessage string
31+
fleetState cproto.State
32+
fleetMessage string
33+
upgradeDetails *details.Details
3034
}{
3135
{
3236
name: "waiting first checkin response",
@@ -49,6 +53,21 @@ func TestStateMapping(t *testing.T) {
4953
fleetState: cproto.State_FAILED,
5054
fleetMessage: "<error value coming from fleet gateway>",
5155
},
56+
{
57+
name: "with upgrade details",
58+
agentState: cproto.State_UPGRADING,
59+
agentMessage: "Upgrading to version 8.13.0",
60+
fleetState: cproto.State_STOPPED,
61+
fleetMessage: "Not enrolled into Fleet",
62+
upgradeDetails: &details.Details{
63+
TargetVersion: "8.13.0",
64+
State: details.StateDownloading,
65+
ActionID: "",
66+
Metadata: details.Metadata{
67+
DownloadPercent: 1.7,
68+
},
69+
},
70+
},
5271
}
5372

5473
for _, tc := range testcases {
@@ -101,6 +120,15 @@ func TestStateMapping(t *testing.T) {
101120
},
102121
}
103122

123+
if tc.upgradeDetails != nil {
124+
inputState.UpgradeDetails = &details.Details{
125+
TargetVersion: tc.upgradeDetails.TargetVersion,
126+
State: tc.upgradeDetails.State,
127+
ActionID: tc.upgradeDetails.ActionID,
128+
Metadata: tc.upgradeDetails.Metadata,
129+
}
130+
}
131+
104132
agentInfo := new(info.AgentInfo)
105133

106134
stateResponse, err := stateToProto(inputState, agentInfo)
@@ -134,7 +162,18 @@ func TestStateMapping(t *testing.T) {
134162
assert.Equal(t, expectedCompState, stateResponse.Components[0])
135163
}
136164

165+
if tc.upgradeDetails != nil {
166+
expectedMetadata := &cproto.UpgradeDetailsMetadata{
167+
ScheduledAt: timestamppb.New(tc.upgradeDetails.Metadata.ScheduledAt),
168+
DownloadPercent: float32(tc.upgradeDetails.Metadata.DownloadPercent),
169+
FailedState: string(tc.upgradeDetails.Metadata.FailedState),
170+
ErrorMsg: tc.upgradeDetails.Metadata.ErrorMsg,
171+
}
172+
assert.Equal(t, string(tc.upgradeDetails.State), stateResponse.UpgradeDetails.State)
173+
assert.Equal(t, tc.upgradeDetails.TargetVersion, stateResponse.UpgradeDetails.TargetVersion)
174+
assert.Equal(t, tc.upgradeDetails.ActionID, stateResponse.UpgradeDetails.ActionId)
175+
assert.Equal(t, expectedMetadata, stateResponse.UpgradeDetails.Metadata)
176+
}
137177
})
138178
}
139-
140179
}

‎sonar-project.properties

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ sonar.projectKey=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ
22
sonar.host.url=https://sonar.elastic.dev
33

44
sonar.sources=.
5-
sonar.exclusions=**/*_test.go, .git/**, dev-tools/**, /magefile.go, changelog/**, _meta/**, deploy/**, docs/**, img/**, specs/**, pkg/testing/**, pkg/component/fake/**, testing/**, **/mocks/*.go
5+
sonar.exclusions=.git/**, dev-tools/**, /magefile.go, changelog/**, \
6+
_meta/**, deploy/**, docs/**, img/**, specs/**, \
7+
*/*_test.go, pkg/testing/**, pkg/component/fake/**, testing/**, **/mocks/*.go, \
8+
pkg/control/v1/proto/*.pb.go, pkg/control/v2/cproto/*.pb.go
69
sonar.tests=.
710
sonar.test.inclusions=**/*_test.go
811

0 commit comments

Comments
 (0)
Please sign in to comment.