mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-05 10:19:50 +00:00
Merge pull request #116469 from RomanBednar/pv-phase-transition-time
PersistentVolume last phase transition time
This commit is contained in:
commit
f3a070f9c6
4
api/openapi-spec/swagger.json
generated
4
api/openapi-spec/swagger.json
generated
@ -7783,6 +7783,10 @@
|
||||
"io.k8s.api.core.v1.PersistentVolumeStatus": {
|
||||
"description": "PersistentVolumeStatus is the current status of a persistent volume.",
|
||||
"properties": {
|
||||
"lastPhaseTransitionTime": {
|
||||
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Time",
|
||||
"description": "lastPhaseTransitionTime is the time the phase transitioned from one to another and automatically resets to current time everytime a volume phase transitions. This is an alpha field and requires enabling PersistentVolumeLastPhaseTransitionTime feature."
|
||||
},
|
||||
"message": {
|
||||
"description": "message is a human-readable message indicating details about why the volume is in this state.",
|
||||
"type": "string"
|
||||
|
@ -4621,6 +4621,14 @@
|
||||
"io.k8s.api.core.v1.PersistentVolumeStatus": {
|
||||
"description": "PersistentVolumeStatus is the current status of a persistent volume.",
|
||||
"properties": {
|
||||
"lastPhaseTransitionTime": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time"
|
||||
}
|
||||
],
|
||||
"description": "lastPhaseTransitionTime is the time the phase transitioned from one to another and automatically resets to current time everytime a volume phase transitions. This is an alpha field and requires enabling PersistentVolumeLastPhaseTransitionTime feature."
|
||||
},
|
||||
"message": {
|
||||
"description": "message is a human-readable message indicating details about why the volume is in this state.",
|
||||
"type": "string"
|
||||
|
@ -31,9 +31,9 @@ const (
|
||||
deprecatedStorageClassAnnotationsMsg = `deprecated since v1.8; use "storageClassName" attribute instead`
|
||||
)
|
||||
|
||||
// DropDisabledFields removes disabled fields from the pv spec.
|
||||
// DropDisabledSpecFields removes disabled fields from the pv spec.
|
||||
// This should be called from PrepareForCreate/PrepareForUpdate for all resources containing a pv spec.
|
||||
func DropDisabledFields(pvSpec *api.PersistentVolumeSpec, oldPVSpec *api.PersistentVolumeSpec) {
|
||||
func DropDisabledSpecFields(pvSpec *api.PersistentVolumeSpec, oldPVSpec *api.PersistentVolumeSpec) {
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.CSINodeExpandSecret) && !hasNodeExpansionSecrets(oldPVSpec) {
|
||||
if pvSpec.CSI != nil {
|
||||
pvSpec.CSI.NodeExpandSecretRef = nil
|
||||
@ -41,6 +41,14 @@ func DropDisabledFields(pvSpec *api.PersistentVolumeSpec, oldPVSpec *api.Persist
|
||||
}
|
||||
}
|
||||
|
||||
// DropDisabledStatusFields removes disabled fields from the pv status.
|
||||
// This should be called from PrepareForUpdate for all resources containing a pv status.
|
||||
func DropDisabledStatusFields(oldStatus, newStatus *api.PersistentVolumeStatus) {
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.PersistentVolumeLastPhaseTransitionTime) && oldStatus.LastPhaseTransitionTime.IsZero() {
|
||||
newStatus.LastPhaseTransitionTime = nil
|
||||
}
|
||||
}
|
||||
|
||||
func hasNodeExpansionSecrets(oldPVSpec *api.PersistentVolumeSpec) bool {
|
||||
if oldPVSpec == nil || oldPVSpec.CSI == nil {
|
||||
return false
|
||||
|
@ -91,7 +91,7 @@ func TestDropDisabledFields(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSINodeExpandSecret, tc.csiExpansionEnabled)()
|
||||
|
||||
DropDisabledFields(tc.newSpec, tc.oldSpec)
|
||||
DropDisabledSpecFields(tc.newSpec, tc.oldSpec)
|
||||
if !reflect.DeepEqual(tc.newSpec, tc.expectNewSpec) {
|
||||
t.Error(cmp.Diff(tc.newSpec, tc.expectNewSpec))
|
||||
}
|
||||
|
@ -380,6 +380,12 @@ type PersistentVolumeStatus struct {
|
||||
// Reason is a brief CamelCase string that describes any failure and is meant for machine parsing and tidy display in the CLI
|
||||
// +optional
|
||||
Reason string
|
||||
// LastPhaseTransitionTime is the time the phase transitioned from one to another
|
||||
// and automatically resets to current time everytime a volume phase transitions.
|
||||
// This is an alpha field and requires enabling PersistentVolumeLastPhaseTransitionTime feature.
|
||||
// +featureGate=PersistentVolumeLastPhaseTransitionTime
|
||||
// +optional
|
||||
LastPhaseTransitionTime *metav1.Time
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
2
pkg/apis/core/v1/zz_generated.conversion.go
generated
2
pkg/apis/core/v1/zz_generated.conversion.go
generated
@ -5574,6 +5574,7 @@ func autoConvert_v1_PersistentVolumeStatus_To_core_PersistentVolumeStatus(in *v1
|
||||
out.Phase = core.PersistentVolumePhase(in.Phase)
|
||||
out.Message = in.Message
|
||||
out.Reason = in.Reason
|
||||
out.LastPhaseTransitionTime = (*metav1.Time)(unsafe.Pointer(in.LastPhaseTransitionTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -5586,6 +5587,7 @@ func autoConvert_core_PersistentVolumeStatus_To_v1_PersistentVolumeStatus(in *co
|
||||
out.Phase = v1.PersistentVolumePhase(in.Phase)
|
||||
out.Message = in.Message
|
||||
out.Reason = in.Reason
|
||||
out.LastPhaseTransitionTime = (*metav1.Time)(unsafe.Pointer(in.LastPhaseTransitionTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
6
pkg/apis/core/zz_generated.deepcopy.go
generated
6
pkg/apis/core/zz_generated.deepcopy.go
generated
@ -2928,7 +2928,7 @@ func (in *PersistentVolume) DeepCopyInto(out *PersistentVolume) {
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
out.Status = in.Status
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
return
|
||||
}
|
||||
|
||||
@ -3370,6 +3370,10 @@ func (in *PersistentVolumeSpec) DeepCopy() *PersistentVolumeSpec {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PersistentVolumeStatus) DeepCopyInto(out *PersistentVolumeStatus) {
|
||||
*out = *in
|
||||
if in.LastPhaseTransitionTime != nil {
|
||||
in, out := &in.LastPhaseTransitionTime, &out.LastPhaseTransitionTime
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -626,6 +626,13 @@ const (
|
||||
// Enables PDBUnhealthyPodEvictionPolicy for PodDisruptionBudgets
|
||||
PDBUnhealthyPodEvictionPolicy featuregate.Feature = "PDBUnhealthyPodEvictionPolicy"
|
||||
|
||||
// owner: @RomanBednar
|
||||
// kep: https://kep.k8s.io/3762
|
||||
// alpha: v1.28
|
||||
//
|
||||
// Adds a new field to persistent volumes which holds a timestamp of when the volume last transitioned its phase.
|
||||
PersistentVolumeLastPhaseTransitionTime featuregate.Feature = "PersistentVolumeLastPhaseTransitionTime"
|
||||
|
||||
// owner: @haircommander
|
||||
// kep: https://kep.k8s.io/2364
|
||||
// alpha: v1.23
|
||||
@ -1104,6 +1111,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||
|
||||
PDBUnhealthyPodEvictionPolicy: {Default: true, PreRelease: featuregate.Beta},
|
||||
|
||||
PersistentVolumeLastPhaseTransitionTime: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
PodAndContainerStatsFromCRI: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
PodDeletionCost: {Default: true, PreRelease: featuregate.Beta},
|
||||
|
8
pkg/generated/openapi/zz_generated.openapi.go
generated
8
pkg/generated/openapi/zz_generated.openapi.go
generated
@ -23110,9 +23110,17 @@ func schema_k8sio_api_core_v1_PersistentVolumeStatus(ref common.ReferenceCallbac
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"lastPhaseTransitionTime": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "lastPhaseTransitionTime is the time the phase transitioned from one to another and automatically resets to current time everytime a volume phase transitions. This is an alpha field and requires enabling PersistentVolumeLastPhaseTransitionTime feature.",
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Time"},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,9 @@ package persistentvolume
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
@ -66,7 +69,13 @@ func (persistentvolumeStrategy) PrepareForCreate(ctx context.Context, obj runtim
|
||||
pv := obj.(*api.PersistentVolume)
|
||||
pv.Status = api.PersistentVolumeStatus{}
|
||||
|
||||
pvutil.DropDisabledFields(&pv.Spec, nil)
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.PersistentVolumeLastPhaseTransitionTime) {
|
||||
pv.Status.Phase = api.VolumePending
|
||||
now := nowFunc()
|
||||
pv.Status.LastPhaseTransitionTime = &now
|
||||
}
|
||||
|
||||
pvutil.DropDisabledSpecFields(&pv.Spec, nil)
|
||||
}
|
||||
|
||||
func (persistentvolumeStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||
@ -95,7 +104,7 @@ func (persistentvolumeStrategy) PrepareForUpdate(ctx context.Context, obj, old r
|
||||
oldPv := old.(*api.PersistentVolume)
|
||||
newPv.Status = oldPv.Status
|
||||
|
||||
pvutil.DropDisabledFields(&newPv.Spec, &oldPv.Spec)
|
||||
pvutil.DropDisabledSpecFields(&newPv.Spec, &oldPv.Spec)
|
||||
}
|
||||
|
||||
func (persistentvolumeStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||
@ -134,11 +143,28 @@ func (persistentvolumeStatusStrategy) GetResetFields() map[fieldpath.APIVersion]
|
||||
return fields
|
||||
}
|
||||
|
||||
var nowFunc = metav1.Now
|
||||
|
||||
// PrepareForUpdate sets the Spec field which is not allowed to be changed when updating a PV's Status
|
||||
func (persistentvolumeStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||
newPv := obj.(*api.PersistentVolume)
|
||||
oldPv := old.(*api.PersistentVolume)
|
||||
newPv.Spec = oldPv.Spec
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.PersistentVolumeLastPhaseTransitionTime) {
|
||||
switch {
|
||||
case oldPv.Status.Phase == newPv.Status.Phase && newPv.Status.LastPhaseTransitionTime == nil:
|
||||
// phase didn't change, preserve the existing transition time if set
|
||||
newPv.Status.LastPhaseTransitionTime = oldPv.Status.LastPhaseTransitionTime
|
||||
|
||||
case oldPv.Status.Phase != newPv.Status.Phase && (newPv.Status.LastPhaseTransitionTime == nil || newPv.Status.LastPhaseTransitionTime.Equal(oldPv.Status.LastPhaseTransitionTime)):
|
||||
// phase changed and client didn't set or didn't change the transition time
|
||||
now := nowFunc()
|
||||
newPv.Status.LastPhaseTransitionTime = &now
|
||||
}
|
||||
}
|
||||
|
||||
pvutil.DropDisabledStatusFields(&oldPv.Status, &newPv.Status)
|
||||
}
|
||||
|
||||
func (persistentvolumeStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||
|
@ -17,10 +17,17 @@ limitations under the License.
|
||||
package persistentvolume
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"context"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
apitesting "k8s.io/kubernetes/pkg/api/testing"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
// ensure types are installed
|
||||
_ "k8s.io/kubernetes/pkg/apis/core/install"
|
||||
@ -34,3 +41,394 @@ func TestSelectableFieldLabelConversions(t *testing.T) {
|
||||
map[string]string{"name": "metadata.name"},
|
||||
)
|
||||
}
|
||||
|
||||
func TestStatusUpdate(t *testing.T) {
|
||||
now := metav1.Now()
|
||||
origin := metav1.NewTime(now.Add(time.Hour))
|
||||
later := metav1.NewTime(now.Add(time.Hour * 2))
|
||||
nowFunc = func() metav1.Time { return now }
|
||||
defer func() {
|
||||
nowFunc = metav1.Now
|
||||
}()
|
||||
tests := []struct {
|
||||
name string
|
||||
fg bool
|
||||
oldObj *api.PersistentVolume
|
||||
newObj *api.PersistentVolume
|
||||
expectedObj *api.PersistentVolume
|
||||
}{
|
||||
{
|
||||
name: "feature enabled: timestamp is updated when phase changes",
|
||||
fg: true,
|
||||
oldObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &now,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature enabled: timestamp is updated when phase changes and old pv has a timestamp",
|
||||
fg: true,
|
||||
oldObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
LastPhaseTransitionTime: &origin,
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &now,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature enabled: user timestamp change is respected on no phase change",
|
||||
fg: true,
|
||||
oldObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
LastPhaseTransitionTime: &later,
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
LastPhaseTransitionTime: &later,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature enabled: user timestamp is respected on phase change",
|
||||
fg: true,
|
||||
oldObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
LastPhaseTransitionTime: &origin,
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &later,
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &later,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature enabled: user timestamp change is respected on no phase change when old pv has a timestamp",
|
||||
fg: true,
|
||||
oldObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &origin,
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &later,
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &later,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature enabled: timestamp is updated when phase changes and both new and old timestamp matches",
|
||||
fg: true,
|
||||
oldObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
LastPhaseTransitionTime: &origin,
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &origin,
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &now,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature disabled: timestamp is not updated",
|
||||
fg: false,
|
||||
oldObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature disabled: user timestamp is overwritten on phase change to nil",
|
||||
fg: false,
|
||||
oldObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
LastPhaseTransitionTime: &later,
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
LastPhaseTransitionTime: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature disabled: user timestamp change is respected on phase change",
|
||||
fg: false,
|
||||
oldObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
LastPhaseTransitionTime: &origin,
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &later,
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &later,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature disabled: user timestamp change is respected on no phase change",
|
||||
fg: false,
|
||||
oldObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &origin,
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &later,
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeBound,
|
||||
LastPhaseTransitionTime: &later,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PersistentVolumeLastPhaseTransitionTime, tc.fg)()
|
||||
|
||||
obj := tc.newObj.DeepCopy()
|
||||
StatusStrategy.PrepareForUpdate(context.TODO(), obj, tc.oldObj.DeepCopy())
|
||||
if !reflect.DeepEqual(obj, tc.expectedObj) {
|
||||
t.Errorf("object diff: %s", cmp.Diff(obj, tc.expectedObj))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusCreate(t *testing.T) {
|
||||
now := metav1.Now()
|
||||
nowFunc = func() metav1.Time { return now }
|
||||
defer func() {
|
||||
nowFunc = metav1.Now
|
||||
}()
|
||||
tests := []struct {
|
||||
name string
|
||||
fg bool
|
||||
newObj *api.PersistentVolume
|
||||
expectedObj *api.PersistentVolume
|
||||
}{
|
||||
{
|
||||
name: "feature enabled: pv is in pending phase and has a timestamp",
|
||||
fg: true,
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
LastPhaseTransitionTime: &now,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature disabled: pv does not have phase and timestamp",
|
||||
fg: false,
|
||||
newObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
expectedObj: &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PersistentVolumeLastPhaseTransitionTime, tc.fg)()
|
||||
obj := tc.newObj.DeepCopy()
|
||||
StatusStrategy.PrepareForCreate(context.TODO(), obj)
|
||||
if !reflect.DeepEqual(obj, tc.expectedObj) {
|
||||
t.Errorf("object diff: %s", cmp.Diff(obj, tc.expectedObj))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
1795
staging/src/k8s.io/api/core/v1/generated.pb.go
generated
1795
staging/src/k8s.io/api/core/v1/generated.pb.go
generated
File diff suppressed because it is too large
Load Diff
@ -3187,6 +3187,13 @@ message PersistentVolumeStatus {
|
||||
// for machine parsing and tidy display in the CLI.
|
||||
// +optional
|
||||
optional string reason = 3;
|
||||
|
||||
// lastPhaseTransitionTime is the time the phase transitioned from one to another
|
||||
// and automatically resets to current time everytime a volume phase transitions.
|
||||
// This is an alpha field and requires enabling PersistentVolumeLastPhaseTransitionTime feature.
|
||||
// +featureGate=PersistentVolumeLastPhaseTransitionTime
|
||||
// +optional
|
||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.Time lastPhaseTransitionTime = 4;
|
||||
}
|
||||
|
||||
// Represents a Photon Controller persistent disk resource.
|
||||
|
@ -411,6 +411,12 @@ type PersistentVolumeStatus struct {
|
||||
// for machine parsing and tidy display in the CLI.
|
||||
// +optional
|
||||
Reason string `json:"reason,omitempty" protobuf:"bytes,3,opt,name=reason"`
|
||||
// lastPhaseTransitionTime is the time the phase transitioned from one to another
|
||||
// and automatically resets to current time everytime a volume phase transitions.
|
||||
// This is an alpha field and requires enabling PersistentVolumeLastPhaseTransitionTime feature.
|
||||
// +featureGate=PersistentVolumeLastPhaseTransitionTime
|
||||
// +optional
|
||||
LastPhaseTransitionTime *metav1.Time `json:"lastPhaseTransitionTime,omitempty" protobuf:"bytes,4,opt,name=lastPhaseTransitionTime"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
@ -1446,10 +1446,11 @@ func (PersistentVolumeSpec) SwaggerDoc() map[string]string {
|
||||
}
|
||||
|
||||
var map_PersistentVolumeStatus = map[string]string{
|
||||
"": "PersistentVolumeStatus is the current status of a persistent volume.",
|
||||
"phase": "phase indicates if a volume is available, bound to a claim, or released by a claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#phase",
|
||||
"message": "message is a human-readable message indicating details about why the volume is in this state.",
|
||||
"reason": "reason is a brief CamelCase string that describes any failure and is meant for machine parsing and tidy display in the CLI.",
|
||||
"": "PersistentVolumeStatus is the current status of a persistent volume.",
|
||||
"phase": "phase indicates if a volume is available, bound to a claim, or released by a claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#phase",
|
||||
"message": "message is a human-readable message indicating details about why the volume is in this state.",
|
||||
"reason": "reason is a brief CamelCase string that describes any failure and is meant for machine parsing and tidy display in the CLI.",
|
||||
"lastPhaseTransitionTime": "lastPhaseTransitionTime is the time the phase transitioned from one to another and automatically resets to current time everytime a volume phase transitions. This is an alpha field and requires enabling PersistentVolumeLastPhaseTransitionTime feature.",
|
||||
}
|
||||
|
||||
func (PersistentVolumeStatus) SwaggerDoc() map[string]string {
|
||||
|
@ -2926,7 +2926,7 @@ func (in *PersistentVolume) DeepCopyInto(out *PersistentVolume) {
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
out.Status = in.Status
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
return
|
||||
}
|
||||
|
||||
@ -3368,6 +3368,10 @@ func (in *PersistentVolumeSpec) DeepCopy() *PersistentVolumeSpec {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PersistentVolumeStatus) DeepCopyInto(out *PersistentVolumeStatus) {
|
||||
*out = *in
|
||||
if in.LastPhaseTransitionTime != nil {
|
||||
in, out := &in.LastPhaseTransitionTime, &out.LastPhaseTransitionTime
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -304,6 +304,7 @@
|
||||
"status": {
|
||||
"phase": "phaseValue",
|
||||
"message": "messageValue",
|
||||
"reason": "reasonValue"
|
||||
"reason": "reasonValue",
|
||||
"lastPhaseTransitionTime": "2004-01-01T01:01:01Z"
|
||||
}
|
||||
}
|
Binary file not shown.
@ -232,6 +232,7 @@ spec:
|
||||
storagePolicyName: storagePolicyNameValue
|
||||
volumePath: volumePathValue
|
||||
status:
|
||||
lastPhaseTransitionTime: "2004-01-01T01:01:01Z"
|
||||
message: messageValue
|
||||
phase: phaseValue
|
||||
reason: reasonValue
|
||||
|
@ -20,14 +20,16 @@ package v1
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// PersistentVolumeStatusApplyConfiguration represents an declarative configuration of the PersistentVolumeStatus type for use
|
||||
// with apply.
|
||||
type PersistentVolumeStatusApplyConfiguration struct {
|
||||
Phase *v1.PersistentVolumePhase `json:"phase,omitempty"`
|
||||
Message *string `json:"message,omitempty"`
|
||||
Reason *string `json:"reason,omitempty"`
|
||||
Phase *v1.PersistentVolumePhase `json:"phase,omitempty"`
|
||||
Message *string `json:"message,omitempty"`
|
||||
Reason *string `json:"reason,omitempty"`
|
||||
LastPhaseTransitionTime *metav1.Time `json:"lastPhaseTransitionTime,omitempty"`
|
||||
}
|
||||
|
||||
// PersistentVolumeStatusApplyConfiguration constructs an declarative configuration of the PersistentVolumeStatus type for use with
|
||||
@ -59,3 +61,11 @@ func (b *PersistentVolumeStatusApplyConfiguration) WithReason(value string) *Per
|
||||
b.Reason = &value
|
||||
return b
|
||||
}
|
||||
|
||||
// WithLastPhaseTransitionTime sets the LastPhaseTransitionTime field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the LastPhaseTransitionTime field is set to the value of the last call.
|
||||
func (b *PersistentVolumeStatusApplyConfiguration) WithLastPhaseTransitionTime(value metav1.Time) *PersistentVolumeStatusApplyConfiguration {
|
||||
b.LastPhaseTransitionTime = &value
|
||||
return b
|
||||
}
|
||||
|
@ -5991,6 +5991,9 @@ var schemaYAML = typed.YAMLObject(`types:
|
||||
- name: io.k8s.api.core.v1.PersistentVolumeStatus
|
||||
map:
|
||||
fields:
|
||||
- name: lastPhaseTransitionTime
|
||||
type:
|
||||
namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time
|
||||
- name: message
|
||||
type:
|
||||
scalar: string
|
||||
|
@ -201,6 +201,57 @@ var _ = utils.SIGDescribe("PersistentVolumes", func() {
|
||||
framework.ExpectNoError(err)
|
||||
completeTest(ctx, f, c, ns, pv, pvc)
|
||||
})
|
||||
|
||||
// Create PV and pre-bound PVC that matches the PV, verify that when PV and PVC bind
|
||||
// the LastPhaseTransitionTime filed of the PV is updated.
|
||||
ginkgo.It("create a PV and a pre-bound PVC: test phase transition timestamp is set [Feature: PersistentVolumeLastPhaseTransitionTime]", func(ctx context.Context) {
|
||||
pv, pvc, err = e2epv.CreatePVPVC(ctx, c, f.Timeouts, pvConfig, pvcConfig, ns, true)
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
// The claim should transition phase to: Bound
|
||||
err = e2epv.WaitForPersistentVolumeClaimPhase(ctx, v1.ClaimBound, c, ns, pvc.Name, 2*time.Second, framework.ClaimProvisionShortTimeout)
|
||||
framework.ExpectNoError(err)
|
||||
pvc, err = c.CoreV1().PersistentVolumeClaims(ns).Get(ctx, pvc.Name, metav1.GetOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
pv, err = c.CoreV1().PersistentVolumes().Get(ctx, pvc.Spec.VolumeName, metav1.GetOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
if pv.Status.LastPhaseTransitionTime == nil {
|
||||
framework.Failf("Persistent volume %v should have LastPhaseTransitionTime value set after transitioning phase, but it's nil.", pv.GetName())
|
||||
}
|
||||
completeTest(ctx, f, c, ns, pv, pvc)
|
||||
})
|
||||
|
||||
// Create PV and pre-bound PVC that matches the PV, verify that when PV and PVC bind
|
||||
// the LastPhaseTransitionTime field of the PV is set, then delete the PVC to change PV phase to
|
||||
// released and validate PV LastPhaseTransitionTime correctly updated timestamp.
|
||||
ginkgo.It("create a PV and a pre-bound PVC: test phase transition timestamp multiple updates [Feature: PersistentVolumeLastPhaseTransitionTime]", func(ctx context.Context) {
|
||||
pv, pvc, err = e2epv.CreatePVPVC(ctx, c, f.Timeouts, pvConfig, pvcConfig, ns, true)
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
// The claim should transition phase to: Bound.
|
||||
err = e2epv.WaitForPersistentVolumeClaimPhase(ctx, v1.ClaimBound, c, ns, pvc.Name, 2*time.Second, framework.ClaimProvisionShortTimeout)
|
||||
framework.ExpectNoError(err)
|
||||
pvc, err = c.CoreV1().PersistentVolumeClaims(ns).Get(ctx, pvc.Name, metav1.GetOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
pv, err = c.CoreV1().PersistentVolumes().Get(ctx, pvc.Spec.VolumeName, metav1.GetOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
// Save first phase transition time.
|
||||
firstPhaseTransition := pv.Status.LastPhaseTransitionTime
|
||||
|
||||
// Let test finish and delete PVC.
|
||||
completeTest(ctx, f, c, ns, pv, pvc)
|
||||
|
||||
// The claim should transition phase to: Released.
|
||||
err = e2epv.WaitForPersistentVolumePhase(ctx, v1.VolumeReleased, c, pv.Name, 2*time.Second, framework.ClaimProvisionShortTimeout)
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
// Verify the phase transition timestamp got updated chronologically *after* first phase transition.
|
||||
pv, err = c.CoreV1().PersistentVolumes().Get(ctx, pvc.Spec.VolumeName, metav1.GetOptions{})
|
||||
if !firstPhaseTransition.Before(pv.Status.LastPhaseTransitionTime) {
|
||||
framework.Failf("Persistent volume %v should have LastPhaseTransitionTime value updated to be chronologically after previous phase change: %v, but it's %v.", pv.GetName(), firstPhaseTransition, pv.Status.LastPhaseTransitionTime)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Create multiple pvs and pvcs, all in the same namespace. The PVs-PVCs are
|
||||
|
Loading…
Reference in New Issue
Block a user