diff --git a/api/v1alpha1/connection_types.go b/api/v1alpha1/connection_types.go index 3c73e74d..4c11bb11 100644 --- a/api/v1alpha1/connection_types.go +++ b/api/v1alpha1/connection_types.go @@ -19,6 +19,15 @@ type SecretReference struct { Name string `json:"name"` } +// ConnectionTLSConfig defines TLS settings for a Connection. +type ConnectionTLSConfig struct { + // ServerName overrides the server name used to verify the Temporal server + // certificate when TLS is enabled. + // +optional + // +kubebuilder:validation:Pattern=`^[a-zA-Z0-9.-]+$` + ServerName string `json:"serverName,omitempty"` +} + // ConnectionSpec defines the desired state of Connection // +kubebuilder:validation:XValidation:rule="!(has(self.mutualTLSSecretRef) && has(self.apiKeySecretRef))",message="Only one of mutualTLSSecretRef or apiKeySecretRef may be set" type ConnectionSpec struct { @@ -26,6 +35,10 @@ type ConnectionSpec struct { // +kubebuilder:validation:Pattern=`^[a-zA-Z0-9.-]+:[0-9]+$` HostPort string `json:"hostPort"` + // TLS configures TLS behavior for the Temporal server connection. + // +optional + TLS *ConnectionTLSConfig `json:"tls,omitempty"` + // MutualTLSSecretRef is the name of the Secret that contains the TLS certificate and key // for mutual TLS authentication. The secret must be `type: kubernetes.io/tls` or // `type: Opaque` and exist in the same Kubernetes namespace as the Connection @@ -46,6 +59,13 @@ type ConnectionSpec struct { APIKeySecretRef *corev1.SecretKeySelector `json:"apiKeySecretRef,omitempty"` } +func (s ConnectionSpec) TLSServerName() string { + if s.TLS == nil { + return "" + } + return s.TLS.ServerName +} + // ConnectionStatus defines the observed state of Connection type ConnectionStatus struct { // TODO(jlegrone): Add additional status fields following Kubernetes API conventions diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f4b8a3c5..ffa4eb1f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -132,6 +132,11 @@ func (in *ConnectionReference) DeepCopy() *ConnectionReference { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConnectionSpec) DeepCopyInto(out *ConnectionSpec) { *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(ConnectionTLSConfig) + **out = **in + } if in.MutualTLSSecretRef != nil { in, out := &in.MutualTLSSecretRef, &out.MutualTLSSecretRef *out = new(SecretReference) @@ -169,6 +174,21 @@ func (in *ConnectionStatus) DeepCopy() *ConnectionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionTLSConfig) DeepCopyInto(out *ConnectionTLSConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionTLSConfig. +func (in *ConnectionTLSConfig) DeepCopy() *ConnectionTLSConfig { + if in == nil { + return nil + } + out := new(ConnectionTLSConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CurrentWorkerDeploymentVersion) DeepCopyInto(out *CurrentWorkerDeploymentVersion) { *out = *in diff --git a/docs/configuration.md b/docs/configuration.md index f4d6ebc3..87f99524 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -156,7 +156,7 @@ workerOptions: ### Connection Configuration -Reference a `Connection` resource that defines server details. You can use either mutual TLS (mTLS) or API key authentication, but not both. +Reference a `Connection` resource that defines server details. You can use either mutual TLS (mTLS) or API key authentication, but not both. If the address in `hostPort` differs from the hostname on the server certificate, set `tls.serverName` to the certificate hostname. **Using mTLS Authentication:** @@ -167,6 +167,9 @@ metadata: name: production-temporal spec: hostPort: "production.abc123.tmprl.cloud:7233" + # Optional: override the TLS server name used for certificate verification + tls: + serverName: "production.abc123.tmprl.cloud" mutualTLSSecretRef: name: temporal-cloud-mtls # Optional: for mTLS ``` @@ -234,6 +237,9 @@ metadata: name: production-temporal spec: hostPort: "production.abc123.tmprl.cloud:7233" + # Optional: override the TLS server name used for certificate verification + tls: + serverName: "production.abc123.tmprl.cloud" apiKeySecretRef: name: temporal-api-key # Name of the Secret key: api-key # Key within the Secret containing the API key token @@ -291,6 +297,7 @@ echo -n "your-api-key-token-here" | base64 - Both secrets must be created in the same Kubernetes namespace as the `Connection` resource - Only one authentication method can be specified per `Connection` (either `mutualTLSSecretRef` or `apiKeySecretRef`) - The secret name and key in `apiKeySecretRef` must match the actual Secret resource and data key +- `tls.serverName` affects TLS certificate verification by the controller and is injected into Worker Pods as `TEMPORAL_TLS_SERVER_NAME` for SDK envconfig users. - For mTLS secrets, the keys must be named exactly `tls.crt` and `tls.key` ## Gate Configuration diff --git a/helm/temporal-worker-controller-crds/templates/temporal.io_connections.yaml b/helm/temporal-worker-controller-crds/templates/temporal.io_connections.yaml index a297f9ca..739b7cc0 100644 --- a/helm/temporal-worker-controller-crds/templates/temporal.io_connections.yaml +++ b/helm/temporal-worker-controller-crds/templates/temporal.io_connections.yaml @@ -59,6 +59,12 @@ spec: required: - name type: object + tls: + properties: + serverName: + pattern: ^[a-zA-Z0-9.-]+$ + type: string + type: object required: - hostPort type: object diff --git a/internal/controller/clientpool/clientpool.go b/internal/controller/clientpool/clientpool.go index fa070f4b..bfc8e169 100644 --- a/internal/controller/clientpool/clientpool.go +++ b/internal/controller/clientpool/clientpool.go @@ -32,10 +32,11 @@ const ( ) type ClientPoolKey struct { - HostPort string - Namespace string // Temporal namespace - SecretName string // Include secret name in key to invalidate cache when the secret name changes - AuthMode AuthMode // Include auth mode in key to invalidate cache when the auth mode changes for the secret + HostPort string + TLSServerName string + Namespace string // Temporal namespace + SecretName string // Include secret name in key to invalidate cache when the secret name changes + AuthMode AuthMode // Include auth mode in key to invalidate cache when the auth mode changes for the secret } type MTLSAuth struct { @@ -123,6 +124,7 @@ type NewClientOptions struct { } func (cp *ClientPool) fetchClientUsingMTLSSecret(secret corev1.Secret, opts NewClientOptions) (*sdkclient.Options, *ClientPoolKey, *ClientAuth, error) { + tlsServerName := opts.Spec.TLSServerName() clientOpts := sdkclient.Options{ Logger: cp.logger, HostPort: opts.Spec.HostPort, @@ -155,6 +157,7 @@ func (cp *ClientPool) fetchClientUsingMTLSSecret(secret corev1.Secret, opts NewC } tlsCfg := &tls.Config{ Certificates: []tls.Certificate{cert}, + ServerName: tlsServerName, } // If the secret contains a CA certificate, append it to the system CA pool for // server certificate verification. This enables connecting to Temporal servers whose @@ -177,10 +180,11 @@ func (cp *ClientPool) fetchClientUsingMTLSSecret(secret corev1.Secret, opts NewC expiryTime = exp key := ClientPoolKey{ - HostPort: opts.Spec.HostPort, - Namespace: opts.TemporalNamespace, - SecretName: opts.Spec.MutualTLSSecretRef.Name, - AuthMode: AuthModeTLS, + HostPort: opts.Spec.HostPort, + TLSServerName: tlsServerName, + Namespace: opts.TemporalNamespace, + SecretName: opts.Spec.MutualTLSSecretRef.Name, + AuthMode: AuthModeTLS, } auth := ClientAuth{ mode: AuthModeTLS, @@ -190,13 +194,14 @@ func (cp *ClientPool) fetchClientUsingMTLSSecret(secret corev1.Secret, opts NewC } func (cp *ClientPool) fetchClientUsingAPIKeySecret(opts NewClientOptions) (*sdkclient.Options, *ClientPoolKey, *ClientAuth, error) { + tlsServerName := opts.Spec.TLSServerName() clientOpts := sdkclient.Options{ Logger: cp.logger, HostPort: opts.Spec.HostPort, Namespace: opts.TemporalNamespace, Identity: opts.Identity, ConnectionOptions: sdkclient.ConnectionOptions{ - TLS: &tls.Config{}, + TLS: &tls.Config{ServerName: tlsServerName}, }, } @@ -208,10 +213,11 @@ func (cp *ClientPool) fetchClientUsingAPIKeySecret(opts NewClientOptions) (*sdkc }) key := ClientPoolKey{ - HostPort: opts.Spec.HostPort, - Namespace: opts.TemporalNamespace, - SecretName: opts.Spec.APIKeySecretRef.Name, - AuthMode: AuthModeAPIKey, + HostPort: opts.Spec.HostPort, + TLSServerName: tlsServerName, + Namespace: opts.TemporalNamespace, + SecretName: opts.Spec.APIKeySecretRef.Name, + AuthMode: AuthModeAPIKey, } auth := ClientAuth{ mode: AuthModeAPIKey, @@ -222,18 +228,23 @@ func (cp *ClientPool) fetchClientUsingAPIKeySecret(opts NewClientOptions) (*sdkc } func (cp *ClientPool) fetchClientUsingNoCredentials(opts NewClientOptions) (*sdkclient.Options, *ClientPoolKey, *ClientAuth, error) { + tlsServerName := opts.Spec.TLSServerName() clientOpts := sdkclient.Options{ Logger: cp.logger, HostPort: opts.Spec.HostPort, Namespace: opts.TemporalNamespace, Identity: opts.Identity, } + if tlsServerName != "" { + clientOpts.ConnectionOptions.TLS = &tls.Config{ServerName: tlsServerName} + } key := ClientPoolKey{ - HostPort: opts.Spec.HostPort, - Namespace: opts.TemporalNamespace, - SecretName: "", - AuthMode: AuthModeNoCredentials, + HostPort: opts.Spec.HostPort, + TLSServerName: tlsServerName, + Namespace: opts.TemporalNamespace, + SecretName: "", + AuthMode: AuthModeNoCredentials, } auth := ClientAuth{ mode: AuthModeNoCredentials, diff --git a/internal/controller/clientpool/clientpool_test.go b/internal/controller/clientpool/clientpool_test.go index 30bd2f57..7f17e2b5 100644 --- a/internal/controller/clientpool/clientpool_test.go +++ b/internal/controller/clientpool/clientpool_test.go @@ -237,6 +237,78 @@ func TestFetchMTLS_ValidCert_Succeeds(t *testing.T) { assert.Len(t, clientOpts.ConnectionOptions.TLS.Certificates, 1) } +func TestFetchMTLS_TLSServerNameOverride(t *testing.T) { + now := time.Now() + caCert, caKey, _ := generateSelfSignedCACert(t, now.Add(-time.Hour), now.Add(time.Hour)) + _, certPEM, keyPEM := generateLeafCert(t, caCert, caKey, "test.example.com", now.Add(-time.Hour), now.Add(time.Hour)) + + cp := newTestPool() + secret := makeTLSSecret(certPEM, keyPEM, nil) + opts := makeOpts("temporal-nlb.example.com:443") + opts.Spec.TLS = &temporaliov1alpha1.ConnectionTLSConfig{ + ServerName: "temporal-cloud.example.com", + } + + clientOpts, key, _, err := cp.fetchClientUsingMTLSSecret(secret, opts) + + require.NoError(t, err) + require.NotNil(t, clientOpts.ConnectionOptions.TLS) + assert.Equal(t, "temporal-cloud.example.com", clientOpts.ConnectionOptions.TLS.ServerName) + assert.Equal(t, "temporal-cloud.example.com", key.TLSServerName) +} + +func TestFetchAPIKey_TLSServerNameOverride(t *testing.T) { + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "api-key-secret", Namespace: "test-ns"}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{"apikey": []byte("test-api-key-value")}, + } + cp := newTestPoolWithFakeClient(&secret) + apiKeySelector := &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "api-key-secret"}, + Key: "apikey", + } + opts := NewClientOptions{ + TemporalNamespace: "default", + K8sNamespace: "test-ns", + Spec: temporaliov1alpha1.ConnectionSpec{ + HostPort: "temporal-nlb.example.com:443", + TLS: &temporaliov1alpha1.ConnectionTLSConfig{ + ServerName: "temporal-cloud.example.com", + }, + APIKeySecretRef: apiKeySelector, + }, + } + + clientOpts, key, _, err := cp.fetchClientUsingAPIKeySecret(opts) + + require.NoError(t, err) + require.NotNil(t, clientOpts.ConnectionOptions.TLS) + assert.Equal(t, "temporal-cloud.example.com", clientOpts.ConnectionOptions.TLS.ServerName) + assert.Equal(t, "temporal-cloud.example.com", key.TLSServerName) +} + +func TestFetchNoCredentials_TLSServerNameOverride(t *testing.T) { + cp := newTestPool() + opts := NewClientOptions{ + TemporalNamespace: "default", + Spec: temporaliov1alpha1.ConnectionSpec{ + HostPort: "temporal-nlb.example.com:443", + TLS: &temporaliov1alpha1.ConnectionTLSConfig{ + ServerName: "temporal-cloud.example.com", + }, + }, + } + + clientOpts, key, auth, err := cp.fetchClientUsingNoCredentials(opts) + + require.NoError(t, err) + require.NotNil(t, clientOpts.ConnectionOptions.TLS) + assert.Equal(t, "temporal-cloud.example.com", clientOpts.ConnectionOptions.TLS.ServerName) + assert.Equal(t, "temporal-cloud.example.com", key.TLSServerName) + assert.Equal(t, AuthModeNoCredentials, auth.mode) +} + func newTestPoolWithFakeClient(objects ...runtime.Object) *ClientPool { scheme := runtime.NewScheme() _ = corev1.AddToScheme(scheme) diff --git a/internal/controller/worker_controller.go b/internal/controller/worker_controller.go index 19ee7b9d..c0acb5cc 100644 --- a/internal/controller/worker_controller.go +++ b/internal/controller/worker_controller.go @@ -273,10 +273,11 @@ func (r *WorkerDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req // Get or update temporal client for connection clientPoolKey := clientpool.ClientPoolKey{ - HostPort: connection.Spec.HostPort, - Namespace: workerDeploy.Spec.WorkerOptions.TemporalNamespace, - SecretName: secretName, - AuthMode: authMode, + HostPort: connection.Spec.HostPort, + TLSServerName: connection.Spec.TLSServerName(), + Namespace: workerDeploy.Spec.WorkerOptions.TemporalNamespace, + SecretName: secretName, + AuthMode: authMode, } temporalClient, ok := r.TemporalClientPool.GetSDKClient(clientPoolKey) if !ok { @@ -576,10 +577,11 @@ func (r *WorkerDeploymentReconciler) handleDeletion( } temporalClient, ok := r.TemporalClientPool.GetSDKClient(clientpool.ClientPoolKey{ - HostPort: temporalConnection.Spec.HostPort, - Namespace: workerDeploy.Spec.WorkerOptions.TemporalNamespace, - SecretName: secretName, - AuthMode: authMode, + HostPort: temporalConnection.Spec.HostPort, + TLSServerName: temporalConnection.Spec.TLSServerName(), + Namespace: workerDeploy.Spec.WorkerOptions.TemporalNamespace, + SecretName: secretName, + AuthMode: authMode, }) if !ok { clientOpts, key, clientAuth, err := r.TemporalClientPool.ParseClientSecret(ctx, secretName, authMode, clientpool.NewClientOptions{ diff --git a/internal/k8s/deployments.go b/internal/k8s/deployments.go index 4d1f19a4..4f7d1229 100644 --- a/internal/k8s/deployments.go +++ b/internal/k8s/deployments.go @@ -317,6 +317,7 @@ func ComputeConnectionSpecHash(connection temporaliov1alpha1.ConnectionSpec) str // Hash connection spec fields in deterministic order _, _ = hasher.Write([]byte(connection.HostPort)) + _, _ = hasher.Write([]byte(connection.TLSServerName())) if connection.MutualTLSSecretRef != nil { _, _ = hasher.Write([]byte(connection.MutualTLSSecretRef.Name)) } else if connection.APIKeySecretRef != nil { @@ -370,6 +371,16 @@ func ApplyControllerPodSpecModifications( podSpec.Containers[i] = container } + if tlsServerName := connection.TLSServerName(); tlsServerName != "" { + for i, container := range podSpec.Containers { + container.Env = append(container.Env, corev1.EnvVar{ + Name: "TEMPORAL_TLS_SERVER_NAME", + Value: tlsServerName, + }) + podSpec.Containers[i] = container + } + } + // Add TLS config if mTLS is enabled if connection.MutualTLSSecretRef != nil { for i, container := range podSpec.Containers { diff --git a/internal/k8s/deployments_test.go b/internal/k8s/deployments_test.go index 37ed22f4..be4cdd40 100644 --- a/internal/k8s/deployments_test.go +++ b/internal/k8s/deployments_test.go @@ -644,6 +644,26 @@ func TestComputeConnectionSpecHash(t *testing.T) { assert.NotEqual(t, hash1, hash2, "Different hostports should produce different hashes") }) + t.Run("different TLS server names produce different hashes", func(t *testing.T) { + spec1 := temporaliov1alpha1.ConnectionSpec{ + HostPort: "localhost:7233", + TLS: &temporaliov1alpha1.ConnectionTLSConfig{ + ServerName: "server1.example.com", + }, + } + spec2 := temporaliov1alpha1.ConnectionSpec{ + HostPort: "localhost:7233", + TLS: &temporaliov1alpha1.ConnectionTLSConfig{ + ServerName: "server2.example.com", + }, + } + + hash1 := k8s.ComputeConnectionSpecHash(spec1) + hash2 := k8s.ComputeConnectionSpecHash(spec2) + + assert.NotEqual(t, hash1, hash2, "Different TLS server names should produce different hashes") + }) + t.Run("different mTLS secrets produce different hashes", func(t *testing.T) { spec1 := temporaliov1alpha1.ConnectionSpec{ HostPort: "localhost:7233", @@ -872,6 +892,22 @@ func TestNewDeploymentWithOwnerRef_EnvironmentVariablesAndVolumes(t *testing.T) }, unexpectedEnvVars: []string{}, }, + "with TLS server name": { + connection: temporaliov1alpha1.ConnectionSpec{ + HostPort: "temporal-nlb.example.com:443", + TLS: &temporaliov1alpha1.ConnectionTLSConfig{ + ServerName: "temporal-cloud.example.com", + }, + }, + expectedEnvVars: map[string]string{ + "TEMPORAL_ADDRESS": "temporal-nlb.example.com:443", + "TEMPORAL_NAMESPACE": "test-namespace", + "TEMPORAL_DEPLOYMENT_NAME": "test-deployment", + "TEMPORAL_WORKER_BUILD_ID": "test-build-id", + "TEMPORAL_TLS_SERVER_NAME": "temporal-cloud.example.com", + }, + unexpectedEnvVars: []string{}, + }, } for name, tt := range tests { diff --git a/internal/planner/planner.go b/internal/planner/planner.go index fd96d58d..a5263c5a 100644 --- a/internal/planner/planner.go +++ b/internal/planner/planner.go @@ -380,13 +380,37 @@ func updateDeploymentWithConnection(deployment *appsv1.Deployment, connection te } // Update any environment variables that reference the connection - for i, container := range deployment.Spec.Template.Spec.Containers { - for j, env := range container.Env { - if env.Name == "TEMPORAL_ADDRESS" { - deployment.Spec.Template.Spec.Containers[i].Env[j].Value = connection.HostPort - } + tlsServerName := connection.TLSServerName() + for i := range deployment.Spec.Template.Spec.Containers { + env := deployment.Spec.Template.Spec.Containers[i].Env + env = setEnvVar(env, "TEMPORAL_ADDRESS", connection.HostPort) + if tlsServerName != "" { + env = setEnvVar(env, "TEMPORAL_TLS_SERVER_NAME", tlsServerName) + } else { + env = removeEnvVar(env, "TEMPORAL_TLS_SERVER_NAME") + } + deployment.Spec.Template.Spec.Containers[i].Env = env + } +} + +func setEnvVar(envVars []corev1.EnvVar, name string, value string) []corev1.EnvVar { + for i := range envVars { + if envVars[i].Name == name { + envVars[i].Value = value + envVars[i].ValueFrom = nil + return envVars + } + } + return append(envVars, corev1.EnvVar{Name: name, Value: value}) +} + +func removeEnvVar(envVars []corev1.EnvVar, name string) []corev1.EnvVar { + for i := range envVars { + if envVars[i].Name == name { + return append(envVars[:i], envVars[i+1:]...) } } + return envVars } // checkAndUpdateDeploymentPodTemplateSpec determines whether the Deployment for the given buildID is diff --git a/internal/planner/planner_test.go b/internal/planner/planner_test.go index e57f2dfe..8efae723 100644 --- a/internal/planner/planner_test.go +++ b/internal/planner/planner_test.go @@ -2275,6 +2275,7 @@ func TestCheckAndUpdateDeploymentConnectionSpec(t *testing.T) { expectUpdate bool expectSecretName string expectHostPortEnv string + expectTLSServerName string expectConnectionHash string }{ { @@ -2370,10 +2371,42 @@ func TestCheckAndUpdateDeploymentConnectionSpec(t *testing.T) { }), }, { - name: "empty mutual tls secret updates correctly", + name: "different TLS server name triggers env update", buildID: "v5", existingDeployment: createTestDeploymentWithConnection( "test-worker", "v5", + temporaliov1alpha1.ConnectionSpec{ + HostPort: defaultHostPort(), + TLS: &temporaliov1alpha1.ConnectionTLSConfig{ + ServerName: "old-server.example.com", + }, + MutualTLSSecretRef: &temporaliov1alpha1.SecretReference{Name: defaultMutualTLSSecret()}, + }, + ), + newConnection: temporaliov1alpha1.ConnectionSpec{ + HostPort: defaultHostPort(), + TLS: &temporaliov1alpha1.ConnectionTLSConfig{ + ServerName: "new-server.example.com", + }, + MutualTLSSecretRef: &temporaliov1alpha1.SecretReference{Name: defaultMutualTLSSecret()}, + }, + expectUpdate: true, + expectSecretName: defaultMutualTLSSecret(), + expectHostPortEnv: defaultHostPort(), + expectTLSServerName: "new-server.example.com", + expectConnectionHash: k8s.ComputeConnectionSpecHash(temporaliov1alpha1.ConnectionSpec{ + HostPort: defaultHostPort(), + TLS: &temporaliov1alpha1.ConnectionTLSConfig{ + ServerName: "new-server.example.com", + }, + MutualTLSSecretRef: &temporaliov1alpha1.SecretReference{Name: defaultMutualTLSSecret()}, + }), + }, + { + name: "empty mutual tls secret updates correctly", + buildID: "v6", + existingDeployment: createTestDeploymentWithConnection( + "test-worker", "v6", temporaliov1alpha1.ConnectionSpec{ HostPort: defaultHostPort(), MutualTLSSecretRef: &temporaliov1alpha1.SecretReference{Name: defaultMutualTLSSecret()}, @@ -2442,6 +2475,20 @@ func TestCheckAndUpdateDeploymentConnectionSpec(t *testing.T) { } } assert.True(t, found, "Should find TEMPORAL_ADDRESS environment variable") + + if tt.expectTLSServerName != "" { + found = false + for _, container := range result.Spec.Template.Spec.Containers { + for _, env := range container.Env { + if env.Name == "TEMPORAL_TLS_SERVER_NAME" { + assert.Equal(t, tt.expectTLSServerName, env.Value, "TEMPORAL_TLS_SERVER_NAME should be updated") + found = true + break + } + } + } + assert.True(t, found, "Should find TEMPORAL_TLS_SERVER_NAME environment variable") + } }) } }