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 <taylor.thomas@microsoft.com>
pull/6679/head
Taylor Thomas 5 years ago
parent 3edad39e08
commit aa429e150a

@ -22,7 +22,7 @@ import (
"os" "os"
"strings" "strings"
"testing" "testing"
"time" stdtime "time"
shellwords "github.com/mattn/go-shellwords" shellwords "github.com/mattn/go-shellwords"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -34,9 +34,10 @@ import (
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage" "helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver" "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() { func init() {
action.Timestamper = testTimestamper action.Timestamper = testTimestamper

@ -19,7 +19,7 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"time" stdtime "time"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -30,6 +30,7 @@ import (
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/cli/output"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/releaseutil"
"helm.sh/helm/v3/pkg/time"
) )
var historyHelp = ` var historyHelp = `
@ -98,7 +99,7 @@ func (r releaseHistory) WriteTable(out io.Writer) error {
tbl := uitable.New() tbl := uitable.New()
tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION") tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION")
for _, item := range r { 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) return output.EncodeTable(out, tbl)
} }

@ -18,20 +18,21 @@ package main
import ( import (
"testing" "testing"
"time" stdtime "time"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/time"
) )
func TestListCmd(t *testing.T) { func TestListCmd(t *testing.T) {
defaultNamespace := "default" defaultNamespace := "default"
sampleTimeSeconds := int64(1452902400) sampleTimeSeconds := int64(1452902400)
timestamp1 := time.Unix(sampleTimeSeconds+1, 0).UTC() timestamp1 := time.Time{Time: stdtime.Unix(sampleTimeSeconds+1, 0).UTC()}
timestamp2 := time.Unix(sampleTimeSeconds+2, 0).UTC() timestamp2 := time.Time{Time: stdtime.Unix(sampleTimeSeconds+2, 0).UTC()}
timestamp3 := time.Unix(sampleTimeSeconds+3, 0).UTC() timestamp3 := time.Time{Time: stdtime.Unix(sampleTimeSeconds+3, 0).UTC()}
timestamp4 := time.Unix(sampleTimeSeconds+4, 0).UTC() timestamp4 := time.Time{Time: stdtime.Unix(sampleTimeSeconds+4, 0).UTC()}
chartInfo := &chart.Chart{ chartInfo := &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
Name: "chickadee", Name: "chickadee",

@ -18,15 +18,16 @@ package main
import ( import (
"testing" "testing"
"time" stdtime "time"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/time"
) )
func TestStatusCmd(t *testing.T) { func TestStatusCmd(t *testing.T) {
releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { 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{{ return []*release.Release{{
Name: "flummoxed-chickadee", Name: "flummoxed-chickadee",
Namespace: "default", Namespace: "default",
@ -104,6 +105,6 @@ func TestStatusCmd(t *testing.T) {
} }
func mustParseTime(t string) time.Time { func mustParseTime(t string) time.Time {
res, _ := time.Parse(time.RFC3339, t) res, _ := stdtime.Parse(stdtime.RFC3339, t)
return res return time.Time{Time: res}
} }

@ -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"}

@ -19,7 +19,6 @@ package action
import ( import (
"path" "path"
"regexp" "regexp"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
@ -34,12 +33,13 @@ import (
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage" "helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver" "helm.sh/helm/v3/pkg/storage/driver"
"helm.sh/helm/v3/pkg/time"
) )
// Timestamper is a function capable of producing a timestamp.Timestamper. // Timestamper is a function capable of producing a timestamp.Timestamper.
// //
// By default, this is a time.Time function. This can be overridden for testing, // By default, this is a time.Time function from the Helm time package. This can
// though, so that timestamps are predictable. // be overridden for testing though, so that timestamps are predictable.
var Timestamper = time.Now var Timestamper = time.Now
var ( var (

@ -21,7 +21,6 @@ import (
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
dockerauth "github.com/deislabs/oras/pkg/auth/docker" dockerauth "github.com/deislabs/oras/pkg/auth/docker"
fakeclientset "k8s.io/client-go/kubernetes/fake" fakeclientset "k8s.io/client-go/kubernetes/fake"
@ -33,6 +32,7 @@ import (
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage" "helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver" "helm.sh/helm/v3/pkg/storage/driver"
"helm.sh/helm/v3/pkg/time"
) )
var verbose = flag.Bool("test.log", false, "enable test logging") var verbose = flag.Bool("test.log", false, "enable test logging")

@ -18,15 +18,16 @@ package action
import ( import (
"bytes" "bytes"
"sort" "sort"
"time" stdtime "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/time"
) )
// execHook executes all of the hooks for the given hook event. // 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{} executingHooks := []*release.Hook{}
for _, h := range rl.Hooks { for _, h := range rl.Hooks {

@ -20,11 +20,12 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"strings" "strings"
"time" stdtime "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/time"
) )
// Rollback is the action for rolling back to a given release. // Rollback is the action for rolling back to a given release.
@ -34,7 +35,7 @@ type Rollback struct {
cfg *Configuration cfg *Configuration
Version int Version int
Timeout time.Duration Timeout stdtime.Duration
Wait bool Wait bool
DisableHooks bool DisableHooks bool
DryRun bool DryRun bool

@ -18,12 +18,13 @@ package action
import ( import (
"strings" "strings"
"time" stdtime "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/releaseutil"
"helm.sh/helm/v3/pkg/time"
) )
// Uninstall is the action for uninstalling releases. // Uninstall is the action for uninstalling releases.
@ -35,7 +36,7 @@ type Uninstall struct {
DisableHooks bool DisableHooks bool
DryRun bool DryRun bool
KeepHistory bool KeepHistory bool
Timeout time.Duration Timeout stdtime.Duration
} }
// NewUninstall creates a new Uninstall object with the given configuration. // NewUninstall creates a new Uninstall object with the given configuration.

@ -19,7 +19,6 @@ package action
import ( import (
"fmt" "fmt"
"testing" "testing"
"time"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
@ -28,6 +27,7 @@ import (
kubefake "helm.sh/helm/v3/pkg/kube/fake" kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/time"
) )
func upgradeAction(t *testing.T) *Upgrade { func upgradeAction(t *testing.T) *Upgrade {

@ -17,7 +17,7 @@ limitations under the License.
package release package release
import ( import (
"time" "helm.sh/helm/v3/pkg/time"
) )
// HookEvent specifies the hook event // HookEvent specifies the hook event

@ -15,7 +15,9 @@ limitations under the License.
package release package release
import "time" import (
"helm.sh/helm/v3/pkg/time"
)
// Info describes release information. // Info describes release information.
type Info struct { type Info struct {
@ -24,9 +26,9 @@ type Info struct {
// LastDeployed is when the release was last deployed. // LastDeployed is when the release was last deployed.
LastDeployed time.Time `json:"last_deployed,omitempty"` LastDeployed time.Time `json:"last_deployed,omitempty"`
// Deleted tracks when this object was deleted. // 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 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 is the current state of the release
Status Status `json:"status,omitempty"` Status Status `json:"status,omitempty"`
// Contains the rendered templates/NOTES.txt if available // Contains the rendered templates/NOTES.txt if available

@ -18,9 +18,10 @@ package release
import ( import (
"math/rand" "math/rand"
"time" stdtime "time"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/time"
) )
// MockHookTemplate is the hook template used for all mock release objects. // 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. // 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 { func Mock(opts *MockReleaseOptions) *Release {
date := time.Unix(242085845, 0).UTC() date := time.Time{Time: stdtime.Unix(242085845, 0).UTC()}
name := opts.Name name := opts.Name
if name == "" { if name == "" {

@ -18,9 +18,10 @@ package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil"
import ( import (
"testing" "testing"
"time" stdtime "time"
rspb "helm.sh/helm/v3/pkg/release" rspb "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/time"
) )
// note: this test data is shared with filter_test.go. // 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), tsRelease("vocal-dogs", 3, 6000, rspb.StatusUninstalled),
} }
func tsRelease(name string, vers int, dur time.Duration, status rspb.Status) *rspb.Release { func tsRelease(name string, vers int, dur stdtime.Duration, status rspb.Status) *rspb.Release {
tmsp := time.Now().Add(dur) info := &rspb.Info{Status: status, LastDeployed: time.Time{Time: stdtime.Now().Add(dur)}}
info := &rspb.Info{Status: status, LastDeployed: tmsp}
return &rspb.Release{ return &rspb.Release{
Name: name, Name: name,
Version: vers, Version: vers,

@ -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)
}

@ -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)
}
}
Loading…
Cancel
Save