Skip to content

Commit 76f26d8

Browse files
spoltiandresllh
andauthored
[RHOAIENG-19717] Old pull secret persists when updating isvcs (#522) (#577)
Signed-off-by: Andres Llausas <[email protected]> Co-authored-by: Andres Llausas <[email protected]>
1 parent 0e8bd06 commit 76f26d8

File tree

2 files changed

+212
-7
lines changed

2 files changed

+212
-7
lines changed

pkg/controller/v1beta1/inferenceservice/rawkube_controller_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package inferenceservice
1919
import (
2020
"context"
2121
"fmt"
22+
"reflect"
2223
"time"
2324

2425
"github.com/kserve/kserve/pkg/apis/serving/v1alpha1"
@@ -3195,6 +3196,143 @@ var _ = Describe("v1beta1 inference service controller", func() {
31953196
verifyTensorParallelSizeDeployments(actualDefaultDeployment, actualWorkerDeployment, "3", constants.NvidiaGPUResourceType)
31963197
})
31973198
})
3199+
Context("When creating an inference service with modelcar and raw deployment", func() {
3200+
It("Should only have the ImagePullSecrets that are specified in the InferenceService", func() {
3201+
By("Updating an InferenceService with a new ImagePullSecret and checking the deployment")
3202+
var configMap = &v1.ConfigMap{
3203+
ObjectMeta: metav1.ObjectMeta{
3204+
Name: constants.InferenceServiceConfigMapName,
3205+
Namespace: constants.KServeNamespace,
3206+
},
3207+
Data: configs,
3208+
}
3209+
Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred())
3210+
defer k8sClient.Delete(context.TODO(), configMap)
3211+
3212+
servingRuntime := &v1alpha1.ServingRuntime{
3213+
ObjectMeta: metav1.ObjectMeta{
3214+
Name: "vllm-runtime",
3215+
Namespace: constants.KServeNamespace,
3216+
},
3217+
Spec: v1alpha1.ServingRuntimeSpec{
3218+
SupportedModelFormats: []v1alpha1.SupportedModelFormat{
3219+
{
3220+
AutoSelect: proto.Bool(true),
3221+
Name: "vLLM",
3222+
},
3223+
},
3224+
ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{
3225+
Containers: []v1.Container{
3226+
{
3227+
Name: constants.InferenceServiceContainerName,
3228+
Image: "kserve/vllm:latest",
3229+
Command: []string{"bash", "-c"},
3230+
Args: []string{
3231+
"python2 -m vllm --model_name=${MODEL_NAME} --model_dir=${MODEL} --tensor-parallel-size=${TENSOR_PARALLEL_SIZE} --pipeline-parallel-size=${PIPELINE_PARALLEL_SIZE}",
3232+
},
3233+
Resources: defaultResource,
3234+
},
3235+
},
3236+
},
3237+
Disabled: proto.Bool(false),
3238+
},
3239+
}
3240+
3241+
k8sClient.Create(context.TODO(), servingRuntime)
3242+
defer k8sClient.Delete(context.TODO(), servingRuntime)
3243+
serviceName := "modelcar-raw-deployment"
3244+
var expectedRequest = reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: constants.KServeNamespace}}
3245+
var serviceKey = expectedRequest.NamespacedName
3246+
var storageUri = "oci://test/mnist/export"
3247+
ctx := context.Background()
3248+
isvc := &v1beta1.InferenceService{
3249+
ObjectMeta: metav1.ObjectMeta{
3250+
Name: serviceKey.Name,
3251+
Namespace: serviceKey.Namespace,
3252+
Annotations: map[string]string{
3253+
"serving.kserve.io/deploymentMode": "RawDeployment",
3254+
"serving.kserve.io/autoscalerClass": "hpa",
3255+
"serving.kserve.io/metrics": "cpu",
3256+
"serving.kserve.io/targetUtilizationPercentage": "75",
3257+
},
3258+
},
3259+
Spec: v1beta1.InferenceServiceSpec{
3260+
Predictor: v1beta1.PredictorSpec{
3261+
ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{
3262+
MinReplicas: v1beta1.GetIntReference(1),
3263+
MaxReplicas: 2,
3264+
},
3265+
PodSpec: v1beta1.PodSpec{
3266+
ImagePullSecrets: []v1.LocalObjectReference{
3267+
{Name: "isvc-image-pull-secret"},
3268+
},
3269+
},
3270+
Model: &v1beta1.ModelSpec{
3271+
ModelFormat: v1beta1.ModelFormat{
3272+
Name: "vLLM",
3273+
},
3274+
PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{
3275+
StorageURI: &storageUri,
3276+
RuntimeVersion: proto.String("0.14.0"),
3277+
Container: v1.Container{
3278+
Name: constants.InferenceServiceContainerName,
3279+
Resources: v1.ResourceRequirements{
3280+
Limits: v1.ResourceList{
3281+
constants.NvidiaGPUResourceType: resource.MustParse("1"),
3282+
},
3283+
Requests: v1.ResourceList{
3284+
constants.NvidiaGPUResourceType: resource.MustParse("1"),
3285+
},
3286+
},
3287+
},
3288+
},
3289+
},
3290+
},
3291+
},
3292+
}
3293+
3294+
isvc.DefaultInferenceService(nil, nil, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, nil)
3295+
Expect(k8sClient.Create(ctx, isvc)).Should(Succeed())
3296+
defer k8sClient.Delete(ctx, isvc)
3297+
3298+
inferenceService := &v1beta1.InferenceService{}
3299+
3300+
Eventually(func() bool {
3301+
return k8sClient.Get(ctx, serviceKey, inferenceService) == nil
3302+
}, timeout, interval).Should(BeTrue())
3303+
3304+
actualDeployment := &appsv1.Deployment{}
3305+
predictorDeploymentKey := types.NamespacedName{Name: constants.PredictorServiceName(serviceKey.Name),
3306+
Namespace: serviceKey.Namespace}
3307+
Eventually(func() error { return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualDeployment) }, timeout, interval).
3308+
Should(Succeed())
3309+
3310+
Expect(actualDeployment.Spec.Template.Spec.ImagePullSecrets).To(HaveLen(1))
3311+
Expect(actualDeployment.Spec.Template.Spec.ImagePullSecrets[0].Name).To(Equal("isvc-image-pull-secret"))
3312+
3313+
Expect(k8sClient.Get(ctx, serviceKey, inferenceService)).Should(Succeed())
3314+
updateForInferenceService := inferenceService.DeepCopy()
3315+
updateForInferenceService.Spec.Predictor.PodSpec.ImagePullSecrets = []v1.LocalObjectReference{
3316+
{Name: "new-image-pull-secret"},
3317+
}
3318+
expectedImagePullSecrets := updateForInferenceService.Spec.Predictor.PodSpec.ImagePullSecrets
3319+
Eventually(func() error {
3320+
return k8sClient.Update(ctx, updateForInferenceService)
3321+
}, timeout, interval).Should(Succeed())
3322+
3323+
updatedDeployment := &appsv1.Deployment{}
3324+
Eventually(func() (bool, error) {
3325+
if err := k8sClient.Get(ctx, predictorDeploymentKey, updatedDeployment); err != nil {
3326+
return false, err
3327+
}
3328+
if len(updatedDeployment.Spec.Template.Spec.ImagePullSecrets) != 1 {
3329+
return false, nil
3330+
}
3331+
return reflect.DeepEqual(updatedDeployment.Spec.Template.Spec.ImagePullSecrets, expectedImagePullSecrets), nil
3332+
}, timeout, interval).Should(BeTrue())
3333+
3334+
})
3335+
})
31983336
})
31993337

