@@ -860,6 +860,128 @@ func TestAdmitBelowTerminatingQuotaLimit(t *testing.T) {
860
860
}
861
861
}
862
862
863
+ // TestAdmitBelowTerminatingQuotaLimitWhenPodScopeUpdated ensures that terminating pods are charged to the right quota.
864
+ // It creates a terminating and non-terminating quota, and changes an existing pod to be terminating.
865
+ // It ensures that the terminating quota is incremented, and the non-terminating quota is not.
866
+ //
867
+ // The Quota admission is intended to fail "high" in this case, and depends on a controller reconciling actual persisted
868
+ // use to lower / free the reserved quota. We need always overcount in the admission plugin if something later causes
869
+ // the request to be rejected, so you can not reduce quota with requests that aren't completed.
870
+ func TestAdmitBelowTerminatingQuotaLimitWhenPodScopeUpdated (t * testing.T ) {
871
+ resourceQuotaNonTerminating := & corev1.ResourceQuota {
872
+ ObjectMeta : metav1.ObjectMeta {Name : "quota-non-terminating" , Namespace : "test" , ResourceVersion : "124" },
873
+ Spec : corev1.ResourceQuotaSpec {
874
+ Scopes : []corev1.ResourceQuotaScope {corev1 .ResourceQuotaScopeNotTerminating },
875
+ },
876
+ Status : corev1.ResourceQuotaStatus {
877
+ Hard : corev1.ResourceList {
878
+ corev1 .ResourceCPU : resource .MustParse ("3" ),
879
+ corev1 .ResourceMemory : resource .MustParse ("100Gi" ),
880
+ corev1 .ResourcePods : resource .MustParse ("5" ),
881
+ },
882
+ Used : corev1.ResourceList {
883
+ corev1 .ResourceCPU : resource .MustParse ("1" ),
884
+ corev1 .ResourceMemory : resource .MustParse ("50Gi" ),
885
+ corev1 .ResourcePods : resource .MustParse ("3" ),
886
+ },
887
+ },
888
+ }
889
+ resourceQuotaTerminating := & corev1.ResourceQuota {
890
+ ObjectMeta : metav1.ObjectMeta {Name : "quota-terminating" , Namespace : "test" , ResourceVersion : "124" },
891
+ Spec : corev1.ResourceQuotaSpec {
892
+ Scopes : []corev1.ResourceQuotaScope {corev1 .ResourceQuotaScopeTerminating },
893
+ },
894
+ Status : corev1.ResourceQuotaStatus {
895
+ Hard : corev1.ResourceList {
896
+ corev1 .ResourceCPU : resource .MustParse ("3" ),
897
+ corev1 .ResourceMemory : resource .MustParse ("100Gi" ),
898
+ corev1 .ResourcePods : resource .MustParse ("5" ),
899
+ },
900
+ Used : corev1.ResourceList {
901
+ corev1 .ResourceCPU : resource .MustParse ("1" ),
902
+ corev1 .ResourceMemory : resource .MustParse ("50Gi" ),
903
+ corev1 .ResourcePods : resource .MustParse ("3" ),
904
+ },
905
+ },
906
+ }
907
+ stopCh := make (chan struct {})
908
+ defer close (stopCh )
909
+
910
+ kubeClient := fake .NewSimpleClientset (resourceQuotaTerminating , resourceQuotaNonTerminating )
911
+ informerFactory := informers .NewSharedInformerFactory (kubeClient , 0 )
912
+
913
+ handler , err := createHandler (kubeClient , informerFactory , stopCh )
914
+ if err != nil {
915
+ t .Errorf ("Error occurred while creating admission plugin: %v" , err )
916
+ }
917
+
918
+ err = informerFactory .Core ().V1 ().ResourceQuotas ().Informer ().GetIndexer ().Add (resourceQuotaNonTerminating )
919
+ if err != nil {
920
+ t .Errorf ("Error occurred while adding resource quota to the indexer: %v" , err )
921
+ }
922
+ err = informerFactory .Core ().V1 ().ResourceQuotas ().Informer ().GetIndexer ().Add (resourceQuotaTerminating )
923
+ if err != nil {
924
+ t .Errorf ("Error occurred while adding resource quota to the indexer: %v" , err )
925
+ }
926
+
927
+ // old pod belonged to the non-terminating scope, but updated version belongs to the terminating scope
928
+ existingPod := validPod ("allowed-pod" , 1 , getResourceRequirements (getResourceList ("100m" , "2Gi" ), getResourceList ("" , "" )))
929
+ existingPod .ResourceVersion = "1"
930
+ newPod := validPod ("allowed-pod" , 1 , getResourceRequirements (getResourceList ("100m" , "2Gi" ), getResourceList ("" , "" )))
931
+ activeDeadlineSeconds := int64 (30 )
932
+ newPod .Spec .ActiveDeadlineSeconds = & activeDeadlineSeconds
933
+ err = handler .Validate (context .TODO (), admission .NewAttributesRecord (newPod , existingPod , api .Kind ("Pod" ).WithVersion ("version" ), newPod .Namespace , newPod .Name , corev1 .Resource ("pods" ).WithVersion ("version" ), "" , admission .Update , & metav1.CreateOptions {}, false , nil ), nil )
934
+ if err != nil {
935
+ t .Errorf ("Unexpected error: %v" , err )
936
+ }
937
+ if len (kubeClient .Actions ()) == 0 {
938
+ t .Errorf ("Expected a client action" )
939
+ }
940
+
941
+ expectedActionSet := sets .NewString (
942
+ strings .Join ([]string {"update" , "resourcequotas" , "status" }, "-" ),
943
+ )
944
+ actionSet := sets .NewString ()
945
+ for _ , action := range kubeClient .Actions () {
946
+ actionSet .Insert (strings .Join ([]string {action .GetVerb (), action .GetResource ().Resource , action .GetSubresource ()}, "-" ))
947
+ }
948
+ if ! actionSet .HasAll (expectedActionSet .List ()... ) {
949
+ t .Errorf ("Expected actions:\n %v\n but got:\n %v\n Difference:\n %v" , expectedActionSet , actionSet , expectedActionSet .Difference (actionSet ))
950
+ }
951
+
952
+ decimatedActions := removeListWatch (kubeClient .Actions ())
953
+ lastActionIndex := len (decimatedActions ) - 1
954
+ usage := decimatedActions [lastActionIndex ].(testcore.UpdateAction ).GetObject ().(* corev1.ResourceQuota )
955
+
956
+ // ensure only the quota-terminating was updated
957
+ if usage .Name != resourceQuotaTerminating .Name {
958
+ t .Errorf ("Incremented the wrong quota, expected %v, actual %v" , resourceQuotaTerminating .Name , usage .Name )
959
+ }
960
+
961
+ expectedUsage := corev1.ResourceQuota {
962
+ Status : corev1.ResourceQuotaStatus {
963
+ Hard : corev1.ResourceList {
964
+ corev1 .ResourceCPU : resource .MustParse ("3" ),
965
+ corev1 .ResourceMemory : resource .MustParse ("100Gi" ),
966
+ corev1 .ResourcePods : resource .MustParse ("5" ),
967
+ },
968
+ Used : corev1.ResourceList {
969
+ corev1 .ResourceCPU : resource .MustParse ("1100m" ),
970
+ corev1 .ResourceMemory : resource .MustParse ("52Gi" ),
971
+ corev1 .ResourcePods : resource .MustParse ("4" ),
972
+ },
973
+ },
974
+ }
975
+ for k , v := range expectedUsage .Status .Used {
976
+ actual := usage .Status .Used [k ]
977
+ actualValue := actual .String ()
978
+ expectedValue := v .String ()
979
+ if expectedValue != actualValue {
980
+ t .Errorf ("Usage Used: Key: %v, Expected: %v, Actual: %v" , k , expectedValue , actualValue )
981
+ }
982
+ }
983
+ }
984
+
863
985
// TestAdmitBelowBestEffortQuotaLimit creates a best effort and non-best effort quota.
864
986
// It verifies that best effort pods are properly scoped to the best effort quota document.
865
987
func TestAdmitBelowBestEffortQuotaLimit (t * testing.T ) {
0 commit comments