2022-06-14 08:51:33 +00:00
/ *
Copyright 2020 - 2022 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 utils
import (
"context"
"fmt"
"os"
"path/filepath"
2023-02-10 09:07:48 +00:00
"reflect"
2022-06-14 08:51:33 +00:00
"runtime"
"strings"
"time"
2023-02-07 15:48:01 +00:00
"github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha2"
2022-06-14 08:51:33 +00:00
topologyclientset "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned"
"github.com/onsi/gomega"
2023-02-10 09:07:48 +00:00
nfdtopologyupdater "sigs.k8s.io/node-feature-discovery/pkg/nfd-topology-updater"
2022-06-14 08:51:33 +00:00
"sigs.k8s.io/node-feature-discovery/pkg/topologypolicy"
2022-10-14 12:28:52 +00:00
corev1 "k8s.io/api/core/v1"
2022-06-14 08:51:33 +00:00
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
extclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2022-11-17 18:23:31 +00:00
"k8s.io/apimachinery/pkg/util/wait"
2022-06-14 08:51:33 +00:00
"k8s.io/client-go/kubernetes/scheme"
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
"k8s.io/kubernetes/test/e2e/framework"
)
func init ( ) {
// make golangci-lint happy
_ = apiextensionsv1 . AddToScheme ( scheme . Scheme )
}
// NewNodeResourceTopologies makes a CRD golang object representing NodeResourceTopology definition
func NewNodeResourceTopologies ( ) ( * apiextensionsv1 . CustomResourceDefinition , error ) {
_ , file , _ , ok := runtime . Caller ( 0 )
if ! ok {
return nil , fmt . Errorf ( "cannot retrieve manifests directory" )
}
baseDir := filepath . Dir ( file )
crdPath := filepath . Clean ( filepath . Join ( baseDir , ".." , ".." , ".." , "deployment" , "base" , "noderesourcetopologies-crd" , "noderesourcetopologies.yaml" ) )
data , err := os . ReadFile ( crdPath )
if err != nil {
return nil , err
}
decode := scheme . Codecs . UniversalDeserializer ( ) . Decode
obj , _ , err := decode ( data , nil , nil )
if err != nil {
return nil , err
}
crd , ok := obj . ( * apiextensionsv1 . CustomResourceDefinition )
if ! ok {
return nil , fmt . Errorf ( "unexpected type, got %t" , obj )
}
return crd , nil
}
// CreateNodeResourceTopologies creates the NodeResourceTopology in the cluster if the CRD doesn't exists already.
// Returns the CRD golang object present in the cluster.
2023-04-18 11:23:03 +00:00
func CreateNodeResourceTopologies ( ctx context . Context , extClient extclient . Interface ) ( * apiextensionsv1 . CustomResourceDefinition , error ) {
2022-06-14 08:51:33 +00:00
crd , err := NewNodeResourceTopologies ( )
if err != nil {
return nil , err
}
2022-11-17 18:23:31 +00:00
// Delete existing CRD (if any) with this we also get rid of stale objects
2023-04-18 11:23:03 +00:00
err = extClient . ApiextensionsV1 ( ) . CustomResourceDefinitions ( ) . Delete ( ctx , crd . Name , metav1 . DeleteOptions { } )
2022-06-14 08:51:33 +00:00
if err != nil && ! errors . IsNotFound ( err ) {
2022-11-17 18:23:31 +00:00
return nil , fmt . Errorf ( "failed to delete NodeResourceTopology CRD: %w" , err )
2022-06-14 08:51:33 +00:00
}
2022-11-17 18:23:31 +00:00
// It takes time for the delete operation, wait until the CRD completely gone
2023-04-18 12:47:20 +00:00
if err = wait . PollUntilContextTimeout ( ctx , 5 * time . Second , 1 * time . Minute , true , func ( ctx context . Context ) ( bool , error ) {
2023-04-18 11:23:03 +00:00
_ , err = extClient . ApiextensionsV1 ( ) . CustomResourceDefinitions ( ) . Get ( ctx , crd . Name , metav1 . GetOptions { } )
2022-11-17 18:23:31 +00:00
if err == nil {
return false , nil
}
if errors . IsNotFound ( err ) {
return true , nil
}
return false , err
} ) ; err != nil {
return nil , fmt . Errorf ( "failed to get NodeResourceTopology CRD: %w" , err )
2022-06-14 08:51:33 +00:00
}
2023-04-18 11:23:03 +00:00
return extClient . ApiextensionsV1 ( ) . CustomResourceDefinitions ( ) . Create ( ctx , crd , metav1 . CreateOptions { } )
2022-06-14 08:51:33 +00:00
}
2023-08-17 18:44:50 +00:00
// CreateNodeResourceTopology creates a dummy NodeResourceTopology object for a node
func CreateNodeResourceTopology ( ctx context . Context , topologyClient * topologyclientset . Clientset , nodeName string ) error {
nrt := & v1alpha2 . NodeResourceTopology {
ObjectMeta : metav1 . ObjectMeta { Name : nodeName } ,
Zones : v1alpha2 . ZoneList { } ,
}
_ , err := topologyClient . TopologyV1alpha2 ( ) . NodeResourceTopologies ( ) . Create ( ctx , nrt , metav1 . CreateOptions { } )
return err
}
2022-06-14 08:51:33 +00:00
// GetNodeTopology returns the NodeResourceTopology data for the node identified by `nodeName`.
2023-04-18 11:23:03 +00:00
func GetNodeTopology ( ctx context . Context , topologyClient * topologyclientset . Clientset , nodeName string ) * v1alpha2 . NodeResourceTopology {
2023-02-07 15:48:01 +00:00
var nodeTopology * v1alpha2 . NodeResourceTopology
2022-06-14 08:51:33 +00:00
var err error
gomega . EventuallyWithOffset ( 1 , func ( ) bool {
2023-04-18 11:23:03 +00:00
nodeTopology , err = topologyClient . TopologyV1alpha2 ( ) . NodeResourceTopologies ( ) . Get ( ctx , nodeName , metav1 . GetOptions { } )
2022-06-14 08:51:33 +00:00
if err != nil {
framework . Logf ( "failed to get the node topology resource: %v" , err )
return false
}
return true
2023-09-01 13:11:04 +00:00
} ) . WithPolling ( 5 * time . Second ) . WithTimeout ( 1 * time . Minute ) . Should ( gomega . BeTrue ( ) )
2022-06-14 08:51:33 +00:00
return nodeTopology
}
// AllocatableResourceListFromNodeResourceTopology extract the map zone:allocatableResources from the given NodeResourceTopology instance.
2023-02-07 15:48:01 +00:00
func AllocatableResourceListFromNodeResourceTopology ( nodeTopo * v1alpha2 . NodeResourceTopology ) map [ string ] corev1 . ResourceList {
2022-10-14 12:28:52 +00:00
allocRes := make ( map [ string ] corev1 . ResourceList )
2022-06-14 08:51:33 +00:00
for _ , zone := range nodeTopo . Zones {
if zone . Type != "Node" {
continue
}
2022-10-14 12:28:52 +00:00
resList := make ( corev1 . ResourceList )
2022-06-14 08:51:33 +00:00
for _ , res := range zone . Resources {
2022-10-14 12:28:52 +00:00
resList [ corev1 . ResourceName ( res . Name ) ] = res . Allocatable . DeepCopy ( )
2022-06-14 08:51:33 +00:00
}
if len ( resList ) == 0 {
continue
}
allocRes [ zone . Name ] = resList
}
return allocRes
}
// CompareAllocatableResources compares `expected` and `got` map zone:allocatableResources respectively (see: AllocatableResourceListFromNodeResourceTopology),
// and informs the caller if the maps are equal. Here `equal` means the same zoneNames with the same resources, where the resources are equal if they have
// the same resources with the same quantities. Returns the name of the different zone, the name of the different resources within the zone,
// the comparison result (same semantic as strings.Compare) and a boolean that reports if the resourceLists are consistent. See `CompareResourceList`.
2022-10-14 12:28:52 +00:00
func CompareAllocatableResources ( expected , got map [ string ] corev1 . ResourceList ) ( string , string , int , bool ) {
2022-06-14 08:51:33 +00:00
if len ( got ) != len ( expected ) {
framework . Logf ( "-> expected=%v (len=%d) got=%v (len=%d)" , expected , len ( expected ) , got , len ( got ) )
return "" , "" , 0 , false
}
for expZoneName , expResList := range expected {
gotResList , ok := got [ expZoneName ]
if ! ok {
return expZoneName , "" , 0 , false
}
if resName , cmp , ok := CompareResourceList ( expResList , gotResList ) ; ! ok || cmp != 0 {
return expZoneName , resName , cmp , ok
}
}
return "" , "" , 0 , true
}
// CompareResourceList compares `expected` and `got` ResourceList respectively, and informs the caller if the two ResourceList
// are equal. Here `equal` means the same resources with the same quantities. Returns the different resource,
// the comparison result (same semantic as strings.Compare) and a boolean that reports if the resourceLists are consistent.
// The ResourceLists are consistent only if the represent the same resource set (all the resources listed in one are
// also present in the another; no ResourceList is a superset nor a subset of the other)
2022-10-14 12:28:52 +00:00
func CompareResourceList ( expected , got corev1 . ResourceList ) ( string , int , bool ) {
2022-06-14 08:51:33 +00:00
if len ( got ) != len ( expected ) {
framework . Logf ( "-> expected=%v (len=%d) got=%v (len=%d)" , expected , len ( expected ) , got , len ( got ) )
return "" , 0 , false
}
for expResName , expResQty := range expected {
gotResQty , ok := got [ expResName ]
if ! ok {
return string ( expResName ) , 0 , false
}
if cmp := gotResQty . Cmp ( expResQty ) ; cmp != 0 {
framework . Logf ( "-> resource=%q cmp=%d expected=%v got=%v" , expResName , cmp , expResQty , gotResQty )
return string ( expResName ) , cmp , true
}
}
return "" , 0 , true
}
// IsValidNodeTopology checks the provided NodeResourceTopology object if it is well-formad, internally consistent and
// consistent with the given kubelet config object. Returns true if the NodeResourceTopology object is consistent and well
// formet, false otherwise; if return false, logs the failure reason.
2023-02-07 15:48:01 +00:00
func IsValidNodeTopology ( nodeTopology * v1alpha2 . NodeResourceTopology , kubeletConfig * kubeletconfig . KubeletConfiguration ) bool {
2022-06-14 08:51:33 +00:00
if nodeTopology == nil || len ( nodeTopology . TopologyPolicies ) == 0 {
framework . Logf ( "failed to get topology policy from the node topology resource" )
return false
}
tmPolicy := string ( topologypolicy . DetectTopologyPolicy ( kubeletConfig . TopologyManagerPolicy , kubeletConfig . TopologyManagerScope ) )
if nodeTopology . TopologyPolicies [ 0 ] != tmPolicy {
framework . Logf ( "topology policy mismatch got %q expected %q" , nodeTopology . TopologyPolicies [ 0 ] , tmPolicy )
return false
}
2023-02-10 09:07:48 +00:00
expectedPolicyAttribute := v1alpha2 . AttributeInfo {
Name : nfdtopologyupdater . TopologyManagerPolicyAttributeName ,
Value : kubeletConfig . TopologyManagerPolicy ,
}
if ! containsAttribute ( nodeTopology . Attributes , expectedPolicyAttribute ) {
framework . Logf ( "topology policy attributes don't have correct topologyManagerPolicy attribute expected %v attributeList %v" , expectedPolicyAttribute , nodeTopology . Attributes )
return false
}
expectedScopeAttribute := v1alpha2 . AttributeInfo {
Name : nfdtopologyupdater . TopologyManagerScopeAttributeName ,
Value : kubeletConfig . TopologyManagerScope ,
}
if ! containsAttribute ( nodeTopology . Attributes , expectedScopeAttribute ) {
framework . Logf ( "topology policy attributes don't have correct topologyManagerScope attribute expected %v attributeList %v" , expectedScopeAttribute , nodeTopology . Attributes )
return false
}
2024-08-30 13:42:45 +00:00
if len ( nodeTopology . Zones ) == 0 {
2022-06-14 08:51:33 +00:00
framework . Logf ( "failed to get topology zones from the node topology resource" )
return false
}
foundNodes := 0
for _ , zone := range nodeTopology . Zones {
// TODO constant not in the APIs
if ! strings . HasPrefix ( strings . ToUpper ( zone . Type ) , "NODE" ) {
continue
}
foundNodes ++
if ! isValidCostList ( zone . Name , zone . Costs ) {
framework . Logf ( "invalid cost list for zone %q" , zone . Name )
return false
}
if ! isValidResourceList ( zone . Name , zone . Resources ) {
framework . Logf ( "invalid resource list for zone %q" , zone . Name )
return false
}
}
return foundNodes > 0
}
2023-02-07 15:48:01 +00:00
func isValidCostList ( zoneName string , costs v1alpha2 . CostList ) bool {
2022-06-14 08:51:33 +00:00
if len ( costs ) == 0 {
framework . Logf ( "failed to get topology costs for zone %q from the node topology resource" , zoneName )
return false
}
// TODO cross-validate zone names
for _ , cost := range costs {
if cost . Name == "" || cost . Value < 0 {
framework . Logf ( "malformed cost %v for zone %q" , cost , zoneName )
}
}
return true
}
2023-02-07 15:48:01 +00:00
func isValidResourceList ( zoneName string , resources v1alpha2 . ResourceInfoList ) bool {
2022-06-14 08:51:33 +00:00
if len ( resources ) == 0 {
framework . Logf ( "failed to get topology resources for zone %q from the node topology resource" , zoneName )
return false
}
foundCpu := false
for _ , resource := range resources {
// TODO constant not in the APIs
if strings . ToUpper ( resource . Name ) == "CPU" {
foundCpu = true
}
allocatable , ok1 := resource . Allocatable . AsInt64 ( )
capacity , ok2 := resource . Capacity . AsInt64 ( )
if ( ! ok1 || ! ok2 ) || ( ( allocatable < 0 || capacity < 0 ) || ( capacity < allocatable ) ) {
framework . Logf ( "malformed resource %v for zone %q" , resource , zoneName )
return false
}
}
return foundCpu
}
2023-02-10 09:07:48 +00:00
func containsAttribute ( attributes v1alpha2 . AttributeList , attribute v1alpha2 . AttributeInfo ) bool {
for _ , attr := range attributes {
if reflect . DeepEqual ( attr , attribute ) {
return true
}
}
return false
}