-
Notifications
You must be signed in to change notification settings - Fork 195
management workload domain isolation automation #3158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
chethanv28
merged 1 commit into
kubernetes-sigs:master
from
sipriyaa:domain-isolation-automation
Jan 21, 2025
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
/* | ||
Copyright 2025 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 e2e | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/onsi/ginkgo/v2" | ||
"github.com/onsi/gomega" | ||
|
||
v1 "k8s.io/api/core/v1" | ||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
clientset "k8s.io/client-go/kubernetes" | ||
"k8s.io/kubernetes/test/e2e/framework" | ||
fss "k8s.io/kubernetes/test/e2e/framework/statefulset" | ||
admissionapi "k8s.io/pod-security-admission/api" | ||
) | ||
|
||
var _ bool = ginkgo.Describe("[domain-isolation] Management-Workload-Domain-Isolation", func() { | ||
|
||
f := framework.NewDefaultFramework("domain-isolation") | ||
f.NamespacePodSecurityEnforceLevel = admissionapi.LevelPrivileged | ||
f.SkipNamespaceCreation = true // tests will create their own namespaces | ||
var ( | ||
client clientset.Interface | ||
namespace string | ||
storageProfileId string | ||
vcRestSessionId string | ||
allowedTopologies []v1.TopologySelectorLabelRequirement | ||
storagePolicyName string | ||
replicas int32 | ||
topkeyStartIndex int | ||
) | ||
|
||
ginkgo.BeforeEach(func() { | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
|
||
// making vc connection | ||
client = f.ClientSet | ||
bootstrap() | ||
|
||
// reading vc session id | ||
vcRestSessionId = createVcSession4RestApis(ctx) | ||
|
||
// reading topology map set for management doamin and workload domain | ||
topologyMap := GetAndExpectStringEnvVar(envTopologyMap) | ||
allowedTopologies = createAllowedTopolgies(topologyMap) | ||
}) | ||
|
||
ginkgo.AfterEach(func() { | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
|
||
ginkgo.By(fmt.Sprintf("Deleting all statefulsets in namespace: %v", namespace)) | ||
fss.DeleteAllStatefulSets(ctx, client, namespace) | ||
|
||
ginkgo.By(fmt.Sprintf("Deleting service nginx in namespace: %v", namespace)) | ||
err := client.CoreV1().Services(namespace).Delete(ctx, servicename, *metav1.NewDeleteOptions(0)) | ||
if !apierrors.IsNotFound(err) { | ||
gomega.Expect(err).NotTo(gomega.HaveOccurred()) | ||
} | ||
}) | ||
|
||
/* | ||
Testcase-1 | ||
Basic test | ||
Deploy statefulsets with 1 replica on namespace-1 in the supervisor cluster using vsan-zonal policy with | ||
immediate volume binding mode storageclass. | ||
|
||
Steps: | ||
1. Create a wcp namespace and tagged it to zone-2 workload zone. | ||
2. Read a zonal storage policy which is tagged to wcp namespace created in step #1 using Immediate Binding mode. | ||
3. Create statefulset with replica count 1. | ||
4. Wait for PVC and PV to reach Bound state. | ||
5. Verify PVC has csi.vsphere.volume-accessible-topology annotation with zone-2 | ||
6. Verify PV has node affinity rule for zone-2 | ||
7. Verify statefulset pod is in up and running state. | ||
8. Veirfy Pod node annoation. | ||
9. Perform cleanup: Delete Statefulset | ||
10. Perform cleanup: Delete PVC | ||
*/ | ||
|
||
ginkgo.It("Verifying volume creation and pv affinities when svc namespace is tagged to zonal-2 policy", func() { | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
|
||
// statefulset replica count | ||
replicas = 1 | ||
|
||
// reading zonal storage policy of zone-2 workload domain | ||
storagePolicyName = GetAndExpectStringEnvVar(envZonal2StoragePolicyName) | ||
storageProfileId = e2eVSphere.GetSpbmPolicyID(storagePolicyName) | ||
|
||
wrkld1ZoneName := GetAndExpectStringEnvVar(envWrkldDomain1ZoneName) | ||
|
||
/* | ||
EX - zone -> zone-1, zone-2, zone-3, zone-4 | ||
so topValStartIndex=1 and topValEndIndex=2 will fetch the 1st index value from topology map string | ||
*/ | ||
topValStartIndex := 1 | ||
topValEndIndex := 2 | ||
|
||
ginkgo.By("Create a WCP namespace tagged to zone-2") | ||
allowedTopologies = setSpecificAllowedTopology(allowedTopologies, topkeyStartIndex, topValStartIndex, | ||
topValEndIndex) | ||
namespace = createTestWcpNsWithZones( | ||
vcRestSessionId, storageProfileId, getSvcId(vcRestSessionId), []string{wrkld1ZoneName}) | ||
|
||
ginkgo.By("Read wcp namespace tagged zonal storage class") | ||
storageclass, err := client.StorageV1().StorageClasses().Get(ctx, storagePolicyName, metav1.GetOptions{}) | ||
if !apierrors.IsNotFound(err) { | ||
gomega.Expect(err).NotTo(gomega.HaveOccurred()) | ||
} | ||
|
||
ginkgo.By("Creating service") | ||
service := CreateService(namespace, client) | ||
defer func() { | ||
deleteService(namespace, client, service) | ||
}() | ||
|
||
ginkgo.By("Creating statefulset") | ||
statefulset := createCustomisedStatefulSets(ctx, client, namespace, true, replicas, false, nil, | ||
false, true, "", "", storageclass, storageclass.Name) | ||
defer func() { | ||
fss.DeleteAllStatefulSets(ctx, client, namespace) | ||
}() | ||
|
||
ginkgo.By("Verify svc pv affinity, pvc annotation and pod node affinity") | ||
err = verifyAnnotationsAndNodeAffinityForStatefulsetinSvc(ctx, client, statefulset, namespace, | ||
allowedTopologies) | ||
gomega.Expect(err).NotTo(gomega.HaveOccurred()) | ||
}) | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
/* | ||
Copyright 2025 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 e2e | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"math/rand" | ||
"strings" | ||
"time" | ||
|
||
"github.com/onsi/gomega" | ||
appsv1 "k8s.io/api/apps/v1" | ||
v1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
clientset "k8s.io/client-go/kubernetes" | ||
"k8s.io/kubernetes/test/e2e/framework" | ||
fnodes "k8s.io/kubernetes/test/e2e/framework/node" | ||
) | ||
|
||
/* | ||
This util will verify supervisor pvc annotation, pv affinity rules, | ||
pod node anotation and cns volume metadata | ||
*/ | ||
func verifyAnnotationsAndNodeAffinityForStatefulsetinSvc(ctx context.Context, client clientset.Interface, | ||
statefulset *appsv1.StatefulSet, namespace string, | ||
allowedTopologies []v1.TopologySelectorLabelRequirement) error { | ||
// Read topology mapping | ||
allowedTopologiesMap := createAllowedTopologiesMap(allowedTopologies) | ||
topologyMap := GetAndExpectStringEnvVar(envTopologyMap) | ||
_, topologyCategories := createTopologyMapLevel5(topologyMap) | ||
|
||
framework.Logf("Reading statefulset pod list for node affinity verification") | ||
ssPodsBeforeScaleDown := GetListOfPodsInSts(client, statefulset) | ||
for _, sspod := range ssPodsBeforeScaleDown.Items { | ||
// Get Pod details | ||
_, err := client.CoreV1().Pods(namespace).Get(ctx, sspod.Name, metav1.GetOptions{}) | ||
if err != nil { | ||
return fmt.Errorf("failed to get pod %s in namespace %s: %w", sspod.Name, namespace, err) | ||
} | ||
|
||
framework.Logf("Verifying PVC annotation and PV affinity rules") | ||
for _, volumespec := range sspod.Spec.Volumes { | ||
if volumespec.PersistentVolumeClaim != nil { | ||
svPvcName := volumespec.PersistentVolumeClaim.ClaimName | ||
pv := getPvFromClaim(client, statefulset.Namespace, svPvcName) | ||
|
||
// Get SVC PVC | ||
svcPVC, err := client.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, svPvcName, metav1.GetOptions{}) | ||
if err != nil { | ||
return fmt.Errorf("failed to get SVC PVC %s in namespace %s: %w", svPvcName, namespace, err) | ||
} | ||
|
||
// Get ready and schedulable nodes | ||
nodeList, err := fnodes.GetReadySchedulableNodes(ctx, client) | ||
if err != nil { | ||
return fmt.Errorf("failed to get ready and schedulable nodes: %w", err) | ||
} | ||
if len(nodeList.Items) <= 0 { | ||
return fmt.Errorf("no ready and schedulable nodes found") | ||
} | ||
|
||
// Verify SV PVC topology annotations | ||
err = checkPvcTopologyAnnotationOnSvc(svcPVC, allowedTopologiesMap, topologyCategories) | ||
if err != nil { | ||
return fmt.Errorf("topology annotation verification failed for SVC PVC %s: %w", svcPVC.Name, err) | ||
} | ||
|
||
// Verify SV PV node affinity details | ||
svcPV := getPvFromClaim(client, namespace, svPvcName) | ||
_, err = verifyVolumeTopologyForLevel5(svcPV, allowedTopologiesMap) | ||
if err != nil { | ||
return fmt.Errorf("topology verification failed for SVC PV %s: %w", svcPV.Name, err) | ||
} | ||
|
||
// Verify pod node annotation | ||
_, err = verifyPodLocationLevel5(&sspod, nodeList, allowedTopologiesMap) | ||
if err != nil { | ||
return fmt.Errorf("pod node annotation verification failed for pod %s: %w", sspod.Name, err) | ||
} | ||
|
||
// Verify CNS volume metadata | ||
err = verifyVolumeMetadataInCNS(&e2eVSphere, pv.Spec.CSI.VolumeHandle, svPvcName, pv.ObjectMeta.Name, sspod.Name) | ||
if err != nil { | ||
return fmt.Errorf("CNS volume metadata verification failed for pod %s: %w", sspod.Name, err) | ||
} | ||
} | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// Function to check annotation on a Supervisor PVC | ||
func checkPvcTopologyAnnotationOnSvc(svcPVC *v1.PersistentVolumeClaim, | ||
allowedTopologies map[string][]string, categories []string) error { | ||
|
||
annotationsMap := svcPVC.Annotations | ||
if accessibleTopoString, exists := annotationsMap[tkgHAccessibleAnnotationKey]; exists { | ||
// Parse the accessible topology string | ||
var accessibleTopologyList []map[string]string | ||
err := json.Unmarshal([]byte(accessibleTopoString), &accessibleTopologyList) | ||
if err != nil { | ||
return fmt.Errorf("failed to parse accessible topology: %v", err) | ||
} | ||
|
||
for _, topo := range accessibleTopologyList { | ||
for topoKey, topoVal := range topo { | ||
if allowedVals, ok := allowedTopologies[topoKey]; ok { | ||
// Check if topoVal exists in allowedVals | ||
found := false | ||
for _, val := range allowedVals { | ||
if val == topoVal { | ||
found = true | ||
break | ||
} | ||
} | ||
if !found { | ||
return fmt.Errorf("couldn't find allowed accessible topology: %v on svc pvc: %s, instead found: %v", | ||
allowedVals, svcPVC.Name, topoVal) | ||
} | ||
} else { | ||
category := strings.SplitN(topoKey, "/", 2) | ||
if len(category) > 1 && !containsItem(categories, category[1]) { | ||
return fmt.Errorf("couldn't find key: %s in allowed categories %v", category[1], categories) | ||
} | ||
} | ||
} | ||
} | ||
} else { | ||
return fmt.Errorf("couldn't find annotation key: %s on svc pvc: %s", | ||
tkgHAccessibleAnnotationKey, svcPVC.Name) | ||
} | ||
return nil | ||
} | ||
|
||
// Helper function to check if a string exists in a slice | ||
func containsItem(slice []string, item string) bool { | ||
for _, val := range slice { | ||
if val == item { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
/* | ||
This util createTestWcpNsWithZones will create a wcp namespace which will be tagged to the zone and | ||
storage policy passed in the util parameters | ||
*/ | ||
func createTestWcpNsWithZones( | ||
vcRestSessionId string, storagePolicyId string, | ||
supervisorId string, zoneNames []string) string { | ||
|
||
vcIp := e2eVSphere.Config.Global.VCenterHostname | ||
r := rand.New(rand.NewSource(time.Now().Unix())) | ||
|
||
namespace := fmt.Sprintf("csi-vmsvcns-%v", r.Intn(10000)) | ||
nsCreationUrl := "https://" + vcIp + "/api/vcenter/namespaces/instances/v2" | ||
|
||
// Create a string to represent the zones array | ||
var zonesString string | ||
for i, zone := range zoneNames { | ||
if i > 0 { | ||
zonesString += "," | ||
} | ||
zonesString += fmt.Sprintf(`{"name": "%s"}`, zone) | ||
} | ||
|
||
reqBody := fmt.Sprintf(`{ | ||
"namespace": "%s", | ||
"storage_specs": [ | ||
{ | ||
"policy": "%s" | ||
} | ||
], | ||
"supervisor": "%s", | ||
"zones": [%s] | ||
}`, namespace, storagePolicyId, supervisorId, zonesString) | ||
|
||
// Print the request body for debugging | ||
fmt.Println(reqBody) | ||
|
||
// Make the API request | ||
_, statusCode := invokeVCRestAPIPostRequest(vcRestSessionId, nsCreationUrl, reqBody) | ||
|
||
// Validate the status code | ||
gomega.Expect(statusCode).Should(gomega.BeNumerically("==", 204)) | ||
framework.Logf("Successfully created namespace %v in SVC.", namespace) | ||
return namespace | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.