diff --git a/pkg/action/action.go b/pkg/action/action.go index 1aa9f9d19..bcf6ca8ef 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -30,7 +30,6 @@ import ( "strings" "sync" "text/template" - "time" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -51,6 +50,7 @@ import ( releaseutil "helm.sh/helm/v4/pkg/release/v1/util" "helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage/driver" + "helm.sh/helm/v4/pkg/time" ) // Timestamper is a function capable of producing a timestamp.Timestamper. diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 78ca01089..b65e40024 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -24,7 +24,6 @@ import ( "log/slog" "strings" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -39,6 +38,7 @@ import ( release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage/driver" + "helm.sh/helm/v4/pkg/time" ) var verbose = flag.Bool("test.log", false, "enable test logging (debug by default)") diff --git a/pkg/action/get_metadata_test.go b/pkg/action/get_metadata_test.go index 7962a2133..ca612fed7 100644 --- a/pkg/action/get_metadata_test.go +++ b/pkg/action/get_metadata_test.go @@ -28,6 +28,7 @@ import ( chart "helm.sh/helm/v4/pkg/chart/v2" kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" + helmtime "helm.sh/helm/v4/pkg/time" ) func TestNewGetMetadata(t *testing.T) { @@ -44,7 +45,7 @@ func TestGetMetadata_Run_BasicMetadata(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := time.Now() + deployedTime := helmtime.Now() rel := &release.Release{ Name: releaseName, @@ -85,7 +86,7 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := time.Now() + deployedTime := helmtime.Now() dependencies := []*chart.Dependency{ { @@ -137,7 +138,7 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := time.Now() + deployedTime := helmtime.Now() dependencies := []*chart.Dependency{ { @@ -193,7 +194,7 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := time.Now() + deployedTime := helmtime.Now() dependencies := []*chart.Dependency{ { @@ -267,7 +268,7 @@ func TestGetMetadata_Run_WithAnnotations(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := time.Now() + deployedTime := helmtime.Now() annotations := map[string]string{ "helm.sh/hook": "pre-install", @@ -312,13 +313,13 @@ func TestGetMetadata_Run_SpecificVersion(t *testing.T) { client.Version = 2 releaseName := "test-release" - deployedTime := time.Now() + deployedTime := helmtime.Now() rel1 := &release.Release{ Name: releaseName, Info: &release.Info{ Status: release.StatusSuperseded, - LastDeployed: deployedTime.Add(-time.Hour), + LastDeployed: helmtime.Time{Time: deployedTime.Time.Add(-time.Hour)}, }, Chart: &chart.Chart{ Metadata: &chart.Metadata{ @@ -383,7 +384,7 @@ func TestGetMetadata_Run_DifferentStatuses(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { releaseName := "test-release-" + tc.name - deployedTime := time.Now() + deployedTime := helmtime.Now() rel := &release.Release{ Name: releaseName, @@ -439,7 +440,7 @@ func TestGetMetadata_Run_EmptyAppVersion(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := time.Now() + deployedTime := helmtime.Now() rel := &release.Release{ Name: releaseName, diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index 4808bc054..458a6342c 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -29,6 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" release "helm.sh/helm/v4/pkg/release/v1" + helmtime "helm.sh/helm/v4/pkg/time" ) // execHook executes all of the hooks for the given hook event. @@ -61,7 +62,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) @@ -75,7 +76,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, if _, err := cfg.KubeClient.Create( resources, kube.ClientCreateOptionServerSideApply(serverSideApply, false)); err != nil { - h.LastRun.CompletedAt = time.Now() + h.LastRun.CompletedAt = helmtime.Now() h.LastRun.Phase = release.HookPhaseFailed return fmt.Errorf("warning: Hook %s %s failed: %w", hook, h.Path, err) } @@ -87,7 +88,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, // Watch hook resources until they have completed err = waiter.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/install_test.go b/pkg/action/install_test.go index aae36152d..e90857a3f 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -49,6 +49,7 @@ import ( kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage/driver" + helmtime "helm.sh/helm/v4/pkg/time" ) type nameTemplateTestCase struct { @@ -855,32 +856,32 @@ func TestNameAndChartGenerateName(t *testing.T) { { "local filepath", "./chart", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "dot filepath", ".", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "empty filepath", "", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "packaged chart", "chart.tgz", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "packaged chart with .tar.gz extension", "chart.tar.gz", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "packaged chart with local extension", "./chart.tgz", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, } diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index f56052988..adaf22615 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -26,6 +26,7 @@ import ( chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" release "helm.sh/helm/v4/pkg/release/v1" + helmtime "helm.sh/helm/v4/pkg/time" ) // Rollback is the action for rolling back to a given release. @@ -157,7 +158,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 057c2118f..866be5d54 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -30,6 +30,7 @@ import ( release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" "helm.sh/helm/v4/pkg/storage/driver" + helmtime "helm.sh/helm/v4/pkg/time" ) // Uninstall is the action for uninstalling releases. @@ -109,7 +110,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) slog.Debug("uninstall: deleting release", "name", 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 0a436534f..d31804b87 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -34,6 +34,7 @@ import ( kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" + helmtime "helm.sh/helm/v4/pkg/time" ) func upgradeAction(t *testing.T) *Upgrade { @@ -259,7 +260,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { withValues(chartDefaultValues), withMetadataDependency(dependency), ) - now := time.Now() + now := helmtime.Now() existingValues := map[string]interface{}{ "subchart": map[string]interface{}{ "enabled": false, diff --git a/pkg/cmd/flags_test.go b/pkg/cmd/flags_test.go index 8d79716f0..dce748a6b 100644 --- a/pkg/cmd/flags_test.go +++ b/pkg/cmd/flags_test.go @@ -19,19 +19,19 @@ package cmd import ( "fmt" "testing" - "time" "github.com/stretchr/testify/require" "helm.sh/helm/v4/pkg/action" chart "helm.sh/helm/v4/pkg/chart/v2" release "helm.sh/helm/v4/pkg/release/v1" + helmtime "helm.sh/helm/v4/pkg/time" ) func outputFlagCompletionTest(t *testing.T, cmdName string) { t.Helper() 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: "athos", Namespace: "default", diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 96bf6434b..55e3a842f 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -22,7 +22,6 @@ import ( "os" "strings" "testing" - "time" shellwords "github.com/mattn/go-shellwords" "github.com/spf13/cobra" @@ -35,6 +34,7 @@ import ( release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage/driver" + "helm.sh/helm/v4/pkg/time" ) func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() } diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index f4dde95e4..9f029268c 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -31,6 +31,7 @@ import ( "helm.sh/helm/v4/pkg/cmd/require" release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" + helmtime "helm.sh/helm/v4/pkg/time" ) var historyHelp = ` @@ -83,12 +84,12 @@ 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 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/pkg/cmd/list_test.go b/pkg/cmd/list_test.go index 22a948fff..82b25a768 100644 --- a/pkg/cmd/list_test.go +++ b/pkg/cmd/list_test.go @@ -18,10 +18,10 @@ package cmd import ( "testing" - "time" chart "helm.sh/helm/v4/pkg/chart/v2" release "helm.sh/helm/v4/pkg/release/v1" + "helm.sh/helm/v4/pkg/time" ) func TestListCmd(t *testing.T) { diff --git a/pkg/cmd/status_test.go b/pkg/cmd/status_test.go index 8c251b76b..cb4e23c59 100644 --- a/pkg/cmd/status_test.go +++ b/pkg/cmd/status_test.go @@ -22,11 +22,12 @@ import ( chart "helm.sh/helm/v4/pkg/chart/v2" release "helm.sh/helm/v4/pkg/release/v1" + helmtime "helm.sh/helm/v4/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", @@ -129,8 +130,8 @@ 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/pkg/cmd/testdata/output/status-with-resources.json b/pkg/cmd/testdata/output/status-with-resources.json index af512bfd1..275e0cfc6 100644 --- a/pkg/cmd/testdata/output/status-with-resources.json +++ b/pkg/cmd/testdata/output/status-with-resources.json @@ -1 +1 @@ -{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00:00:00Z","status":"deployed"},"namespace":"default"} +{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed"},"namespace":"default"} diff --git a/pkg/cmd/testdata/output/status.json b/pkg/cmd/testdata/output/status.json index 4727dd100..4b499c935 100644 --- a/pkg/cmd/testdata/output/status.json +++ b/pkg/cmd/testdata/output/status.json @@ -1 +1 @@ -{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00: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/pusher/ocipusher.go b/pkg/pusher/ocipusher.go index 25682969b..699d27caf 100644 --- a/pkg/pusher/ocipusher.go +++ b/pkg/pusher/ocipusher.go @@ -29,6 +29,7 @@ import ( "helm.sh/helm/v4/internal/tlsutil" "helm.sh/helm/v4/pkg/chart/v2/loader" "helm.sh/helm/v4/pkg/registry" + "helm.sh/helm/v4/pkg/time/ctime" ) // OCIPusher is the default OCI backend handler @@ -90,7 +91,7 @@ func (pusher *OCIPusher) push(chartRef, href string) error { meta.Metadata.Version) // The time the chart was "created" is semantically the time the chart archive file was last written(modified) - chartArchiveFileCreatedTime := stat.ModTime() + chartArchiveFileCreatedTime := ctime.Modified(stat) pushOpts = append(pushOpts, registry.PushOptCreationTime(chartArchiveFileCreatedTime.Format(time.RFC3339))) _, err = client.Push(chartBytes, ref, pushOpts...) diff --git a/pkg/registry/util.go b/pkg/registry/util.go index 6071c66c3..b31ab63fe 100644 --- a/pkg/registry/util.go +++ b/pkg/registry/util.go @@ -28,6 +28,7 @@ import ( "helm.sh/helm/v4/internal/tlsutil" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" + helmtime "helm.sh/helm/v4/pkg/time" "github.com/Masterminds/semver/v3" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -156,7 +157,7 @@ func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[ chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home) if len(creationTime) == 0 { - creationTime = time.Now().UTC().Format(time.RFC3339) + creationTime = helmtime.Now().UTC().Format(time.RFC3339) } chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, creationTime) diff --git a/pkg/registry/util_test.go b/pkg/registry/util_test.go index a67bc853a..c8ce4e4a4 100644 --- a/pkg/registry/util_test.go +++ b/pkg/registry/util_test.go @@ -24,11 +24,12 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" chart "helm.sh/helm/v4/pkg/chart/v2" + helmtime "helm.sh/helm/v4/pkg/time" ) func TestGenerateOCIChartAnnotations(t *testing.T) { - nowString := time.Now().Format(time.RFC3339) + nowString := helmtime.Now().Format(time.RFC3339) tests := []struct { name string @@ -159,7 +160,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { func TestGenerateOCIAnnotations(t *testing.T) { - nowString := time.Now().Format(time.RFC3339) + nowString := helmtime.Now().Format(time.RFC3339) tests := []struct { name string @@ -233,7 +234,7 @@ func TestGenerateOCIAnnotations(t *testing.T) { func TestGenerateOCICreatedAnnotations(t *testing.T) { - nowTime := time.Now() + nowTime := helmtime.Now() nowTimeString := nowTime.Format(time.RFC3339) chart := &chart.Metadata{ @@ -249,7 +250,7 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) { } // Verify value of created artifact in RFC3339 format - if _, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { + if _, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) } @@ -261,7 +262,7 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) { t.Errorf("%s annotation not created", ocispec.AnnotationCreated) } - if createdTimeAnnotation, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { + if createdTimeAnnotation, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) // Verify creation annotation after time test began diff --git a/pkg/release/v1/hook.go b/pkg/release/v1/hook.go index b7d3c3992..1ef5c1eb8 100644 --- a/pkg/release/v1/hook.go +++ b/pkg/release/v1/hook.go @@ -17,7 +17,7 @@ limitations under the License. package v1 import ( - "time" + "helm.sh/helm/v4/pkg/time" ) // HookEvent specifies the hook event @@ -97,9 +97,9 @@ type Hook struct { // A HookExecution records the result for the last execution of a hook for a given release. type HookExecution struct { // StartedAt indicates the date/time this hook was started - StartedAt time.Time `json:"started_at,omitzero"` + StartedAt time.Time `json:"started_at,omitempty"` // CompletedAt indicates the date/time this hook was completed. - CompletedAt time.Time `json:"completed_at,omitzero"` + CompletedAt time.Time `json:"completed_at,omitempty"` // Phase indicates whether the hook completed successfully Phase HookPhase `json:"phase"` } diff --git a/pkg/release/v1/info.go b/pkg/release/v1/info.go index cef7e9960..ff98ab63e 100644 --- a/pkg/release/v1/info.go +++ b/pkg/release/v1/info.go @@ -16,19 +16,19 @@ limitations under the License. package v1 import ( - "time" - "k8s.io/apimachinery/pkg/runtime" + + "helm.sh/helm/v4/pkg/time" ) // Info describes release information. type Info struct { // FirstDeployed is when the release was first deployed. - FirstDeployed time.Time `json:"first_deployed,omitzero"` + FirstDeployed time.Time `json:"first_deployed,omitempty"` // LastDeployed is when the release was last deployed. - LastDeployed time.Time `json:"last_deployed,omitzero"` + LastDeployed time.Time `json:"last_deployed,omitempty"` // Deleted tracks when this object was deleted. - Deleted time.Time `json:"deleted,omitzero"` + Deleted time.Time `json:"deleted"` // Description is human-friendly "log entry" about this release. Description string `json:"description,omitempty"` // Status is the current state of the release diff --git a/pkg/release/v1/mock.go b/pkg/release/v1/mock.go index 818cd777e..c3a6594cc 100644 --- a/pkg/release/v1/mock.go +++ b/pkg/release/v1/mock.go @@ -19,10 +19,10 @@ package v1 import ( "fmt" "math/rand" - "time" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/time" ) // MockHookTemplate is the hook template used for all mock release objects. diff --git a/pkg/release/v1/util/sorter_test.go b/pkg/release/v1/util/sorter_test.go index 0889ddb94..4628a5192 100644 --- a/pkg/release/v1/util/sorter_test.go +++ b/pkg/release/v1/util/sorter_test.go @@ -21,6 +21,7 @@ import ( "time" rspb "helm.sh/helm/v4/pkg/release/v1" + helmtime "helm.sh/helm/v4/pkg/time" ) // note: this test data is shared with filter_test.go. @@ -33,7 +34,7 @@ var releases = []*rspb.Release{ } func tsRelease(name string, vers int, dur time.Duration, status rspb.Status) *rspb.Release { - info := &rspb.Info{Status: status, LastDeployed: time.Now().Add(dur)} + info := &rspb.Info{Status: status, LastDeployed: helmtime.Now().Add(dur)} return &rspb.Release{ Name: name, Version: vers, diff --git a/pkg/time/ctime/ctime.go b/pkg/time/ctime/ctime.go new file mode 100644 index 000000000..63a41c0bf --- /dev/null +++ b/pkg/time/ctime/ctime.go @@ -0,0 +1,29 @@ +/* +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 ctime + +import ( + "os" + "time" +) + +func Created(fi os.FileInfo) time.Time { + return modified(fi) +} + +func Modified(fi os.FileInfo) time.Time { + return modified(fi) +} diff --git a/pkg/time/ctime/ctime_linux.go b/pkg/time/ctime/ctime_linux.go new file mode 100644 index 000000000..d8a6ea1a1 --- /dev/null +++ b/pkg/time/ctime/ctime_linux.go @@ -0,0 +1,30 @@ +//go:build linux + +/* +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 ctime + +import ( + "os" + "syscall" + "time" +) + +func modified(fi os.FileInfo) time.Time { + st := fi.Sys().(*syscall.Stat_t) + //nolint + return time.Unix(int64(st.Mtim.Sec), int64(st.Mtim.Nsec)) +} diff --git a/pkg/time/ctime/ctime_other.go b/pkg/time/ctime/ctime_other.go new file mode 100644 index 000000000..12afc6df2 --- /dev/null +++ b/pkg/time/ctime/ctime_other.go @@ -0,0 +1,27 @@ +//go:build !linux + +/* +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 ctime + +import ( + "os" + "time" +) + +func modified(fi os.FileInfo) time.Time { + return fi.ModTime() +} diff --git a/pkg/time/time.go b/pkg/time/time.go new file mode 100644 index 000000000..16973b455 --- /dev/null +++ b/pkg/time/time.go @@ -0,0 +1,92 @@ +/* +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 work around 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 unmarshalling 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.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, minute, second, nanoSecond int, loc *time.Location) Time { + return Time{Time: time.Date(year, month, day, hour, minute, second, nanoSecond, 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..342ca4a10 --- /dev/null +++ b/pkg/time/time_test.go @@ -0,0 +1,153 @@ +/* +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" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + timeParseString = `"1977-09-02T22:04:05Z"` + timeString = "1977-09-02 22:04:05 +0000 UTC" +) + +func givenTime(t *testing.T) Time { + t.Helper() + result, err := Parse(time.RFC3339, "1977-09-02T22:04:05Z") + require.NoError(t, err) + return result +} + +func TestDate(t *testing.T) { + testingTime := givenTime(t) + got := Date(1977, 9, 2, 22, 04, 05, 0, time.UTC) + assert.Equal(t, timeString, got.String()) + assert.True(t, testingTime.Equal(got)) + assert.True(t, got.Equal(testingTime)) +} + +func TestNow(t *testing.T) { + testingTime := givenTime(t) + got := Now() + assert.True(t, testingTime.Before(got)) + assert.True(t, got.After(testingTime)) +} + +func TestTime_Add(t *testing.T) { + testingTime := givenTime(t) + got := testingTime.Add(time.Hour) + assert.Equal(t, timeString, testingTime.String()) + assert.Equal(t, "1977-09-02 23:04:05 +0000 UTC", got.String()) +} + +func TestTime_AddDate(t *testing.T) { + testingTime := givenTime(t) + got := testingTime.AddDate(1, 1, 1) + assert.Equal(t, "1978-10-03 22:04:05 +0000 UTC", got.String()) +} + +func TestTime_In(t *testing.T) { + testingTime := givenTime(t) + edt, err := time.LoadLocation("America/New_York") + assert.NoError(t, err) + got := testingTime.In(edt) + assert.Equal(t, "America/New_York", got.Location().String()) +} + +func TestTime_MarshalJSONNonZero(t *testing.T) { + testingTime := givenTime(t) + res, err := json.Marshal(testingTime) + assert.NoError(t, err) + assert.Equal(t, timeParseString, string(res)) +} + +func TestTime_MarshalJSONZeroValue(t *testing.T) { + res, err := json.Marshal(Time{}) + assert.NoError(t, err) + assert.Equal(t, `""`, string(res)) +} + +func TestTime_Round(t *testing.T) { + testingTime := givenTime(t) + got := testingTime.Round(time.Hour) + assert.Equal(t, timeString, testingTime.String()) + assert.Equal(t, "1977-09-02 22:00:00 +0000 UTC", got.String()) +} + +func TestTime_Sub(t *testing.T) { + testingTime := givenTime(t) + before, err := Parse(time.RFC3339, "1977-09-01T22:04:05Z") + require.NoError(t, err) + got := testingTime.Sub(before) + assert.Equal(t, "24h0m0s", got.String()) +} + +func TestTime_Truncate(t *testing.T) { + testingTime := givenTime(t) + got := testingTime.Truncate(time.Hour) + assert.Equal(t, timeString, testingTime.String()) + assert.Equal(t, "1977-09-02 22:00:00 +0000 UTC", got.String()) +} + +func TestTime_UTC(t *testing.T) { + edtTime, err := Parse(time.RFC3339, "1977-09-03T05:04:05+07:00") + require.NoError(t, err) + got := edtTime.UTC() + assert.Equal(t, timeString, got.String()) +} + +func TestTime_UnmarshalJSONNonZeroValue(t *testing.T) { + testingTime := givenTime(t) + var myTime Time + err := json.Unmarshal([]byte(timeParseString), &myTime) + assert.NoError(t, err) + assert.True(t, testingTime.Equal(myTime)) +} + +func TestTime_UnmarshalJSONEmptyString(t *testing.T) { + var myTime Time + err := json.Unmarshal([]byte(emptyString), &myTime) + assert.NoError(t, err) + assert.True(t, myTime.IsZero()) +} + +func TestTime_UnmarshalJSONNullString(t *testing.T) { + var myTime Time + err := json.Unmarshal([]byte("null"), &myTime) + assert.NoError(t, err) + assert.True(t, myTime.IsZero()) +} + +func TestTime_UnmarshalJSONZeroValue(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) + assert.NoError(t, err) + assert.True(t, myTime.IsZero()) +} + +func TestUnix(t *testing.T) { + got := Unix(242085845, 0) + assert.Equal(t, int64(242085845), got.Unix()) + assert.Equal(t, timeString, got.UTC().String()) +}