diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index fc4a3a879..934f14f86 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -22,7 +22,6 @@ import ( "os" "strings" "testing" - "time" shellwords "github.com/mattn/go-shellwords" "github.com/spf13/cobra" @@ -34,6 +33,7 @@ import ( "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage" "helm.sh/helm/v3/pkg/storage/driver" + "helm.sh/helm/v3/pkg/time" ) func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() } diff --git a/cmd/helm/history.go b/cmd/helm/history.go index c4a74bb2c..99f3444eb 100644 --- a/cmd/helm/history.go +++ b/cmd/helm/history.go @@ -30,6 +30,7 @@ import ( "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" + helmtime "helm.sh/helm/v3/pkg/time" ) var historyHelp = ` @@ -76,12 +77,12 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } type releaseInfo struct { - Revision int `json:"revision"` - Updated time.Time `json:"updated"` - Status string `json:"status"` - Chart string `json:"chart"` - AppVersion string `json:"app_version"` - Description string `json:"description"` + Revision int `json:"revision"` + Updated helmtime.Time `json:"updated"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` + Description string `json:"description"` } type releaseHistory []releaseInfo diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index 2de7c5dd2..b5833fd72 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -18,10 +18,10 @@ package main import ( "testing" - "time" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/time" ) func TestListCmd(t *testing.T) { diff --git a/cmd/helm/status_test.go b/cmd/helm/status_test.go index 198063220..91a008e5a 100644 --- a/cmd/helm/status_test.go +++ b/cmd/helm/status_test.go @@ -22,11 +22,12 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" ) func TestStatusCmd(t *testing.T) { releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { - info.LastDeployed = time.Unix(1452902400, 0).UTC() + info.LastDeployed = helmtime.Unix(1452902400, 0).UTC() return []*release.Release{{ Name: "flummoxed-chickadee", Namespace: "default", @@ -103,7 +104,7 @@ func TestStatusCmd(t *testing.T) { runTestCmd(t, tests) } -func mustParseTime(t string) time.Time { - res, _ := time.Parse(time.RFC3339, t) +func mustParseTime(t string) helmtime.Time { + res, _ := helmtime.Parse(time.RFC3339, t) return res } diff --git a/cmd/helm/testdata/output/status.json b/cmd/helm/testdata/output/status.json index 4be4c7210..4b499c935 100644 --- a/cmd/helm/testdata/output/status.json +++ b/cmd/helm/testdata/output/status.json @@ -1 +1 @@ -{"name":"flummoxed-chickadee","info":{"first_deployed":"0001-01-01T00:00:00Z","last_deployed":"2016-01-16T00:00:00Z","deleted":"0001-01-01T00:00:00Z","status":"deployed","notes":"release notes"},"namespace":"default"} +{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed","notes":"release notes"},"namespace":"default"} diff --git a/pkg/action/action.go b/pkg/action/action.go index 28d88c3ed..653a57830 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -19,7 +19,6 @@ package action import ( "path" "regexp" - "time" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -34,12 +33,13 @@ import ( "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage" "helm.sh/helm/v3/pkg/storage/driver" + "helm.sh/helm/v3/pkg/time" ) // Timestamper is a function capable of producing a timestamp.Timestamper. // -// By default, this is a time.Time function. This can be overridden for testing, -// though, so that timestamps are predictable. +// By default, this is a time.Time function from the Helm time package. This can +// be overridden for testing though, so that timestamps are predictable. var Timestamper = time.Now var ( diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index e1d2cca6e..a5baec97d 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -21,7 +21,6 @@ import ( "io/ioutil" "path/filepath" "testing" - "time" dockerauth "github.com/deislabs/oras/pkg/auth/docker" fakeclientset "k8s.io/client-go/kubernetes/fake" @@ -33,6 +32,7 @@ import ( "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage" "helm.sh/helm/v3/pkg/storage/driver" + "helm.sh/helm/v3/pkg/time" ) var verbose = flag.Bool("test.log", false, "enable test logging") diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index ab576be1e..a161f9377 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -23,6 +23,7 @@ import ( "github.com/pkg/errors" "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" ) // execHook executes all of the hooks for the given hook event. @@ -60,7 +61,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, // Record the time at which the hook was applied to the cluster h.LastRun = release.HookExecution{ - StartedAt: time.Now(), + StartedAt: helmtime.Now(), Phase: release.HookPhaseRunning, } cfg.recordRelease(rl) @@ -72,7 +73,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, // Create hook resources if _, err := cfg.KubeClient.Create(resources); err != nil { - h.LastRun.CompletedAt = time.Now() + h.LastRun.CompletedAt = helmtime.Now() h.LastRun.Phase = release.HookPhaseFailed return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path) } @@ -80,7 +81,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, // Watch hook resources until they have completed err = cfg.KubeClient.WatchUntilReady(resources, timeout) // Note the time of success/failure - h.LastRun.CompletedAt = time.Now() + h.LastRun.CompletedAt = helmtime.Now() // Mark hook as succeeded or failed if err != nil { h.LastRun.Phase = release.HookPhaseFailed diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index b565aa9b0..942c9d8af 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -25,6 +25,7 @@ import ( "github.com/pkg/errors" "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" ) // Rollback is the action for rolling back to a given release. @@ -119,7 +120,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele Config: previousRelease.Config, Info: &release.Info{ FirstDeployed: currentRelease.Info.FirstDeployed, - LastDeployed: time.Now(), + LastDeployed: helmtime.Now(), Status: release.StatusPendingRollback, Notes: previousRelease.Info.Notes, // Because we lose the reference to previous version elsewhere, we set the diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index c5aaba629..fb72a845b 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -24,6 +24,7 @@ import ( "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" + helmtime "helm.sh/helm/v3/pkg/time" ) // Uninstall is the action for uninstalling releases. @@ -89,7 +90,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) u.cfg.Log("uninstall: Deleting %s", name) rel.Info.Status = release.StatusUninstalling - rel.Info.Deleted = time.Now() + rel.Info.Deleted = helmtime.Now() rel.Info.Description = "Deletion in progress (or silently failed)" res := &release.UninstallReleaseResponse{Release: rel} diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 0c37ec158..e7bfeefc5 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -19,7 +19,6 @@ package action import ( "fmt" "testing" - "time" "helm.sh/helm/v3/pkg/chart" @@ -28,6 +27,7 @@ import ( kubefake "helm.sh/helm/v3/pkg/kube/fake" "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/time" ) func upgradeAction(t *testing.T) *Upgrade { diff --git a/pkg/release/hook.go b/pkg/release/hook.go index 96cc4f250..662320f06 100644 --- a/pkg/release/hook.go +++ b/pkg/release/hook.go @@ -17,7 +17,7 @@ limitations under the License. package release import ( - "time" + "helm.sh/helm/v3/pkg/time" ) // HookEvent specifies the hook event diff --git a/pkg/release/info.go b/pkg/release/info.go index 51b2ecf83..0cb2bab64 100644 --- a/pkg/release/info.go +++ b/pkg/release/info.go @@ -15,7 +15,9 @@ limitations under the License. package release -import "time" +import ( + "helm.sh/helm/v3/pkg/time" +) // Info describes release information. type Info struct { @@ -24,9 +26,9 @@ type Info struct { // LastDeployed is when the release was last deployed. LastDeployed time.Time `json:"last_deployed,omitempty"` // Deleted tracks when this object was deleted. - Deleted time.Time `json:"deleted,omitempty"` + Deleted time.Time `json:"deleted"` // Description is human-friendly "log entry" about this release. - Description string `json:"Description,omitempty"` + Description string `json:"description,omitempty"` // Status is the current state of the release Status Status `json:"status,omitempty"` // Contains the rendered templates/NOTES.txt if available diff --git a/pkg/release/mock.go b/pkg/release/mock.go index 3e0bf0a6d..3abbd574b 100644 --- a/pkg/release/mock.go +++ b/pkg/release/mock.go @@ -18,9 +18,9 @@ package release import ( "math/rand" - "time" "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/time" ) // MockHookTemplate is the hook template used for all mock release objects. diff --git a/pkg/releaseutil/sorter_test.go b/pkg/releaseutil/sorter_test.go index d8b455124..69a6543ad 100644 --- a/pkg/releaseutil/sorter_test.go +++ b/pkg/releaseutil/sorter_test.go @@ -21,6 +21,7 @@ import ( "time" rspb "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" ) // note: this test data is shared with filter_test.go. @@ -33,8 +34,7 @@ var releases = []*rspb.Release{ } func tsRelease(name string, vers int, dur time.Duration, status rspb.Status) *rspb.Release { - tmsp := time.Now().Add(dur) - info := &rspb.Info{Status: status, LastDeployed: tmsp} + info := &rspb.Info{Status: status, LastDeployed: helmtime.Now().Add(dur)} return &rspb.Release{ Name: name, Version: vers, diff --git a/pkg/time/time.go b/pkg/time/time.go new file mode 100644 index 000000000..44f3fedfb --- /dev/null +++ b/pkg/time/time.go @@ -0,0 +1,91 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package time contains a wrapper for time.Time in the standard library and +// associated methods. This package mainly exists to workaround an issue in Go +// where the serializer doesn't omit an empty value for time: +// https://github.com/golang/go/issues/11939. As such, this can be removed if a +// proposal is ever accepted for Go +package time + +import ( + "bytes" + "time" +) + +// emptyString contains an empty JSON string value to be used as output +var emptyString = `""` + +// Time is a convenience wrapper around stdlib time, but with different +// marshalling and unmarshaling for zero values +type Time struct { + time.Time +} + +// Now returns the current time. It is a convenience wrapper around time.Now() +func Now() Time { + return Time{time.Now()} +} + +func (t Time) MarshalJSON() ([]byte, error) { + if t.Time.IsZero() { + return []byte(emptyString), nil + } + + return t.Time.MarshalJSON() +} + +func (t *Time) UnmarshalJSON(b []byte) error { + if bytes.Equal(b, []byte("null")) { + return nil + } + // If it is empty, we don't have to set anything since time.Time is not a + // pointer and will be set to the zero value + if bytes.Equal([]byte(emptyString), b) { + return nil + } + + return t.Time.UnmarshalJSON(b) +} + +func Parse(layout, value string) (Time, error) { + t, err := time.Parse(layout, value) + return Time{Time: t}, err +} +func ParseInLocation(layout, value string, loc *time.Location) (Time, error) { + t, err := time.ParseInLocation(layout, value, loc) + return Time{Time: t}, err +} + +func Date(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) Time { + return Time{Time: time.Date(year, month, day, hour, min, sec, nsec, loc)} +} + +func Unix(sec int64, nsec int64) Time { return Time{Time: time.Unix(sec, nsec)} } + +func (t Time) Add(d time.Duration) Time { return Time{Time: t.Time.Add(d)} } +func (t Time) AddDate(years int, months int, days int) Time { + return Time{Time: t.Time.AddDate(years, months, days)} +} +func (t Time) After(u Time) bool { return t.Time.After(u.Time) } +func (t Time) Before(u Time) bool { return t.Time.Before(u.Time) } +func (t Time) Equal(u Time) bool { return t.Time.Equal(u.Time) } +func (t Time) In(loc *time.Location) Time { return Time{Time: t.Time.In(loc)} } +func (t Time) Local() Time { return Time{Time: t.Time.Local()} } +func (t Time) Round(d time.Duration) Time { return Time{Time: t.Time.Round(d)} } +func (t Time) Sub(u Time) time.Duration { return t.Time.Sub(u.Time) } +func (t Time) Truncate(d time.Duration) Time { return Time{Time: t.Time.Truncate(d)} } +func (t Time) UTC() Time { return Time{Time: t.Time.UTC()} } diff --git a/pkg/time/time_test.go b/pkg/time/time_test.go new file mode 100644 index 000000000..20f0f8e29 --- /dev/null +++ b/pkg/time/time_test.go @@ -0,0 +1,83 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package time + +import ( + "encoding/json" + "testing" + "time" +) + +var ( + testingTime, _ = Parse(time.RFC3339, "1977-09-02T22:04:05Z") + testingTimeString = `"1977-09-02T22:04:05Z"` +) + +func TestNonZeroValueMarshal(t *testing.T) { + res, err := json.Marshal(testingTime) + if err != nil { + t.Fatal(err) + } + if testingTimeString != string(res) { + t.Errorf("expected a marshaled value of %s, got %s", testingTimeString, res) + } +} + +func TestZeroValueMarshal(t *testing.T) { + res, err := json.Marshal(Time{}) + if err != nil { + t.Fatal(err) + } + if string(res) != emptyString { + t.Errorf("expected zero value to marshal to empty string, got %s", res) + } +} + +func TestNonZeroValueUnmarshal(t *testing.T) { + var myTime Time + err := json.Unmarshal([]byte(testingTimeString), &myTime) + if err != nil { + t.Fatal(err) + } + if !myTime.Equal(testingTime) { + t.Errorf("expected time to be equal to %v, got %v", testingTime, myTime) + } +} + +func TestEmptyStringUnmarshal(t *testing.T) { + var myTime Time + err := json.Unmarshal([]byte(emptyString), &myTime) + if err != nil { + t.Fatal(err) + } + if !myTime.IsZero() { + t.Errorf("expected time to be equal to zero value, got %v", myTime) + } +} + +func TestZeroValueUnmarshal(t *testing.T) { + // This test ensures that we can unmarshal any time value that was output + // with the current go default value of "0001-01-01T00:00:00Z" + var myTime Time + err := json.Unmarshal([]byte(`"0001-01-01T00:00:00Z"`), &myTime) + if err != nil { + t.Fatal(err) + } + if !myTime.IsZero() { + t.Errorf("expected time to be equal to zero value, got %v", myTime) + } +}