Skip to content

Commit e660098

Browse files
authored
Include upgrade details in output of elastic-agent status during ongoing upgrade (#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
}

0 commit comments

Comments
 (0)