blob: 39ee2fd0cdff3433760e5453a6460d7da4ca2f6e [file] [log] [blame]
/*
Copyright 2018 The Kubernetes 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 nodeinfomanager
import (
"encoding/json"
"fmt"
"testing"
"github.com/container-storage-interface/spec/lib/go/csi"
"github.com/stretchr/testify/assert"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/strategicpatch"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
"k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing"
utiltesting "k8s.io/client-go/util/testing"
csiv1alpha1 "k8s.io/csi-api/pkg/apis/csi/v1alpha1"
csifake "k8s.io/csi-api/pkg/client/clientset/versioned/fake"
"k8s.io/kubernetes/pkg/apis/core/helper"
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
"k8s.io/kubernetes/pkg/features"
volumetest "k8s.io/kubernetes/pkg/volume/testing"
"k8s.io/kubernetes/pkg/volume/util"
)
type testcase struct {
name string
driverName string
existingNode *v1.Node
existingNodeInfo *csiv1alpha1.CSINodeInfo
inputNodeID string
inputTopology *csi.Topology
inputVolumeLimit int64
expectedNodeIDMap map[string]string
expectedTopologyMap map[string]sets.String
expectedLabels map[string]string
expectedVolumeLimit int64
expectFail bool
}
type nodeIDMap map[string]string
type topologyKeyMap map[string][]string
type labelMap map[string]string
// TestInstallCSIDriver tests InstallCSIDriver with various existing Node and/or CSINodeInfo objects.
// The node IDs in all test cases below are the same between the Node annotation and CSINodeInfo.
func TestInstallCSIDriver(t *testing.T) {
testcases := []testcase{
{
name: "empty node",
driverName: "com.example.csi/driver1",
existingNode: generateNode(nil /* nodeIDs */, nil /* labels */, nil /*capacity*/),
inputNodeID: "com.example.csi/csi-node1",
inputTopology: &csi.Topology{
Segments: map[string]string{
"com.example.csi/zone": "zoneA",
},
},
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
expectedTopologyMap: map[string]sets.String{
"com.example.csi/driver1": sets.NewString("com.example.csi/zone"),
},
expectedLabels: map[string]string{"com.example.csi/zone": "zoneA"},
},
{
name: "pre-existing node info from the same driver",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
labelMap{
"com.example.csi/zone": "zoneA",
},
nil /*capacity*/),
existingNodeInfo: generateNodeInfo(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
topologyKeyMap{
"com.example.csi/driver1": {"com.example.csi/zone"},
},
),
inputNodeID: "com.example.csi/csi-node1",
inputTopology: &csi.Topology{
Segments: map[string]string{
"com.example.csi/zone": "zoneA",
},
},
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
expectedTopologyMap: map[string]sets.String{
"com.example.csi/driver1": sets.NewString("com.example.csi/zone"),
},
expectedLabels: map[string]string{
"com.example.csi/zone": "zoneA",
},
},
{
name: "pre-existing node info from the same driver, but without topology info",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
nil /* labels */, nil /*capacity*/),
existingNodeInfo: generateNodeInfo(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
nil, /* topologyKeys */
),
inputNodeID: "com.example.csi/csi-node1",
inputTopology: &csi.Topology{
Segments: map[string]string{
"com.example.csi/zone": "zoneA",
},
},
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
expectedTopologyMap: map[string]sets.String{
"com.example.csi/driver1": sets.NewString("com.example.csi/zone"),
},
expectedLabels: map[string]string{
"com.example.csi/zone": "zoneA",
},
},
{
name: "pre-existing node info from different driver",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"net.example.storage/other-driver": "net.example.storage/test-node",
},
labelMap{
"net.example.storage/rack": "rack1",
}, nil /*capacity*/),
existingNodeInfo: generateNodeInfo(
nodeIDMap{
"net.example.storage/other-driver": "net.example.storage/test-node",
},
topologyKeyMap{
"net.example.storage/other-driver": {"net.example.storage/rack"},
},
),
inputNodeID: "com.example.csi/csi-node1",
inputTopology: &csi.Topology{
Segments: map[string]string{
"com.example.csi/zone": "zoneA",
},
},
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
"net.example.storage/other-driver": "net.example.storage/test-node",
},
expectedTopologyMap: map[string]sets.String{
"com.example.csi/driver1": sets.NewString("com.example.csi/zone"),
"net.example.storage/other-driver": sets.NewString("net.example.storage/rack"),
},
expectedLabels: map[string]string{
"com.example.csi/zone": "zoneA",
"net.example.storage/rack": "rack1",
},
},
{
name: "pre-existing node info from the same driver, but different node ID and topology values; labels should conflict",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
labelMap{
"com.example.csi/zone": "zoneA",
}, nil /*capacity*/),
existingNodeInfo: generateNodeInfo(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
topologyKeyMap{
"com.example.csi/driver1": {"com.example.csi/zone"},
},
),
inputNodeID: "com.example.csi/csi-node1",
inputTopology: &csi.Topology{
Segments: map[string]string{
"com.example.csi/zone": "other-zone",
},
},
expectFail: true,
},
{
name: "pre-existing node info from the same driver, but different node ID and topology keys; new labels should be added",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
labelMap{
"com.example.csi/zone": "zoneA",
}, nil /*capacity*/),
existingNodeInfo: generateNodeInfo(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
topologyKeyMap{
"com.example.csi/driver1": {"com.example.csi/zone"},
},
),
inputNodeID: "com.example.csi/other-node",
inputTopology: &csi.Topology{
Segments: map[string]string{
"com.example.csi/rack": "rack1",
},
},
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/other-node",
},
expectedTopologyMap: map[string]sets.String{
"com.example.csi/driver1": sets.NewString("com.example.csi/rack"),
},
expectedLabels: map[string]string{
"com.example.csi/zone": "zoneA",
"com.example.csi/rack": "rack1",
},
},
{
name: "nil topology, empty node",
driverName: "com.example.csi/driver1",
existingNode: generateNode(nil /* nodeIDs */, nil /* labels */, nil /*capacity*/),
inputNodeID: "com.example.csi/csi-node1",
inputTopology: nil,
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
expectedTopologyMap: map[string]sets.String{
"com.example.csi/driver1": nil,
},
expectedLabels: nil,
},
{
name: "nil topology, pre-existing node info from the same driver",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
labelMap{
"com.example.csi/zone": "zoneA",
}, nil /*capacity*/),
existingNodeInfo: generateNodeInfo(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
topologyKeyMap{
"com.example.csi/driver1": {"com.example.csi/zone"},
},
),
inputNodeID: "com.example.csi/csi-node1",
inputTopology: nil,
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
expectedTopologyMap: map[string]sets.String{
"com.example.csi/driver1": nil,
},
expectedLabels: map[string]string{
"com.example.csi/zone": "zoneA", // old labels are not removed
},
},
{
name: "nil topology, pre-existing node info from different driver",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"net.example.storage/other-driver": "net.example.storage/test-node",
},
labelMap{
"net.example.storage/rack": "rack1",
}, nil /*capacity*/),
existingNodeInfo: generateNodeInfo(
nodeIDMap{
"net.example.storage/other-driver": "net.example.storage/test-node",
},
topologyKeyMap{
"net.example.storage/other-driver": {"net.example.storage/rack"},
},
),
inputNodeID: "com.example.csi/csi-node1",
inputTopology: nil,
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
"net.example.storage/other-driver": "net.example.storage/test-node",
},
expectedTopologyMap: map[string]sets.String{
"net.example.storage/other-driver": sets.NewString("net.example.storage/rack"),
"com.example.csi/driver1": nil,
},
expectedLabels: map[string]string{
"net.example.storage/rack": "rack1",
},
},
{
name: "empty node ID",
driverName: "com.example.csi/driver1",
existingNode: generateNode(nil /* nodeIDs */, nil /* labels */, nil /*capacity*/),
inputNodeID: "",
expectFail: true,
},
{
name: "new node with valid max limit",
driverName: "com.example.csi/driver1",
existingNode: generateNode(nil /*nodeIDs*/, nil /*labels*/, nil /*capacity*/),
inputVolumeLimit: 10,
inputTopology: nil,
inputNodeID: "com.example.csi/csi-node1",
expectedVolumeLimit: 10,
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
expectedTopologyMap: map[string]sets.String{
"com.example.csi/driver1": nil,
},
expectedLabels: nil,
},
{
name: "node with existing valid max limit",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nil, /*nodeIDs*/
nil, /*labels*/
map[v1.ResourceName]resource.Quantity{
v1.ResourceCPU: *resource.NewScaledQuantity(4, -3),
v1.ResourceName(util.GetCSIAttachLimitKey("com.example.csi/driver1")): *resource.NewQuantity(10, resource.DecimalSI),
}),
inputVolumeLimit: 20,
inputTopology: nil,
inputNodeID: "com.example.csi/csi-node1",
expectedVolumeLimit: 20,
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
expectedTopologyMap: map[string]sets.String{
"com.example.csi/driver1": nil,
},
expectedLabels: nil,
},
}
test(t, true /* addNodeInfo */, true /* csiNodeInfoEnabled */, testcases)
}
// TestInstallCSIDriver_CSINodeInfoDisabled tests InstallCSIDriver with various existing Node annotations
// and CSINodeInfo feature gate disabled.
func TestInstallCSIDriver_CSINodeInfoDisabled(t *testing.T) {
testcases := []testcase{
{
name: "empty node",
driverName: "com.example.csi/driver1",
existingNode: generateNode(nil /* nodeIDs */, nil /* labels */, nil /*capacity*/),
inputNodeID: "com.example.csi/csi-node1",
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
},
{
name: "pre-existing node info from the same driver",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
nil /* labels */, nil /*capacity*/),
inputNodeID: "com.example.csi/csi-node1",
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
},
{
name: "pre-existing node info from different driver",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"net.example.storage/other-driver": "net.example.storage/test-node",
},
nil /* labels */, nil /*capacity*/),
inputNodeID: "com.example.csi/csi-node1",
expectedNodeIDMap: map[string]string{
"com.example.csi/driver1": "com.example.csi/csi-node1",
"net.example.storage/other-driver": "net.example.storage/test-node",
},
},
}
test(t, true /* addNodeInfo */, false /* csiNodeInfoEnabled */, testcases)
}
// TestUninstallCSIDriver tests UninstallCSIDriver with various existing Node and/or CSINodeInfo objects.
func TestUninstallCSIDriver(t *testing.T) {
testcases := []testcase{
{
name: "empty node and empty CSINodeInfo",
driverName: "com.example.csi/driver1",
existingNode: generateNode(nil /* nodeIDs */, nil /* labels */, nil /*capacity*/),
expectedNodeIDMap: nil,
expectedLabels: nil,
},
{
name: "pre-existing node info from the same driver",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
labelMap{
"com.example.csi/zone": "zoneA",
}, nil /*capacity*/),
existingNodeInfo: generateNodeInfo(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
topologyKeyMap{
"com.example.csi/driver1": {"com.example.csi/zone"},
},
),
expectedNodeIDMap: nil,
expectedLabels: map[string]string{"com.example.csi/zone": "zoneA"},
},
{
name: "pre-existing node info from different driver",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"net.example.storage/other-driver": "net.example.storage/csi-node1",
},
labelMap{
"net.example.storage/zone": "zoneA",
}, nil /*capacity*/),
existingNodeInfo: generateNodeInfo(
nodeIDMap{
"net.example.storage/other-driver": "net.example.storage/csi-node1",
},
topologyKeyMap{
"net.example.storage/other-driver": {"net.example.storage/zone"},
},
),
expectedNodeIDMap: map[string]string{
"net.example.storage/other-driver": "net.example.storage/csi-node1",
},
expectedTopologyMap: map[string]sets.String{
"net.example.storage/other-driver": sets.NewString("net.example.storage/zone"),
},
expectedLabels: map[string]string{"net.example.storage/zone": "zoneA"},
},
{
name: "pre-existing info about the same driver in node, but empty CSINodeInfo",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
nil /* labels */, nil /*capacity*/),
expectedNodeIDMap: nil,
expectedLabels: nil,
},
{
name: "pre-existing info about a different driver in node, but empty CSINodeInfo",
existingNode: generateNode(
nodeIDMap{
"net.example.storage/other-driver": "net.example.storage/csi-node1",
},
nil /* labels */, nil /*capacity*/),
expectedNodeIDMap: map[string]string{
"net.example.storage/other-driver": "net.example.storage/csi-node1",
},
expectedLabels: nil,
},
{
name: "new node with valid max limit",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nil, /*nodeIDs*/
nil, /*labels*/
map[v1.ResourceName]resource.Quantity{
v1.ResourceCPU: *resource.NewScaledQuantity(4, -3),
v1.ResourceName(util.GetCSIAttachLimitKey("com.example.csi/driver1")): *resource.NewQuantity(10, resource.DecimalSI),
},
),
inputTopology: nil,
inputNodeID: "com.example.csi/csi-node1",
expectedVolumeLimit: 0,
},
}
test(t, false /* addNodeInfo */, true /* csiNodeInfoEnabled */, testcases)
}
// TestUninstallCSIDriver tests UninstallCSIDriver with various existing Node objects and CSINodeInfo
// feature disabled.
func TestUninstallCSIDriver_CSINodeInfoDisabled(t *testing.T) {
testcases := []testcase{
{
name: "empty node",
driverName: "com.example.csi/driver1",
existingNode: generateNode(nil /* nodeIDs */, nil /* labels */, nil /*capacity*/),
expectedNodeIDMap: nil,
},
{
name: "pre-existing node info from the same driver",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
nil /* labels */, nil /*capacity*/),
expectedNodeIDMap: nil,
},
{
name: "pre-existing node info from different driver",
driverName: "com.example.csi/driver1",
existingNode: generateNode(
nodeIDMap{
"net.example.storage/other-driver": "net.example.storage/csi-node1",
},
nil /* labels */, nil /*capacity*/),
expectedNodeIDMap: map[string]string{
"net.example.storage/other-driver": "net.example.storage/csi-node1",
},
},
}
test(t, false /* addNodeInfo */, false /* csiNodeInfoEnabled */, testcases)
}
func TestInstallCSIDriverExistingAnnotation(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSINodeInfo, true)()
driverName := "com.example.csi/driver1"
nodeID := "com.example.csi/some-node"
testcases := []struct {
name string
existingNode *v1.Node
}{
{
name: "pre-existing info about the same driver in node, but empty CSINodeInfo",
existingNode: generateNode(
nodeIDMap{
"com.example.csi/driver1": "com.example.csi/csi-node1",
},
nil /* labels */, nil /*capacity*/),
},
{
name: "pre-existing info about a different driver in node, but empty CSINodeInfo",
existingNode: generateNode(
nodeIDMap{
"net.example.storage/other-driver": "net.example.storage/test-node",
},
nil /* labels */, nil /*capacity*/),
},
}
for _, tc := range testcases {
t.Logf("test case: %q", tc.name)
// Arrange
nodeName := tc.existingNode.Name
client := fake.NewSimpleClientset(tc.existingNode)
csiClient := csifake.NewSimpleClientset()
tmpDir, err := utiltesting.MkTmpdir("nodeinfomanager-test")
if err != nil {
t.Fatalf("can't create temp dir: %v", err)
}
host := volumetest.NewFakeVolumeHostWithCSINodeName(
tmpDir,
client,
csiClient,
nil,
nodeName,
)
nim := NewNodeInfoManager(types.NodeName(nodeName), host)
// Act
_, err = nim.CreateCSINodeInfo()
if err != nil {
t.Errorf("expected no error from creating CSINodeinfo but got: %v", err)
continue
}
err = nim.InstallCSIDriver(driverName, nodeID, 0 /* maxVolumeLimit */, nil) // TODO test maxVolumeLimit
if err != nil {
t.Errorf("expected no error from InstallCSIDriver call but got: %v", err)
continue
}
// Assert
nodeInfo, err := csiClient.Csi().CSINodeInfos().Get(nodeName, metav1.GetOptions{})
if err != nil {
t.Errorf("error getting CSINodeInfo: %v", err)
continue
}
if len(nodeInfo.Spec.Drivers) != 1 || len(nodeInfo.Status.Drivers) != 1 {
t.Errorf("expected 1 CSIDriverInfoSpec and 1 CSIDriverInfoStatus entry but got: %d, %d",
len(nodeInfo.Spec.Drivers), len(nodeInfo.Status.Drivers))
continue
}
driver := nodeInfo.Spec.Drivers[0]
if driver.Name != driverName || driver.NodeID != nodeID {
t.Errorf("expected Driver to be %q and NodeID to be %q, but got: %q:%q", driverName, nodeID, driver.Name, driver.NodeID)
}
}
}
func TestValidateCSINodeInfo(t *testing.T) {
testcases := []struct {
name string
nodeInfo *csiv1alpha1.CSINodeInfo
expectErr bool
}{
{
name: "multiple drivers with different node IDs, topology keys and status",
nodeInfo: &csiv1alpha1.CSINodeInfo{
Spec: csiv1alpha1.CSINodeInfoSpec{
Drivers: []csiv1alpha1.CSIDriverInfoSpec{
{
Name: "driver1",
NodeID: "node1",
TopologyKeys: []string{"key1, key2"},
},
{
Name: "driverB",
NodeID: "nodeA",
TopologyKeys: []string{"keyA", "keyB"},
},
},
},
Status: csiv1alpha1.CSINodeInfoStatus{
Drivers: []csiv1alpha1.CSIDriverInfoStatus{
{
Name: "driver1",
Available: true,
VolumePluginMechanism: "in-tree",
},
{
Name: "driverB",
Available: false,
VolumePluginMechanism: "csi",
},
},
},
},
expectErr: false,
},
{
name: "multiple drivers with same node IDs, topology keys and status",
nodeInfo: &csiv1alpha1.CSINodeInfo{
Spec: csiv1alpha1.CSINodeInfoSpec{
Drivers: []csiv1alpha1.CSIDriverInfoSpec{
{
Name: "driver1",
NodeID: "node1",
TopologyKeys: []string{"key1"},
},
{
Name: "driver2",
NodeID: "node1",
TopologyKeys: []string{"key1"},
},
},
},
Status: csiv1alpha1.CSINodeInfoStatus{
Drivers: []csiv1alpha1.CSIDriverInfoStatus{
{
Name: "driver1",
Available: true,
VolumePluginMechanism: "csi",
},
{
Name: "driver2",
Available: true,
VolumePluginMechanism: "csi",
},
},
},
},
expectErr: false,
},
{
name: "duplicate drivers in driver specs",
nodeInfo: &csiv1alpha1.CSINodeInfo{
Spec: csiv1alpha1.CSINodeInfoSpec{
Drivers: []csiv1alpha1.CSIDriverInfoSpec{
{
Name: "driver1",
NodeID: "node1",
TopologyKeys: []string{"key1", "key2"},
},
{
Name: "driver1",
NodeID: "nodeX",
TopologyKeys: []string{"keyA", "keyB"},
},
},
},
Status: csiv1alpha1.CSINodeInfoStatus{
Drivers: []csiv1alpha1.CSIDriverInfoStatus{
{
Name: "driver1",
Available: true,
VolumePluginMechanism: "csi",
},
},
},
},
expectErr: true,
},
{
name: "duplicate drivers in driver statuses",
nodeInfo: &csiv1alpha1.CSINodeInfo{
Spec: csiv1alpha1.CSINodeInfoSpec{
Drivers: []csiv1alpha1.CSIDriverInfoSpec{
{
Name: "driver1",
NodeID: "node1",
TopologyKeys: []string{"key1", "key2"},
},
},
},
Status: csiv1alpha1.CSINodeInfoStatus{
Drivers: []csiv1alpha1.CSIDriverInfoStatus{
{
Name: "driver1",
Available: true,
VolumePluginMechanism: "in-tree",
},
{
Name: "driver1",
Available: false,
VolumePluginMechanism: "csi",
},
},
},
},
expectErr: true,
},
{
name: "single driver with duplicate topology keys in driver specs",
nodeInfo: &csiv1alpha1.CSINodeInfo{
Spec: csiv1alpha1.CSINodeInfoSpec{
Drivers: []csiv1alpha1.CSIDriverInfoSpec{
{
Name: "driver1",
NodeID: "node1",
TopologyKeys: []string{"key1", "key1"},
},
},
},
Status: csiv1alpha1.CSINodeInfoStatus{
Drivers: []csiv1alpha1.CSIDriverInfoStatus{
{
Name: "driver1",
Available: true,
VolumePluginMechanism: "csi",
},
},
},
},
expectErr: true,
},
{
name: "multiple drivers with one set of duplicate topology keys in driver specs",
nodeInfo: &csiv1alpha1.CSINodeInfo{
Spec: csiv1alpha1.CSINodeInfoSpec{
Drivers: []csiv1alpha1.CSIDriverInfoSpec{
{
Name: "driver1",
NodeID: "node1",
TopologyKeys: []string{"key1"},
},
{
Name: "driver2",
NodeID: "nodeX",
TopologyKeys: []string{"keyA", "keyA"},
},
},
},
Status: csiv1alpha1.CSINodeInfoStatus{
Drivers: []csiv1alpha1.CSIDriverInfoStatus{
{
Name: "driver1",
Available: true,
VolumePluginMechanism: "csi",
},
{
Name: "driver2",
Available: true,
VolumePluginMechanism: "csi",
},
},
},
},
expectErr: true,
},
{
name: "mismatch between drivers in specs and status (null intersection)",
nodeInfo: &csiv1alpha1.CSINodeInfo{
Spec: csiv1alpha1.CSINodeInfoSpec{
Drivers: []csiv1alpha1.CSIDriverInfoSpec{
{
Name: "driver1",
NodeID: "node1",
TopologyKeys: []string{"key1"},
},
{
Name: "driver2",
NodeID: "nodeX",
TopologyKeys: []string{"keyA", "keyA"},
},
},
},
Status: csiv1alpha1.CSINodeInfoStatus{
Drivers: []csiv1alpha1.CSIDriverInfoStatus{
{
Name: "driver3",
Available: true,
VolumePluginMechanism: "csi",
},
},
},
},
expectErr: true,
},
{
name: "mismatch between drivers in specs and status (specs superset of status)",
nodeInfo: &csiv1alpha1.CSINodeInfo{
Spec: csiv1alpha1.CSINodeInfoSpec{
Drivers: []csiv1alpha1.CSIDriverInfoSpec{
{
Name: "driver1",
NodeID: "node1",
TopologyKeys: []string{"key1"},
},
{
Name: "driver2",
NodeID: "nodeX",
TopologyKeys: []string{"keyA", "keyA"},
},
},
},
Status: csiv1alpha1.CSINodeInfoStatus{
Drivers: []csiv1alpha1.CSIDriverInfoStatus{
{
Name: "driver1",
Available: true,
VolumePluginMechanism: "csi",
},
},
},
},
expectErr: true,
},
{
name: "mismatch between drivers in specs and status (specs subset of status)",
nodeInfo: &csiv1alpha1.CSINodeInfo{
Spec: csiv1alpha1.CSINodeInfoSpec{
Drivers: []csiv1alpha1.CSIDriverInfoSpec{
{
Name: "driver1",
NodeID: "node1",
TopologyKeys: []string{"key1"},
},
},
},
Status: csiv1alpha1.CSINodeInfoStatus{
Drivers: []csiv1alpha1.CSIDriverInfoStatus{
{
Name: "driver1",
Available: true,
VolumePluginMechanism: "csi",
},
{
Name: "driver2",
Available: true,
VolumePluginMechanism: "csi",
},
},
},
},
expectErr: true,
},
}
for _, tc := range testcases {
t.Logf("test case: %q", tc.name)
err := validateCSINodeInfo(tc.nodeInfo)
if err != nil && !tc.expectErr {
t.Errorf("expected no errors from validateCSINodeInfo but got error %v", err)
}
if err == nil && tc.expectErr {
t.Errorf("expected error from validateCSINodeInfo but got no errors")
}
}
}
func test(t *testing.T, addNodeInfo bool, csiNodeInfoEnabled bool, testcases []testcase) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSINodeInfo, csiNodeInfoEnabled)()
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AttachVolumeLimit, true)()
for _, tc := range testcases {
t.Logf("test case: %q", tc.name)
//// Arrange
nodeName := tc.existingNode.Name
client := fake.NewSimpleClientset(tc.existingNode)
var csiClient *csifake.Clientset
if tc.existingNodeInfo == nil {
csiClient = csifake.NewSimpleClientset()
} else {
csiClient = csifake.NewSimpleClientset(tc.existingNodeInfo)
}
tmpDir, err := utiltesting.MkTmpdir("nodeinfomanager-test")
if err != nil {
t.Fatalf("can't create temp dir: %v", err)
}
host := volumetest.NewFakeVolumeHostWithCSINodeName(
tmpDir,
client,
csiClient,
nil,
nodeName,
)
nim := NewNodeInfoManager(types.NodeName(nodeName), host)
//// Act
nim.CreateCSINodeInfo()
if addNodeInfo {
err = nim.InstallCSIDriver(tc.driverName, tc.inputNodeID, tc.inputVolumeLimit, tc.inputTopology)
} else {
err = nim.UninstallCSIDriver(tc.driverName)
}
//// Assert
if tc.expectFail {
if err == nil {
t.Errorf("expected an error from InstallCSIDriver call but got none")
}
continue
} else if err != nil {
t.Errorf("expected no error from InstallCSIDriver call but got: %v", err)
continue
}
actions := client.Actions()
var node *v1.Node
if action := hasPatchAction(actions); action != nil {
node, err = applyNodeStatusPatch(tc.existingNode, action.(clienttesting.PatchActionImpl).GetPatch())
assert.NoError(t, err)
} else {
node, err = client.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{})
assert.NoError(t, err)
}
if node == nil {
t.Errorf("error getting node: %v", err)
continue
}
// We are testing max volume limits
attachLimit := getVolumeLimit(node, tc.driverName)
if attachLimit != tc.expectedVolumeLimit {
t.Errorf("expected volume limit to be %d got %d", tc.expectedVolumeLimit, attachLimit)
continue
}
// Node ID annotation
foundInNode := false
annNodeID, ok := node.Annotations[annotationKeyNodeID]
if ok {
if tc.expectedNodeIDMap == nil {
t.Errorf("expected annotation %q to not exist, but got: %q", annotationKeyNodeID, annNodeID)
} else {
var actualNodeIDs map[string]string
err = json.Unmarshal([]byte(annNodeID), &actualNodeIDs)
if err != nil {
t.Errorf("expected no error when parsing annotation %q, but got error: %v", annotationKeyNodeID, err)
}
if !helper.Semantic.DeepEqual(actualNodeIDs, tc.expectedNodeIDMap) {
t.Errorf("expected annotation %v; got: %v", tc.expectedNodeIDMap, actualNodeIDs)
} else {
foundInNode = true
}
}
} else {
if tc.expectedNodeIDMap != nil {
t.Errorf("expected annotation %q, but got none", annotationKeyNodeID)
}
}
if csiNodeInfoEnabled {
// Topology labels
if !helper.Semantic.DeepEqual(node.Labels, tc.expectedLabels) {
t.Errorf("expected topology labels to be %v; got: %v", tc.expectedLabels, node.Labels)
}
/* CSINodeInfo validation */
nodeInfo, err := csiClient.Csi().CSINodeInfos().Get(nodeName, metav1.GetOptions{})
if err != nil {
t.Errorf("error getting CSINodeInfo: %v", err)
continue
}
// Extract node IDs and topology keys
availableDrivers := sets.String{}
actualNodeIDs := make(map[string]string)
actualTopologyKeys := make(map[string]sets.String)
for _, driver := range nodeInfo.Status.Drivers {
if driver.Available {
availableDrivers.Insert(driver.Name)
}
}
for _, driver := range nodeInfo.Spec.Drivers {
if availableDrivers.Has(driver.Name) {
actualNodeIDs[driver.Name] = driver.NodeID
actualTopologyKeys[driver.Name] = sets.NewString(driver.TopologyKeys...)
}
}
// Node IDs
// No need to check if Node ID found in NodeInfo if it was present in the NodeID
if !foundInNode {
if !helper.Semantic.DeepEqual(actualNodeIDs, tc.expectedNodeIDMap) {
t.Errorf("expected node IDs %v from CSINodeInfo; got: %v", tc.expectedNodeIDMap, actualNodeIDs)
}
}
// Topology keys
if !helper.Semantic.DeepEqual(actualTopologyKeys, tc.expectedTopologyMap) {
t.Errorf("expected topology keys %v from CSINodeInfo; got: %v", tc.expectedTopologyMap, actualTopologyKeys)
}
}
}
}
func getVolumeLimit(node *v1.Node, driverName string) int64 {
volumeLimits := map[v1.ResourceName]int64{}
nodeAllocatables := node.Status.Allocatable
for k, v := range nodeAllocatables {
if v1helper.IsAttachableVolumeResourceName(k) {
volumeLimits[k] = v.Value()
}
}
attachKey := v1.ResourceName(util.GetCSIAttachLimitKey(driverName))
attachLimit := volumeLimits[attachKey]
return attachLimit
}
func generateNode(nodeIDs, labels map[string]string, capacity map[v1.ResourceName]resource.Quantity) *v1.Node {
var annotations map[string]string
if len(nodeIDs) > 0 {
b, _ := json.Marshal(nodeIDs)
annotations = map[string]string{annotationKeyNodeID: string(b)}
}
node := &v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
Annotations: annotations,
Labels: labels,
},
}
if len(capacity) > 0 {
node.Status.Capacity = v1.ResourceList(capacity)
node.Status.Allocatable = v1.ResourceList(capacity)
}
return node
}
func generateNodeInfo(nodeIDs map[string]string, topologyKeys map[string][]string) *csiv1alpha1.CSINodeInfo {
driverInfoSpecs := []csiv1alpha1.CSIDriverInfoSpec{}
driverInfoStatuses := []csiv1alpha1.CSIDriverInfoStatus{}
for k, nodeID := range nodeIDs {
dspec := csiv1alpha1.CSIDriverInfoSpec{
Name: k,
NodeID: nodeID,
}
dstatus := csiv1alpha1.CSIDriverInfoStatus{
Name: k,
Available: true,
VolumePluginMechanism: csiv1alpha1.VolumePluginMechanismInTree,
}
if top, exists := topologyKeys[k]; exists {
dspec.TopologyKeys = top
}
driverInfoSpecs = append(driverInfoSpecs, dspec)
driverInfoStatuses = append(driverInfoStatuses, dstatus)
}
return &csiv1alpha1.CSINodeInfo{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Spec: csiv1alpha1.CSINodeInfoSpec{
Drivers: driverInfoSpecs,
},
Status: csiv1alpha1.CSINodeInfoStatus{
Drivers: driverInfoStatuses,
},
}
}
func applyNodeStatusPatch(originalNode *v1.Node, patch []byte) (*v1.Node, error) {
original, err := json.Marshal(originalNode)
if err != nil {
return nil, fmt.Errorf("failed to marshal original node %#v: %v", originalNode, err)
}
updated, err := strategicpatch.StrategicMergePatch(original, patch, v1.Node{})
if err != nil {
return nil, fmt.Errorf("failed to apply strategic merge patch %q on node %#v: %v",
patch, originalNode, err)
}
updatedNode := &v1.Node{}
if err := json.Unmarshal(updated, updatedNode); err != nil {
return nil, fmt.Errorf("failed to unmarshal updated node %q: %v", updated, err)
}
return updatedNode, nil
}
func hasPatchAction(actions []clienttesting.Action) clienttesting.Action {
for _, action := range actions {
if action.GetVerb() == "patch" {
return action
}
}
return nil
}