32003338
func verifyPipelineParallelSizeDeployments(actualDefaultDeployment *appsv1.Deployment, actualWorkerDeployment *appsv1.Deployment, pipelineParallelSize string, replicas *int32) {

pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler.go

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"crypto/rand"
2222
"encoding/base64"
2323
"encoding/json"
24+
"errors"
2425
"fmt"
2526
"strconv"
2627
"strings"
@@ -604,11 +605,6 @@ func (r *DeploymentReconciler) Reconcile() ([]*appsv1.Deployment, error) {
604605
// get the current deployment
605606
_ = r.client.Get(context.TODO(), types.NamespacedName{Name: deployment.Name, Namespace: deployment.Namespace}, originalDeployment)
606607
// we need to remove the Replicas field from the deployment spec
607-
originalDeployment.Spec.Replicas = nil
608-
curJson, err := json.Marshal(originalDeployment)
609-
if err != nil {
610-
return nil, err
611-
}
612608

613609
// Check if there are any envs to remove
614610
// If there, its value will be set to "delete" so we can update the patchBytes with
@@ -635,10 +631,31 @@ func (r *DeploymentReconciler) Reconcile() ([]*appsv1.Deployment, error) {
635631
deployment.Spec.Template.Spec.Containers[i].Env = envs
636632
}
637633

634+
originalDeployment.Spec.Replicas = nil
635+
curJson, err := json.Marshal(originalDeployment)
636+
if err != nil {
637+
return nil, err
638+
}
638639
// To avoid the conflict between HPA and Deployment,
639640
// we need to remove the Replicas field from the deployment spec
640641
deployment.Spec.Replicas = nil
641642

643+
imagePullSecretsDesired := deployment.Spec.Template.Spec.ImagePullSecrets
644+
originalDeploymentPullSecrets := originalDeployment.Spec.Template.Spec.ImagePullSecrets
645+
imagePullSecretsToRemove := []string{}
646+
for _, secret := range originalDeploymentPullSecrets {
647+
found := false
648+
for _, desiredSecret := range imagePullSecretsDesired {
649+
if secret.Name == desiredSecret.Name {
650+
found = true
651+
break
652+
}
653+
}
654+
if !found {
655+
imagePullSecretsToRemove = append(imagePullSecretsToRemove, secret.Name)
656+
}
657+
}
658+
642659
modJson, err := json.Marshal(deployment)
643660
if err != nil {
644661
return nil, err
@@ -650,10 +667,60 @@ func (r *DeploymentReconciler) Reconcile() ([]*appsv1.Deployment, error) {
650667
return nil, err
651668
}
652669

653-
// override the envs that needs to be removed with "$patch": "delete"
670+
// Patch the deployment object with the strategic merge patch
654671
patchByte = []byte(strings.ReplaceAll(string(patchByte), "\"value\":\""+utils.PLACEHOLDER_FOR_DELETION+"\"", "\"$patch\":\"delete\""))
655672

656-
// Patch the deployment object with the strategic merge patch
673+
// The strategic merge patch does not remove items from list just by removing it from the patch,
674+
// to delete lists items using strategic merge patch, the $patch delete pattern is used.
675+
// Example:
676+
// imagePullSecrets:
677+
// - "name": "pull-secret-1",
678+
// "$patch": "delete"
679+
if len(imagePullSecretsToRemove) > 0 {
680+
patchJson := map[string]interface{}{}
681+
err = json.Unmarshal(patchByte, &patchJson)
682+
if err != nil {
683+
return nil, err
684+
}
685+
spec, ok := patchJson["spec"].(map[string]interface{})
686+
if !ok {
687+
return nil, errors.New("spec not found")
688+
}
689+
template, ok := spec["template"].(map[string]interface{})
690+
if !ok {
691+
return nil, errors.New("template not found")
692+
}
693+
specTemplate, ok := template["spec"].(map[string]interface{})
694+
if !ok {
695+
return nil, errors.New("template.spec not found")
696+
}
697+
698+
// Ensure imagePullSecrets is a slice, defaulting to an empty slice if nil.
699+
ipsField, exists := specTemplate["imagePullSecrets"]
700+
var imagePullSecrets []interface{}
701+
if exists && ipsField != nil {
702+
var ok bool
703+
imagePullSecrets, ok = ipsField.([]interface{})
704+
if !ok {
705+
return nil, errors.New("imagePullSecrets is not the expected type")
706+
}
707+
} else {
708+
imagePullSecrets = []interface{}{}
709+
}
710+
711+
for _, secret := range imagePullSecretsToRemove {
712+
for _, secretMap := range imagePullSecrets {
713+
if secretMap.(map[string]interface{})["name"] == secret {
714+
secretMap.(map[string]interface{})["$patch"] = "delete"
715+
}
716+
}
717+
}
718+
patchJson["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["imagePullSecrets"] = imagePullSecrets
719+
patchByte, err = json.Marshal(patchJson)
720+
if err != nil {
721+
return nil, err
722+
}
723+
}
657724
opErr = r.client.Patch(context.TODO(), deployment, kclient.RawPatch(types.StrategicMergePatchType, patchByte))
658725
}
659726

0 commit comments

Comments
 (0)