Merge branch 'main' into adding-error-handling

Signed-off-by: Robert Sirchia <rsirchia@outlook.com>
pull/31300/head
Robert Sirchia 1 week ago committed by GitHub
commit 57af4ab119
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -20,9 +20,12 @@ import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
// TestAtomicWriteFile tests the happy path of AtomicWriteFile function.
// It verifies that the function correctly writes content to a file with the specified mode.
func TestAtomicWriteFile(t *testing.T) {
dir := t.TempDir()
@ -55,3 +58,64 @@ func TestAtomicWriteFile(t *testing.T) {
mode, gotinfo.Mode())
}
}
// TestAtomicWriteFile_CreateTempError tests the error path when os.CreateTemp fails
func TestAtomicWriteFile_CreateTempError(t *testing.T) {
invalidPath := "/invalid/path/that/does/not/exist/testfile"
reader := bytes.NewReader([]byte("test content"))
mode := os.FileMode(0644)
err := AtomicWriteFile(invalidPath, reader, mode)
if err == nil {
t.Error("Expected error when CreateTemp fails, but got nil")
}
}
// TestAtomicWriteFile_EmptyContent tests with empty content
func TestAtomicWriteFile_EmptyContent(t *testing.T) {
dir := t.TempDir()
testpath := filepath.Join(dir, "empty_helm")
reader := bytes.NewReader([]byte(""))
mode := os.FileMode(0644)
err := AtomicWriteFile(testpath, reader, mode)
if err != nil {
t.Errorf("AtomicWriteFile error with empty content: %s", err)
}
got, err := os.ReadFile(testpath)
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("expected empty content, got: %s", string(got))
}
}
// TestAtomicWriteFile_LargeContent tests with large content
func TestAtomicWriteFile_LargeContent(t *testing.T) {
dir := t.TempDir()
testpath := filepath.Join(dir, "large_test")
// Create a large content string
largeContent := strings.Repeat("HELM", 1024*1024)
reader := bytes.NewReader([]byte(largeContent))
mode := os.FileMode(0644)
err := AtomicWriteFile(testpath, reader, mode)
if err != nil {
t.Errorf("AtomicWriteFile error with large content: %s", err)
}
got, err := os.ReadFile(testpath)
if err != nil {
t.Fatal(err)
}
if largeContent != string(got) {
t.Fatalf("expected large content to match, got different length: %d vs %d", len(largeContent), len(got))
}
}

