mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-29 21:29:24 +00:00
feature: add CSIVolumeHealth feature and gate
1. add EventRecorder to ResourceAnalyzer 2. add CSIVolumeHealth feature and gate
This commit is contained in:
@@ -30,7 +30,9 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
api "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
volumetypes "k8s.io/kubernetes/pkg/volume/util/types"
|
||||
)
|
||||
@@ -624,6 +626,19 @@ func (c *csiDriverClient) NodeGetVolumeStats(ctx context.Context, volID string,
|
||||
Inodes: resource.NewQuantity(int64(0), resource.BinarySI),
|
||||
InodesFree: resource.NewQuantity(int64(0), resource.BinarySI),
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.CSIVolumeHealth) {
|
||||
isSupportNodeVolumeCondition, err := supportNodeGetVolumeCondition(ctx, nodeClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isSupportNodeVolumeCondition {
|
||||
abnormal, message := resp.VolumeCondition.GetAbnormal(), resp.VolumeCondition.GetMessage()
|
||||
metrics.Abnormal, metrics.Message = &abnormal, &message
|
||||
}
|
||||
}
|
||||
|
||||
for _, usage := range usages {
|
||||
if usage == nil {
|
||||
continue
|
||||
@@ -646,6 +661,30 @@ func (c *csiDriverClient) NodeGetVolumeStats(ctx context.Context, volID string,
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
func supportNodeGetVolumeCondition(ctx context.Context, nodeClient csipbv1.NodeClient) (supportNodeGetVolumeCondition bool, err error) {
|
||||
req := csipbv1.NodeGetCapabilitiesRequest{}
|
||||
rsp, err := nodeClient.NodeGetCapabilities(ctx, &req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, cap := range rsp.GetCapabilities() {
|
||||
if cap == nil {
|
||||
continue
|
||||
}
|
||||
rpc := cap.GetRpc()
|
||||
if rpc == nil {
|
||||
continue
|
||||
}
|
||||
t := rpc.GetType()
|
||||
if t == csipbv1.NodeServiceCapability_RPC_VOLUME_CONDITION {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func isFinalError(err error) bool {
|
||||
// Sources:
|
||||
// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
|
||||
|
||||
@@ -26,9 +26,14 @@ import (
|
||||
"testing"
|
||||
|
||||
csipbv1 "github.com/container-storage-interface/spec/lib/go/csi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
utiltesting "k8s.io/client-go/util/testing"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
"k8s.io/kubernetes/pkg/volume/csi/fake"
|
||||
volumetypes "k8s.io/kubernetes/pkg/volume/util/types"
|
||||
@@ -60,6 +65,13 @@ func newFakeCsiDriverClientWithVolumeStats(t *testing.T, volumeStatsSet bool) *f
|
||||
}
|
||||
}
|
||||
|
||||
func newFakeCsiDriverClientWithVolumeStatsAndCondition(t *testing.T, volumeStatsSet, volumeConditionSet bool) *fakeCsiDriverClient {
|
||||
return &fakeCsiDriverClient{
|
||||
t: t,
|
||||
nodeClient: fake.NewNodeClientWithVolumeStatsAndCondition(volumeStatsSet, volumeConditionSet),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *fakeCsiDriverClient) NodeGetInfo(ctx context.Context) (
|
||||
nodeID string,
|
||||
maxVolumePerNode int64,
|
||||
@@ -80,15 +92,30 @@ func (c *fakeCsiDriverClient) NodeGetVolumeStats(ctx context.Context, volID stri
|
||||
VolumeId: volID,
|
||||
VolumePath: targetPath,
|
||||
}
|
||||
|
||||
c.nodeClient.SetNodeVolumeStatsResp(getRawVolumeInfo())
|
||||
resp, err := c.nodeClient.NodeGetVolumeStats(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usages := resp.GetUsage()
|
||||
metrics := &volume.Metrics{}
|
||||
if usages == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metrics := &volume.Metrics{}
|
||||
|
||||
isSupportNodeVolumeCondition, err := supportNodeGetVolumeCondition(ctx, c.nodeClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.CSIVolumeHealth) && isSupportNodeVolumeCondition {
|
||||
abnormal, message := resp.VolumeCondition.GetAbnormal(), resp.VolumeCondition.GetMessage()
|
||||
metrics.Abnormal, metrics.Message = &abnormal, &message
|
||||
}
|
||||
|
||||
for _, usage := range usages {
|
||||
if usage == nil {
|
||||
continue
|
||||
@@ -325,6 +352,10 @@ func setupClientWithExpansion(t *testing.T, stageUnstageSet bool, expansionSet b
|
||||
return newFakeCsiDriverClientWithExpansion(t, stageUnstageSet, expansionSet)
|
||||
}
|
||||
|
||||
func setupClientWithVolumeStatsAndCondition(t *testing.T, volumeStatsSet, volumeConditionSet bool) csiClient {
|
||||
return newFakeCsiDriverClientWithVolumeStatsAndCondition(t, volumeStatsSet, volumeConditionSet)
|
||||
}
|
||||
|
||||
func setupClientWithVolumeStats(t *testing.T, volumeStatsSet bool) csiClient {
|
||||
return newFakeCsiDriverClientWithVolumeStats(t, volumeStatsSet)
|
||||
}
|
||||
@@ -674,13 +705,108 @@ type VolumeStatsOptions struct {
|
||||
DeviceMountPath string
|
||||
}
|
||||
|
||||
func TestVolumeStats(t *testing.T) {
|
||||
func TestVolumeHealthEnable(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSIVolumeHealth, true)()
|
||||
spec := volume.NewSpecFromPersistentVolume(makeTestPV("test-pv", 10, "metrics", "test-vol"), false)
|
||||
tests := []struct {
|
||||
name string
|
||||
volumeStatsSet bool
|
||||
volumeConditionSet bool
|
||||
volumeData VolumeStatsOptions
|
||||
success bool
|
||||
}{
|
||||
{
|
||||
name: "when nodeVolumeStats=on, VolumeID=on, DeviceMountPath=on, VolumeCondition=on",
|
||||
volumeStatsSet: true,
|
||||
volumeConditionSet: true,
|
||||
volumeData: VolumeStatsOptions{
|
||||
VolumeSpec: spec,
|
||||
VolumeID: "volume1",
|
||||
DeviceMountPath: "/foo/bar",
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
name: "when nodeVolumeStats=on, VolumeID=on, DeviceMountPath=on, VolumeCondition=off",
|
||||
volumeStatsSet: true,
|
||||
volumeConditionSet: false,
|
||||
volumeData: VolumeStatsOptions{
|
||||
VolumeSpec: spec,
|
||||
VolumeID: "volume1",
|
||||
DeviceMountPath: "/foo/bar",
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), csiTimeout)
|
||||
defer cancel()
|
||||
csiSource, _ := getCSISourceFromSpec(tc.volumeData.VolumeSpec)
|
||||
csClient := setupClientWithVolumeStatsAndCondition(t, tc.volumeStatsSet, tc.volumeConditionSet)
|
||||
metrics, err := csClient.NodeGetVolumeStats(ctx, csiSource.VolumeHandle, tc.volumeData.DeviceMountPath)
|
||||
if tc.success {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
if tc.volumeConditionSet {
|
||||
assert.NotNil(t, metrics.Abnormal)
|
||||
assert.NotNil(t, metrics.Message)
|
||||
} else {
|
||||
assert.Nil(t, metrics.Abnormal)
|
||||
assert.Nil(t, metrics.Message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolumeHealthDisable(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSIVolumeHealth, false)()
|
||||
spec := volume.NewSpecFromPersistentVolume(makeTestPV("test-pv", 10, "metrics", "test-vol"), false)
|
||||
tests := []struct {
|
||||
name string
|
||||
volumeStatsSet bool
|
||||
volumeData VolumeStatsOptions
|
||||
success bool
|
||||
}{
|
||||
{
|
||||
name: "when nodeVolumeStats=on, VolumeID=on, DeviceMountPath=on, VolumeCondition=off",
|
||||
volumeStatsSet: true,
|
||||
volumeData: VolumeStatsOptions{
|
||||
VolumeSpec: spec,
|
||||
VolumeID: "volume1",
|
||||
DeviceMountPath: "/foo/bar",
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), csiTimeout)
|
||||
defer cancel()
|
||||
csiSource, _ := getCSISourceFromSpec(tc.volumeData.VolumeSpec)
|
||||
csClient := setupClientWithVolumeStatsAndCondition(t, tc.volumeStatsSet, false)
|
||||
metrics, err := csClient.NodeGetVolumeStats(ctx, csiSource.VolumeHandle, tc.volumeData.DeviceMountPath)
|
||||
if tc.success {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
assert.Nil(t, metrics.Abnormal)
|
||||
assert.Nil(t, metrics.Message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolumeStats(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSIVolumeHealth, true)()
|
||||
spec := volume.NewSpecFromPersistentVolume(makeTestPV("test-pv", 10, "metrics", "test-vol"), false)
|
||||
tests := []struct {
|
||||
name string
|
||||
volumeStatsSet bool
|
||||
volumeConditionSet bool
|
||||
volumeData VolumeStatsOptions
|
||||
success bool
|
||||
}{
|
||||
{
|
||||
name: "when nodeVolumeStats=on, VolumeID=on, DeviceMountPath=on",
|
||||
|
||||
@@ -84,6 +84,7 @@ type NodeClient struct {
|
||||
stageUnstageSet bool
|
||||
expansionSet bool
|
||||
volumeStatsSet bool
|
||||
volumeConditionSet bool
|
||||
nodeGetInfoResp *csipb.NodeGetInfoResponse
|
||||
nodeVolumeStatsResp *csipb.NodeGetVolumeStatsResponse
|
||||
FakeNodeExpansionRequest *csipb.NodeExpandVolumeRequest
|
||||
@@ -115,6 +116,13 @@ func NewNodeClientWithVolumeStats(volumeStatsSet bool) *NodeClient {
|
||||
}
|
||||
}
|
||||
|
||||
func NewNodeClientWithVolumeStatsAndCondition(volumeStatsSet, volumeConditionSet bool) *NodeClient {
|
||||
return &NodeClient{
|
||||
volumeStatsSet: volumeStatsSet,
|
||||
volumeConditionSet: volumeConditionSet,
|
||||
}
|
||||
}
|
||||
|
||||
// SetNextError injects next expected error
|
||||
func (f *NodeClient) SetNextError(err error) {
|
||||
f.nextErr = err
|
||||
@@ -346,6 +354,16 @@ func (f *NodeClient) NodeGetCapabilities(ctx context.Context, in *csipb.NodeGetC
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if f.volumeConditionSet {
|
||||
resp.Capabilities = append(resp.Capabilities, &csipb.NodeServiceCapability{
|
||||
Type: &csipb.NodeServiceCapability_Rpc{
|
||||
Rpc: &csipb.NodeServiceCapability_RPC{
|
||||
Type: csipb.NodeServiceCapability_RPC_VOLUME_CONDITION,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,17 @@ type Metrics struct {
|
||||
// a filesystem with the host (e.g. emptydir, hostpath), this is the free inodes
|
||||
// on the underlying storage, and is shared with host processes and other volumes
|
||||
InodesFree *resource.Quantity
|
||||
|
||||
// Normal volumes are available for use and operating optimally.
|
||||
// An abnormal volume does not meet these criteria.
|
||||
// This field is OPTIONAL. Only some csi drivers which support NodeServiceCapability_RPC_VOLUME_CONDITION
|
||||
// need to fill it.
|
||||
Abnormal *bool
|
||||
|
||||
// The message describing the condition of the volume.
|
||||
// This field is OPTIONAL. Only some csi drivers which support capability_RPC_VOLUME_CONDITION
|
||||
// need to fill it.
|
||||
Message *string
|
||||
}
|
||||
|
||||
// Attributes represents the attributes of this mounter.
|
||||
|
||||
Reference in New Issue
Block a user