|
| 1 | +/* |
| 2 | +Copyright 2025 The Kubernetes Authors. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package e2e |
| 18 | + |
| 19 | +import ( |
| 20 | + "context" |
| 21 | + "encoding/json" |
| 22 | + "fmt" |
| 23 | + "math/rand" |
| 24 | + "strings" |
| 25 | + "time" |
| 26 | + |
| 27 | + "github.com/onsi/gomega" |
| 28 | + appsv1 "k8s.io/api/apps/v1" |
| 29 | + v1 "k8s.io/api/core/v1" |
| 30 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 31 | + clientset "k8s.io/client-go/kubernetes" |
| 32 | + "k8s.io/kubernetes/test/e2e/framework" |
| 33 | + fnodes "k8s.io/kubernetes/test/e2e/framework/node" |
| 34 | +) |
| 35 | + |
| 36 | +/* |
| 37 | +This util will verify supervisor pvc annotation, pv affinity rules, |
| 38 | +pod node anotation and cns volume metadata |
| 39 | +*/ |
| 40 | +func verifyAnnotationsAndNodeAffinityForStatefulsetinSvc(ctx context.Context, client clientset.Interface, |
| 41 | + statefulset *appsv1.StatefulSet, namespace string, |
| 42 | + allowedTopologies []v1.TopologySelectorLabelRequirement) error { |
| 43 | + // Read topology mapping |
| 44 | + allowedTopologiesMap := createAllowedTopologiesMap(allowedTopologies) |
| 45 | + topologyMap := GetAndExpectStringEnvVar(envTopologyMap) |
| 46 | + _, topologyCategories := createTopologyMapLevel5(topologyMap) |
| 47 | + |
| 48 | + framework.Logf("Reading statefulset pod list for node affinity verification") |
| 49 | + ssPodsBeforeScaleDown := GetListOfPodsInSts(client, statefulset) |
| 50 | + for _, sspod := range ssPodsBeforeScaleDown.Items { |
| 51 | + // Get Pod details |
| 52 | + _, err := client.CoreV1().Pods(namespace).Get(ctx, sspod.Name, metav1.GetOptions{}) |
| 53 | + if err != nil { |
| 54 | + return fmt.Errorf("failed to get pod %s in namespace %s: %w", sspod.Name, namespace, err) |
| 55 | + } |
| 56 | + |
| 57 | + framework.Logf("Verifying PVC annotation and PV affinity rules") |
| 58 | + for _, volumespec := range sspod.Spec.Volumes { |
| 59 | + if volumespec.PersistentVolumeClaim != nil { |
| 60 | + svPvcName := volumespec.PersistentVolumeClaim.ClaimName |
| 61 | + pv := getPvFromClaim(client, statefulset.Namespace, svPvcName) |
| 62 | + |
| 63 | + // Get SVC PVC |
| 64 | + svcPVC, err := client.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, svPvcName, metav1.GetOptions{}) |
| 65 | + if err != nil { |
| 66 | + return fmt.Errorf("failed to get SVC PVC %s in namespace %s: %w", svPvcName, namespace, err) |
| 67 | + } |
| 68 | + |
| 69 | + // Get ready and schedulable nodes |
| 70 | + nodeList, err := fnodes.GetReadySchedulableNodes(ctx, client) |
| 71 | + if err != nil { |
| 72 | + return fmt.Errorf("failed to get ready and schedulable nodes: %w", err) |
| 73 | + } |
| 74 | + if len(nodeList.Items) <= 0 { |
| 75 | + return fmt.Errorf("no ready and schedulable nodes found") |
| 76 | + } |
| 77 | + |
| 78 | + // Verify SV PVC topology annotations |
| 79 | + err = checkPvcTopologyAnnotationOnSvc(svcPVC, allowedTopologiesMap, topologyCategories) |
| 80 | + if err != nil { |
| 81 | + return fmt.Errorf("topology annotation verification failed for SVC PVC %s: %w", svcPVC.Name, err) |
| 82 | + } |
| 83 | + |
| 84 | + // Verify SV PV node affinity details |
| 85 | + svcPV := getPvFromClaim(client, namespace, svPvcName) |
| 86 | + _, err = verifyVolumeTopologyForLevel5(svcPV, allowedTopologiesMap) |
| 87 | + if err != nil { |
| 88 | + return fmt.Errorf("topology verification failed for SVC PV %s: %w", svcPV.Name, err) |
| 89 | + } |
| 90 | + |
| 91 | + // Verify pod node annotation |
| 92 | + _, err = verifyPodLocationLevel5(&sspod, nodeList, allowedTopologiesMap) |
| 93 | + if err != nil { |
| 94 | + return fmt.Errorf("pod node annotation verification failed for pod %s: %w", sspod.Name, err) |
| 95 | + } |
| 96 | + |
| 97 | + // Verify CNS volume metadata |
| 98 | + err = verifyVolumeMetadataInCNS(&e2eVSphere, pv.Spec.CSI.VolumeHandle, svPvcName, pv.ObjectMeta.Name, sspod.Name) |
| 99 | + if err != nil { |
| 100 | + return fmt.Errorf("CNS volume metadata verification failed for pod %s: %w", sspod.Name, err) |
| 101 | + } |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + return nil |
| 106 | +} |
| 107 | + |
| 108 | + |
| 109 | +// Function to check annotation on a Supervisor PVC |
| 110 | +func checkPvcTopologyAnnotationOnSvc(svcPVC *v1.PersistentVolumeClaim, |
| 111 | + allowedTopologies map[string][]string, categories []string) error { |
| 112 | + |
| 113 | + annotationsMap := svcPVC.Annotations |
| 114 | + if accessibleTopoString, exists := annotationsMap[tkgHAccessibleAnnotationKey]; exists { |
| 115 | + // Parse the accessible topology string |
| 116 | + var accessibleTopologyList []map[string]string |
| 117 | + err := json.Unmarshal([]byte(accessibleTopoString), &accessibleTopologyList) |
| 118 | + if err != nil { |
| 119 | + return fmt.Errorf("failed to parse accessible topology: %v", err) |
| 120 | + } |
| 121 | + |
| 122 | + for _, topo := range accessibleTopologyList { |
| 123 | + for topoKey, topoVal := range topo { |
| 124 | + if allowedVals, ok := allowedTopologies[topoKey]; ok { |
| 125 | + // Check if topoVal exists in allowedVals |
| 126 | + found := false |
| 127 | + for _, val := range allowedVals { |
| 128 | + if val == topoVal { |
| 129 | + found = true |
| 130 | + break |
| 131 | + } |
| 132 | + } |
| 133 | + if !found { |
| 134 | + return fmt.Errorf("couldn't find allowed accessible topology: %v on svc pvc: %s, instead found: %v", |
| 135 | + allowedVals, svcPVC.Name, topoVal) |
| 136 | + } |
| 137 | + } else { |
| 138 | + category := strings.SplitN(topoKey, "/", 2) |
| 139 | + if len(category) > 1 && !containsItem(categories, category[1]) { |
| 140 | + return fmt.Errorf("couldn't find key: %s in allowed categories %v", category[1], categories) |
| 141 | + } |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + } else { |
| 146 | + return fmt.Errorf("couldn't find annotation key: %s on svc pvc: %s", |
| 147 | + tkgHAccessibleAnnotationKey, svcPVC.Name) |
| 148 | + } |
| 149 | + return nil |
| 150 | +} |
| 151 | + |
| 152 | +// Helper function to check if a string exists in a slice |
| 153 | +func containsItem(slice []string, item string) bool { |
| 154 | + for _, val := range slice { |
| 155 | + if val == item { |
| 156 | + return true |
| 157 | + } |
| 158 | + } |
| 159 | + return false |
| 160 | +} |
| 161 | + |
| 162 | +/* |
| 163 | +This util createTestWcpNsWithZones will create a wcp namespace which will be tagged to the zone and |
| 164 | +storage policy passed in the util parameters |
| 165 | +*/ |
| 166 | +func createTestWcpNsWithZones( |
| 167 | + vcRestSessionId string, storagePolicyId string, |
| 168 | + supervisorId string, zoneNames []string) string { |
| 169 | + |
| 170 | + vcIp := e2eVSphere.Config.Global.VCenterHostname |
| 171 | + r := rand.New(rand.NewSource(time.Now().Unix())) |
| 172 | + |
| 173 | + namespace := fmt.Sprintf("csi-vmsvcns-%v", r.Intn(10000)) |
| 174 | + nsCreationUrl := "https://" + vcIp + "/api/vcenter/namespaces/instances/v2" |
| 175 | + |
| 176 | + // Create a string to represent the zones array |
| 177 | + var zonesString string |
| 178 | + for i, zone := range zoneNames { |
| 179 | + if i > 0 { |
| 180 | + zonesString += "," |
| 181 | + } |
| 182 | + zonesString += fmt.Sprintf(`{"name": "%s"}`, zone) |
| 183 | + } |
| 184 | + |
| 185 | + reqBody := fmt.Sprintf(`{ |
| 186 | + "namespace": "%s", |
| 187 | + "storage_specs": [ |
| 188 | + { |
| 189 | + "policy": "%s" |
| 190 | + } |
| 191 | + ], |
| 192 | + "supervisor": "%s", |
| 193 | + "zones": [%s] |
| 194 | + }`, namespace, storagePolicyId, supervisorId, zonesString) |
| 195 | + |
| 196 | + // Print the request body for debugging |
| 197 | + fmt.Println(reqBody) |
| 198 | + |
| 199 | + // Make the API request |
| 200 | + _, statusCode := invokeVCRestAPIPostRequest(vcRestSessionId, nsCreationUrl, reqBody) |
| 201 | + |
| 202 | + // Validate the status code |
| 203 | + gomega.Expect(statusCode).Should(gomega.BeNumerically("==", 204)) |
| 204 | + framework.Logf("Successfully created namespace %v in SVC.", namespace) |
| 205 | + return namespace |
| 206 | +} |
0 commit comments