@ -30,6 +30,7 @@ import (
"strings"
"sync"
"text/template"
"time"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
@ -50,7 +51,6 @@ 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.

@ -24,6 +24,7 @@ import (
"log/slog"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -38,7 +39,6 @@ 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)")

@ -28,7 +28,6 @@ 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) {
@ -45,7 +44,7 @@ func TestGetMetadata_Run_BasicMetadata(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
rel := &release.Release{
Name: releaseName,
@ -86,7 +85,7 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
dependencies := []*chart.Dependency{
{
@ -138,7 +137,7 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
dependencies := []*chart.Dependency{
{
@ -194,7 +193,7 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
dependencies := []*chart.Dependency{
{
@ -268,7 +267,7 @@ func TestGetMetadata_Run_WithAnnotations(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
annotations := map[string]string{
"helm.sh/hook": "pre-install",
@ -313,13 +312,13 @@ func TestGetMetadata_Run_SpecificVersion(t *testing.T) {
client.Version = 2
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
rel1 := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusSuperseded,
LastDeployed: helmtime.Time{Time: deployedTime.Time.Add(-time.Hour)},
LastDeployed: deployedTime.Add(-time.Hour),
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
@ -384,7 +383,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 := helmtime.Now()
deployedTime := time.Now()
rel := &release.Release{
Name: releaseName,
@ -440,7 +439,7 @@ func TestGetMetadata_Run_EmptyAppVersion(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
rel := &release.Release{
Name: releaseName,

@ -29,7 +29,6 @@ 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.
@ -62,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: helmtime.Now(),
StartedAt: time.Now(),
Phase: release.HookPhaseRunning,
}
cfg.recordRelease(rl)
@ -76,7 +75,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 = helmtime.Now()
h.LastRun.CompletedAt = time.Now()
h.LastRun.Phase = release.HookPhaseFailed
return fmt.Errorf("warning: Hook %s %s failed: %w", hook, h.Path, err)
}
@ -88,7 +87,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 = helmtime.Now()
h.LastRun.CompletedAt = time.Now()
// Mark hook as succeeded or failed
if err != nil {
h.LastRun.Phase = release.HookPhaseFailed

@ -21,6 +21,7 @@ import (
"fmt"
"io"
"reflect"
"strings"
"testing"
"time"
@ -112,15 +113,15 @@ spec:
}
func convertHooksToCommaSeparated(hookDefinitions []release.HookOutputLogPolicy) string {
var commaSeparated string
var commaSeparated strings.Builder
for i, policy := range hookDefinitions {
if i+1 == len(hookDefinitions) {
commaSeparated += policy.String()
commaSeparated.WriteString(policy.String())
} else {
commaSeparated += policy.String() + ","
commaSeparated.WriteString(policy.String() + ",")
}
}
return commaSeparated
return commaSeparated.String()
}
func TestInstallRelease_HookOutputLogsOnFailure(t *testing.T) {

@ -49,7 +49,6 @@ 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 {
@ -856,32 +855,32 @@ func TestNameAndChartGenerateName(t *testing.T) {
{
"local filepath",
"./chart",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
{
"dot filepath",
".",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
{
"empty filepath",
"",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
{
"packaged chart",
"chart.tgz",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
{
"packaged chart with .tar.gz extension",
"chart.tar.gz",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
{
"packaged chart with local extension",
"./chart.tgz",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
}

@ -26,7 +26,6 @@ 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.
@ -158,7 +157,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
Config: previousRelease.Config,
Info: &release.Info{
FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: helmtime.Now(),
LastDeployed: time.Now(),
Status: release.StatusPendingRollback,
Notes: previousRelease.Info.Notes,
// Because we lose the reference to previous version elsewhere, we set the

@ -30,7 +30,6 @@ 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.
@ -110,7 +109,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 = helmtime.Now()
rel.Info.Deleted = time.Now()
rel.Info.Description = "Deletion in progress (or silently failed)"
res := &release.UninstallReleaseResponse{Release: rel}

@ -34,7 +34,6 @@ 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 {
@ -260,7 +259,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
withValues(chartDefaultValues),
withMetadataDependency(dependency),
)
now := helmtime.Now()
now := time.Now()
existingValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": false,

@ -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 = helmtime.Unix(1452902400, 0).UTC()
info.LastDeployed = time.Unix(1452902400, 0).UTC()
return []*release.Release{{
Name: "athos",
Namespace: "default",

@ -22,6 +22,7 @@ import (
"os"
"strings"
"testing"
"time"
shellwords "github.com/mattn/go-shellwords"
"github.com/spf13/cobra"
@ -34,7 +35,6 @@ 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() }

@ -31,7 +31,6 @@ 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 = `
@ -84,12 +83,12 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
type releaseInfo struct {
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"`
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"`
}
type releaseHistory []releaseInfo

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

@ -22,12 +22,11 @@ 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 = helmtime.Unix(1452902400, 0).UTC()
info.LastDeployed = time.Unix(1452902400, 0).UTC()
return []*release.Release{{
Name: "flummoxed-chickadee",
Namespace: "default",
@ -130,8 +129,8 @@ func TestStatusCmd(t *testing.T) {
runTestCmd(t, tests)
}
func mustParseTime(t string) helmtime.Time {
res, _ := helmtime.Parse(time.RFC3339, t)
func mustParseTime(t string) time.Time {
res, _ := time.Parse(time.RFC3339, t)
return res
}

@ -1 +1 @@
{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed"},"namespace":"default"}
{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00:00:00Z","status":"deployed"},"namespace":"default"}

@ -1 +1 @@
{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed","notes":"release notes"},"namespace":"default"}
{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00:00:00Z","status":"deployed","notes":"release notes"},"namespace":"default"}

@ -34,18 +34,6 @@ import (
"helm.sh/helm/v4/pkg/chart/common"
)
// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141
// > "template: %s: executing %q at <%s>: %s"
var execErrFmt = regexp.MustCompile(`^template: (?P<templateName>(?U).+): executing (?P<functionName>(?U).+) at (?P<location>(?U).+): (?P<errMsg>(?U).+)(?P<nextErr>( template:.*)?)$`)
// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138
// > "template: %s: %s"
var execErrFmtWithoutTemplate = regexp.MustCompile(`^template: (?P<templateName>(?U).+): (?P<errMsg>.*)(?P<nextErr>( template:.*)?)$`)
// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191
// > "template: no template %q associated with template %q"
var execErrNoTemplateAssociated = regexp.MustCompile(`^template: no template (?P<location>.*) associated with template (?P<functionName>(.*)?)$`)
// Engine is an implementation of the Helm rendering implementation for templates.
type Engine struct {
// If strict is enabled, template rendering will fail if a template references
@ -338,7 +326,7 @@ func cleanupParseError(filename string, err error) error {
location := tokens[1]
// The remaining tokens make up a stacktrace-like chain, ending with the relevant error
errMsg := tokens[len(tokens)-1]
return fmt.Errorf("parse error at (%s): %s", string(location), errMsg)
return fmt.Errorf("parse error at (%s): %s", location, errMsg)
}
type TraceableError struct {
@ -370,14 +358,117 @@ func (t TraceableError) String() string {
return errorString.String()
}
// parseTemplateExecErrorString parses a template execution error string from text/template
// without using regular expressions. It returns a TraceableError and true if parsing succeeded.
func parseTemplateExecErrorString(s string) (TraceableError, bool) {
const prefix = "template: "
if !strings.HasPrefix(s, prefix) {
return TraceableError{}, false
}
remainder := s[len(prefix):]
// Special case: "template: no template %q associated with template %q"
// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191
traceableError, done := parseTemplateNoTemplateError(s, remainder)
if done {
return traceableError, true
}
// Executing form: "<templateName>: executing \"<funcName>\" at <<location>>: <errMsg>[ template:...]"
// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141
traceableError, done = parseTemplateExecutingAtErrorType(remainder)
if done {
return traceableError, true
}
// Simple form: "<templateName>: <errMsg>"
// Use LastIndex to avoid splitting colons within line:col info.
// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138
traceableError, done = parseTemplateSimpleErrorString(remainder)
if done {
return traceableError, true
}
return TraceableError{}, false
}
// Special case: "template: no template %q associated with template %q"
// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191
func parseTemplateNoTemplateError(s string, remainder string) (TraceableError, bool) {
if strings.HasPrefix(remainder, "no template ") {
return TraceableError{message: s}, true
}
return TraceableError{}, false
}
// Simple form: "<templateName>: <errMsg>"
// Use LastIndex to avoid splitting colons within line:col info.
// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138
func parseTemplateSimpleErrorString(remainder string) (TraceableError, bool) {
if sep := strings.LastIndex(remainder, ": "); sep != -1 {
templateName := remainder[:sep]
errMsg := remainder[sep+2:]
if cut := strings.Index(errMsg, " template:"); cut != -1 {
errMsg = errMsg[:cut]
}
return TraceableError{location: templateName, message: errMsg}, true
}
return TraceableError{}, false
}
// Executing form: "<templateName>: executing \"<funcName>\" at <<location>>: <errMsg>[ template:...]"
// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141
func parseTemplateExecutingAtErrorType(remainder string) (TraceableError, bool) {
if idx := strings.Index(remainder, ": executing "); idx != -1 {
templateName := remainder[:idx]
after := remainder[idx+len(": executing "):]
if len(after) == 0 || after[0] != '"' {
return TraceableError{}, false
}
// find closing quote for function name
endQuote := strings.IndexByte(after[1:], '"')
if endQuote == -1 {
return TraceableError{}, false
}
endQuote++ // account for offset we started at 1
functionName := after[1:endQuote]
afterFunc := after[endQuote+1:]
// expect: " at <" then location then ">: " then message
const atPrefix = " at <"
if !strings.HasPrefix(afterFunc, atPrefix) {
return TraceableError{}, false
}
afterAt := afterFunc[len(atPrefix):]
endLoc := strings.Index(afterAt, ">: ")
if endLoc == -1 {
return TraceableError{}, false
}
locationName := afterAt[:endLoc]
errMsg := afterAt[endLoc+len(">: "):]
// trim chained next error starting with space + "template:" if present
if cut := strings.Index(errMsg, " template:"); cut != -1 {
errMsg = errMsg[:cut]
}
return TraceableError{
location: templateName,
message: errMsg,
executedFunction: "executing \"" + functionName + "\" at <" + locationName + ">:",
}, true
}
return TraceableError{}, false
}
// reformatExecErrorMsg takes an error message for template rendering and formats it into a formatted
// multi-line error string
func reformatExecErrorMsg(filename string, err error) error {
// This function matches the error message against regex's for the text/template package.
// If the regex's can parse out details from that error message such as the line number, template it failed on,
// This function parses the error message produced by text/template package.
// If it can parse out details from that error message such as the line number, template it failed on,
// and error description, then it will construct a new error that displays these details in a structured way.
// If there are issues with parsing the error message, the err passed into the function should return instead.
if _, isExecError := err.(template.ExecError); !isExecError {
var execError template.ExecError
if !errors.As(err, &execError) {
return err
}
@ -393,39 +484,18 @@ func reformatExecErrorMsg(filename string, err error) error {
parts := warnRegex.FindStringSubmatch(tokens[2])
if len(parts) >= 2 {
return fmt.Errorf("execution error at (%s): %s", string(location), parts[1])
return fmt.Errorf("execution error at (%s): %s", location, parts[1])
}
current := err
fileLocations := []TraceableError{}
var fileLocations []TraceableError
for current != nil {
var traceable TraceableError
if matches := execErrFmt.FindStringSubmatch(current.Error()); matches != nil {
templateName := matches[execErrFmt.SubexpIndex("templateName")]
functionName := matches[execErrFmt.SubexpIndex("functionName")]
locationName := matches[execErrFmt.SubexpIndex("location")]
errMsg := matches[execErrFmt.SubexpIndex("errMsg")]
traceable = TraceableError{
location: templateName,
message: errMsg,
executedFunction: "executing " + functionName + " at " + locationName + ":",
}
} else if matches := execErrFmtWithoutTemplate.FindStringSubmatch(current.Error()); matches != nil {
templateName := matches[execErrFmt.SubexpIndex("templateName")]
errMsg := matches[execErrFmt.SubexpIndex("errMsg")]
traceable = TraceableError{
location: templateName,
message: errMsg,
}
} else if matches := execErrNoTemplateAssociated.FindStringSubmatch(current.Error()); matches != nil {
traceable = TraceableError{
message: current.Error(),
if tr, ok := parseTemplateExecErrorString(current.Error()); ok {
if len(fileLocations) == 0 || fileLocations[len(fileLocations)-1] != tr {
fileLocations = append(fileLocations, tr)
}
} else {
return err
}
if len(fileLocations) == 0 || fileLocations[len(fileLocations)-1] != traceable {
fileLocations = append(fileLocations, traceable)
}
current = errors.Unwrap(current)
}

@ -1024,15 +1024,15 @@ func TestRenderRecursionLimit(t *testing.T) {
times := 4000
phrase := "All work and no play makes Jack a dull boy"
printFunc := `{{define "overlook"}}{{printf "` + phrase + `\n"}}{{end}}`
var repeatedIncl string
var repeatedIncl strings.Builder
for i := 0; i < times; i++ {
repeatedIncl += `{{include "overlook" . }}`
repeatedIncl.WriteString(`{{include "overlook" . }}`)
}
d := &chart.Chart{
Metadata: &chart.Metadata{Name: "overlook"},
Templates: []*common.File{
{Name: "templates/quote", Data: []byte(repeatedIncl)},
{Name: "templates/quote", Data: []byte(repeatedIncl.String())},
{Name: "templates/_function", Data: []byte(printFunc)},
},
}
@ -1429,3 +1429,50 @@ func TestRenderCustomTemplateFuncs(t *testing.T) {
t.Errorf("Expected %q, got %q", expected, rendered)
}
}
func TestTraceableError_SimpleForm(t *testing.T) {
testStrings := []string{
"function_not_found/templates/secret.yaml: error calling include",
}
for _, errString := range testStrings {
trace, done := parseTemplateSimpleErrorString(errString)
if !done {
t.Errorf("Expected parse to pass but did not")
}
if trace.message != "error calling include" {
t.Errorf("Expected %q, got %q", errString, trace.message)
}
}
}
func TestTraceableError_ExecutingForm(t *testing.T) {
testStrings := [][]string{
{"function_not_found/templates/secret.yaml:6:11: executing \"function_not_found/templates/secret.yaml\" at <include \"name\" .>: ", "function_not_found/templates/secret.yaml:6:11"},
{"divide_by_zero/templates/secret.yaml:6:11: executing \"divide_by_zero/templates/secret.yaml\" at <include \"division\" .>: ", "divide_by_zero/templates/secret.yaml:6:11"},
}
for _, errTuple := range testStrings {
errString := errTuple[0]
expectedLocation := errTuple[1]
trace, done := parseTemplateExecutingAtErrorType(errString)
if !done {
t.Errorf("Expected parse to pass but did not")
}
if trace.location != expectedLocation {
t.Errorf("Expected %q, got %q", expectedLocation, trace.location)
}
}
}
func TestTraceableError_NoTemplateForm(t *testing.T) {
testStrings := []string{
"no template \"common.names.get_name\" associated with template \"gotpl\"",
}
for _, errString := range testStrings {
trace, done := parseTemplateNoTemplateError(errString, errString)
if !done {
t.Errorf("Expected parse to pass but did not")
}
if trace.message != errString {
t.Errorf("Expected %q, got %q", errString, trace.message)
}
}
}

@ -29,7 +29,6 @@ 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
@ -91,7 +90,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 := ctime.Modified(stat)
chartArchiveFileCreatedTime := stat.ModTime()
pushOpts = append(pushOpts, registry.PushOptCreationTime(chartArchiveFileCreatedTime.Format(time.RFC3339)))
_, err = client.Push(chartBytes, ref, pushOpts...)

@ -28,7 +28,6 @@ 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"
@ -157,7 +156,7 @@ func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home)
if len(creationTime) == 0 {
creationTime = helmtime.Now().UTC().Format(time.RFC3339)
creationTime = time.Now().UTC().Format(time.RFC3339)
}
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, creationTime)

@ -24,12 +24,11 @@ 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 := helmtime.Now().Format(time.RFC3339)
nowString := time.Now().Format(time.RFC3339)
tests := []struct {
name string
@ -160,7 +159,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) {
func TestGenerateOCIAnnotations(t *testing.T) {
nowString := helmtime.Now().Format(time.RFC3339)
nowString := time.Now().Format(time.RFC3339)
tests := []struct {
name string
@ -234,7 +233,7 @@ func TestGenerateOCIAnnotations(t *testing.T) {
func TestGenerateOCICreatedAnnotations(t *testing.T) {
nowTime := helmtime.Now()
nowTime := time.Now()
nowTimeString := nowTime.Format(time.RFC3339)
chart := &chart.Metadata{
@ -250,7 +249,7 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) {
}
// Verify value of created artifact in RFC3339 format
if _, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil {
if _, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil {
t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated])
}
@ -262,7 +261,7 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) {
t.Errorf("%s annotation not created", ocispec.AnnotationCreated)
}
if createdTimeAnnotation, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil {
if createdTimeAnnotation, err := time.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

@ -17,7 +17,7 @@ limitations under the License.
package v1
import (
"helm.sh/helm/v4/pkg/time"
"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,omitempty"`
StartedAt time.Time `json:"started_at,omitzero"`
// CompletedAt indicates the date/time this hook was completed.
CompletedAt time.Time `json:"completed_at,omitempty"`
CompletedAt time.Time `json:"completed_at,omitzero"`
// Phase indicates whether the hook completed successfully
Phase HookPhase `json:"phase"`
}

@ -16,19 +16,19 @@ limitations under the License.
package v1
import (
"k8s.io/apimachinery/pkg/runtime"
"time"
"helm.sh/helm/v4/pkg/time"
"k8s.io/apimachinery/pkg/runtime"
)
// Info describes release information.
type Info struct {
// FirstDeployed is when the release was first deployed.
FirstDeployed time.Time `json:"first_deployed,omitempty"`
FirstDeployed time.Time `json:"first_deployed,omitzero"`
// LastDeployed is when the release was last deployed.
LastDeployed time.Time `json:"last_deployed,omitempty"`
LastDeployed time.Time `json:"last_deployed,omitzero"`
// Deleted tracks when this object was deleted.
Deleted time.Time `json:"deleted"`
Deleted time.Time `json:"deleted,omitzero"`
// Description is human-friendly "log entry" about this release.
Description string `json:"description,omitempty"`
// Status is the current state of the release

@ -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.

@ -21,7 +21,6 @@ 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.
@ -34,7 +33,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: helmtime.Now().Add(dur)}
info := &rspb.Info{Status: status, LastDeployed: time.Now().Add(dur)}
return &rspb.Release{
Name: name,
Version: vers,

@ -17,6 +17,7 @@ package strvals
import (
"fmt"
"strings"
"testing"
"sigs.k8s.io/yaml"
@ -416,14 +417,14 @@ func TestParseLiteralInto(t *testing.T) {
}
func TestParseLiteralNestedLevels(t *testing.T) {
var keyMultipleNestedLevels string
var keyMultipleNestedLevels strings.Builder
for i := 1; i <= MaxNestedNameLevel+2; i++ {
tmpStr := fmt.Sprintf("name%d", i)
if i <= MaxNestedNameLevel+1 {
tmpStr = tmpStr + "."
}
keyMultipleNestedLevels += tmpStr
keyMultipleNestedLevels.WriteString(tmpStr)
}
tests := []struct {
@ -439,7 +440,7 @@ func TestParseLiteralNestedLevels(t *testing.T) {
"",
},
{
str: keyMultipleNestedLevels + "=value",
str: keyMultipleNestedLevels.String() + "=value",
err: true,
errStr: fmt.Sprintf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel),
},

@ -17,6 +17,7 @@ package strvals
import (
"fmt"
"strings"
"testing"
"sigs.k8s.io/yaml"
@ -757,13 +758,13 @@ func TestToYAML(t *testing.T) {
}
func TestParseSetNestedLevels(t *testing.T) {
var keyMultipleNestedLevels string
var keyMultipleNestedLevels strings.Builder
for i := 1; i <= MaxNestedNameLevel+2; i++ {
tmpStr := fmt.Sprintf("name%d", i)
if i <= MaxNestedNameLevel+1 {
tmpStr = tmpStr + "."
}
keyMultipleNestedLevels += tmpStr
keyMultipleNestedLevels.WriteString(tmpStr)
}
tests := []struct {
str string
@ -778,7 +779,7 @@ func TestParseSetNestedLevels(t *testing.T) {
"",
},
{
str: keyMultipleNestedLevels + "=value",
str: keyMultipleNestedLevels.String() + "=value",
err: true,
errStr: fmt.Sprintf("value name nested level is greater than maximum supported nested level of %d",
MaxNestedNameLevel),

@ -1,29 +0,0 @@
/*
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)
}

@ -1,30 +0,0 @@
//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))
}

@ -1,27 +0,0 @@
//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()
}

@ -1,92 +0,0 @@
/*
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()} }

@ -1,153 +0,0 @@
/*
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())
}
Loading…
Cancel
Save