feat(history): add rollback revision column to helm history output

Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com>
pull/31859/head
MrJack 5 months ago
parent 3ad9ccdb19
commit 300f71b1eb

@ -36,6 +36,8 @@ type Info struct {
Description string `json:"description,omitempty"`
// Status is the current state of the release
Status common.Status `json:"status,omitempty"`
// RollbackRevision is the revision that was rolled back to. Zero means not a rollback.
RollbackRevision int `json:"rollback_revision,omitempty"`
// Contains the rendered templates/NOTES.txt if available
Notes string `json:"notes,omitempty"`
// Contains the deployed resources information
@ -44,13 +46,14 @@ type Info struct {
// infoJSON is used for custom JSON marshaling/unmarshaling
type infoJSON struct {
FirstDeployed *time.Time `json:"first_deployed,omitempty"`
LastDeployed *time.Time `json:"last_deployed,omitempty"`
Deleted *time.Time `json:"deleted,omitempty"`
Description string `json:"description,omitempty"`
Status common.Status `json:"status,omitempty"`
Notes string `json:"notes,omitempty"`
Resources map[string][]runtime.Object `json:"resources,omitempty"`
FirstDeployed *time.Time `json:"first_deployed,omitempty"`
LastDeployed *time.Time `json:"last_deployed,omitempty"`
Deleted *time.Time `json:"deleted,omitempty"`
Description string `json:"description,omitempty"`
Status common.Status `json:"status,omitempty"`
RollbackRevision int `json:"rollback_revision,omitempty"`
Notes string `json:"notes,omitempty"`
Resources map[string][]runtime.Object `json:"resources,omitempty"`
}
// UnmarshalJSON implements the json.Unmarshaler interface.
@ -95,6 +98,7 @@ func (i *Info) UnmarshalJSON(data []byte) error {
}
i.Description = tmp.Description
i.Status = tmp.Status
i.RollbackRevision = tmp.RollbackRevision
i.Notes = tmp.Notes
i.Resources = tmp.Resources
@ -105,10 +109,11 @@ func (i *Info) UnmarshalJSON(data []byte) error {
// It omits zero-value time fields from the JSON output.
func (i Info) MarshalJSON() ([]byte, error) {
tmp := infoJSON{
Description: i.Description,
Status: i.Status,
Notes: i.Notes,
Resources: i.Resources,
Description: i.Description,
Status: i.Status,
RollbackRevision: i.RollbackRevision,
Notes: i.Notes,
Resources: i.Resources,
}
if !i.FirstDeployed.IsZero() {

@ -176,10 +176,11 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
Chart: previousRelease.Chart,
Config: previousRelease.Config,
Info: &release.Info{
FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: time.Now(),
Status: common.StatusPendingRollback,
Notes: previousRelease.Info.Notes,
FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: time.Now(),
Status: common.StatusPendingRollback,
Notes: previousRelease.Info.Notes,
RollbackRevision: previousVersion,
// Because we lose the reference to previous version elsewhere, we set the
// message here, and only override it later if we experience failure.
Description: fmt.Sprintf("Rollback to %d", previousVersion),

@ -83,3 +83,51 @@ func TestRollback_WaitOptionsPassedDownstream(t *testing.T) {
// Verify that WaitOptions were passed to GetWaiter
is.NotEmpty(failer.RecordedWaitOptions, "WaitOptions should be passed to GetWaiter")
}
func TestRollbackSetsRollbackRevision(t *testing.T) {
config := actionConfigFixture(t)
rel1 := releaseStub()
rel1.Name = "rollback-rev-test"
rel1.Version = 1
rel1.Info.Status = "superseded"
rel1.ApplyMethod = "csa"
require.NoError(t, config.Releases.Create(rel1))
rel2 := releaseStub()
rel2.Name = "rollback-rev-test"
rel2.Version = 2
rel2.Info.Status = "deployed"
rel2.ApplyMethod = "csa"
require.NoError(t, config.Releases.Create(rel2))
client := NewRollback(config)
client.Version = 1
client.ServerSideApply = "auto"
require.NoError(t, client.Run("rollback-rev-test"))
reli, err := config.Releases.Get("rollback-rev-test", 3)
require.NoError(t, err)
rel, err := releaserToV1Release(reli)
require.NoError(t, err)
assert.Equal(t, 1, rel.Info.RollbackRevision)
assert.Equal(t, "Rollback to 1", rel.Info.Description)
}
func TestRollbackRevisionZeroForNonRollback(t *testing.T) {
config := actionConfigFixture(t)
rel := releaseStub()
rel.Name = "non-rollback"
rel.Info.Status = "deployed"
require.NoError(t, config.Releases.Create(rel))
reli, err := config.Releases.Get("non-rollback", 1)
require.NoError(t, err)
r, err := releaserToV1Release(reli)
require.NoError(t, err)
assert.Equal(t, 0, r.Info.RollbackRevision)
}

@ -43,11 +43,11 @@ configures the maximum length of the revision list returned.
The historical release set is printed as a formatted table, e.g:
$ helm history angry-bird
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install
2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully
3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Rolled back to 2
4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully
REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION
1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install
2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully
3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 2 Rolled back to 2
4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully
`
func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
@ -84,22 +84,24 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
type releaseInfo struct {
Revision int `json:"revision"`
Updated time.Time `json:"updated,omitzero"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
Revision int `json:"revision"`
Updated time.Time `json:"updated,omitzero"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
RollbackRevision int `json:"rollback_revision,omitempty"`
Description string `json:"description"`
}
// releaseInfoJSON is used for custom JSON marshaling/unmarshaling
type releaseInfoJSON struct {
Revision int `json:"revision"`
Updated *time.Time `json:"updated,omitempty"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
Revision int `json:"revision"`
Updated *time.Time `json:"updated,omitempty"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
RollbackRevision int `json:"rollback_revision,omitempty"`
Description string `json:"description"`
}
// UnmarshalJSON implements the json.Unmarshaler interface.
@ -138,6 +140,7 @@ func (r *releaseInfo) UnmarshalJSON(data []byte) error {
r.Status = tmp.Status
r.Chart = tmp.Chart
r.AppVersion = tmp.AppVersion
r.RollbackRevision = tmp.RollbackRevision
r.Description = tmp.Description
return nil
@ -147,11 +150,12 @@ func (r *releaseInfo) UnmarshalJSON(data []byte) error {
// It omits zero-value time fields from the JSON output.
func (r releaseInfo) MarshalJSON() ([]byte, error) {
tmp := releaseInfoJSON{
Revision: r.Revision,
Status: r.Status,
Chart: r.Chart,
AppVersion: r.AppVersion,
Description: r.Description,
Revision: r.Revision,
Status: r.Status,
Chart: r.Chart,
AppVersion: r.AppVersion,
RollbackRevision: r.RollbackRevision,
Description: r.Description,
}
if !r.Updated.IsZero() {
@ -173,9 +177,13 @@ func (r releaseHistory) WriteYAML(out io.Writer) error {
func (r releaseHistory) WriteTable(out io.Writer) error {
tbl := uitable.New()
tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION")
tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "ROLLBACK", "DESCRIPTION")
for _, item := range r {
tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, item.Description)
rollback := ""
if item.RollbackRevision > 0 {
rollback = strconv.Itoa(item.RollbackRevision)
}
tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, rollback, item.Description)
}
return output.EncodeTable(out, tbl)
}
@ -216,11 +224,12 @@ func getReleaseHistory(rls []*release.Release) (history releaseHistory) {
a := formatAppVersion(r.Chart)
rInfo := releaseInfo{
Revision: v,
Status: s,
Chart: c,
AppVersion: a,
Description: d,
Revision: v,
Status: s,
Chart: c,
AppVersion: a,
RollbackRevision: r.Info.RollbackRevision,
Description: d,
}
if !r.Info.LastDeployed.IsZero() {
rInfo.Updated = r.Info.LastDeployed

@ -27,6 +27,8 @@ import (
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
func TestHistoryCmd(t *testing.T) {
@ -76,6 +78,72 @@ func TestHistoryCmd(t *testing.T) {
runTestCmd(t, tests)
}
func TestHistoryWithRollback(t *testing.T) {
date := time.Unix(242085845, 0).UTC()
ch := &chart.Chart{
Metadata: &chart.Metadata{
Name: "foo",
Version: "0.1.0-beta.1",
AppVersion: "1.0",
},
}
rels := []*release.Release{
{
Name: "angry-bird",
Version: 1,
Info: &release.Info{
FirstDeployed: date,
LastDeployed: date,
Status: common.StatusSuperseded,
Description: "Install complete",
},
Chart: ch,
},
{
Name: "angry-bird",
Version: 2,
Info: &release.Info{
FirstDeployed: date,
LastDeployed: date,
Status: common.StatusSuperseded,
Description: "Upgrade complete",
},
Chart: ch,
},
{
Name: "angry-bird",
Version: 3,
Info: &release.Info{
FirstDeployed: date,
LastDeployed: date,
Status: common.StatusDeployed,
RollbackRevision: 1,
Description: "Rollback to 1",
},
Chart: ch,
},
}
tests := []cmdTestCase{{
name: "history with rollback revision",
cmd: "history angry-bird",
rels: rels,
golden: "output/history-with-rollback.txt",
}, {
name: "history with rollback revision json",
cmd: "history angry-bird --output json",
rels: rels,
golden: "output/history-with-rollback.json",
}, {
name: "history with rollback revision yaml",
cmd: "history angry-bird --output yaml",
rels: rels,
golden: "output/history-with-rollback.yaml",
}}
runTestCmd(t, tests)
}
func TestHistoryOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "history")
}
@ -173,6 +241,31 @@ func TestReleaseInfoMarshalJSON(t *testing.T) {
},
expected: `{"revision":0,"updated":"2025-10-08T12:00:00Z","status":"failed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Install failed"}`,
},
{
name: "with rollback revision",
info: releaseInfo{
Revision: 3,
Updated: updated,
Status: "deployed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
RollbackRevision: 1,
Description: "Rollback to 1",
},
expected: `{"revision":3,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","rollback_revision":1,"description":"Rollback to 1"}`,
},
{
name: "without rollback revision",
info: releaseInfo{
Revision: 1,
Updated: updated,
Status: "deployed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
Description: "Initial install",
},
expected: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Initial install"}`,
},
}
for _, tt := range tests {
@ -255,6 +348,31 @@ func TestReleaseInfoUnmarshalJSON(t *testing.T) {
Description: "Installing",
},
},
{
name: "with rollback revision",
input: `{"revision":3,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","rollback_revision":1,"description":"Rollback to 1"}`,
expected: releaseInfo{
Revision: 3,
Updated: updated,
Status: "deployed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
RollbackRevision: 1,
Description: "Rollback to 1",
},
},
{
name: "without rollback revision field",
input: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Install"}`,
expected: releaseInfo{
Revision: 1,
Updated: updated,
Status: "deployed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
Description: "Install",
},
},
}
for _, tt := range tests {
@ -271,6 +389,7 @@ func TestReleaseInfoUnmarshalJSON(t *testing.T) {
assert.Equal(t, tt.expected.Status, info.Status)
assert.Equal(t, tt.expected.Chart, info.Chart)
assert.Equal(t, tt.expected.AppVersion, info.AppVersion)
assert.Equal(t, tt.expected.RollbackRevision, info.RollbackRevision)
assert.Equal(t, tt.expected.Description, info.Description)
})
}
@ -300,6 +419,7 @@ func TestReleaseInfoRoundTrip(t *testing.T) {
assert.Equal(t, original.Status, decoded.Status)
assert.Equal(t, original.Chart, decoded.Chart)
assert.Equal(t, original.AppVersion, decoded.AppVersion)
assert.Equal(t, original.RollbackRevision, decoded.RollbackRevision)
assert.Equal(t, original.Description, decoded.Description)
}

@ -1,3 +1,3 @@
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock
4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock
REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION
3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock
4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock

@ -1,5 +1,5 @@
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock
2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock
3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock
4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock
REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION
1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock
2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock
3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock
4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock

@ -36,6 +36,8 @@ type Info struct {
Description string `json:"description,omitempty"`
// Status is the current state of the release
Status common.Status `json:"status,omitempty"`
// RollbackRevision is the revision that was rolled back to. Zero means not a rollback.
RollbackRevision int `json:"rollback_revision,omitempty"`
// Contains the rendered templates/NOTES.txt if available
Notes string `json:"notes,omitempty"`
// Contains the deployed resources information
@ -44,13 +46,14 @@ type Info struct {
// infoJSON is used for custom JSON marshaling/unmarshaling
type infoJSON struct {
FirstDeployed *time.Time `json:"first_deployed,omitempty"`
LastDeployed *time.Time `json:"last_deployed,omitempty"`
Deleted *time.Time `json:"deleted,omitempty"`
Description string `json:"description,omitempty"`
Status common.Status `json:"status,omitempty"`
Notes string `json:"notes,omitempty"`
Resources map[string][]runtime.Object `json:"resources,omitempty"`
FirstDeployed *time.Time `json:"first_deployed,omitempty"`
LastDeployed *time.Time `json:"last_deployed,omitempty"`
Deleted *time.Time `json:"deleted,omitempty"`
Description string `json:"description,omitempty"`
Status common.Status `json:"status,omitempty"`
RollbackRevision int `json:"rollback_revision,omitempty"`
Notes string `json:"notes,omitempty"`
Resources map[string][]runtime.Object `json:"resources,omitempty"`
}
// UnmarshalJSON implements the json.Unmarshaler interface.
@ -95,6 +98,7 @@ func (i *Info) UnmarshalJSON(data []byte) error {
}
i.Description = tmp.Description
i.Status = tmp.Status
i.RollbackRevision = tmp.RollbackRevision
i.Notes = tmp.Notes
i.Resources = tmp.Resources
@ -105,10 +109,11 @@ func (i *Info) UnmarshalJSON(data []byte) error {
// It omits zero-value time fields from the JSON output.
func (i Info) MarshalJSON() ([]byte, error) {
tmp := infoJSON{
Description: i.Description,
Status: i.Status,
Notes: i.Notes,
Resources: i.Resources,
Description: i.Description,
Status: i.Status,
RollbackRevision: i.RollbackRevision,
Notes: i.Notes,
Resources: i.Resources,
}
if !i.FirstDeployed.IsZero() {

Loading…
Cancel
Save