@@ -29,6 +29,7 @@ import (
2929
3030 "github.com/google/go-cmp/cmp"
3131 corev1 "k8s.io/api/core/v1"
32+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
3233 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3334 "k8s.io/apimachinery/pkg/runtime"
3435 "k8s.io/apimachinery/pkg/selection"
@@ -1097,6 +1098,222 @@ func TestStopSidecars(t *testing.T) {
10971098 }
10981099}
10991100
1101+ // TestStopSidecarsUsesPatch verifies that StopSidecars uses PATCH instead of UPDATE.
1102+ func TestStopSidecarsUsesPatch (t * testing.T ) {
1103+ stepContainer := corev1.Container {
1104+ Name : stepPrefix + "my-step" ,
1105+ Image : "foo" ,
1106+ }
1107+ sidecarContainer := corev1.Container {
1108+ Name : sidecarPrefix + "my-sidecar" ,
1109+ Image : "original-sidecar-image" ,
1110+ }
1111+ injectedSidecar := corev1.Container {
1112+ Name : "injected-sidecar" ,
1113+ Image : "original-injected-image" ,
1114+ }
1115+
1116+ pod := & corev1.Pod {
1117+ ObjectMeta : metav1.ObjectMeta {
1118+ Name : "test-pod" ,
1119+ Namespace : "default" ,
1120+ ResourceVersion : "1000" ,
1121+ },
1122+ Spec : corev1.PodSpec {
1123+ Containers : []corev1.Container {stepContainer , sidecarContainer , injectedSidecar },
1124+ },
1125+ Status : corev1.PodStatus {
1126+ Phase : corev1 .PodRunning ,
1127+ ContainerStatuses : []corev1.ContainerStatus {{
1128+ Name : stepContainer .Name ,
1129+ State : corev1.ContainerState {
1130+ Terminated : & corev1.ContainerStateTerminated {
1131+ ExitCode : 0 ,
1132+ },
1133+ },
1134+ }, {
1135+ Name : sidecarContainer .Name ,
1136+ State : corev1.ContainerState {
1137+ Running : & corev1.ContainerStateRunning {
1138+ StartedAt : metav1 .NewTime (time .Now ()),
1139+ },
1140+ },
1141+ }, {
1142+ Name : injectedSidecar .Name ,
1143+ State : corev1.ContainerState {
1144+ Running : & corev1.ContainerStateRunning {
1145+ StartedAt : metav1 .NewTime (time .Now ()),
1146+ },
1147+ },
1148+ }},
1149+ },
1150+ }
1151+
1152+ kubeclient := fakek8s .NewSimpleClientset (pod )
1153+
1154+ var patchCalled , updateCalled bool
1155+ var patchType string
1156+ var patchBytes []byte
1157+
1158+ kubeclient .PrependReactor ("patch" , "pods" , func (action k8stesting.Action ) (bool , runtime.Object , error ) {
1159+ patchAction := action .(k8stesting.PatchAction )
1160+ patchCalled = true
1161+ patchType = string (patchAction .GetPatchType ())
1162+ patchBytes = patchAction .GetPatch ()
1163+
1164+ patchedPod := pod .DeepCopy ()
1165+ patchedPod .Spec .Containers [1 ].Image = nopImage
1166+ patchedPod .Spec .Containers [2 ].Image = nopImage
1167+ return true , patchedPod , nil
1168+ })
1169+ kubeclient .PrependReactor ("update" , "pods" , func (action k8stesting.Action ) (bool , runtime.Object , error ) {
1170+ updateCalled = true
1171+ return true , nil , nil
1172+ })
1173+
1174+ ctx , cancel := context .WithCancel (t .Context ())
1175+ defer cancel ()
1176+
1177+ got , err := StopSidecars (ctx , nopImage , kubeclient , pod .Namespace , pod .Name )
1178+ if err != nil {
1179+ t .Fatalf ("StopSidecars failed: %v" , err )
1180+ }
1181+
1182+ if ! patchCalled {
1183+ t .Error ("expected PATCH to be called" )
1184+ }
1185+ if updateCalled {
1186+ t .Error ("expected UPDATE not to be called" )
1187+ }
1188+
1189+ if patchType != "application/json-patch+json" {
1190+ t .Errorf ("expected patch type 'application/json-patch+json', got %q" , patchType )
1191+ }
1192+
1193+ patchStr := string (patchBytes )
1194+ if ! containsSubstr (patchStr , "/spec/containers/1/image" ) {
1195+ t .Errorf ("patch should target container 1, got: %s" , patchStr )
1196+ }
1197+ if ! containsSubstr (patchStr , "/spec/containers/2/image" ) {
1198+ t .Errorf ("patch should target container 2, got: %s" , patchStr )
1199+ }
1200+ if containsSubstr (patchStr , "/spec/containers/0/image" ) {
1201+ t .Errorf ("patch should not target step container, got: %s" , patchStr )
1202+ }
1203+
1204+ if got .Spec .Containers [1 ].Image != nopImage {
1205+ t .Errorf ("expected sidecar image %q, got %q" , nopImage , got .Spec .Containers [1 ].Image )
1206+ }
1207+ if got .Spec .Containers [2 ].Image != nopImage {
1208+ t .Errorf ("expected injected sidecar image %q, got %q" , nopImage , got .Spec .Containers [2 ].Image )
1209+ }
1210+ if got .Spec .Containers [0 ].Image != stepContainer .Image {
1211+ t .Errorf ("step container should be unchanged, got %q" , got .Spec .Containers [0 ].Image )
1212+ }
1213+ }
1214+
1215+ // TestStopSidecarsWithConflictSimulation simulates concurrent pod updates
1216+ // that cause 409 conflicts with UPDATE but succeed with PATCH.
1217+ func TestStopSidecarsWithConflictSimulation (t * testing.T ) {
1218+ stepContainer := corev1.Container {
1219+ Name : stepPrefix + "my-step" ,
1220+ Image : "foo" ,
1221+ }
1222+ sidecarContainer := corev1.Container {
1223+ Name : sidecarPrefix + "my-sidecar" ,
1224+ Image : "original-image" ,
1225+ }
1226+
1227+ pod := & corev1.Pod {
1228+ ObjectMeta : metav1.ObjectMeta {
1229+ Name : "test-pod" ,
1230+ Namespace : "default" ,
1231+ ResourceVersion : "1000" ,
1232+ },
1233+ Spec : corev1.PodSpec {
1234+ Containers : []corev1.Container {stepContainer , sidecarContainer },
1235+ },
1236+ Status : corev1.PodStatus {
1237+ Phase : corev1 .PodRunning ,
1238+ ContainerStatuses : []corev1.ContainerStatus {{
1239+ Name : stepContainer .Name ,
1240+ State : corev1.ContainerState {
1241+ Terminated : & corev1.ContainerStateTerminated {},
1242+ },
1243+ }, {
1244+ Name : sidecarContainer .Name ,
1245+ State : corev1.ContainerState {
1246+ Running : & corev1.ContainerStateRunning {
1247+ StartedAt : metav1 .NewTime (time .Now ()),
1248+ },
1249+ },
1250+ }},
1251+ },
1252+ }
1253+
1254+ kubeclient := fakek8s .NewSimpleClientset (pod )
1255+
1256+ podUpdated := false
1257+
1258+ kubeclient .PrependReactor ("get" , "pods" , func (action k8stesting.Action ) (bool , runtime.Object , error ) {
1259+ p := pod .DeepCopy ()
1260+ if ! podUpdated {
1261+ p .ResourceVersion = "1000"
1262+ podUpdated = true
1263+ } else {
1264+ p .ResourceVersion = "1001"
1265+ p .Status .ContainerStatuses [0 ].State .Terminated .FinishedAt = metav1 .NewTime (time .Now ())
1266+ }
1267+ return true , p , nil
1268+ })
1269+
1270+ kubeclient .PrependReactor ("update" , "pods" , func (action k8stesting.Action ) (bool , runtime.Object , error ) {
1271+ updateAction := action .(k8stesting.UpdateAction )
1272+ updatedPod := updateAction .GetObject ().(* corev1.Pod )
1273+
1274+ if podUpdated && updatedPod .ResourceVersion == "1000" {
1275+ return true , nil , k8serrors .NewConflict (
1276+ corev1 .Resource ("pods" ),
1277+ pod .Name ,
1278+ errors .New ("the object has been modified; please apply your changes to the latest version" ),
1279+ )
1280+ }
1281+ return false , nil , nil
1282+ })
1283+
1284+ kubeclient .PrependReactor ("patch" , "pods" , func (action k8stesting.Action ) (bool , runtime.Object , error ) {
1285+ patchedPod := pod .DeepCopy ()
1286+ patchedPod .Spec .Containers [1 ].Image = nopImage
1287+ patchedPod .ResourceVersion = "1002"
1288+ return true , patchedPod , nil
1289+ })
1290+
1291+ ctx , cancel := context .WithCancel (t .Context ())
1292+ defer cancel ()
1293+
1294+ got , err := StopSidecars (ctx , nopImage , kubeclient , pod .Namespace , pod .Name )
1295+ if err != nil {
1296+ if k8serrors .IsConflict (err ) {
1297+ t .Fatalf ("got 409 conflict, this indicates UPDATE is being used instead of PATCH: %v" , err )
1298+ }
1299+ t .Fatalf ("unexpected error: %v" , err )
1300+ }
1301+
1302+ if got .Spec .Containers [1 ].Image != nopImage {
1303+ t .Errorf ("expected sidecar image %q, got %q" , nopImage , got .Spec .Containers [1 ].Image )
1304+ }
1305+ }
1306+
1307+ // Helper function to check if a string contains a substring
1308+ func containsSubstr (s , substr string ) bool {
1309+ for i := 0 ; i <= len (s )- len (substr ); i ++ {
1310+ if s [i :i + len (substr )] == substr {
1311+ return true
1312+ }
1313+ }
1314+ return false
1315+ }
1316+
11001317func TestCancelPod (t * testing.T ) {
11011318 pod := & corev1.Pod {
11021319 ObjectMeta : metav1.ObjectMeta {
0 commit comments