From aa429e150abf0f64689a0df59e0994772c4bd2e0 Mon Sep 17 00:00:00 2001 From: Taylor Thomas Date: Mon, 14 Oct 2019 15:31:25 -0600 Subject: [PATCH] feat(*): Adds custom time package for better marshalling 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. This replaces all release and hook object time references with the new time package so things actually marshal correctly Signed-off-by: Taylor Thomas --- cmd/helm/helm_test.go | 5 +- cmd/helm/history.go | 5 +- cmd/helm/list_test.go | 11 ++-- cmd/helm/status_test.go | 9 +-- cmd/helm/testdata/output/status.json | 2 +- pkg/action/action.go | 6 +- pkg/action/action_test.go | 2 +- pkg/action/hooks.go | 5 +- pkg/action/rollback.go | 5 +- pkg/action/uninstall.go | 5 +- pkg/action/upgrade_test.go | 2 +- pkg/release/hook.go | 2 +- pkg/release/info.go | 8 ++- pkg/release/mock.go | 5 +- pkg/releaseutil/sorter_test.go | 8 +-- pkg/time/time.go | 62 ++++++++++++++++++++ pkg/time/time_test.go | 84 ++++++++++++++++++++++++++++ 17 files changed, 191 insertions(+), 35 deletions(-) create mode 100644 pkg/time/time.go create mode 100644 pkg/time/time_test.go diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index fc4a3a879..392400a72 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -22,7 +22,7 @@ import ( "os" "strings" "testing" - "time" + stdtime "time" shellwords "github.com/mattn/go-shellwords" "github.com/spf13/cobra" @@ -34,9 +34,10 @@ 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() } +func testTimestamper() time.Time { return time.Time{Time: stdtime.Unix(242085845, 0).UTC()} } func init() { action.Timestamper = testTimestamper diff --git a/cmd/helm/history.go b/cmd/helm/history.go index c4a74bb2c..8e355c56e 100644 --- a/cmd/helm/history.go +++ b/cmd/helm/history.go @@ -19,7 +19,7 @@ package main import ( "fmt" "io" - "time" + stdtime "time" "github.com/gosuri/uitable" "github.com/spf13/cobra" @@ -30,6 +30,7 @@ import ( "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" + "helm.sh/helm/v3/pkg/time" ) var historyHelp = ` @@ -98,7 +99,7 @@ func (r releaseHistory) WriteTable(out io.Writer) error { tbl := uitable.New() tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION") for _, item := range r { - tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, item.Description) + tbl.AddRow(item.Revision, item.Updated.Format(stdtime.ANSIC), item.Status, item.Chart, item.AppVersion, item.Description) } return output.EncodeTable(out, tbl) } diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index 2de7c5dd2..80be98b6c 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -18,20 +18,21 @@ package main import ( "testing" - "time" + stdtime "time" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/time" ) func TestListCmd(t *testing.T) { defaultNamespace := "default" sampleTimeSeconds := int64(1452902400) - timestamp1 := time.Unix(sampleTimeSeconds+1, 0).UTC() - timestamp2 := time.Unix(sampleTimeSeconds+2, 0).UTC() - timestamp3 := time.Unix(sampleTimeSeconds+3, 0).UTC() - timestamp4 := time.Unix(sampleTimeSeconds+4, 0).UTC() + timestamp1 := time.Time{Time: stdtime.Unix(sampleTimeSeconds+1, 0).UTC()} + timestamp2 := time.Time{Time: stdtime.Unix(sampleTimeSeconds+2, 0).UTC()} + timestamp3 := time.Time{Time: stdtime.Unix(sampleTimeSeconds+3, 0).UTC()} + timestamp4 := time.Time{Time: stdtime.Unix(sampleTimeSeconds+4, 0).UTC()} chartInfo := &chart.Chart{ Metadata: &chart.Metadata{ Name: "chickadee", diff --git a/cmd/helm/status_test.go b/cmd/helm/status_test.go index 198063220..4e3cab40f 100644 --- a/cmd/helm/status_test.go +++ b/cmd/helm/status_test.go @@ -18,15 +18,16 @@ package main import ( "testing" - "time" + stdtime "time" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/release" + "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 = time.Time{Time: stdtime.Unix(1452902400, 0).UTC()} return []*release.Release{{ Name: "flummoxed-chickadee", Namespace: "default", @@ -104,6 +105,6 @@ func TestStatusCmd(t *testing.T) { } func mustParseTime(t string) time.Time { - res, _ := time.Parse(time.RFC3339, t) - return res + res, _ := stdtime.Parse(stdtime.RFC3339, t) + return time.Time{Time: 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 49d1bad41..857f01f03 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..828ea1dbb 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -18,15 +18,16 @@ package action import ( "bytes" "sort" - "time" + stdtime "time" "github.com/pkg/errors" "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/time" ) // execHook executes all of the hooks for the given hook event. -func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, timeout time.Duration) error { +func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, timeout stdtime.Duration) error { executingHooks := []*release.Hook{} for _, h := range rl.Hooks { diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index b565aa9b0..eebc67d7c 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -20,11 +20,12 @@ import ( "bytes" "fmt" "strings" - "time" + stdtime "time" "github.com/pkg/errors" "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/time" ) // Rollback is the action for rolling back to a given release. @@ -34,7 +35,7 @@ type Rollback struct { cfg *Configuration Version int - Timeout time.Duration + Timeout stdtime.Duration Wait bool DisableHooks bool DryRun bool diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index c5aaba629..21c237709 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -18,12 +18,13 @@ package action import ( "strings" - "time" + stdtime "time" "github.com/pkg/errors" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" + "helm.sh/helm/v3/pkg/time" ) // Uninstall is the action for uninstalling releases. @@ -35,7 +36,7 @@ type Uninstall struct { DisableHooks bool DryRun bool KeepHistory bool - Timeout time.Duration + Timeout stdtime.Duration } // NewUninstall creates a new Uninstall object with the given configuration. diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 69ee25cd6..b2808781f 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..575f2d65a 100644 --- a/pkg/release/mock.go +++ b/pkg/release/mock.go @@ -18,9 +18,10 @@ package release import ( "math/rand" - "time" + stdtime "time" "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/time" ) // MockHookTemplate is the hook template used for all mock release objects. @@ -49,7 +50,7 @@ type MockReleaseOptions struct { // Mock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing. func Mock(opts *MockReleaseOptions) *Release { - date := time.Unix(242085845, 0).UTC() + date := time.Time{Time: stdtime.Unix(242085845, 0).UTC()} name := opts.Name if name == "" { diff --git a/pkg/releaseutil/sorter_test.go b/pkg/releaseutil/sorter_test.go index d8b455124..85d6753e3 100644 --- a/pkg/releaseutil/sorter_test.go +++ b/pkg/releaseutil/sorter_test.go @@ -18,9 +18,10 @@ package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil" import ( "testing" - "time" + stdtime "time" rspb "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/time" ) // note: this test data is shared with filter_test.go. @@ -32,9 +33,8 @@ var releases = []*rspb.Release{ tsRelease("vocal-dogs", 3, 6000, rspb.StatusUninstalled), } -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} +func tsRelease(name string, vers int, dur stdtime.Duration, status rspb.Status) *rspb.Release { + info := &rspb.Info{Status: status, LastDeployed: time.Time{Time: stdtime.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..a372e61e9 --- /dev/null +++ b/pkg/time/time.go @@ -0,0 +1,62 @@ +/* +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) +} diff --git a/pkg/time/time_test.go b/pkg/time/time_test.go new file mode 100644 index 000000000..32a3e685b --- /dev/null +++ b/pkg/time/time_test.go @@ -0,0 +1,84 @@ +/* +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, _ = time.Parse(time.RFC3339, "1977-09-02T22:04:05Z") + testingTimeString = `"1977-09-02T22:04:05Z"` +) + +func TestNonZeroValueMarshal(t *testing.T) { + myTime := Time{testingTime} + res, err := json.Marshal(myTime) + 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) + } +}