Repository: GoogleCloudPlatform/cloud-sql-proxy-operator
Pull request #253 (2023-03-07 22:24:28)

feat: When the operator's default proxy image changes, workload containers should be updated.
When an AuthProxyWorkload uses the default proxy image, use the operator's default proxy image 
and the AuthProxyWorkload.Generation together as a key to determine if a workload's proxy containers
need to be updated. 

Add tests to ensure that when the default image hardcoded into the operator changes, workloads using 
the old default will be identified and reconfigured properly.

There is another PR coming after this one which will automatically check and apply configuration 
changes to AuthProxyWorkload resources on operator startup.

Related to #87 


Patch:
diff --git a/config/crd/bases/cloudsql.cloud.google.com_authproxyworkloads.yaml b/config/crd/bases/cloudsql.cloud.google.com_authproxyworkloads.yaml
index 346f4a6..5a2edb9 100644
--- a/config/crd/bases/cloudsql.cloud.google.com_authproxyworkloads.yaml
+++ b/config/crd/bases/cloudsql.cloud.google.com_authproxyworkloads.yaml
@@ -863,7 +863,7 @@ spec:
                         - name
                       type: object
                     image:
-                      description: Image is the URL to the proxy image. Optional, by default the operator will use the latest known compatible proxy image.
+                      description: "Image is the URL to the proxy image. Optional, by default the operator will use the latest Cloud SQL Auth Proxy version as of the release of the operator. \n The operator ensures that all workloads configured with the default proxy image are upgraded automatically to use to the latest released proxy image. \n When the customer upgrades the operator, the operator upgrades all workloads using the default proxy image to the latest proxy image. The change to the proxy container image is applied in accordance with the RolloutStrategy."
                       type: string
                     maxConnections:
                       description: MaxConnections limits the number of connections. Default value is no limit. This sets the proxy container's CLI argument `--max-connections`
diff --git a/docs/api.md b/docs/api.md
index 9e3af8e..efa4aff 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -48,7 +48,9 @@ _Appears in:_
 | `maxConnections` _integer_ | MaxConnections limits the number of connections. Default value is no limit. This sets the proxy container's CLI argument `--max-connections` |
 | `maxSigtermDelay` _integer_ | MaxSigtermDelay is the maximum number of seconds to wait for connections to close after receiving a TERM signal. This sets the proxy container's CLI argument `--max-sigterm-delay` and configures `terminationGracePeriodSeconds` on the workload's PodSpec. |
 | `sqlAdminAPIEndpoint` _string_ | SQLAdminAPIEndpoint is a debugging parameter that when specified will change the Google Cloud api endpoint used by the proxy. |
-| `image` _string_ | Image is the URL to the proxy image. Optional, by default the operator will use the latest known compatible proxy image. |
+| `image` _string_ | Image is the URL to the proxy image. Optional, by default the operator will use the latest Cloud SQL Auth Proxy version as of the release of the operator. 
+ The operator ensures that all workloads configured with the default proxy image are upgraded automatically to use to the latest released proxy image. 
+ When the customer upgrades the operator, the operator upgrades all workloads using the default proxy image to the latest proxy image. The change to the proxy container image is applied in accordance with the RolloutStrategy. |
 | `rolloutStrategy` _string_ | RolloutStrategy indicates the strategy to use when rolling out changes to the workloads affected by the results. When this is set to `Workload`, changes to this resource will be automatically applied to a running Deployment, StatefulSet, DaemonSet, or ReplicaSet in accordance with the Strategy set on that workload. When this is set to `None`, the operator will take no action to roll out changes to affected workloads. `Workload` will be used by default if no value is set. See: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy |
 
 
diff --git a/installer/cloud-sql-proxy-operator.yaml b/installer/cloud-sql-proxy-operator.yaml
index 310ba0a..6ee9f68 100644
--- a/installer/cloud-sql-proxy-operator.yaml
+++ b/installer/cloud-sql-proxy-operator.yaml
@@ -881,7 +881,7 @@ spec:
                         - name
                       type: object
                     image:
-                      description: Image is the URL to the proxy image. Optional, by default the operator will use the latest known compatible proxy image.
+                      description: "Image is the URL to the proxy image. Optional, by default the operator will use the latest Cloud SQL Auth Proxy version as of the release of the operator. \n The operator ensures that all workloads configured with the default proxy image are upgraded automatically to use to the latest released proxy image. \n When the customer upgrades the operator, the operator upgrades all workloads using the default proxy image to the latest proxy image. The change to the proxy container image is applied in accordance with the RolloutStrategy."
                       type: string
                     maxConnections:
                       description: MaxConnections limits the number of connections. Default value is no limit. This sets the proxy container's CLI argument `--max-connections`
diff --git a/internal/api/v1alpha1/authproxyworkload_types.go b/internal/api/v1alpha1/authproxyworkload_types.go
index 346319c..927b8ab 100644
--- a/internal/api/v1alpha1/authproxyworkload_types.go
+++ b/internal/api/v1alpha1/authproxyworkload_types.go
@@ -179,7 +179,17 @@ type AuthProxyContainerSpec struct {
 	SQLAdminAPIEndpoint string `json:"sqlAdminAPIEndpoint,omitempty"`
 
 	// Image is the URL to the proxy image. Optional, by default the operator
-	// will use the latest known compatible proxy image.
+	// will use the latest Cloud SQL Auth Proxy version as of the release of the
+	// operator.
+	//
+	// The operator ensures that all workloads configured with the default proxy
+	// image are upgraded automatically to use to the latest released proxy image.
+	//
+	// When the customer upgrades the operator, the operator upgrades all
+	// workloads using the default proxy image to the latest proxy image. The
+	// change to the proxy container image is applied in accordance with
+	// the RolloutStrategy.
+	//
 	//+kubebuilder:validation:Optional
 	Image string `json:"image,omitempty"`
 
@@ -337,7 +347,6 @@ type InstanceSpec struct {
 // AuthProxyWorkloadStatus presents the observed state of AuthProxyWorkload using
 // standard Kubernetes Conditions.
 type AuthProxyWorkloadStatus struct {
-
 	// Conditions show the overall status of the AuthProxyWorkload resource on all
 	// matching workloads.
 	//
diff --git a/internal/controller/authproxyworkload_controller.go b/internal/controller/authproxyworkload_controller.go
index d5961e8..f2ae27e 100644
--- a/internal/controller/authproxyworkload_controller.go
+++ b/internal/controller/authproxyworkload_controller.go
@@ -280,7 +280,7 @@ func (r *AuthProxyWorkloadReconciler) needsAnnotationUpdate(wl workload.Workload
 		return false
 	}
 
-	k, v := workload.PodAnnotation(resource)
+	k, v := r.updater.PodAnnotation(resource)
 	// Check if the correct annotation exists
 	an := wl.PodTemplateAnnotations()
 	if an != nil && an[k] == v {
@@ -304,7 +304,7 @@ func (r *AuthProxyWorkloadReconciler) updateAnnotation(wl workload.Workload, res
 		return
 	}
 
-	k, v := workload.PodAnnotation(resource)
+	k, v := r.updater.PodAnnotation(resource)
 
 	// add the annotation if needed...
 	an := wl.PodTemplateAnnotations()
diff --git a/internal/controller/setup.go b/internal/controller/setup.go
index 3fd317d..8b96d82 100644
--- a/internal/controller/setup.go
+++ b/internal/controller/setup.go
@@ -38,8 +38,8 @@ func InitScheme(scheme *runtime.Scheme) {
 
 // SetupManagers was moved out of ../main.go to here so that it can be invoked
 // from the testintegration tests AND from the actual operator.
-func SetupManagers(mgr manager.Manager, userAgent string) error {
-	u := workload.NewUpdater(userAgent)
+func SetupManagers(mgr manager.Manager, userAgent, defaultProxyImage string) error {
+	u := workload.NewUpdater(userAgent, defaultProxyImage)
 
 	setupLog.Info("Configuring reconcilers...")
 	var err error
diff --git a/internal/workload/podspec_updates.go b/internal/workload/podspec_updates.go
index faf3bd1..d1bfe82 100644
--- a/internal/workload/podspec_updates.go
+++ b/internal/workload/podspec_updates.go
@@ -36,6 +36,9 @@ import (
 // package and documented here so that they appear in the godoc. These also
 // need to be documented in the CRD
 const (
+	// DefaultProxyImage is the latest version of the proxy as of the release
+	// of this operator. This is managed as a dependency. We update this constant
+	// when the Cloud SQL Auth Proxy releases a new version.
 	DefaultProxyImage = "gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.1.1"
 
 	// DefaultFirstPort is the first port number chose for an instance listener by the
@@ -56,27 +59,42 @@ var l = logf.Log.WithName("internal.workload")
 // PodAnnotation returns the annotation (key, value) that should be added to
 // pods that are configured with this AuthProxyWorkload resource. This takes
 // into account whether the AuthProxyWorkload exists or was recently deleted.
-func PodAnnotation(r *cloudsqlapi.AuthProxyWorkload) (string, string) {
+// The defaultProxyImage is part of the annotation value.
+func PodAnnotation(r *cloudsqlapi.AuthProxyWorkload, defaultProxyImage string) (string, string) {
+	img := defaultProxyImage
+	if r.Spec.AuthProxyContainer != nil && r.Spec.AuthProxyContainer.Image != "" {
+		img = ""
+	}
 	k := fmt.Sprintf("%s/%s", cloudsqlapi.AnnotationPrefix, r.Name)
-	v := fmt.Sprintf("%d", r.Generation)
+	v := fmt.Sprintf("%d,%s", r.Generation, img)
 	// if r was deleted, use a different value
 	if !r.GetDeletionTimestamp().IsZero() {
-		v = fmt.Sprintf("%d-deleted-%s", r.Generation, r.GetDeletionTimestamp().Format(time.RFC3339))
+		v = fmt.Sprintf("%d-deleted-%s,%s", r.Generation, r.GetDeletionTimestamp().Format(time.RFC3339), img)
 	}
 
 	return k, v
 }
 
+// PodAnnotation returns the annotation (key, value) that should be added to
+// pods that are configured with this AuthProxyWorkload resource. This takes
+// into account whether the AuthProxyWorkload exists or was recently deleted.
+func (u *Updater) PodAnnotation(r *cloudsqlapi.AuthProxyWorkload) (string, string) {
+	return PodAnnotation(r, u.defaultProxyImage)
+}
+
 // Updater holds global state used while reconciling workloads.
 type Updater struct {
 	// userAgent is the userAgent of the operator
 	userAgent string
+
+	// defaultProxyImage is the current default proxy image for the operator
+	defaultProxyImage string
 }
 
 // NewUpdater creates a new instance of Updater with a supplier
 // that loads the default proxy impage from the public docker registry
-func NewUpdater(userAgent string) *Updater {
-	return &Updater{userAgent: userAgent}
+func NewUpdater(userAgent string, defaultProxyImage string) *Updater {
+	return &Updater{userAgent: userAgent, defaultProxyImage: defaultProxyImage}
 }
 
 // ConfigError is an error with extra details about why an AuthProxyWorkload
@@ -466,7 +484,7 @@ func (s *updateState) update(wl *PodWorkload, matches []*cloudsqlapi.AuthProxyWo
 		containers = append(containers, newContainer)
 
 		// Add pod annotation for each instance
-		k, v := PodAnnotation(inst)
+		k, v := s.updater.PodAnnotation(inst)
 		ann[k] = v
 	}
 
@@ -845,7 +863,7 @@ func (s *updateState) addError(errorCode, description string, p *cloudsqlapi.Aut
 }
 
 func (s *updateState) defaultProxyImage() string {
-	return DefaultProxyImage
+	return s.updater.defaultProxyImage
 }
 
 func (s *updateState) usePort(configValue *int32, defaultValue int32, p *cloudsqlapi.AuthProxyWorkload) int32 {
diff --git a/main.go b/main.go
index ffefb14..4d0982f 100644
--- a/main.go
+++ b/main.go
@@ -20,6 +20,8 @@ import (
 	"runtime"
 
 	"github.com/GoogleCloudPlatform/cloud-sql-proxy-operator/internal/controller"
+	"github.com/GoogleCloudPlatform/cloud-sql-proxy-operator/internal/workload"
+
 	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
 	// to ensure that exec-entrypoint and run can make use of them.
 	_ "k8s.io/client-go/plugin/pkg/client/auth"
@@ -87,7 +89,7 @@ func main() {
 		os.Exit(1)
 	}
 
-	err = controller.SetupManagers(mgr, userAgent)
+	err = controller.SetupManagers(mgr, userAgent, workload.DefaultProxyImage)
 	if err != nil {
 		setupLog.Error(err, "unable to set up the controllers")
 		os.Exit(1)


Test patch:
diff --git a/internal/controller/authproxyworkload_controller_test.go b/internal/controller/authproxyworkload_controller_test.go
index 0be06d6..45e4b0c 100644
--- a/internal/controller/authproxyworkload_controller_test.go
+++ b/internal/controller/authproxyworkload_controller_test.go
@@ -79,7 +79,7 @@ func TestReconcileDeleted(t *testing.T) {
 		t.Error(err) // shouldn't ever happen
 	}
 	c := cb.WithObjects(p).Build()
-	r, req, ctx := reconciler(p, c)
+	r, req, ctx := reconciler(p, c, workload.DefaultProxyImage)
 
 	c.Delete(ctx, p)
 	if err != nil {
@@ -262,7 +262,7 @@ func TestReconcileState33(t *testing.T) {
 	addSelectorWorkload(p, "Deployment", labelK, labelV)
 
 	// mimic a pod that was updated by the webhook
-	reqName := cloudsqlapi.AnnotationPrefix + "/" + p.Name
+	reqName, reqVal := workload.PodAnnotation(p, workload.DefaultProxyImage)
 	pod := &appsv1.Deployment{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "thing",
@@ -270,7 +270,7 @@ func TestReconcileState33(t *testing.T) {
 			Labels:    map[string]string{labelK: labelV},
 		},
 		Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{
-			ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{reqName: "1"}},
+			ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{reqName: reqVal}},
 		}},
 	}
 
@@ -294,7 +294,7 @@ func TestReconcileDeleteUpdatesWorkload(t *testing.T) {
 	addFinalizers(resource)
 	addSelectorWorkload(resource, "Deployment", labelK, labelV)
 
-	k, v := workload.PodAnnotation(resource)
+	k, v := workload.PodAnnotation(resource, workload.DefaultProxyImage)
 
 	// mimic a deployment that was updated by the webhook
 	deployment := &appsv1.Deployment{
@@ -314,7 +314,7 @@ func TestReconcileDeleteUpdatesWorkload(t *testing.T) {
 		t.Error(err) // shouldn't ever happen
 	}
 	c := cb.WithObjects(resource, deployment).Build()
-	r, req, ctx := reconciler(resource, c)
+	r, req, ctx := reconciler(resource, c, workload.DefaultProxyImage)
 
 	// Delete the resource
 	c.Delete(ctx, resource)
@@ -361,6 +361,73 @@ func TestReconcileDeleteUpdatesWorkload(t *testing.T) {
 
 }
 
+func TestWorkloadUpdatedAfterDefaultProxyImageChanged(t *testing.T) {
+	const (
+		labelK = "app"
+		labelV = "things"
+	)
+	resource := testhelpers.BuildAuthProxyWorkload(types.NamespacedName{
+		Namespace: "default",
+		Name:      "test",
+	}, "project:region:db")
+	resource.Generation = 1
+	addFinalizers(resource)
+	addSelectorWorkload(resource, "Deployment", labelK, labelV)
+
+	// Deployment annotation should be updated to this after reconcile:
+	_, wantV := workload.PodAnnotation(resource, "gcr.io/cloud-sql-connectors/cloud-sql-proxy:999.9.9")
+
+	// mimic a deployment that was updated by the webhook
+	// annotate the deployment with the default image
+	k, v := workload.PodAnnotation(resource, "gcr.io/cloud-sql-connectors/cloud-sql-proxy:1.1.1")
+	deployment := &appsv1.Deployment{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "thing",
+			Namespace: "default",
+			Labels:    map[string]string{labelK: labelV},
+		},
+		Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{
+			ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{k: v}},
+		}},
+	}
+
+	// Build a client with the resource and deployment
+	cb, _, err := clientBuilder()
+	if err != nil {
+		t.Error(err) // shouldn't ever happen
+	}
+	c := cb.WithObjects(resource, deployment).Build()
+
+	// Create a reconciler with the default proxy image at a different version
+	r, req, ctx := reconciler(resource, c, "gcr.io/cloud-sql-connectors/cloud-sql-proxy:999.9.9")
+
+	// Run Reconcile on the resource
+	res, err := r.Reconcile(ctx, req)
+	if err != nil {
+		t.Error(err)
+	}
+
+	if !res.Requeue {
+		t.Errorf("got %v, want %v for requeue", res.Requeue, true)
+	}
+
+	// Fetch the deployment and make sure the annotations show the
+	// deleted resource.
+	d := &appsv1.Deployment{}
+	err = c.Get(ctx, types.NamespacedName{
+		Namespace: deployment.GetNamespace(),
+		Name:      deployment.GetName(),
+	}, d)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got := d.Spec.Template.ObjectMeta.Annotations[k]; got != wantV {
+		t.Fatalf("got %v, wants annotation value %v", got, wantV)
+	}
+
+}
+
 func runReconcileTestcase(p *cloudsqlapi.AuthProxyWorkload, clientObjects []client.Object, wantRequeue bool, wantStatus metav1.ConditionStatus, wantReason string) (client.WithWatch, context.Context, error) {
 	cb, _, err := clientBuilder()
 	if err != nil {
@@ -369,7 +436,7 @@ func runReconcileTestcase(p *cloudsqlapi.AuthProxyWorkload, clientObjects []clie
 
 	c := cb.WithObjects(clientObjects...).Build()
 
-	r, req, ctx := reconciler(p, c)
+	r, req, ctx := reconciler(p, c, workload.DefaultProxyImage)
 	res, err := r.Reconcile(ctx, req)
 	if err != nil {
 		return nil, nil, err
@@ -418,12 +485,12 @@ func clientBuilder() (*fake.ClientBuilder, *runtime.Scheme, error) {
 
 }
 
-func reconciler(p *cloudsqlapi.AuthProxyWorkload, cb client.Client) (*AuthProxyWorkloadReconciler, ctrl.Request, context.Context) {
+func reconciler(p *cloudsqlapi.AuthProxyWorkload, cb client.Client, defaultProxyImage string) (*AuthProxyWorkloadReconciler, ctrl.Request, context.Context) {
 	ctx := log.IntoContext(context.Background(), logger)
 	r := &AuthProxyWorkloadReconciler{
 		Client:          cb,
 		recentlyDeleted: &recentlyDeletedCache{},
-		updater:         workload.NewUpdater("cloud-sql-proxy-operator/dev"),
+		updater:         workload.NewUpdater("cloud-sql-proxy-operator/dev", defaultProxyImage),
 	}
 	req := ctrl.Request{
 		NamespacedName: types.NamespacedName{
diff --git a/internal/controller/pod_controller_test.go b/internal/controller/pod_controller_test.go
index cd5efc3..5c24e28 100644
--- a/internal/controller/pod_controller_test.go
+++ b/internal/controller/pod_controller_test.go
@@ -151,7 +151,7 @@ func podWebhookController(cb client.Client) (*PodAdmissionWebhook, context.Conte
 	r := &PodAdmissionWebhook{
 		Client:  cb,
 		decoder: d,
-		updater: workload.NewUpdater("cloud-sql-proxy-operator/dev"),
+		updater: workload.NewUpdater("cloud-sql-proxy-operator/dev", workload.DefaultProxyImage),
 	}
 
 	return r, ctx, nil
diff --git a/internal/testintegration/setup.go b/internal/testintegration/setup.go
index a684026..bf3d0ca 100644
--- a/internal/testintegration/setup.go
+++ b/internal/testintegration/setup.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/GoogleCloudPlatform/cloud-sql-proxy-operator/internal/controller"
 	"github.com/GoogleCloudPlatform/cloud-sql-proxy-operator/internal/testhelpers"
+	"github.com/GoogleCloudPlatform/cloud-sql-proxy-operator/internal/workload"
 	"github.com/go-logr/logr"
 	"go.uber.org/zap/zapcore"
 	"k8s.io/client-go/kubernetes/scheme"
@@ -143,7 +144,7 @@ func EnvTestSetup() (func(), error) {
 		return teardownFunc, fmt.Errorf("unable to start kuberenetes envtest %v", err)
 	}
 
-	err = controller.SetupManagers(mgr, "cloud-sql-proxy-operator/dev")
+	err = controller.SetupManagers(mgr, "cloud-sql-proxy-operator/dev", workload.DefaultProxyImage)
 	if err != nil {
 		return teardownFunc, fmt.Errorf("unable to start kuberenetes envtest %v", err)
 	}
diff --git a/internal/workload/podspec_updates_test.go b/internal/workload/podspec_updates_test.go
index fa49159..9f87585 100644
--- a/internal/workload/podspec_updates_test.go
+++ b/internal/workload/podspec_updates_test.go
@@ -139,7 +139,7 @@ func TestUpdatePodWorkload(t *testing.T) {
 		wantContainerName       = "csql-default-" + wantsName
 		wantsInstanceName       = "project:server:db"
 		wantsInstanceArg        = fmt.Sprintf("%s?port=%d", wantsInstanceName, wantsPort)
-		u                       = workload.NewUpdater("cloud-sql-proxy-operator/dev")
+		u                       = workload.NewUpdater("cloud-sql-proxy-operator/dev", workload.DefaultProxyImage)
 	)
 	var err error
 
@@ -194,7 +194,7 @@ func TestUpdateWorkloadFixedPort(t *testing.T) {
 			"DB_HOST": "127.0.0.1",
 			"DB_PORT": strconv.Itoa(int(wantsPort)),
 		}
-		u = workload.NewUpdater("cloud-sql-proxy-operator/dev")
+		u = workload.NewUpdater("cloud-sql-proxy-operator/dev", workload.DefaultProxyImage)
 	)
 
 	// Create a pod
@@ -262,7 +262,7 @@ func TestWorkloadNoPortSet(t *testing.T) {
 			"DB_PORT": strconv.Itoa(int(wantsPort)),
 		}
 	)
-	u := workload.NewUpdater("cloud-sql-proxy-operator/dev")
+	u := workload.NewUpdater("cloud-sql-proxy-operator/dev", workload.DefaultProxyImage)
 
 	// Create a pod
 	wl := podWorkload()
@@ -320,7 +320,7 @@ func TestContainerImageChanged(t *testing.T) {
 	var (
 		wantsInstanceName = "project:server:db"
 		wantImage         = "custom-image:latest"
-		u                 = workload.NewUpdater("cloud-sql-proxy-operator/dev")
+		u                 = workload.NewUpdater("cloud-sql-proxy-operator/dev", workload.DefaultProxyImage)
 	)
 
 	// Create a pod
@@ -362,7 +362,7 @@ func TestContainerImageEmpty(t *testing.T) {
 	var (
 		wantsInstanceName = "project:server:db"
 		wantImage         = workload.DefaultProxyImage
-		u                 = workload.NewUpdater("cloud-sql-proxy-operator/dev")
+		u                 = workload.NewUpdater("cloud-sql-proxy-operator/dev", workload.DefaultProxyImage)
 	)
 	// Create a AuthProxyWorkload that matches the deployment
 
@@ -421,7 +421,7 @@ func TestContainerReplaced(t *testing.T) {
 		wantContainer     = &corev1.Container{
 			Name: "sample", Image: "debian:latest", Command: []string{"/bin/bash"},
 		}
-		u = workload.NewUpdater("cloud-sql-proxy-operator/dev")
+		u = workload.NewUpdater("cloud-sql-proxy-operator/dev", workload.DefaultProxyImage)
 	)
 
 	// Create a pod
@@ -475,7 +475,7 @@ func TestResourcesFromSpec(t *testing.T) {
 			},
 		}
 
-		u = workload.NewUpdater("cloud-sql-proxy-operator/dev")
+		u = workload.NewUpdater("cloud-sql-proxy-operator/dev", workload.DefaultProxyImage)
 	)
 
 	// Create a pod
@@ -725,7 +725,7 @@ func TestProxyCLIArgs(t *testing.T) {
 
 	for _, tc := range testcases {
 		t.Run(tc.desc, func(t *testing.T) {
-			u := workload.NewUpdater("cloud-sql-proxy-operator/dev")
+			u := workload.NewUpdater("cloud-sql-proxy-operator/dev", workload.DefaultProxyImage)
 
 			// Create a pod
 			wl := &workload.PodWorkload{Pod: &corev1.Pod{
@@ -857,11 +857,11 @@ func TestPodTemplateAnnotations(t *testing.T) {
 		now = metav1.Now()
 
 		wantAnnotations = map[string]string{
-			"cloudsql.cloud.google.com/instance1": "1",
-			"cloudsql.cloud.google.com/instance2": "2",
+			"cloudsql.cloud.google.com/instance1": "1," + workload.DefaultProxyImage,
+			"cloudsql.cloud.google.com/instance2": "2," + workload.DefaultProxyImage,
 		}
 
-		u = workload.NewUpdater("cloud-sql-proxy-operator/dev")
+		u = workload.NewUpdater("cloud-sql-proxy-operator/dev", workload.DefaultProxyImage)
 	)
 
 	// Create a pod
@@ -908,17 +908,17 @@ func TestPodAnnotation(t *testing.T) {
 			name:  "instance1",
 			r:     server,
 			wantK: "cloudsql.cloud.google.com/instance1",
-			wantV: "1",
+			wantV: fmt.Sprintf("1,%s", workload.DefaultProxyImage),
 		}, {
 			name:  "instance2",
 			r:     deletedServer,
 			wantK: "cloudsql.cloud.google.com/instance2",
-			wantV: fmt.Sprintf("2-deleted-%s", now.Format(time.RFC3339)),
+			wantV: fmt.Sprintf("2-deleted-%s,%s", now.Format(time.RFC3339), workload.DefaultProxyImage),
 		},
 	}
 
 	for _, tc := range testcases {
-		gotK, gotV := workload.PodAnnotation(tc.r)
+		gotK, gotV := workload.PodAnnotation(tc.r, workload.DefaultProxyImage)
 		if tc.wantK != gotK {
 			t.Errorf("got %v, want %v for key", gotK, tc.wantK)
 		}
@@ -942,7 +942,7 @@ func TestWorkloadUnixVolume(t *testing.T) {
 		wantWorkloadEnv = map[string]string{
 			"DB_SOCKET_PATH": wantsUnixSocketPath,
 		}
-		u = workload.NewUpdater("authproxyworkload/dev")
+		u = workload.NewUpdater("authproxyworkload/dev", workload.DefaultProxyImage)
 	)
 
 	// Create a pod