mirror of https://github.com/helm/helm
Introduce release/v2 that mirrors pkg/release/v1 but uses *v3.Chart from internal/chart/v3. The code is structurally identical to v1 with only import paths changed to reference internal/chart/v3 instead of pkg/chart/v2. - Add internal/release/v2 with Release, Info, Hook types - Add internal/release/v2/util with filter, sorter, manifest utilities - Update pkg/release/common.go with v2Accessor and v2HookAccessor - Copy all test files from pkg/release/v1 and add a v2 test in common_test.go Signed-off-by: Evans Mungai <mbuevans@gmail.com>pull/31709/head
parent
2e2cb05855
commit
6de5465762
@ -0,0 +1,17 @@
|
||||
/*
|
||||
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 v2 provides release handling for apiVersion v3 charts.
|
||||
package v2
|
||||
@ -0,0 +1,189 @@
|
||||
/*
|
||||
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 v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HookEvent specifies the hook event
|
||||
type HookEvent string
|
||||
|
||||
// Hook event types
|
||||
const (
|
||||
HookPreInstall HookEvent = "pre-install"
|
||||
HookPostInstall HookEvent = "post-install"
|
||||
HookPreDelete HookEvent = "pre-delete"
|
||||
HookPostDelete HookEvent = "post-delete"
|
||||
HookPreUpgrade HookEvent = "pre-upgrade"
|
||||
HookPostUpgrade HookEvent = "post-upgrade"
|
||||
HookPreRollback HookEvent = "pre-rollback"
|
||||
HookPostRollback HookEvent = "post-rollback"
|
||||
HookTest HookEvent = "test"
|
||||
)
|
||||
|
||||
func (x HookEvent) String() string { return string(x) }
|
||||
|
||||
// HookDeletePolicy specifies the hook delete policy
|
||||
type HookDeletePolicy string
|
||||
|
||||
// Hook delete policy types
|
||||
const (
|
||||
HookSucceeded HookDeletePolicy = "hook-succeeded"
|
||||
HookFailed HookDeletePolicy = "hook-failed"
|
||||
HookBeforeHookCreation HookDeletePolicy = "before-hook-creation"
|
||||
)
|
||||
|
||||
func (x HookDeletePolicy) String() string { return string(x) }
|
||||
|
||||
// HookOutputLogPolicy specifies the hook output log policy
|
||||
type HookOutputLogPolicy string
|
||||
|
||||
// Hook output log policy types
|
||||
const (
|
||||
HookOutputOnSucceeded HookOutputLogPolicy = "hook-succeeded"
|
||||
HookOutputOnFailed HookOutputLogPolicy = "hook-failed"
|
||||
)
|
||||
|
||||
func (x HookOutputLogPolicy) String() string { return string(x) }
|
||||
|
||||
// HookAnnotation is the label name for a hook
|
||||
const HookAnnotation = "helm.sh/hook"
|
||||
|
||||
// HookWeightAnnotation is the label name for a hook weight
|
||||
const HookWeightAnnotation = "helm.sh/hook-weight"
|
||||
|
||||
// HookDeleteAnnotation is the label name for the delete policy for a hook
|
||||
const HookDeleteAnnotation = "helm.sh/hook-delete-policy"
|
||||
|
||||
// HookOutputLogAnnotation is the label name for the output log policy for a hook
|
||||
const HookOutputLogAnnotation = "helm.sh/hook-output-log-policy"
|
||||
|
||||
// Hook defines a hook object.
|
||||
type Hook struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
// Kind is the Kubernetes kind.
|
||||
Kind string `json:"kind,omitempty"`
|
||||
// Path is the chart-relative path to the template.
|
||||
Path string `json:"path,omitempty"`
|
||||
// Manifest is the manifest contents.
|
||||
Manifest string `json:"manifest,omitempty"`
|
||||
// Events are the events that this hook fires on.
|
||||
Events []HookEvent `json:"events,omitempty"`
|
||||
// LastRun indicates the date/time this was last run.
|
||||
LastRun HookExecution `json:"last_run,omitempty"`
|
||||
// Weight indicates the sort order for execution among similar Hook type
|
||||
Weight int `json:"weight,omitempty"`
|
||||
// DeletePolicies are the policies that indicate when to delete the hook
|
||||
DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"`
|
||||
// OutputLogPolicies defines whether we should copy hook logs back to main process
|
||||
OutputLogPolicies []HookOutputLogPolicy `json:"output_log_policies,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// CompletedAt indicates the date/time this hook was completed.
|
||||
CompletedAt time.Time `json:"completed_at,omitzero"`
|
||||
// Phase indicates whether the hook completed successfully
|
||||
Phase HookPhase `json:"phase"`
|
||||
}
|
||||
|
||||
// A HookPhase indicates the state of a hook execution
|
||||
type HookPhase string
|
||||
|
||||
const (
|
||||
// HookPhaseUnknown indicates that a hook is in an unknown state
|
||||
HookPhaseUnknown HookPhase = "Unknown"
|
||||
// HookPhaseRunning indicates that a hook is currently executing
|
||||
HookPhaseRunning HookPhase = "Running"
|
||||
// HookPhaseSucceeded indicates that hook execution succeeded
|
||||
HookPhaseSucceeded HookPhase = "Succeeded"
|
||||
// HookPhaseFailed indicates that hook execution failed
|
||||
HookPhaseFailed HookPhase = "Failed"
|
||||
)
|
||||
|
||||
// String converts a hook phase to a printable string
|
||||
func (x HookPhase) String() string { return string(x) }
|
||||
|
||||
// hookExecutionJSON is used for custom JSON marshaling/unmarshaling
|
||||
type hookExecutionJSON struct {
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Phase HookPhase `json:"phase"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
// It handles empty string time fields by treating them as zero values.
|
||||
func (h *HookExecution) UnmarshalJSON(data []byte) error {
|
||||
// First try to unmarshal into a map to handle empty string time fields
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Replace empty string time fields with nil
|
||||
for _, field := range []string{"started_at", "completed_at"} {
|
||||
if val, ok := raw[field]; ok {
|
||||
if str, ok := val.(string); ok && str == "" {
|
||||
raw[field] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-marshal with cleaned data
|
||||
cleaned, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unmarshal into temporary struct with pointer time fields
|
||||
var tmp hookExecutionJSON
|
||||
if err := json.Unmarshal(cleaned, &tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy values to HookExecution struct
|
||||
if tmp.StartedAt != nil {
|
||||
h.StartedAt = *tmp.StartedAt
|
||||
}
|
||||
if tmp.CompletedAt != nil {
|
||||
h.CompletedAt = *tmp.CompletedAt
|
||||
}
|
||||
h.Phase = tmp.Phase
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
// It omits zero-value time fields from the JSON output.
|
||||
func (h HookExecution) MarshalJSON() ([]byte, error) {
|
||||
tmp := hookExecutionJSON{
|
||||
Phase: h.Phase,
|
||||
}
|
||||
|
||||
if !h.StartedAt.IsZero() {
|
||||
tmp.StartedAt = &h.StartedAt
|
||||
}
|
||||
if !h.CompletedAt.IsZero() {
|
||||
tmp.CompletedAt = &h.CompletedAt
|
||||
}
|
||||
|
||||
return json.Marshal(tmp)
|
||||
}
|
||||
@ -0,0 +1,231 @@
|
||||
/*
|
||||
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 v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHookExecutionMarshalJSON(t *testing.T) {
|
||||
started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
|
||||
completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
exec HookExecution
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "all fields populated",
|
||||
exec: HookExecution{
|
||||
StartedAt: started,
|
||||
CompletedAt: completed,
|
||||
Phase: HookPhaseSucceeded,
|
||||
},
|
||||
expected: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Succeeded"}`,
|
||||
},
|
||||
{
|
||||
name: "only phase",
|
||||
exec: HookExecution{
|
||||
Phase: HookPhaseRunning,
|
||||
},
|
||||
expected: `{"phase":"Running"}`,
|
||||
},
|
||||
{
|
||||
name: "with started time only",
|
||||
exec: HookExecution{
|
||||
StartedAt: started,
|
||||
Phase: HookPhaseRunning,
|
||||
},
|
||||
expected: `{"started_at":"2025-10-08T12:00:00Z","phase":"Running"}`,
|
||||
},
|
||||
{
|
||||
name: "failed phase",
|
||||
exec: HookExecution{
|
||||
StartedAt: started,
|
||||
CompletedAt: completed,
|
||||
Phase: HookPhaseFailed,
|
||||
},
|
||||
expected: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Failed"}`,
|
||||
},
|
||||
{
|
||||
name: "unknown phase",
|
||||
exec: HookExecution{
|
||||
Phase: HookPhaseUnknown,
|
||||
},
|
||||
expected: `{"phase":"Unknown"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := json.Marshal(&tt.exec)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, tt.expected, string(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHookExecutionUnmarshalJSON(t *testing.T) {
|
||||
started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
|
||||
completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected HookExecution
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "all fields populated",
|
||||
input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Succeeded"}`,
|
||||
expected: HookExecution{
|
||||
StartedAt: started,
|
||||
CompletedAt: completed,
|
||||
Phase: HookPhaseSucceeded,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only phase",
|
||||
input: `{"phase":"Running"}`,
|
||||
expected: HookExecution{
|
||||
Phase: HookPhaseRunning,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty string time fields",
|
||||
input: `{"started_at":"","completed_at":"","phase":"Succeeded"}`,
|
||||
expected: HookExecution{
|
||||
Phase: HookPhaseSucceeded,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing time fields",
|
||||
input: `{"phase":"Failed"}`,
|
||||
expected: HookExecution{
|
||||
Phase: HookPhaseFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "null time fields",
|
||||
input: `{"started_at":null,"completed_at":null,"phase":"Unknown"}`,
|
||||
expected: HookExecution{
|
||||
Phase: HookPhaseUnknown,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed empty and valid time fields",
|
||||
input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"","phase":"Running"}`,
|
||||
expected: HookExecution{
|
||||
StartedAt: started,
|
||||
Phase: HookPhaseRunning,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with started time only",
|
||||
input: `{"started_at":"2025-10-08T12:00:00Z","phase":"Running"}`,
|
||||
expected: HookExecution{
|
||||
StartedAt: started,
|
||||
Phase: HookPhaseRunning,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed phase with times",
|
||||
input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Failed"}`,
|
||||
expected: HookExecution{
|
||||
StartedAt: started,
|
||||
CompletedAt: completed,
|
||||
Phase: HookPhaseFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid time format",
|
||||
input: `{"started_at":"invalid-time","phase":"Running"}`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var exec HookExecution
|
||||
err := json.Unmarshal([]byte(tt.input), &exec)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected.StartedAt.Unix(), exec.StartedAt.Unix())
|
||||
assert.Equal(t, tt.expected.CompletedAt.Unix(), exec.CompletedAt.Unix())
|
||||
assert.Equal(t, tt.expected.Phase, exec.Phase)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHookExecutionRoundTrip(t *testing.T) {
|
||||
started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
|
||||
completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC)
|
||||
|
||||
original := HookExecution{
|
||||
StartedAt: started,
|
||||
CompletedAt: completed,
|
||||
Phase: HookPhaseSucceeded,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(&original)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded HookExecution
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, original.StartedAt.Unix(), decoded.StartedAt.Unix())
|
||||
assert.Equal(t, original.CompletedAt.Unix(), decoded.CompletedAt.Unix())
|
||||
assert.Equal(t, original.Phase, decoded.Phase)
|
||||
}
|
||||
|
||||
func TestHookExecutionEmptyStringRoundTrip(t *testing.T) {
|
||||
// This test specifically verifies that empty string time fields
|
||||
// are handled correctly during parsing
|
||||
input := `{"started_at":"","completed_at":"","phase":"Succeeded"}`
|
||||
|
||||
var exec HookExecution
|
||||
err := json.Unmarshal([]byte(input), &exec)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify time fields are zero values
|
||||
assert.True(t, exec.StartedAt.IsZero())
|
||||
assert.True(t, exec.CompletedAt.IsZero())
|
||||
assert.Equal(t, HookPhaseSucceeded, exec.Phase)
|
||||
|
||||
// Marshal back and verify empty time fields are omitted
|
||||
data, err := json.Marshal(&exec)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Zero time values should be omitted
|
||||
assert.NotContains(t, result, "started_at")
|
||||
assert.NotContains(t, result, "completed_at")
|
||||
assert.Equal(t, "Succeeded", result["phase"])
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
/*
|
||||
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 v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"helm.sh/helm/v4/pkg/release/common"
|
||||
|
||||
"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,omitzero"`
|
||||
// LastDeployed is when the release was last deployed.
|
||||
LastDeployed time.Time `json:"last_deployed,omitzero"`
|
||||
// Deleted tracks when this object was 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
|
||||
Status common.Status `json:"status,omitempty"`
|
||||
// Contains the rendered templates/NOTES.txt if available
|
||||
Notes string `json:"notes,omitempty"`
|
||||
// Contains the deployed resources information
|
||||
Resources map[string][]runtime.Object `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
// infoJSON is used for custom JSON marshaling/unmarshaling
|
||||
type infoJSON struct {
|
||||
FirstDeployed *time.Time `json:"first_deployed,omitempty"`
|
||||
LastDeployed *time.Time `json:"last_deployed,omitempty"`
|
||||
Deleted *time.Time `json:"deleted,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Status common.Status `json:"status,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Resources map[string][]runtime.Object `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
// It handles empty string time fields by treating them as zero values.
|
||||
func (i *Info) UnmarshalJSON(data []byte) error {
|
||||
// First try to unmarshal into a map to handle empty string time fields
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Replace empty string time fields with nil
|
||||
for _, field := range []string{"first_deployed", "last_deployed", "deleted"} {
|
||||
if val, ok := raw[field]; ok {
|
||||
if str, ok := val.(string); ok && str == "" {
|
||||
raw[field] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-marshal with cleaned data
|
||||
cleaned, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unmarshal into temporary struct with pointer time fields
|
||||
var tmp infoJSON
|
||||
if err := json.Unmarshal(cleaned, &tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy values to Info struct
|
||||
if tmp.FirstDeployed != nil {
|
||||
i.FirstDeployed = *tmp.FirstDeployed
|
||||
}
|
||||
if tmp.LastDeployed != nil {
|
||||
i.LastDeployed = *tmp.LastDeployed
|
||||
}
|
||||
if tmp.Deleted != nil {
|
||||
i.Deleted = *tmp.Deleted
|
||||
}
|
||||
i.Description = tmp.Description
|
||||
i.Status = tmp.Status
|
||||
i.Notes = tmp.Notes
|
||||
i.Resources = tmp.Resources
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
// It omits zero-value time fields from the JSON output.
|
||||
func (i Info) MarshalJSON() ([]byte, error) {
|
||||
tmp := infoJSON{
|
||||
Description: i.Description,
|
||||
Status: i.Status,
|
||||
Notes: i.Notes,
|
||||
Resources: i.Resources,
|
||||
}
|
||||
|
||||
if !i.FirstDeployed.IsZero() {
|
||||
tmp.FirstDeployed = &i.FirstDeployed
|
||||
}
|
||||
if !i.LastDeployed.IsZero() {
|
||||
tmp.LastDeployed = &i.LastDeployed
|
||||
}
|
||||
if !i.Deleted.IsZero() {
|
||||
tmp.Deleted = &i.Deleted
|
||||
}
|
||||
|
||||
return json.Marshal(tmp)
|
||||
}
|
||||
@ -0,0 +1,285 @@
|
||||
/*
|
||||
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 v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"helm.sh/helm/v4/pkg/release/common"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInfoMarshalJSON(t *testing.T) {
|
||||
now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
|
||||
later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC)
|
||||
deleted := time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
info Info
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "all fields populated",
|
||||
info: Info{
|
||||
FirstDeployed: now,
|
||||
LastDeployed: later,
|
||||
Deleted: deleted,
|
||||
Description: "Test release",
|
||||
Status: common.StatusDeployed,
|
||||
Notes: "Test notes",
|
||||
},
|
||||
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Test release","status":"deployed","notes":"Test notes"}`,
|
||||
},
|
||||
{
|
||||
name: "only required fields",
|
||||
info: Info{
|
||||
FirstDeployed: now,
|
||||
LastDeployed: later,
|
||||
Status: common.StatusDeployed,
|
||||
},
|
||||
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed"}`,
|
||||
},
|
||||
{
|
||||
name: "zero time values omitted",
|
||||
info: Info{
|
||||
Description: "Test release",
|
||||
Status: common.StatusDeployed,
|
||||
},
|
||||
expected: `{"description":"Test release","status":"deployed"}`,
|
||||
},
|
||||
{
|
||||
name: "with pending status",
|
||||
info: Info{
|
||||
FirstDeployed: now,
|
||||
LastDeployed: later,
|
||||
Status: common.StatusPendingInstall,
|
||||
Description: "Installing release",
|
||||
},
|
||||
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","description":"Installing release","status":"pending-install"}`,
|
||||
},
|
||||
{
|
||||
name: "uninstalled with deleted time",
|
||||
info: Info{
|
||||
FirstDeployed: now,
|
||||
LastDeployed: later,
|
||||
Deleted: deleted,
|
||||
Status: common.StatusUninstalled,
|
||||
Description: "Uninstalled release",
|
||||
},
|
||||
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Uninstalled release","status":"uninstalled"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := json.Marshal(&tt.info)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, tt.expected, string(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoUnmarshalJSON(t *testing.T) {
|
||||
now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
|
||||
later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC)
|
||||
deleted := time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected Info
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "all fields populated",
|
||||
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Test release","status":"deployed","notes":"Test notes"}`,
|
||||
expected: Info{
|
||||
FirstDeployed: now,
|
||||
LastDeployed: later,
|
||||
Deleted: deleted,
|
||||
Description: "Test release",
|
||||
Status: common.StatusDeployed,
|
||||
Notes: "Test notes",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only required fields",
|
||||
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed"}`,
|
||||
expected: Info{
|
||||
FirstDeployed: now,
|
||||
LastDeployed: later,
|
||||
Status: common.StatusDeployed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty string time fields",
|
||||
input: `{"first_deployed":"","last_deployed":"","deleted":"","description":"Test release","status":"deployed"}`,
|
||||
expected: Info{
|
||||
Description: "Test release",
|
||||
Status: common.StatusDeployed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing time fields",
|
||||
input: `{"description":"Test release","status":"deployed"}`,
|
||||
expected: Info{
|
||||
Description: "Test release",
|
||||
Status: common.StatusDeployed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "null time fields",
|
||||
input: `{"first_deployed":null,"last_deployed":null,"deleted":null,"description":"Test release","status":"deployed"}`,
|
||||
expected: Info{
|
||||
Description: "Test release",
|
||||
Status: common.StatusDeployed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed empty and valid time fields",
|
||||
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"","deleted":"","status":"deployed"}`,
|
||||
expected: Info{
|
||||
FirstDeployed: now,
|
||||
Status: common.StatusDeployed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending install status",
|
||||
input: `{"first_deployed":"2025-10-08T12:00:00Z","status":"pending-install","description":"Installing"}`,
|
||||
expected: Info{
|
||||
FirstDeployed: now,
|
||||
Status: common.StatusPendingInstall,
|
||||
Description: "Installing",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uninstalled with deleted time",
|
||||
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","status":"uninstalled"}`,
|
||||
expected: Info{
|
||||
FirstDeployed: now,
|
||||
LastDeployed: later,
|
||||
Deleted: deleted,
|
||||
Status: common.StatusUninstalled,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed status",
|
||||
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"failed","description":"Deployment failed"}`,
|
||||
expected: Info{
|
||||
FirstDeployed: now,
|
||||
LastDeployed: later,
|
||||
Status: common.StatusFailed,
|
||||
Description: "Deployment failed",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid time format",
|
||||
input: `{"first_deployed":"invalid-time","status":"deployed"}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty object",
|
||||
input: `{}`,
|
||||
expected: Info{
|
||||
Status: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var info Info
|
||||
err := json.Unmarshal([]byte(tt.input), &info)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected.FirstDeployed.Unix(), info.FirstDeployed.Unix())
|
||||
assert.Equal(t, tt.expected.LastDeployed.Unix(), info.LastDeployed.Unix())
|
||||
assert.Equal(t, tt.expected.Deleted.Unix(), info.Deleted.Unix())
|
||||
assert.Equal(t, tt.expected.Description, info.Description)
|
||||
assert.Equal(t, tt.expected.Status, info.Status)
|
||||
assert.Equal(t, tt.expected.Notes, info.Notes)
|
||||
assert.Equal(t, tt.expected.Resources, info.Resources)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoRoundTrip(t *testing.T) {
|
||||
now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
|
||||
later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC)
|
||||
|
||||
original := Info{
|
||||
FirstDeployed: now,
|
||||
LastDeployed: later,
|
||||
Description: "Test release",
|
||||
Status: common.StatusDeployed,
|
||||
Notes: "Release notes",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(&original)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded Info
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, original.FirstDeployed.Unix(), decoded.FirstDeployed.Unix())
|
||||
assert.Equal(t, original.LastDeployed.Unix(), decoded.LastDeployed.Unix())
|
||||
assert.Equal(t, original.Deleted.Unix(), decoded.Deleted.Unix())
|
||||
assert.Equal(t, original.Description, decoded.Description)
|
||||
assert.Equal(t, original.Status, decoded.Status)
|
||||
assert.Equal(t, original.Notes, decoded.Notes)
|
||||
}
|
||||
|
||||
func TestInfoEmptyStringRoundTrip(t *testing.T) {
|
||||
// This test specifically verifies that empty string time fields
|
||||
// are handled correctly during parsing
|
||||
input := `{"first_deployed":"","last_deployed":"","deleted":"","status":"deployed","description":"test"}`
|
||||
|
||||
var info Info
|
||||
err := json.Unmarshal([]byte(input), &info)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify time fields are zero values
|
||||
assert.True(t, info.FirstDeployed.IsZero())
|
||||
assert.True(t, info.LastDeployed.IsZero())
|
||||
assert.True(t, info.Deleted.IsZero())
|
||||
assert.Equal(t, common.StatusDeployed, info.Status)
|
||||
assert.Equal(t, "test", info.Description)
|
||||
|
||||
// Marshal back and verify empty time fields are omitted
|
||||
data, err := json.Marshal(&info)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Zero time values should be omitted due to omitzero tag
|
||||
assert.NotContains(t, result, "first_deployed")
|
||||
assert.NotContains(t, result, "last_deployed")
|
||||
assert.NotContains(t, result, "deleted")
|
||||
assert.Equal(t, "deployed", result["status"])
|
||||
assert.Equal(t, "test", result["description"])
|
||||
}
|
||||
@ -0,0 +1,143 @@
|
||||
/*
|
||||
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 v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
v3 "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
rcommon "helm.sh/helm/v4/pkg/release/common"
|
||||
)
|
||||
|
||||
// MockHookTemplate is the hook template used for all mock release objects.
|
||||
var MockHookTemplate = `apiVersion: v1
|
||||
kind: Job
|
||||
metadata:
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install
|
||||
`
|
||||
|
||||
// MockManifest is the manifest used for all mock release objects.
|
||||
var MockManifest = `apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: fixture
|
||||
`
|
||||
|
||||
// MockReleaseOptions allows for user-configurable options on mock release objects.
|
||||
type MockReleaseOptions struct {
|
||||
Name string
|
||||
Version int
|
||||
Chart *v3.Chart
|
||||
Status rcommon.Status
|
||||
Namespace string
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
name := opts.Name
|
||||
if name == "" {
|
||||
name = "testrelease-" + fmt.Sprint(rand.Intn(100))
|
||||
}
|
||||
|
||||
version := 1
|
||||
if opts.Version != 0 {
|
||||
version = opts.Version
|
||||
}
|
||||
|
||||
namespace := opts.Namespace
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
var labels map[string]string
|
||||
if len(opts.Labels) > 0 {
|
||||
labels = opts.Labels
|
||||
}
|
||||
|
||||
ch := opts.Chart
|
||||
if opts.Chart == nil {
|
||||
ch = &v3.Chart{
|
||||
Metadata: &v3.Metadata{
|
||||
Name: "foo",
|
||||
Version: "0.1.0-beta.1",
|
||||
AppVersion: "1.0",
|
||||
APIVersion: v3.APIVersionV3,
|
||||
Annotations: map[string]string{
|
||||
"category": "web-apps",
|
||||
"supported": "true",
|
||||
},
|
||||
Dependencies: []*v3.Dependency{
|
||||
{
|
||||
Name: "cool-plugin",
|
||||
Version: "1.0.0",
|
||||
Repository: "https://coolplugin.io/charts",
|
||||
Condition: "coolPlugin.enabled",
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
Name: "crds",
|
||||
Version: "2.7.1",
|
||||
Condition: "crds.enabled",
|
||||
},
|
||||
},
|
||||
},
|
||||
Templates: []*common.File{
|
||||
{Name: "templates/foo.tpl", ModTime: time.Now(), Data: []byte(MockManifest)},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
scode := rcommon.StatusDeployed
|
||||
if len(opts.Status) > 0 {
|
||||
scode = opts.Status
|
||||
}
|
||||
|
||||
info := &Info{
|
||||
FirstDeployed: date,
|
||||
LastDeployed: date,
|
||||
Status: scode,
|
||||
Description: "Release mock",
|
||||
Notes: "Some mock release notes!",
|
||||
}
|
||||
|
||||
return &Release{
|
||||
Name: name,
|
||||
Info: info,
|
||||
Chart: ch,
|
||||
Config: map[string]interface{}{"name": "value"},
|
||||
Version: version,
|
||||
Namespace: namespace,
|
||||
Hooks: []*Hook{
|
||||
{
|
||||
Name: "pre-install-hook",
|
||||
Kind: "Job",
|
||||
Path: "pre-install-hook.yaml",
|
||||
Manifest: MockHookTemplate,
|
||||
LastRun: HookExecution{},
|
||||
Events: []HookEvent{HookPreInstall},
|
||||
},
|
||||
},
|
||||
Manifest: MockManifest,
|
||||
Labels: labels,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
/*
|
||||
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 v2
|
||||
|
||||
import (
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/pkg/release/common"
|
||||
)
|
||||
|
||||
type ApplyMethod string
|
||||
|
||||
const ApplyMethodClientSideApply ApplyMethod = "csa"
|
||||
const ApplyMethodServerSideApply ApplyMethod = "ssa"
|
||||
|
||||
// Release describes a deployment of a chart, together with the chart
|
||||
// and the variables used to deploy that chart.
|
||||
type Release struct {
|
||||
// Name is the name of the release
|
||||
Name string `json:"name,omitempty"`
|
||||
// Info provides information about a release
|
||||
Info *Info `json:"info,omitempty"`
|
||||
// Chart is the chart that was released.
|
||||
Chart *chart.Chart `json:"chart,omitempty"`
|
||||
// Config is the set of extra Values added to the chart.
|
||||
// These values override the default values inside of the chart.
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
// Manifest is the string representation of the rendered template.
|
||||
Manifest string `json:"manifest,omitempty"`
|
||||
// Hooks are all of the hooks declared for this release.
|
||||
Hooks []*Hook `json:"hooks,omitempty"`
|
||||
// Version is an int which represents the revision of the release.
|
||||
Version int `json:"version,omitempty"`
|
||||
// Namespace is the kubernetes namespace of the release.
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
// Labels of the release.
|
||||
// Disabled encoding into Json cause labels are stored in storage driver metadata field.
|
||||
Labels map[string]string `json:"-"`
|
||||
// ApplyMethod stores whether server-side or client-side apply was used for the release
|
||||
// Unset (empty string) should be treated as the default of client-side apply
|
||||
ApplyMethod string `json:"apply_method,omitempty"` // "ssa" | "csa"
|
||||
}
|
||||
|
||||
// SetStatus is a helper for setting the status on a release.
|
||||
func (r *Release) SetStatus(status common.Status, msg string) {
|
||||
r.Info.Status = status
|
||||
r.Info.Description = msg
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
/*
|
||||
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
|
||||
|
||||
import (
|
||||
v2 "helm.sh/helm/v4/internal/release/v2"
|
||||
"helm.sh/helm/v4/pkg/release/common"
|
||||
)
|
||||
|
||||
// FilterFunc returns true if the release object satisfies
|
||||
// the predicate of the underlying filter func.
|
||||
type FilterFunc func(*v2.Release) bool
|
||||
|
||||
// Check applies the FilterFunc to the release object.
|
||||
func (fn FilterFunc) Check(rls *v2.Release) bool {
|
||||
if rls == nil {
|
||||
return false
|
||||
}
|
||||
return fn(rls)
|
||||
}
|
||||
|
||||
// Filter applies the filter(s) to the list of provided releases
|
||||
// returning the list that satisfies the filtering predicate.
|
||||
func (fn FilterFunc) Filter(rels []*v2.Release) (rets []*v2.Release) {
|
||||
for _, rel := range rels {
|
||||
if fn.Check(rel) {
|
||||
rets = append(rets, rel)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Any returns a FilterFunc that filters a list of releases
|
||||
// determined by the predicate 'f0 || f1 || ... || fn'.
|
||||
func Any(filters ...FilterFunc) FilterFunc {
|
||||
return func(rls *v2.Release) bool {
|
||||
for _, filter := range filters {
|
||||
if filter(rls) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// All returns a FilterFunc that filters a list of releases
|
||||
// determined by the predicate 'f0 && f1 && ... && fn'.
|
||||
func All(filters ...FilterFunc) FilterFunc {
|
||||
return func(rls *v2.Release) bool {
|
||||
for _, filter := range filters {
|
||||
if !filter(rls) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// StatusFilter filters a set of releases by status code.
|
||||
func StatusFilter(status common.Status) FilterFunc {
|
||||
return FilterFunc(func(rls *v2.Release) bool {
|
||||
if rls == nil {
|
||||
return true
|
||||
}
|
||||
return rls.Info.Status == status
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
/*
|
||||
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
rspb "helm.sh/helm/v4/internal/release/v2"
|
||||
"helm.sh/helm/v4/pkg/release/common"
|
||||
)
|
||||
|
||||
func TestFilterAny(t *testing.T) {
|
||||
ls := Any(StatusFilter(common.StatusUninstalled)).Filter(releases)
|
||||
if len(ls) != 2 {
|
||||
t.Fatalf("expected 2 results, got '%d'", len(ls))
|
||||
}
|
||||
|
||||
r0, r1 := ls[0], ls[1]
|
||||
switch {
|
||||
case r0.Info.Status != common.StatusUninstalled:
|
||||
t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String())
|
||||
case r1.Info.Status != common.StatusUninstalled:
|
||||
t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterAll(t *testing.T) {
|
||||
fn := FilterFunc(func(rls *rspb.Release) bool {
|
||||
// true if not uninstalled and version < 4
|
||||
v0 := !StatusFilter(common.StatusUninstalled).Check(rls)
|
||||
v1 := rls.Version < 4
|
||||
return v0 && v1
|
||||
})
|
||||
|
||||
ls := All(fn).Filter(releases)
|
||||
if len(ls) != 1 {
|
||||
t.Fatalf("expected 1 result, got '%d'", len(ls))
|
||||
}
|
||||
|
||||
switch r0 := ls[0]; {
|
||||
case r0.Version == 4:
|
||||
t.Fatal("got release with status revision 4")
|
||||
case r0.Info.Status == common.StatusUninstalled:
|
||||
t.Fatal("got release with status UNINSTALLED")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
/*
|
||||
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
release "helm.sh/helm/v4/internal/release/v2"
|
||||
)
|
||||
|
||||
// KindSortOrder is an ordering of Kinds.
|
||||
type KindSortOrder []string
|
||||
|
||||
// InstallOrder is the order in which manifests should be installed (by Kind).
|
||||
//
|
||||
// Those occurring earlier in the list get installed before those occurring later in the list.
|
||||
var InstallOrder KindSortOrder = []string{
|
||||
"PriorityClass",
|
||||
"Namespace",
|
||||
"NetworkPolicy",
|
||||
"ResourceQuota",
|
||||
"LimitRange",
|
||||
"PodSecurityPolicy",
|
||||
"PodDisruptionBudget",
|
||||
"ServiceAccount",
|
||||
"Secret",
|
||||
"SecretList",
|
||||
"ConfigMap",
|
||||
"StorageClass",
|
||||
"PersistentVolume",
|
||||
"PersistentVolumeClaim",
|
||||
"CustomResourceDefinition",
|
||||
"ClusterRole",
|
||||
"ClusterRoleList",
|
||||
"ClusterRoleBinding",
|
||||
"ClusterRoleBindingList",
|
||||
"Role",
|
||||
"RoleList",
|
||||
"RoleBinding",
|
||||
"RoleBindingList",
|
||||
"Service",
|
||||
"DaemonSet",
|
||||
"Pod",
|
||||
"ReplicationController",
|
||||
"ReplicaSet",
|
||||
"Deployment",
|
||||
"HorizontalPodAutoscaler",
|
||||
"StatefulSet",
|
||||
"Job",
|
||||
"CronJob",
|
||||
"IngressClass",
|
||||
"Ingress",
|
||||
"APIService",
|
||||
"MutatingWebhookConfiguration",
|
||||
"ValidatingWebhookConfiguration",
|
||||
}
|
||||
|
||||
// UninstallOrder is the order in which manifests should be uninstalled (by Kind).
|
||||
//
|
||||
// Those occurring earlier in the list get uninstalled before those occurring later in the list.
|
||||
var UninstallOrder KindSortOrder = []string{
|
||||
// For uninstall, we remove validation before mutation to ensure webhooks don't block removal
|
||||
"ValidatingWebhookConfiguration",
|
||||
"MutatingWebhookConfiguration",
|
||||
"APIService",
|
||||
"Ingress",
|
||||
"IngressClass",
|
||||
"Service",
|
||||
"CronJob",
|
||||
"Job",
|
||||
"StatefulSet",
|
||||
"HorizontalPodAutoscaler",
|
||||
"Deployment",
|
||||
"ReplicaSet",
|
||||
"ReplicationController",
|
||||
"Pod",
|
||||
"DaemonSet",
|
||||
"RoleBindingList",
|
||||
"RoleBinding",
|
||||
"RoleList",
|
||||
"Role",
|
||||
"ClusterRoleBindingList",
|
||||
"ClusterRoleBinding",
|
||||
"ClusterRoleList",
|
||||
"ClusterRole",
|
||||
"CustomResourceDefinition",
|
||||
"PersistentVolumeClaim",
|
||||
"PersistentVolume",
|
||||
"StorageClass",
|
||||
"ConfigMap",
|
||||
"SecretList",
|
||||
"Secret",
|
||||
"ServiceAccount",
|
||||
"PodDisruptionBudget",
|
||||
"PodSecurityPolicy",
|
||||
"LimitRange",
|
||||
"ResourceQuota",
|
||||
"NetworkPolicy",
|
||||
"Namespace",
|
||||
"PriorityClass",
|
||||
}
|
||||
|
||||
// sort manifests by kind.
|
||||
//
|
||||
// Results are sorted by 'ordering', keeping order of items with equal kind/priority
|
||||
func sortManifestsByKind(manifests []Manifest, ordering KindSortOrder) []Manifest {
|
||||
sort.SliceStable(manifests, func(i, j int) bool {
|
||||
return lessByKind(manifests[i], manifests[j], manifests[i].Head.Kind, manifests[j].Head.Kind, ordering)
|
||||
})
|
||||
|
||||
return manifests
|
||||
}
|
||||
|
||||
// sort hooks by kind, using an out-of-place sort to preserve the input parameters.
|
||||
//
|
||||
// Results are sorted by 'ordering', keeping order of items with equal kind/priority
|
||||
func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.Hook {
|
||||
h := hooks
|
||||
sort.SliceStable(h, func(i, j int) bool {
|
||||
return lessByKind(h[i], h[j], h[i].Kind, h[j].Kind, ordering)
|
||||
})
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func lessByKind(_ interface{}, _ interface{}, kindA string, kindB string, o KindSortOrder) bool {
|
||||
ordering := make(map[string]int, len(o))
|
||||
for v, k := range o {
|
||||
ordering[k] = v
|
||||
}
|
||||
|
||||
first, aok := ordering[kindA]
|
||||
second, bok := ordering[kindB]
|
||||
|
||||
if !aok && !bok {
|
||||
// if both are unknown then sort alphabetically by kind, keep original order if same kind
|
||||
if kindA != kindB {
|
||||
return kindA < kindB
|
||||
}
|
||||
return first < second
|
||||
}
|
||||
// unknown kind is last
|
||||
if !aok {
|
||||
return false
|
||||
}
|
||||
if !bok {
|
||||
return true
|
||||
}
|
||||
// sort different kinds, keep original order if same priority
|
||||
return first < second
|
||||
}
|
||||
@ -0,0 +1,347 @@
|
||||
/*
|
||||
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
release "helm.sh/helm/v4/internal/release/v2"
|
||||
)
|
||||
|
||||
func TestKindSorter(t *testing.T) {
|
||||
manifests := []Manifest{
|
||||
{
|
||||
Name: "U",
|
||||
Head: &SimpleHead{Kind: "IngressClass"},
|
||||
},
|
||||
{
|
||||
Name: "E",
|
||||
Head: &SimpleHead{Kind: "SecretList"},
|
||||
},
|
||||
{
|
||||
Name: "i",
|
||||
Head: &SimpleHead{Kind: "ClusterRole"},
|
||||
},
|
||||
{
|
||||
Name: "I",
|
||||
Head: &SimpleHead{Kind: "ClusterRoleList"},
|
||||
},
|
||||
{
|
||||
Name: "j",
|
||||
Head: &SimpleHead{Kind: "ClusterRoleBinding"},
|
||||
},
|
||||
{
|
||||
Name: "J",
|
||||
Head: &SimpleHead{Kind: "ClusterRoleBindingList"},
|
||||
},
|
||||
{
|
||||
Name: "f",
|
||||
Head: &SimpleHead{Kind: "ConfigMap"},
|
||||
},
|
||||
{
|
||||
Name: "u",
|
||||
Head: &SimpleHead{Kind: "CronJob"},
|
||||
},
|
||||
{
|
||||
Name: "2",
|
||||
Head: &SimpleHead{Kind: "CustomResourceDefinition"},
|
||||
},
|
||||
{
|
||||
Name: "n",
|
||||
Head: &SimpleHead{Kind: "DaemonSet"},
|
||||
},
|
||||
{
|
||||
Name: "r",
|
||||
Head: &SimpleHead{Kind: "Deployment"},
|
||||
},
|
||||
{
|
||||
Name: "!",
|
||||
Head: &SimpleHead{Kind: "HonkyTonkSet"},
|
||||
},
|
||||
{
|
||||
Name: "v",
|
||||
Head: &SimpleHead{Kind: "Ingress"},
|
||||
},
|
||||
{
|
||||
Name: "t",
|
||||
Head: &SimpleHead{Kind: "Job"},
|
||||
},
|
||||
{
|
||||
Name: "c",
|
||||
Head: &SimpleHead{Kind: "LimitRange"},
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
Head: &SimpleHead{Kind: "Namespace"},
|
||||
},
|
||||
{
|
||||
Name: "A",
|
||||
Head: &SimpleHead{Kind: "NetworkPolicy"},
|
||||
},
|
||||
{
|
||||
Name: "g",
|
||||
Head: &SimpleHead{Kind: "PersistentVolume"},
|
||||
},
|
||||
{
|
||||
Name: "h",
|
||||
Head: &SimpleHead{Kind: "PersistentVolumeClaim"},
|
||||
},
|
||||
{
|
||||
Name: "o",
|
||||
Head: &SimpleHead{Kind: "Pod"},
|
||||
},
|
||||
{
|
||||
Name: "3",
|
||||
Head: &SimpleHead{Kind: "PodDisruptionBudget"},
|
||||
},
|
||||
{
|
||||
Name: "C",
|
||||
Head: &SimpleHead{Kind: "PodSecurityPolicy"},
|
||||
},
|
||||
{
|
||||
Name: "q",
|
||||
Head: &SimpleHead{Kind: "ReplicaSet"},
|
||||
},
|
||||
{
|
||||
Name: "p",
|
||||
Head: &SimpleHead{Kind: "ReplicationController"},
|
||||
},
|
||||
{
|
||||
Name: "b",
|
||||
Head: &SimpleHead{Kind: "ResourceQuota"},
|
||||
},
|
||||
{
|
||||
Name: "k",
|
||||
Head: &SimpleHead{Kind: "Role"},
|
||||
},
|
||||
{
|
||||
Name: "K",
|
||||
Head: &SimpleHead{Kind: "RoleList"},
|
||||
},
|
||||
{
|
||||
Name: "l",
|
||||
Head: &SimpleHead{Kind: "RoleBinding"},
|
||||
},
|
||||
{
|
||||
Name: "L",
|
||||
Head: &SimpleHead{Kind: "RoleBindingList"},
|
||||
},
|
||||
{
|
||||
Name: "e",
|
||||
Head: &SimpleHead{Kind: "Secret"},
|
||||
},
|
||||
{
|
||||
Name: "m",
|
||||
Head: &SimpleHead{Kind: "Service"},
|
||||
},
|
||||
{
|
||||
Name: "d",
|
||||
Head: &SimpleHead{Kind: "ServiceAccount"},
|
||||
},
|
||||
{
|
||||
Name: "s",
|
||||
Head: &SimpleHead{Kind: "StatefulSet"},
|
||||
},
|
||||
{
|
||||
Name: "1",
|
||||
Head: &SimpleHead{Kind: "StorageClass"},
|
||||
},
|
||||
{
|
||||
Name: "w",
|
||||
Head: &SimpleHead{Kind: "APIService"},
|
||||
},
|
||||
{
|
||||
Name: "x",
|
||||
Head: &SimpleHead{Kind: "HorizontalPodAutoscaler"},
|
||||
},
|
||||
{
|
||||
Name: "F",
|
||||
Head: &SimpleHead{Kind: "PriorityClass"},
|
||||
},
|
||||
{
|
||||
Name: "M",
|
||||
Head: &SimpleHead{Kind: "MutatingWebhookConfiguration"},
|
||||
},
|
||||
{
|
||||
Name: "V",
|
||||
Head: &SimpleHead{Kind: "ValidatingWebhookConfiguration"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
description string
|
||||
order KindSortOrder
|
||||
expected string
|
||||
}{
|
||||
{"install", InstallOrder, "FaAbcC3deEf1gh2iIjJkKlLmnopqrxstuUvwMV!"},
|
||||
{"uninstall", UninstallOrder, "VMwvUmutsxrqponLlKkJjIi2hg1fEed3CcbAaF!"},
|
||||
} {
|
||||
var buf bytes.Buffer
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
if got, want := len(test.expected), len(manifests); got != want {
|
||||
t.Fatalf("Expected %d names in order, got %d", want, got)
|
||||
}
|
||||
defer buf.Reset()
|
||||
orig := manifests
|
||||
for _, r := range sortManifestsByKind(manifests, test.order) {
|
||||
buf.WriteString(r.Name)
|
||||
}
|
||||
if got := buf.String(); got != test.expected {
|
||||
t.Errorf("Expected %q, got %q", test.expected, got)
|
||||
}
|
||||
for i, manifest := range orig {
|
||||
if manifest != manifests[i] {
|
||||
t.Fatal("Expected input to sortManifestsByKind to stay the same")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestKindSorterKeepOriginalOrder verifies manifests of same kind are kept in original order
|
||||
func TestKindSorterKeepOriginalOrder(t *testing.T) {
|
||||
manifests := []Manifest{
|
||||
{
|
||||
Name: "a",
|
||||
Head: &SimpleHead{Kind: "ClusterRole"},
|
||||
},
|
||||
{
|
||||
Name: "A",
|
||||
Head: &SimpleHead{Kind: "ClusterRole"},
|
||||
},
|
||||
{
|
||||
Name: "0",
|
||||
Head: &SimpleHead{Kind: "ConfigMap"},
|
||||
},
|
||||
{
|
||||
Name: "1",
|
||||
Head: &SimpleHead{Kind: "ConfigMap"},
|
||||
},
|
||||
{
|
||||
Name: "z",
|
||||
Head: &SimpleHead{Kind: "ClusterRoleBinding"},
|
||||
},
|
||||
{
|
||||
Name: "!",
|
||||
Head: &SimpleHead{Kind: "ClusterRoleBinding"},
|
||||
},
|
||||
{
|
||||
Name: "u2",
|
||||
Head: &SimpleHead{Kind: "Unknown"},
|
||||
},
|
||||
{
|
||||
Name: "u1",
|
||||
Head: &SimpleHead{Kind: "Unknown"},
|
||||
},
|
||||
{
|
||||
Name: "t3",
|
||||
Head: &SimpleHead{Kind: "Unknown2"},
|
||||
},
|
||||
}
|
||||
for _, test := range []struct {
|
||||
description string
|
||||
order KindSortOrder
|
||||
expected string
|
||||
}{
|
||||
// expectation is sorted by kind (unknown is last) and within each group of same kind, the order is kept
|
||||
{"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", InstallOrder, "01aAz!u2u1t3"},
|
||||
} {
|
||||
var buf bytes.Buffer
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
defer buf.Reset()
|
||||
for _, r := range sortManifestsByKind(manifests, test.order) {
|
||||
buf.WriteString(r.Name)
|
||||
}
|
||||
if got := buf.String(); got != test.expected {
|
||||
t.Errorf("Expected %q, got %q", test.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKindSorterNamespaceAgainstUnknown(t *testing.T) {
|
||||
unknown := Manifest{
|
||||
Name: "a",
|
||||
Head: &SimpleHead{Kind: "Unknown"},
|
||||
}
|
||||
namespace := Manifest{
|
||||
Name: "b",
|
||||
Head: &SimpleHead{Kind: "Namespace"},
|
||||
}
|
||||
|
||||
manifests := []Manifest{unknown, namespace}
|
||||
manifests = sortManifestsByKind(manifests, InstallOrder)
|
||||
|
||||
expectedOrder := []Manifest{namespace, unknown}
|
||||
for i, manifest := range manifests {
|
||||
if expectedOrder[i].Name != manifest.Name {
|
||||
t.Errorf("Expected %s, got %s", expectedOrder[i].Name, manifest.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test hook sorting with a small subset of kinds, since it uses the same algorithm as sortManifestsByKind
|
||||
func TestKindSorterForHooks(t *testing.T) {
|
||||
hooks := []*release.Hook{
|
||||
{
|
||||
Name: "i",
|
||||
Kind: "ClusterRole",
|
||||
},
|
||||
{
|
||||
Name: "j",
|
||||
Kind: "ClusterRoleBinding",
|
||||
},
|
||||
{
|
||||
Name: "c",
|
||||
Kind: "LimitRange",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
Kind: "Namespace",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
description string
|
||||
order KindSortOrder
|
||||
expected string
|
||||
}{
|
||||
{"install", InstallOrder, "acij"},
|
||||
{"uninstall", UninstallOrder, "jica"},
|
||||
} {
|
||||
var buf bytes.Buffer
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
if got, want := len(test.expected), len(hooks); got != want {
|
||||
t.Fatalf("Expected %d names in order, got %d", want, got)
|
||||
}
|
||||
defer buf.Reset()
|
||||
orig := hooks
|
||||
for _, r := range sortHooksByKind(hooks, test.order) {
|
||||
buf.WriteString(r.Name)
|
||||
}
|
||||
for i, hook := range orig {
|
||||
if hook != hooks[i] {
|
||||
t.Fatal("Expected input to sortHooksByKind to stay the same")
|
||||
}
|
||||
}
|
||||
if got := buf.String(); got != test.expected {
|
||||
t.Errorf("Expected %q, got %q", test.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
/*
|
||||
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SimpleHead defines what the structure of the head of a manifest file
|
||||
type SimpleHead struct {
|
||||
Version string `json:"apiVersion"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Metadata *struct {
|
||||
Name string `json:"name"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*")
|
||||
|
||||
// SplitManifests takes a string of manifest and returns a map contains individual manifests
|
||||
func SplitManifests(bigFile string) map[string]string {
|
||||
// Basically, we're quickly splitting a stream of YAML documents into an
|
||||
// array of YAML docs. The file name is just a place holder, but should be
|
||||
// integer-sortable so that manifests get output in the same order as the
|
||||
// input (see `BySplitManifestsOrder`).
|
||||
tpl := "manifest-%d"
|
||||
res := map[string]string{}
|
||||
// Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly.
|
||||
bigFileTmp := strings.TrimSpace(bigFile)
|
||||
docs := sep.Split(bigFileTmp, -1)
|
||||
var count int
|
||||
for _, d := range docs {
|
||||
if d == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
d = strings.TrimSpace(d)
|
||||
res[fmt.Sprintf(tpl, count)] = d
|
||||
count = count + 1
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// BySplitManifestsOrder sorts by in-file manifest order, as provided in function `SplitManifests`
|
||||
type BySplitManifestsOrder []string
|
||||
|
||||
func (a BySplitManifestsOrder) Len() int { return len(a) }
|
||||
func (a BySplitManifestsOrder) Less(i, j int) bool {
|
||||
// Split `manifest-%d`
|
||||
anum, _ := strconv.ParseInt(a[i][len("manifest-"):], 10, 0)
|
||||
bnum, _ := strconv.ParseInt(a[j][len("manifest-"):], 10, 0)
|
||||
return anum < bnum
|
||||
}
|
||||
func (a BySplitManifestsOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
@ -0,0 +1,244 @@
|
||||
/*
|
||||
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
v2 "helm.sh/helm/v4/internal/release/v2"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
)
|
||||
|
||||
// Manifest represents a manifest file, which has a name and some content.
|
||||
type Manifest struct {
|
||||
Name string
|
||||
Content string
|
||||
Head *SimpleHead
|
||||
}
|
||||
|
||||
// manifestFile represents a file that contains a manifest.
|
||||
type manifestFile struct {
|
||||
entries map[string]string
|
||||
path string
|
||||
}
|
||||
|
||||
// result is an intermediate structure used during sorting.
|
||||
type result struct {
|
||||
hooks []*v2.Hook
|
||||
generic []Manifest
|
||||
}
|
||||
|
||||
// TODO: Refactor this out. It's here because naming conventions were not followed through.
|
||||
// So fix the Test hook names and then remove this.
|
||||
var events = map[string]v2.HookEvent{
|
||||
v2.HookPreInstall.String(): v2.HookPreInstall,
|
||||
v2.HookPostInstall.String(): v2.HookPostInstall,
|
||||
v2.HookPreDelete.String(): v2.HookPreDelete,
|
||||
v2.HookPostDelete.String(): v2.HookPostDelete,
|
||||
v2.HookPreUpgrade.String(): v2.HookPreUpgrade,
|
||||
v2.HookPostUpgrade.String(): v2.HookPostUpgrade,
|
||||
v2.HookPreRollback.String(): v2.HookPreRollback,
|
||||
v2.HookPostRollback.String(): v2.HookPostRollback,
|
||||
v2.HookTest.String(): v2.HookTest,
|
||||
// Support test-success for backward compatibility with Helm 2 tests
|
||||
"test-success": v2.HookTest,
|
||||
}
|
||||
|
||||
// SortManifests takes a map of filename/YAML contents, splits the file
|
||||
// by manifest entries, and sorts the entries into hook types.
|
||||
//
|
||||
// The resulting hooks struct will be populated with all of the generated hooks.
|
||||
// Any file that does not declare one of the hook types will be placed in the
|
||||
// 'generic' bucket.
|
||||
//
|
||||
// Files that do not parse into the expected format are simply placed into a map and
|
||||
// returned.
|
||||
func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSortOrder) ([]*v2.Hook, []Manifest, error) {
|
||||
result := &result{}
|
||||
|
||||
var sortedFilePaths []string
|
||||
for filePath := range files {
|
||||
sortedFilePaths = append(sortedFilePaths, filePath)
|
||||
}
|
||||
sort.Strings(sortedFilePaths)
|
||||
|
||||
for _, filePath := range sortedFilePaths {
|
||||
content := files[filePath]
|
||||
|
||||
// Skip partials. We could return these as a separate map, but there doesn't
|
||||
// seem to be any need for that at this time.
|
||||
if strings.HasPrefix(path.Base(filePath), "_") {
|
||||
continue
|
||||
}
|
||||
// Skip empty files and log this.
|
||||
if strings.TrimSpace(content) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
manifestFile := &manifestFile{
|
||||
entries: SplitManifests(content),
|
||||
path: filePath,
|
||||
}
|
||||
|
||||
if err := manifestFile.sort(result); err != nil {
|
||||
return result.hooks, result.generic, err
|
||||
}
|
||||
}
|
||||
|
||||
return sortHooksByKind(result.hooks, ordering), sortManifestsByKind(result.generic, ordering), nil
|
||||
}
|
||||
|
||||
// sort takes a manifestFile object which may contain multiple resource definition
|
||||
// entries and sorts each entry by hook types, and saves the resulting hooks and
|
||||
// generic manifests (or non-hooks) to the result struct.
|
||||
//
|
||||
// To determine hook type, it looks for a YAML structure like this:
|
||||
//
|
||||
// kind: SomeKind
|
||||
// apiVersion: v1
|
||||
// metadata:
|
||||
// annotations:
|
||||
// helm.sh/hook: pre-install
|
||||
//
|
||||
// To determine the policy to delete the hook, it looks for a YAML structure like this:
|
||||
//
|
||||
// kind: SomeKind
|
||||
// apiVersion: v1
|
||||
// metadata:
|
||||
// annotations:
|
||||
// helm.sh/hook-delete-policy: hook-succeeded
|
||||
//
|
||||
// To determine the policy to output logs of the hook (for Pod and Job only), it looks for a YAML structure like this:
|
||||
//
|
||||
// kind: Pod
|
||||
// apiVersion: v1
|
||||
// metadata:
|
||||
// annotations:
|
||||
// helm.sh/hook-output-log-policy: hook-succeeded,hook-failed
|
||||
func (file *manifestFile) sort(result *result) error {
|
||||
// Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys)
|
||||
var sortedEntryKeys []string
|
||||
for entryKey := range file.entries {
|
||||
sortedEntryKeys = append(sortedEntryKeys, entryKey)
|
||||
}
|
||||
sort.Sort(BySplitManifestsOrder(sortedEntryKeys))
|
||||
|
||||
for _, entryKey := range sortedEntryKeys {
|
||||
m := file.entries[entryKey]
|
||||
|
||||
var entry SimpleHead
|
||||
if err := yaml.Unmarshal([]byte(m), &entry); err != nil {
|
||||
return fmt.Errorf("YAML parse error on %s: %w", file.path, err)
|
||||
}
|
||||
|
||||
if !hasAnyAnnotation(entry) {
|
||||
result.generic = append(result.generic, Manifest{
|
||||
Name: file.path,
|
||||
Content: m,
|
||||
Head: &entry,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
hookTypes, ok := entry.Metadata.Annotations[v2.HookAnnotation]
|
||||
if !ok {
|
||||
result.generic = append(result.generic, Manifest{
|
||||
Name: file.path,
|
||||
Content: m,
|
||||
Head: &entry,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
hw := calculateHookWeight(entry)
|
||||
|
||||
h := &v2.Hook{
|
||||
Name: entry.Metadata.Name,
|
||||
Kind: entry.Kind,
|
||||
Path: file.path,
|
||||
Manifest: m,
|
||||
Events: []v2.HookEvent{},
|
||||
Weight: hw,
|
||||
DeletePolicies: []v2.HookDeletePolicy{},
|
||||
OutputLogPolicies: []v2.HookOutputLogPolicy{},
|
||||
}
|
||||
|
||||
isUnknownHook := false
|
||||
for hookType := range strings.SplitSeq(hookTypes, ",") {
|
||||
hookType = strings.ToLower(strings.TrimSpace(hookType))
|
||||
e, ok := events[hookType]
|
||||
if !ok {
|
||||
isUnknownHook = true
|
||||
break
|
||||
}
|
||||
h.Events = append(h.Events, e)
|
||||
}
|
||||
|
||||
if isUnknownHook {
|
||||
slog.Info("skipping unknown hooks", "hookTypes", hookTypes)
|
||||
continue
|
||||
}
|
||||
|
||||
result.hooks = append(result.hooks, h)
|
||||
|
||||
operateAnnotationValues(entry, v2.HookDeleteAnnotation, func(value string) {
|
||||
h.DeletePolicies = append(h.DeletePolicies, v2.HookDeletePolicy(value))
|
||||
})
|
||||
|
||||
operateAnnotationValues(entry, v2.HookOutputLogAnnotation, func(value string) {
|
||||
h.OutputLogPolicies = append(h.OutputLogPolicies, v2.HookOutputLogPolicy(value))
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasAnyAnnotation returns true if the given entry has any annotations at all.
|
||||
func hasAnyAnnotation(entry SimpleHead) bool {
|
||||
return entry.Metadata != nil &&
|
||||
entry.Metadata.Annotations != nil &&
|
||||
len(entry.Metadata.Annotations) != 0
|
||||
}
|
||||
|
||||
// calculateHookWeight finds the weight in the hook weight annotation.
|
||||
//
|
||||
// If no weight is found, the assigned weight is 0
|
||||
func calculateHookWeight(entry SimpleHead) int {
|
||||
hws := entry.Metadata.Annotations[v2.HookWeightAnnotation]
|
||||
hw, err := strconv.Atoi(hws)
|
||||
if err != nil {
|
||||
hw = 0
|
||||
}
|
||||
return hw
|
||||
}
|
||||
|
||||
// operateAnnotationValues finds the given annotation and runs the operate function with the value of that annotation
|
||||
func operateAnnotationValues(entry SimpleHead, annotation string, operate func(p string)) {
|
||||
if dps, ok := entry.Metadata.Annotations[annotation]; ok {
|
||||
for dp := range strings.SplitSeq(dps, ",") {
|
||||
dp = strings.ToLower(strings.TrimSpace(dp))
|
||||
operate(dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,227 @@
|
||||
/*
|
||||
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
release "helm.sh/helm/v4/internal/release/v2"
|
||||
)
|
||||
|
||||
func TestSortManifests(t *testing.T) {
|
||||
|
||||
data := []struct {
|
||||
name []string
|
||||
path string
|
||||
kind []string
|
||||
hooks map[string][]release.HookEvent
|
||||
manifest string
|
||||
}{
|
||||
{
|
||||
name: []string{"first"},
|
||||
path: "one",
|
||||
kind: []string{"Job"},
|
||||
hooks: map[string][]release.HookEvent{"first": {release.HookPreInstall}},
|
||||
manifest: `apiVersion: v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: first
|
||||
labels:
|
||||
doesnot: matter
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: []string{"second"},
|
||||
path: "two",
|
||||
kind: []string{"ReplicaSet"},
|
||||
hooks: map[string][]release.HookEvent{"second": {release.HookPostInstall}},
|
||||
manifest: `kind: ReplicaSet
|
||||
apiVersion: v1beta1
|
||||
metadata:
|
||||
name: second
|
||||
annotations:
|
||||
"helm.sh/hook": post-install
|
||||
`,
|
||||
}, {
|
||||
name: []string{"third"},
|
||||
path: "three",
|
||||
kind: []string{"ReplicaSet"},
|
||||
hooks: map[string][]release.HookEvent{"third": nil},
|
||||
manifest: `kind: ReplicaSet
|
||||
apiVersion: v1beta1
|
||||
metadata:
|
||||
name: third
|
||||
annotations:
|
||||
"helm.sh/hook": no-such-hook
|
||||
`,
|
||||
}, {
|
||||
name: []string{"fourth"},
|
||||
path: "four",
|
||||
kind: []string{"Pod"},
|
||||
hooks: map[string][]release.HookEvent{"fourth": nil},
|
||||
manifest: `kind: Pod
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: fourth
|
||||
annotations:
|
||||
nothing: here`,
|
||||
}, {
|
||||
name: []string{"fifth"},
|
||||
path: "five",
|
||||
kind: []string{"ReplicaSet"},
|
||||
hooks: map[string][]release.HookEvent{"fifth": {release.HookPostDelete, release.HookPostInstall}},
|
||||
manifest: `kind: ReplicaSet
|
||||
apiVersion: v1beta1
|
||||
metadata:
|
||||
name: fifth
|
||||
annotations:
|
||||
"helm.sh/hook": post-delete, post-install
|
||||
`,
|
||||
}, {
|
||||
// Regression test: files with an underscore in the base name should be skipped.
|
||||
name: []string{"sixth"},
|
||||
path: "six/_six",
|
||||
kind: []string{"ReplicaSet"},
|
||||
hooks: map[string][]release.HookEvent{"sixth": nil},
|
||||
manifest: `invalid manifest`, // This will fail if partial is not skipped.
|
||||
}, {
|
||||
// Regression test: files with no content should be skipped.
|
||||
name: []string{"seventh"},
|
||||
path: "seven",
|
||||
kind: []string{"ReplicaSet"},
|
||||
hooks: map[string][]release.HookEvent{"seventh": nil},
|
||||
manifest: "",
|
||||
},
|
||||
{
|
||||
name: []string{"eighth", "example-test"},
|
||||
path: "eight",
|
||||
kind: []string{"ConfigMap", "Pod"},
|
||||
hooks: map[string][]release.HookEvent{"eighth": nil, "example-test": {release.HookTest}},
|
||||
manifest: `kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: eighth
|
||||
data:
|
||||
name: value
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: example-test
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
manifests := make(map[string]string, len(data))
|
||||
for _, o := range data {
|
||||
manifests[o.path] = o.manifest
|
||||
}
|
||||
|
||||
hs, generic, err := SortManifests(manifests, nil, InstallOrder)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// This test will fail if 'six' or 'seven' was added.
|
||||
if len(generic) != 2 {
|
||||
t.Errorf("Expected 2 generic manifests, got %d", len(generic))
|
||||
}
|
||||
|
||||
if len(hs) != 4 {
|
||||
t.Errorf("Expected 4 hooks, got %d", len(hs))
|
||||
}
|
||||
|
||||
for _, out := range hs {
|
||||
found := false
|
||||
for _, expect := range data {
|
||||
if out.Path == expect.path {
|
||||
found = true
|
||||
if out.Path != expect.path {
|
||||
t.Errorf("Expected path %s, got %s", expect.path, out.Path)
|
||||
}
|
||||
nameFound := false
|
||||
for _, expectedName := range expect.name {
|
||||
if out.Name == expectedName {
|
||||
nameFound = true
|
||||
}
|
||||
}
|
||||
if !nameFound {
|
||||
t.Errorf("Got unexpected name %s", out.Name)
|
||||
}
|
||||
kindFound := false
|
||||
for _, expectedKind := range expect.kind {
|
||||
if out.Kind == expectedKind {
|
||||
kindFound = true
|
||||
}
|
||||
}
|
||||
if !kindFound {
|
||||
t.Errorf("Got unexpected kind %s", out.Kind)
|
||||
}
|
||||
|
||||
expectedHooks := expect.hooks[out.Name]
|
||||
if !reflect.DeepEqual(expectedHooks, out.Events) {
|
||||
t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Result not found: %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the sort order
|
||||
sorted := []Manifest{}
|
||||
for _, s := range data {
|
||||
manifests := SplitManifests(s.manifest)
|
||||
|
||||
for _, m := range manifests {
|
||||
var sh SimpleHead
|
||||
if err := yaml.Unmarshal([]byte(m), &sh); err != nil {
|
||||
// This is expected for manifests that are corrupt or empty.
|
||||
t.Log(err)
|
||||
continue
|
||||
}
|
||||
|
||||
name := sh.Metadata.Name
|
||||
|
||||
// only keep track of non-hook manifests
|
||||
if s.hooks[name] == nil {
|
||||
another := Manifest{
|
||||
Content: m,
|
||||
Name: name,
|
||||
Head: &sh,
|
||||
}
|
||||
sorted = append(sorted, another)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sorted = sortManifestsByKind(sorted, InstallOrder)
|
||||
for i, m := range generic {
|
||||
if m.Content != sorted[i].Content {
|
||||
t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
/*
|
||||
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const mockManifestFile = `
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: finding-nemo,
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: nemo-test
|
||||
image: fake-image
|
||||
cmd: fake-command
|
||||
`
|
||||
|
||||
const expectedManifest = `apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: finding-nemo,
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: nemo-test
|
||||
image: fake-image
|
||||
cmd: fake-command`
|
||||
|
||||
func TestSplitManifest(t *testing.T) {
|
||||
manifests := SplitManifests(mockManifestFile)
|
||||
if len(manifests) != 1 {
|
||||
t.Errorf("Expected 1 manifest, got %v", len(manifests))
|
||||
}
|
||||
expected := map[string]string{"manifest-0": expectedManifest}
|
||||
if !reflect.DeepEqual(manifests, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, manifests)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
/*
|
||||
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
rspb "helm.sh/helm/v4/internal/release/v2"
|
||||
)
|
||||
|
||||
// Reverse reverses the list of releases sorted by the sort func.
|
||||
func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) {
|
||||
sortFn(list)
|
||||
for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 {
|
||||
list[i], list[j] = list[j], list[i]
|
||||
}
|
||||
}
|
||||
|
||||
// SortByName returns the list of releases sorted
|
||||
// in lexicographical order.
|
||||
func SortByName(list []*rspb.Release) {
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
return list[i].Name < list[j].Name
|
||||
})
|
||||
}
|
||||
|
||||
// SortByDate returns the list of releases sorted by a
|
||||
// release's last deployed time (in seconds).
|
||||
func SortByDate(list []*rspb.Release) {
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
ti := list[i].Info.LastDeployed.Unix()
|
||||
tj := list[j].Info.LastDeployed.Unix()
|
||||
if ti != tj {
|
||||
return ti < tj
|
||||
}
|
||||
// Use name as tie-breaker for stable sorting
|
||||
return list[i].Name < list[j].Name
|
||||
})
|
||||
}
|
||||
|
||||
// SortByRevision returns the list of releases sorted by a
|
||||
// release's revision number (release.Version).
|
||||
func SortByRevision(list []*rspb.Release) {
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
return list[i].Version < list[j].Version
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
/*
|
||||
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
rspb "helm.sh/helm/v4/internal/release/v2"
|
||||
"helm.sh/helm/v4/pkg/release/common"
|
||||
)
|
||||
|
||||
// note: this test data is shared with filter_test.go.
|
||||
|
||||
var releases = []*rspb.Release{
|
||||
tsRelease("quiet-bear", 2, 2000, common.StatusSuperseded),
|
||||
tsRelease("angry-bird", 4, 3000, common.StatusDeployed),
|
||||
tsRelease("happy-cats", 1, 4000, common.StatusUninstalled),
|
||||
tsRelease("vocal-dogs", 3, 6000, common.StatusUninstalled),
|
||||
}
|
||||
|
||||
func tsRelease(name string, vers int, dur time.Duration, status common.Status) *rspb.Release {
|
||||
info := &rspb.Info{Status: status, LastDeployed: time.Now().Add(dur)}
|
||||
return &rspb.Release{
|
||||
Name: name,
|
||||
Version: vers,
|
||||
Info: info,
|
||||
}
|
||||
}
|
||||
|
||||
func check(t *testing.T, by string, fn func(int, int) bool) {
|
||||
t.Helper()
|
||||
for i := len(releases) - 1; i > 0; i-- {
|
||||
if fn(i, i-1) {
|
||||
t.Errorf("release at positions '(%d,%d)' not sorted by %s", i-1, i, by)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortByName(t *testing.T) {
|
||||
SortByName(releases)
|
||||
|
||||
check(t, "ByName", func(i, j int) bool {
|
||||
ni := releases[i].Name
|
||||
nj := releases[j].Name
|
||||
return ni < nj
|
||||
})
|
||||
}
|
||||
|
||||
func TestSortByDate(t *testing.T) {
|
||||
SortByDate(releases)
|
||||
|
||||
check(t, "ByDate", func(i, j int) bool {
|
||||
ti := releases[i].Info.LastDeployed.Second()
|
||||
tj := releases[j].Info.LastDeployed.Second()
|
||||
return ti < tj
|
||||
})
|
||||
}
|
||||
|
||||
func TestSortByRevision(t *testing.T) {
|
||||
SortByRevision(releases)
|
||||
|
||||
check(t, "ByRevision", func(i, j int) bool {
|
||||
vi := releases[i].Version
|
||||
vj := releases[j].Version
|
||||
return vi < vj
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverseSortByName(t *testing.T) {
|
||||
Reverse(releases, SortByName)
|
||||
check(t, "ByName", func(i, j int) bool {
|
||||
ni := releases[i].Name
|
||||
nj := releases[j].Name
|
||||
return ni > nj
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverseSortByDate(t *testing.T) {
|
||||
Reverse(releases, SortByDate)
|
||||
check(t, "ByDate", func(i, j int) bool {
|
||||
ti := releases[i].Info.LastDeployed.Second()
|
||||
tj := releases[j].Info.LastDeployed.Second()
|
||||
return ti > tj
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverseSortByRevision(t *testing.T) {
|
||||
Reverse(releases, SortByRevision)
|
||||
check(t, "ByRevision", func(i, j int) bool {
|
||||
vi := releases[i].Version
|
||||
vj := releases[j].Version
|
||||
return vi > vj
|
||||
})
|
||||
}
|
||||
Loading…
Reference in new issue