Repository: GoogleCloudPlatform/cloud-sql-proxy-operator
Pull request #210 (2023-02-17 16:45:02)

feat: add telemetry settings to configure health check ports
Allow the user to configure the admin and health check ports for a proxy container. This adds TelemetrySpec
which will hold the ports as well as other telemetry-related configuration.

Related to #45


Patch:
diff --git a/Makefile b/Makefile
index c36de67..a7ac5d6 100644
--- a/Makefile
+++ b/Makefile
@@ -375,6 +375,23 @@ e2e_image_push: generate # Build and push a operator image to the e2e artifact r
 	DOCKER_FILE_NAME=Dockerfile-operator \
 	$(PWD)/tools/docker-build.sh
 
+###
+# Build a version of the cloud-sql-proxy from local sources
+E2E_LOCAL_PROXY_PROJECT_DIR?=/not-set
+E2E_LOCAL_PROXY_BUILD_URL_FILE=$(PWD)/bin/last-local-proxy-url.txt
+E2E_LOCAL_PROXY_BUILD_URL=$(shell cat $(E2E_LOCAL_PROXY_BUILD_URL_FILE) | tr -d "\n")
+
+.PHONY: e2e_image_push
+e2e_local_proxy_image_push: # Build and push the proxy image from a local working directory to the e2e artifact repo
+	test -d $(E2E_LOCAL_PROXY_PROJECT_DIR) && \
+	PROJECT_DIR=$(E2E_LOCAL_PROXY_PROJECT_DIR) \
+	IMAGE_NAME=cloud-sql-proxy-dev \
+	REPO_URL=$(E2E_DOCKER_URL) \
+	IMAGE_URL_OUT=$(E2E_LOCAL_PROXY_BUILD_URL_FILE) \
+	PLATFORMS=linux/amd64 \
+	DOCKER_FILE_NAME=Dockerfile \
+	$(PWD)/tools/docker-build.sh
+
 
 ##
 # Build tool dependencies
diff --git a/build.sample.env b/build.sample.env
index d968cb0..e339fc2 100644
--- a/build.sample.env
+++ b/build.sample.env
@@ -6,3 +6,9 @@
 ##
 # The gcloud project id to use for the end-to-end tests
 E2E_PROJECT_ID = my-gcloud-project
+
+##
+# To run the e2e tests using the proxy code from your local machine,
+# Uncomment this line and set the path to your local working directory
+# of the cloud-sql-proxy project.
+# E2E_LOCAL_PROXY_PROJECT_DIR = /home/me/projects/cloud-sql-proxy
diff --git a/config/crd/bases/cloudsql.cloud.google.com_authproxyworkloads.yaml b/config/crd/bases/cloudsql.cloud.google.com_authproxyworkloads.yaml
index 9f01f75..e68e2ee 100644
--- a/config/crd/bases/cloudsql.cloud.google.com_authproxyworkloads.yaml
+++ b/config/crd/bases/cloudsql.cloud.google.com_authproxyworkloads.yaml
@@ -902,6 +902,14 @@ spec:
                     sqlAdminAPIEndpoint:
                       description: SQLAdminAPIEndpoint is a debugging parameter that when specified will change the Google Cloud api endpoint used by the proxy.
                       type: string
+                    telemetry:
+                      description: Telemetry specifies how the proxy should expose telemetry. Optional, by default
+                      properties:
+                        httpPort:
+                          description: HTTPPort the port for Prometheus and health check server. This sets the proxy container's CLI argument `--http-port`
+                          format: int32
+                          type: integer
+                      type: object
                   type: object
                 instances:
                   description: Instances lists the Cloud SQL instances to connect
