Merge pull request #116469 from RomanBednar/pv-phase-transition-time

PersistentVolume last phase transition time
This commit is contained in:
Kubernetes Prow Robot 2023-07-21 16:10:07 -07:00 committed by GitHub
commit f3a070f9c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1499 additions and 887 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -304,6 +304,7 @@
"status": {
"phase": "phaseValue",
"message": "messageValue",
"reason": "reasonValue"
"reason": "reasonValue",
"lastPhaseTransitionTime": "2004-01-01T01:01:01Z"
}
}

View File

@ -232,6 +232,7 @@ spec:
storagePolicyName: storagePolicyNameValue
volumePath: volumePathValue
status:
lastPhaseTransitionTime: "2004-01-01T01:01:01Z"
message: messageValue
phase: phaseValue
reason: reasonValue

View File

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

View File

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

View File

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