diff --git a/docs/dev.md b/docs/dev.md
index 7a5b362..475b443 100644
--- a/docs/dev.md
+++ b/docs/dev.md
@@ -31,3 +31,23 @@ Then, to create the CRD for Workload
 ```
 
 
+## Running E2E tests with a custom proxy image
+
+You may want to write e2e tests for a proxy feature that has
+not been released yet. 
+
+Step 1: Check out the cloud-sql-proxy repo.
+
+Step 2: Add `E2E_LOCAL_PROXY_PROJECT_DIR = /home/me/projects/cloud-sql-proxy`
+to your `build.env`. This tells your Makefile where your proxy
+directory is. Set it to the path of your cloud-sql-proxy working directory.
+
+Step 3: Build a custom image and push it to the e2e environment
+repo. Run `make e2e_local_proxy_image_push` This will build and push
+a docker image from the proxy repo, and write the file `bin/last-local-proxy-url.txt`
+
+Step 4: Run your e2e tests. The tests will read the contents of
+the file `bin/last-local-proxy-url.txt`.
+
+Delete the file `bin/last-local-proxy-url.txt` to go back to using
+the public proxy iamge again
\ No newline at end of file
diff --git a/installer/cloud-sql-proxy-operator.yaml b/installer/cloud-sql-proxy-operator.yaml
index fadf2fb..81e45f1 100644
--- a/installer/cloud-sql-proxy-operator.yaml
+++ b/installer/cloud-sql-proxy-operator.yaml
@@ -920,6 +920,14 @@ spec:
                     sqlAdminAPIEndpoint:
                       description: SQLAdminAPIEndpoint is a debugging parameter that when specified will change the Google Cloud api endpoint used by the proxy.
                       type: string
+                    telemetry:
+                      description: Telemetry specifies how the proxy should expose telemetry. Optional, by default
+                      properties:
+                        httpPort:
+                          description: HTTPPort the port for Prometheus and health check server. This sets the proxy container's CLI argument `--http-port`
+                          format: int32
+                          type: integer
+                      type: object
                   type: object
                 instances:
                   description: Instances lists the Cloud SQL instances to connect
diff --git a/internal/api/v1alpha1/authproxyworkload_types.go b/internal/api/v1alpha1/authproxyworkload_types.go
index 659ca74..e3d88c6 100644
--- a/internal/api/v1alpha1/authproxyworkload_types.go
+++ b/internal/api/v1alpha1/authproxyworkload_types.go
@@ -136,6 +136,11 @@ type AuthProxyContainerSpec struct {
 	//+kubebuilder:validation:Optional
 	Resources *v1.ResourceRequirements `json:"resources,omitempty"`
 
+	// Telemetry specifies how the proxy should expose telemetry.
+	// Optional, by default
+	//+kubebuilder:validation:Optional
+	Telemetry *TelemetrySpec `json:"telemetry,omitempty"`
+
 	// MaxConnections limits the number of connections. Default value is no limit.
 	// This sets the proxy container's CLI argument `--max-connections`
 	//+kubebuilder:validation:Optional
@@ -171,6 +176,14 @@ type AuthProxyContainerSpec struct {
 	RolloutStrategy string `json:"rolloutStrategy,omitempty"`
 }
 
+// TelemetrySpec specifies how the proxy container will expose telemetry.
+type TelemetrySpec struct {
+	// HTTPPort the port for Prometheus and health check server.
+	// This sets the proxy container's CLI argument `--http-port`
+	//+kubebuilder:validation:Optional
+	HTTPPort *int32 `json:"httpPort,omitempty"`
+}
+
 // InstanceSpec describes the configuration for how the proxy should expose
 // a Cloud SQL database instance to a workload. The simplest possible configuration
 // declares just the connection string and the port number or unix socket.
diff --git a/internal/api/v1alpha1/authproxyworkload_webhook.go b/internal/api/v1alpha1/authproxyworkload_webhook.go
index d088090..c05cafd 100644
--- a/internal/api/v1alpha1/authproxyworkload_webhook.go
+++ b/internal/api/v1alpha1/authproxyworkload_webhook.go
@@ -15,7 +15,13 @@
 package v1alpha1
 
 import (
+	"fmt"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/util/validation/field"
 	ctrl "sigs.k8s.io/controller-runtime"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
@@ -30,10 +36,7 @@ func (r *AuthProxyWorkload) SetupWebhookWithManager(mgr ctrl.Manager) error {
 		Complete()
 }
 
-// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
-
-//+kubebuilder:webhook:path=/mutate-cloudsql-cloud-google-com-v1alpha1-authproxyworkload,mutating=true,failurePolicy=fail,sideEffects=None,groups=cloudsql.cloud.google.com,resources=authproxyworkloads,verbs=create;update,versions=v1alpha1,name=mauthproxyworkload.kb.io,admissionReviewVersions=v1
-
+// +kubebuilder:webhook:path=/mutate-cloudsql-cloud-google-com-v1alpha1-authproxyworkload,mutating=true,failurePolicy=fail,sideEffects=None,groups=cloudsql.cloud.google.com,resources=authproxyworkloads,verbs=create;update,versions=v1alpha1,name=mauthproxyworkload.kb.io,admissionReviewVersions=v1
 var _ webhook.Defaulter = &AuthProxyWorkload{}
 
 // Default implements webhook.Defaulter so a webhook will be registered for the type
@@ -45,31 +48,71 @@ func (r *AuthProxyWorkload) Default() {
 	}
 }
 
-// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
-//+kubebuilder:webhook:path=/validate-cloudsql-cloud-google-com-v1alpha1-authproxyworkload,mutating=false,failurePolicy=fail,sideEffects=None,groups=cloudsql.cloud.google.com,resources=authproxyworkloads,verbs=create;update,versions=v1alpha1,name=vauthproxyworkload.kb.io,admissionReviewVersions=v1
-
+// +kubebuilder:webhook:path=/validate-cloudsql-cloud-google-com-v1alpha1-authproxyworkload,mutating=false,failurePolicy=fail,sideEffects=None,groups=cloudsql.cloud.google.com,resources=authproxyworkloads,verbs=create;update,versions=v1alpha1,name=vauthproxyworkload.kb.io,admissionReviewVersions=v1
 var _ webhook.Validator = &AuthProxyWorkload{}
 
 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type
 func (r *AuthProxyWorkload) ValidateCreate() error {
-	authproxyworkloadlog.Info("validate create", "name", r.Name)
-
-	// TODO(user): fill in your validation logic upon object creation.
-	return nil
+	return r.validate()
 }
 
 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
 func (r *AuthProxyWorkload) ValidateUpdate(_ runtime.Object) error {
-	authproxyworkloadlog.Info("validate update", "name", r.Name)
-
-	// TODO(user): fill in your validation logic upon object update.
-	return nil
+	return r.validate()
 }
 
 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type
 func (r *AuthProxyWorkload) ValidateDelete() error {
-	authproxyworkloadlog.Info("validate delete", "name", r.Name)
+	return nil
+}
 
-	// TODO(user): fill in your validation logic upon object deletion.
+func (r *AuthProxyWorkload) validate() error {
+	var allErrs field.ErrorList
+
+	allErrs = append(allErrs, validation.ValidateLabelName(r.Name, field.NewPath("metadata", "name"))...)
+	allErrs = append(allErrs, validateWorkload(&r.Spec.Workload, field.NewPath("spec", "workload"))...)
+
+	if len(allErrs) > 0 {
+		return apierrors.NewInvalid(
+			schema.GroupKind{
+				Group: GroupVersion.Group,
+				Kind:  "AuthProxyWorkload"},
+			r.Name, allErrs)
+	}
 	return nil
 }
+
+var supportedKinds = []string{"CronJob", "Job", "StatefulSet", "Deployment", "DaemonSet", "ReplicaSet", "Pod"}
+
+func validateWorkload(spec *WorkloadSelectorSpec, f *field.Path) field.ErrorList {
+	var errs field.ErrorList
+	if spec.Selector != nil {
+		verr := validation.ValidateLabelSelector(spec.Selector, validation.LabelSelectorValidationOptions{}, f.Child("selector"))
+		errs = append(errs, verr...)
+	}
+
+	if spec.Name != "" && spec.Selector != nil {
+		errs = append(errs, field.Invalid(f.Child("name"), spec,
+			"WorkloadSelectorSpec must specify either name or selector. Both were set."))
+	}
+	if spec.Name == "" && spec.Selector == nil {
+		errs = append(errs, field.Invalid(f.Child("name"), spec,
+			"WorkloadSelectorSpec must specify either name or selector. Neither was set."))
+	}
+
+	_, gk := schema.ParseKindArg(spec.Kind)
+	var found bool
+	for _, kind := range supportedKinds {
+		if kind == gk.Kind {
+			found = true
+			break
+		}
+	}
+	if !found {
+		errs = append(errs, field.Invalid(f.Child("kind"), spec.Kind,
+			fmt.Sprintf("Kind was %q, must be one of CronJob, Job, StatefulSet, Deployment, DaemonSet or Pod", gk.Kind)))
+
+	}
+
+	return errs
+}
diff --git a/internal/api/v1alpha1/zz_generated.deepcopy.go b/internal/api/v1alpha1/zz_generated.deepcopy.go
index e0340fe..97ec981 100644
--- a/internal/api/v1alpha1/zz_generated.deepcopy.go
+++ b/internal/api/v1alpha1/zz_generated.deepcopy.go
@@ -38,6 +38,11 @@ func (in *AuthProxyContainerSpec) DeepCopyInto(out *AuthProxyContainerSpec) {
 		*out = new(corev1.ResourceRequirements)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Telemetry != nil {
+		in, out := &in.Telemetry, &out.Telemetry
+		*out = new(TelemetrySpec)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.MaxConnections != nil {
 		in, out := &in.MaxConnections, &out.MaxConnections
 		*out = new(int64)
@@ -214,6 +219,26 @@ func (in *InstanceSpec) DeepCopy() *InstanceSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TelemetrySpec) DeepCopyInto(out *TelemetrySpec) {
+	*out = *in
+	if in.HTTPPort != nil {
+		in, out := &in.HTTPPort, &out.HTTPPort
+		*out = new(int32)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TelemetrySpec.
+func (in *TelemetrySpec) DeepCopy() *TelemetrySpec {
+	if in == nil {
+		return nil
+	}
+	out := new(TelemetrySpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *WorkloadSelectorSpec) DeepCopyInto(out *WorkloadSelectorSpec) {
 	*out = *in
diff --git a/internal/workload/podspec_updates.go b/internal/workload/podspec_updates.go
index 30fc942..e418e10 100644
--- a/internal/workload/podspec_updates.go
+++ b/internal/workload/podspec_updates.go
@@ -252,10 +252,15 @@ func (s *updateState) addWorkloadPort(p int32) {
 	s.addPort(p, proxyInstanceID{})
 }
 
-func (s *updateState) addProxyPort(p int32) {
+func (s *updateState) addProxyPort(port int32, p *cloudsqlapi.AuthProxyWorkload) {
 	// This port is associated with the workload, not the proxy.
 	// so this uses an empty proxyInstanceID{}
-	s.addPort(p, proxyInstanceID{})
+	s.addPort(port, proxyInstanceID{
+		AuthProxyWorkload: types.NamespacedName{
+			Namespace: p.Namespace,
+			Name:      p.Name,
+		},
+	})
 }
 
 // isPortInUse checks if the port is in use.
@@ -627,7 +632,18 @@ func (s *updateState) updateContainerEnv(c *corev1.Container) {
 
 // addHealthCheck adds the health check declaration to this workload.
 func (s *updateState) addHealthCheck(p *cloudsqlapi.AuthProxyWorkload, c *corev1.Container) {
-	port := DefaultHealthCheckPort
+	var portPtr *int32
+
+	cs := p.Spec.AuthProxyContainer
+
+	// if the TelemetrySpec.exists, get Port values
+	if cs != nil && cs.Telemetry != nil {
+		if cs.Telemetry.HTTPPort != nil {
+			portPtr = cs.Telemetry.HTTPPort
+		}
+	}
+
+	port := s.usePort(portPtr, DefaultHealthCheckPort, p)
 
 	c.StartupProbe = &corev1.Probe{
 		ProbeHandler: corev1.ProbeHandler{HTTPGet: &corev1.HTTPGetAction{
@@ -665,3 +681,19 @@ func (s *updateState) addError(errorCode, description string, p *cloudsqlapi.Aut
 func (s *updateState) defaultProxyImage() string {
 	return DefaultProxyImage
 }
+
+func (s *updateState) usePort(configValue *int32, defaultValue int32, p *cloudsqlapi.AuthProxyWorkload) int32 {
+	if configValue != nil {
+		s.addProxyPort(*configValue, p)
+		return *configValue
+	}
+
+	port := defaultValue
+	if configValue == nil {
+		for s.isPortInUse(port) {
+			port++
+		}
+	}
+	s.addProxyPort(port, p)
+	return port
+}


Test patch:
diff --git a/internal/api/v1alpha1/authproxyworkload_test.go b/internal/api/v1alpha1/authproxyworkload_test.go
new file mode 100644
index 0000000..91faef8
--- /dev/null
+++ b/internal/api/v1alpha1/authproxyworkload_test.go
@@ -0,0 +1,185 @@
+// Copyright 2023 Google LLC
+//
+// 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 v1alpha1_test
+
+import (
+	"testing"
+
+	cloudsqlapi "github.com/GoogleCloudPlatform/cloud-sql-proxy-operator/internal/api/v1alpha1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestAuthProxyWorkload_ValidateCreate(t *testing.T) {
+	data := []struct {
+		desc      string
+		spec      cloudsqlapi.AuthProxyWorkloadSpec
+		wantValid bool
+	}{
+		{
+			desc: "Valid WorkloadSelectorSpec with Name",
+			spec: cloudsqlapi.AuthProxyWorkloadSpec{
+				Workload: cloudsqlapi.WorkloadSelectorSpec{
+					Kind: "Deployment",
+					Name: "webapp",
+				},
+			},
+			wantValid: true,
+		},
+		{
+			desc: "Valid WorkloadSelectorSpec with Selector",
+			spec: cloudsqlapi.AuthProxyWorkloadSpec{
+				Workload: cloudsqlapi.WorkloadSelectorSpec{
+					Kind: "Deployment",
+					Selector: &v1.LabelSelector{
+						MatchLabels: map[string]string{"app": "sample"},
+					},
+				},
+			},
+			wantValid: true,
+		},
+		{
+			desc: "Invalid, both workload selector and name both set",
+			spec: cloudsqlapi.AuthProxyWorkloadSpec{
+				Workload: cloudsqlapi.WorkloadSelectorSpec{
+					Kind: "Deployment",
+					Name: "webapp",
+					Selector: &v1.LabelSelector{
+						MatchLabels: map[string]string{"app": "sample"},
+					},
+				},
+			},
+			wantValid: false,
+		},
+		{
+			desc: "Invalid, WorkloadSelector missing name and selector",
+			spec: cloudsqlapi.AuthProxyWorkloadSpec{
+				Workload: cloudsqlapi.WorkloadSelectorSpec{Kind: "Deployment"},
+			},
+			wantValid: false,
+		},
+		{
+			desc: "Valid, Instance configured with PortEnvName",
+			spec: cloudsqlapi.AuthProxyWorkloadSpec{
+				Workload: cloudsqlapi.WorkloadSelectorSpec{
+					Kind: "Deployment",
+					Name: "webapp",
+				},
+				Instances: []cloudsqlapi.InstanceSpec{{
+					ConnectionString: "proj:region:db2",
+					PortEnvName:      "DB_PORT",
+				}},
+			},
+			wantValid: true,
+		},
+	}
+
+	for _, tc := range data {
+		t.Run(tc.desc, func(t *testing.T) {
+			p := cloudsqlapi.AuthProxyWorkload{
+				ObjectMeta: v1.ObjectMeta{Name: "sample"},
+				Spec:       tc.spec,
+			}
+			err := p.ValidateCreate()
+			gotValid := err == nil
+			switch {
+			case tc.wantValid && !gotValid:
+				t.Errorf("wants create valid, got error %v", err)
+				printFieldErrors(t, err)
+			case !tc.wantValid && gotValid:
+				t.Errorf("wants an error on create, got no error")
+			default:
+				t.Logf("create passed %s", tc.desc)
+				// test passes, do nothing.
+			}
+		})
+	}
+}
+
+func TestAuthProxyWorkload_ValidateUpdate(t *testing.T) {
+	data := []struct {
+		desc      string
+		spec      cloudsqlapi.AuthProxyWorkloadSpec
+		oldSpec   cloudsqlapi.AuthProxyWorkloadSpec
+		wantValid bool
+	}{
+		{
+			desc: "Valid, update adds another instance",
+			spec: cloudsqlapi.AuthProxyWorkloadSpec{
+				Workload: cloudsqlapi.WorkloadSelectorSpec{
+					Kind: "Deployment",
+					Name: "webapp",
+				},
+				Instances: []cloudsqlapi.InstanceSpec{{
+					ConnectionString: "proj:region:db1",
+					PortEnvName:      "DB_PORT",
+				}},
+			},
+			oldSpec: cloudsqlapi.AuthProxyWorkloadSpec{
+				Workload: cloudsqlapi.WorkloadSelectorSpec{
+					Kind: "Deployment",
+					Name: "webapp",
+				},
+				Instances: []cloudsqlapi.InstanceSpec{
+					{
+						ConnectionString: "proj:region:db1",
+						PortEnvName:      "DB_PORT",
+					},
+					{
+						ConnectionString: "proj:region:db2",
+						PortEnvName:      "DB_PORT2",
+					},
+				},
+			},
+			wantValid: true,
+		},
+	}
+
+	for _, tc := range data {
+		t.Run(tc.desc, func(t *testing.T) {
+			p := cloudsqlapi.AuthProxyWorkload{
+				ObjectMeta: v1.ObjectMeta{Name: "sample"},
+				Spec:       tc.spec,
+			}
+			oldP := cloudsqlapi.AuthProxyWorkload{
+				ObjectMeta: v1.ObjectMeta{Name: "sample"},
+				Spec:       tc.oldSpec}
+
+			err := p.ValidateUpdate(&oldP)
+			gotValid := err == nil
+
+			switch {
+			case tc.wantValid && !gotValid:
+				t.Errorf("wants create valid, got error %v", err)
+			case !tc.wantValid && gotValid:
+				t.Errorf("wants an error on create, got no error")
+			default:
+				t.Logf("update passed %s", tc.desc)
+				// test passes, do nothing.
+			}
+		})
+	}
+}
+
+func printFieldErrors(t *testing.T, err error) {
+	t.Helper()
+	statusErr, ok := err.(*apierrors.StatusError)
+	if ok {
+		t.Errorf("Field status errors: ")
+		for _, v := range statusErr.Status().Details.Causes {
+			t.Errorf("   %v %v: %v ", v.Field, v.Type, v.Message)
+		}
+	}
+}
diff --git a/internal/workload/podspec_updates_test.go b/internal/workload/podspec_updates_test.go
index a4b7dd0..b592294 100644
--- a/internal/workload/podspec_updates_test.go
+++ b/internal/workload/podspec_updates_test.go
@@ -622,6 +622,32 @@ func TestProxyCLIArgs(t *testing.T) {
 				fmt.Sprintf("hello:world:one?port=%d&private-ip=true", workload.DefaultFirstPort),
 				fmt.Sprintf("hello:world:two?port=%d&private-ip=false", workload.DefaultFirstPort+1)},
 		},
+		{
+			desc: "global flags",
+			proxySpec: v1alpha1.AuthProxyWorkloadSpec{
+				AuthProxyContainer: &v1alpha1.AuthProxyContainerSpec{
+					SQLAdminAPIEndpoint: "https://example.com",
+					Telemetry: &v1alpha1.TelemetrySpec{
+						HTTPPort: ptr(int32(9092)),
+					},
+					MaxConnections:  ptr(int64(10)),
+					MaxSigtermDelay: ptr(int64(20)),
+				},
+				Instances: []v1alpha1.InstanceSpec{{
+					ConnectionString: "hello:world:one",
+				}},
+			},
+			wantProxyArgContains: []string{
+				fmt.Sprintf("hello:world:one?port=%d", workload.DefaultFirstPort),
+			},
+			wantWorkloadEnv: map[string]string{
+				"CSQL_PROXY_SQLADMIN_API_ENDPOINT": "https://example.com",
+				"CSQL_PROXY_HTTP_PORT":             "9092",
+				"CSQL_PROXY_HEALTH_CHECK":          "true",
+				"CSQL_PROXY_MAX_CONNECTIONS":       "10",
+				"CSQL_PROXY_MAX_SIGTERM_DELAY":     "20",
+			},
+		},
 		{
 			desc: "port conflict with other instance causes error",
 			proxySpec: v1alpha1.AuthProxyWorkloadSpec{
diff --git a/tests/setup_test.go b/tests/setup_test.go
index 9244c95..5b86bf4 100644
--- a/tests/setup_test.go
+++ b/tests/setup_test.go
@@ -110,7 +110,7 @@ func setupTests() (func(), error) {
 	}
 
 	// Read e2e test configuration
-	proxyImageURL = loadValue("PROXY_IMAGE_URL", "../bin/last-proxy-image-url.txt", workload.DefaultProxyImage)
+	proxyImageURL = loadValue("PROXY_IMAGE_URL", "../bin/last-local-proxy-url.txt", workload.DefaultProxyImage)
 	operatorURL = loadValue("OPERATOR_IMAGE_URL", "../bin/last-gcloud-operator-url.txt", "operator:latest")
 	testInfraPath := loadValue("TEST_INFRA_JSON", "", "../bin/testinfra.json")
 	ti, err := loadTestInfra(testInfraPath)