xds-k8s: Allow multiple instance of the driver to run concurrently (#26542)

diff --git a/tools/run_tests/xds_k8s_test_driver/README.md b/tools/run_tests/xds_k8s_test_driver/README.md
index 1bf932a..28ddab7 100644
--- a/tools/run_tests/xds_k8s_test_driver/README.md
+++ b/tools/run_tests/xds_k8s_test_driver/README.md
@@ -8,9 +8,9 @@
 
 ### Stabilization roadmap 
 - [ ] Replace retrying with tenacity
-- [ ] Generate namespace for each test to prevent resource name conflicts and
+- [x] Generate namespace for each test to prevent resource name conflicts and
       allow running tests in parallel
-- [ ] Security: run server and client in separate namespaces
+- [x] Security: run server and client in separate namespaces
 - [ ] Make framework.infrastructure.gcp resources [first-class
       citizen](https://en.wikipedia.org/wiki/First-class_citizen), support
       simpler CRUD
@@ -198,23 +198,6 @@
   --client_image="gcr.io/grpc-testing/xds-interop/java-client:d22f93e1ade22a1e026b57210f6fc21f7a3ca0cf"
 ```
 
-### Test namespace
-It's possible to run multiple xDS interop test workloads in the same project.
-But we need to ensure the name of the global resources won't conflict. This can
-be solved by supplying `--namespace` and `--server_xds_port`. The xDS port needs
-to be unique across the entire project (default port range is [8080, 8280],
-avoid if possible). Here is an example:
-
-```shell
-python3 -m tests.baseline_test \
-  --flagfile="config/grpc-testing.cfg" \
-  --kube_context="${KUBE_CONTEXT}" \
-  --server_image="gcr.io/grpc-testing/xds-interop/java-server:d22f93e1ade22a1e026b57210f6fc21f7a3ca0cf" \
-  --client_image="gcr.io/grpc-testing/xds-interop/java-client:d22f93e1ade22a1e026b57210f6fc21f7a3ca0cf" \
-  --namespace="box-$(date +"%F-%R")" \
-  --server_xds_port="$(($RANDOM%1000 + 34567))"
-```
-
 ## Local development
 This test driver allows running tests locally against remote GKE clusters, right
 from your dev environment. You need:
@@ -290,7 +273,7 @@
 EXAMPLES:
 ./run.sh bin/run_td_setup.py --help
 ./run.sh bin/run_td_setup.py --helpfull
-XDS_K8S_CONFIG=./path-to-flagfile.cfg ./run.sh bin/run_td_setup.py --namespace=override-namespace
+XDS_K8S_CONFIG=./path-to-flagfile.cfg ./run.sh bin/run_td_setup.py --resource_suffix=override-suffix
 ./run.sh tests/baseline_test.py
 ./run.sh tests/security_test.py --verbosity=1 --logger_levels=__main__:DEBUG,framework:DEBUG
 ./run.sh tests/security_test.py SecurityTest.test_mtls --nocheck_local_certs
diff --git a/tools/run_tests/xds_k8s_test_driver/bin/cleanup.sh b/tools/run_tests/xds_k8s_test_driver/bin/cleanup.sh
index 4ee29eb..ba53491 100755
--- a/tools/run_tests/xds_k8s_test_driver/bin/cleanup.sh
+++ b/tools/run_tests/xds_k8s_test_driver/bin/cleanup.sh
@@ -32,7 +32,7 @@
 EXAMPLES:
 $0
 $0 --secure
-XDS_K8S_CONFIG=./path-to-flagfile.cfg $0 --namespace=override-namespace
+XDS_K8S_CONFIG=./path-to-flagfile.cfg $0 --resource_suffix=override-suffix
 EOF
   exit 1
 }
diff --git a/tools/run_tests/xds_k8s_test_driver/bin/run_channelz.py b/tools/run_tests/xds_k8s_test_driver/bin/run_channelz.py
index a3ccb3a..5c2c0ac 100755
--- a/tools/run_tests/xds_k8s_test_driver/bin/run_channelz.py
+++ b/tools/run_tests/xds_k8s_test_driver/bin/run_channelz.py
@@ -58,6 +58,8 @@
                               help='Show info for a security setup')
 flags.adopt_module_key_flags(xds_flags)
 flags.adopt_module_key_flags(xds_k8s_flags)
+# Running outside of a test suite, so require explicit resource_suffix.
+flags.mark_flag_as_required("resource_suffix")
 
 # Type aliases
 _Channel = grpc_channelz.Channel
@@ -174,9 +176,13 @@
 
     k8s_api_manager = k8s.KubernetesApiManager(xds_k8s_flags.KUBE_CONTEXT.value)
 
+    # Resource names.
+    resource_prefix: str = xds_flags.RESOURCE_PREFIX.value
+    resource_suffix: str = xds_flags.RESOURCE_SUFFIX.value
+
     # Server
     server_name = xds_flags.SERVER_NAME.value
-    server_namespace = xds_flags.NAMESPACE.value
+    server_namespace = resource_prefix
     server_k8s_ns = k8s.KubernetesNamespace(k8s_api_manager, server_namespace)
     server_pod_ip = get_deployment_pod_ips(server_k8s_ns, server_name)[0]
     test_server: _XdsTestServer = _XdsTestServer(
@@ -188,7 +194,7 @@
 
     # Client
     client_name = xds_flags.CLIENT_NAME.value
-    client_namespace = xds_flags.NAMESPACE.value
+    client_namespace = resource_prefix
     client_k8s_ns = k8s.KubernetesNamespace(k8s_api_manager, client_namespace)
     client_pod_ip = get_deployment_pod_ips(client_k8s_ns, client_name)[0]
     test_client: _XdsTestClient = _XdsTestClient(
diff --git a/tools/run_tests/xds_k8s_test_driver/bin/run_td_setup.py b/tools/run_tests/xds_k8s_test_driver/bin/run_td_setup.py
index d866986..efcb591 100755
--- a/tools/run_tests/xds_k8s_test_driver/bin/run_td_setup.py
+++ b/tools/run_tests/xds_k8s_test_driver/bin/run_td_setup.py
@@ -31,13 +31,13 @@
     python -m bin.run_td_setup --helpfull
 """
 import logging
-import uuid
 
 from absl import app
 from absl import flags
 
 from framework import xds_flags
 from framework import xds_k8s_flags
+from framework.helpers import rand
 from framework.infrastructure import gcp
 from framework.infrastructure import k8s
 from framework.infrastructure import traffic_director
@@ -49,7 +49,7 @@
                          default='create',
                          enum_values=[
                              'cycle', 'create', 'cleanup', 'backends-add',
-                             'backends-cleanup'
+                             'backends-cleanup', 'unused-xds-port'
                          ],
                          help='Command')
 _SECURITY = flags.DEFINE_enum('security',
@@ -61,9 +61,10 @@
                               help='Configure TD with security')
 flags.adopt_module_key_flags(xds_flags)
 flags.adopt_module_key_flags(xds_k8s_flags)
+# Running outside of a test suite, so require explicit resource_suffix.
+flags.mark_flag_as_required("resource_suffix")
 
-_DEFAULT_SECURE_MODE_MAINTENANCE_PORT = \
-    server_app.KubernetesServerRunner.DEFAULT_SECURE_MODE_MAINTENANCE_PORT
+KubernetesServerRunner = server_app.KubernetesServerRunner
 
 
 def main(argv):
@@ -75,7 +76,10 @@
 
     project: str = xds_flags.PROJECT.value
     network: str = xds_flags.NETWORK.value
-    namespace = xds_flags.NAMESPACE.value
+
+    # Resource names.
+    resource_prefix: str = xds_flags.RESOURCE_PREFIX.value
+    resource_suffix: str = xds_flags.RESOURCE_SUFFIX.value
 
     # Test server
     server_name = xds_flags.SERVER_NAME.value
@@ -83,22 +87,27 @@
     server_maintenance_port = xds_flags.SERVER_MAINTENANCE_PORT.value
     server_xds_host = xds_flags.SERVER_XDS_HOST.value
     server_xds_port = xds_flags.SERVER_XDS_PORT.value
+    server_namespace = KubernetesServerRunner.make_namespace_name(
+        resource_prefix, resource_suffix)
 
     gcp_api_manager = gcp.api.GcpApiManager()
 
     if security_mode is None:
-        td = traffic_director.TrafficDirectorManager(gcp_api_manager,
-                                                     project=project,
-                                                     resource_prefix=namespace,
-                                                     network=network)
+        td = traffic_director.TrafficDirectorManager(
+            gcp_api_manager,
+            project=project,
+            network=network,
+            resource_prefix=resource_prefix,
+            resource_suffix=resource_suffix)
     else:
         td = traffic_director.TrafficDirectorSecureManager(
             gcp_api_manager,
             project=project,
-            resource_prefix=namespace,
-            network=network)
+            network=network,
+            resource_prefix=resource_prefix,
+            resource_suffix=resource_suffix)
         if server_maintenance_port is None:
-            server_maintenance_port = _DEFAULT_SECURE_MODE_MAINTENANCE_PORT
+            server_maintenance_port = KubernetesServerRunner.DEFAULT_SECURE_MODE_MAINTENANCE_PORT
 
     try:
         if command in ('create', 'cycle'):
@@ -114,12 +123,12 @@
                 td.setup_for_grpc(server_xds_host,
                                   server_xds_port,
                                   health_check_port=server_maintenance_port)
-                td.setup_server_security(server_namespace=namespace,
+                td.setup_server_security(server_namespace=server_namespace,
                                          server_name=server_name,
                                          server_port=server_port,
                                          tls=True,
                                          mtls=True)
-                td.setup_client_security(server_namespace=namespace,
+                td.setup_client_security(server_namespace=server_namespace,
                                          server_name=server_name,
                                          tls=True,
                                          mtls=True)
@@ -129,12 +138,12 @@
                 td.setup_for_grpc(server_xds_host,
                                   server_xds_port,
                                   health_check_port=server_maintenance_port)
-                td.setup_server_security(server_namespace=namespace,
+                td.setup_server_security(server_namespace=server_namespace,
                                          server_name=server_name,
                                          server_port=server_port,
                                          tls=True,
                                          mtls=False)
-                td.setup_client_security(server_namespace=namespace,
+                td.setup_client_security(server_namespace=server_namespace,
                                          server_name=server_name,
                                          tls=True,
                                          mtls=False)
@@ -144,12 +153,12 @@
                 td.setup_for_grpc(server_xds_host,
                                   server_xds_port,
                                   health_check_port=server_maintenance_port)
-                td.setup_server_security(server_namespace=namespace,
+                td.setup_server_security(server_namespace=server_namespace,
                                          server_name=server_name,
                                          server_port=server_port,
                                          tls=False,
                                          mtls=False)
-                td.setup_client_security(server_namespace=namespace,
+                td.setup_client_security(server_namespace=server_namespace,
                                          server_name=server_name,
                                          tls=False,
                                          mtls=False)
@@ -161,12 +170,12 @@
                 td.setup_for_grpc(server_xds_host,
                                   server_xds_port,
                                   health_check_port=server_maintenance_port)
-                td.setup_server_security(server_namespace=namespace,
+                td.setup_server_security(server_namespace=server_namespace,
                                          server_name=server_name,
                                          server_port=server_port,
                                          tls=True,
                                          mtls=True)
-                td.setup_client_security(server_namespace=namespace,
+                td.setup_client_security(server_namespace=server_namespace,
                                          server_name=server_name,
                                          tls=True,
                                          mtls=False)
@@ -180,16 +189,16 @@
                                   health_check_port=server_maintenance_port)
                 # Regular TLS setup, but with client policy configured using
                 # intentionality incorrect server_namespace.
-                td.setup_server_security(server_namespace=namespace,
+                td.setup_server_security(server_namespace=server_namespace,
                                          server_name=server_name,
                                          server_port=server_port,
                                          tls=True,
                                          mtls=False)
-                incorrect_namespace = f'incorrect-namespace-{uuid.uuid4().hex}'
-                td.setup_client_security(server_namespace=incorrect_namespace,
-                                         server_name=server_name,
-                                         tls=True,
-                                         mtls=False)
+                td.setup_client_security(
+                    server_namespace=f'incorrect-namespace-{rand.rand_string()}',
+                    server_name=server_name,
+                    tls=True,
+                    mtls=False)
 
             logger.info('Works!')
     except Exception:  # noqa pylint: disable=broad-except
@@ -203,7 +212,8 @@
         logger.info('Adding backends')
         k8s_api_manager = k8s.KubernetesApiManager(
             xds_k8s_flags.KUBE_CONTEXT.value)
-        k8s_namespace = k8s.KubernetesNamespace(k8s_api_manager, namespace)
+        k8s_namespace = k8s.KubernetesNamespace(k8s_api_manager,
+                                                server_namespace)
 
         neg_name, neg_zones = k8s_namespace.get_service_neg(
             server_name, server_port)
@@ -211,10 +221,16 @@
         td.load_backend_service()
         td.backend_service_add_neg_backends(neg_name, neg_zones)
         td.wait_for_backends_healthy_status()
-        # TODO(sergiitk): wait until client reports rpc health
     elif command == 'backends-cleanup':
         td.load_backend_service()
         td.backend_service_remove_all_backends()
+    elif command == 'unused-xds-port':
+        try:
+            unused_xds_port = td.find_unused_forwarding_rule_port()
+            logger.info('Found unused forwarding rule port: %s',
+                        unused_xds_port)
+        except Exception:  # noqa pylint: disable=broad-except
+            logger.exception("Couldn't find unused forwarding rule port")
 
 
 if __name__ == '__main__':
diff --git a/tools/run_tests/xds_k8s_test_driver/bin/run_test_client.py b/tools/run_tests/xds_k8s_test_driver/bin/run_test_client.py
index f351127..1e60093 100755
--- a/tools/run_tests/xds_k8s_test_driver/bin/run_test_client.py
+++ b/tools/run_tests/xds_k8s_test_driver/bin/run_test_client.py
@@ -44,20 +44,22 @@
     help="Delete namespace during resource cleanup")
 flags.adopt_module_key_flags(xds_flags)
 flags.adopt_module_key_flags(xds_k8s_flags)
+# Running outside of a test suite, so require explicit resource_suffix.
+flags.mark_flag_as_required("resource_suffix")
+
+# Type aliases
+KubernetesClientRunner = client_app.KubernetesClientRunner
 
 
 def main(argv):
     if len(argv) > 1:
         raise app.UsageError('Too many command-line arguments.')
 
-    # Flag shortcuts.
     project: str = xds_flags.PROJECT.value
     # GCP Service Account email
     gcp_service_account: str = xds_k8s_flags.GCP_SERVICE_ACCOUNT.value
-    # Base namespace
-    namespace = xds_flags.NAMESPACE.value
-    client_namespace = namespace
 
+    # KubernetesClientRunner arguments.
     runner_kwargs = dict(
         deployment_name=xds_flags.CLIENT_NAME.value,
         image_name=xds_k8s_flags.CLIENT_IMAGE.value,
@@ -75,7 +77,9 @@
             deployment_template='client-secure.deployment.yaml')
 
     k8s_api_manager = k8s.KubernetesApiManager(xds_k8s_flags.KUBE_CONTEXT.value)
-    client_runner = client_app.KubernetesClientRunner(
+    client_namespace = KubernetesClientRunner.make_namespace_name(
+        xds_flags.RESOURCE_PREFIX.value, xds_flags.RESOURCE_SUFFIX.value)
+    client_runner = KubernetesClientRunner(
         k8s.KubernetesNamespace(k8s_api_manager, client_namespace),
         **runner_kwargs)
 
diff --git a/tools/run_tests/xds_k8s_test_driver/bin/run_test_server.py b/tools/run_tests/xds_k8s_test_driver/bin/run_test_server.py
index 8085b2d..af44d46 100755
--- a/tools/run_tests/xds_k8s_test_driver/bin/run_test_server.py
+++ b/tools/run_tests/xds_k8s_test_driver/bin/run_test_server.py
@@ -40,20 +40,25 @@
     help="Delete namespace during resource cleanup")
 flags.adopt_module_key_flags(xds_flags)
 flags.adopt_module_key_flags(xds_k8s_flags)
+# Running outside of a test suite, so require explicit resource_suffix.
+flags.mark_flag_as_required("resource_suffix")
+
+KubernetesServerRunner = server_app.KubernetesServerRunner
 
 
 def main(argv):
     if len(argv) > 1:
         raise app.UsageError('Too many command-line arguments.')
 
-    # Flag shortcuts.
     project: str = xds_flags.PROJECT.value
     # GCP Service Account email
     gcp_service_account: str = xds_k8s_flags.GCP_SERVICE_ACCOUNT.value
-    # Base namespace
-    namespace = xds_flags.NAMESPACE.value
-    server_namespace = namespace
 
+    # Resource names.
+    resource_prefix: str = xds_flags.RESOURCE_PREFIX.value
+    resource_suffix: str = xds_flags.RESOURCE_SUFFIX.value
+
+    # KubernetesServerRunner arguments.
     runner_kwargs = dict(
         deployment_name=xds_flags.SERVER_NAME.value,
         image_name=xds_k8s_flags.SERVER_IMAGE.value,
@@ -70,7 +75,9 @@
             deployment_template='server-secure.deployment.yaml')
 
     k8s_api_manager = k8s.KubernetesApiManager(xds_k8s_flags.KUBE_CONTEXT.value)
-    server_runner = server_app.KubernetesServerRunner(
+    server_namespace = KubernetesServerRunner.make_namespace_name(
+        resource_prefix, resource_suffix)
+    server_runner = KubernetesServerRunner(
         k8s.KubernetesNamespace(k8s_api_manager, server_namespace),
         **runner_kwargs)
 
diff --git a/tools/run_tests/xds_k8s_test_driver/config/common.cfg b/tools/run_tests/xds_k8s_test_driver/config/common.cfg
index e4c8090..e7a2380 100644
--- a/tools/run_tests/xds_k8s_test_driver/config/common.cfg
+++ b/tools/run_tests/xds_k8s_test_driver/config/common.cfg
@@ -1,4 +1,4 @@
---namespace=interop-psm-security
+--resource_prefix=xds-k8s-security
 --td_bootstrap_image=gcr.io/grpc-testing/td-grpc-bootstrap:2558ec79df06984ed0d37e9e69f34688ffe301bb
 --logger_levels=__main__:DEBUG,framework:INFO
 --verbosity=0
diff --git a/tools/run_tests/xds_k8s_test_driver/config/grpc-testing.cfg b/tools/run_tests/xds_k8s_test_driver/config/grpc-testing.cfg
index ddd70a4..93c8a6d 100644
--- a/tools/run_tests/xds_k8s_test_driver/config/grpc-testing.cfg
+++ b/tools/run_tests/xds_k8s_test_driver/config/grpc-testing.cfg
@@ -3,3 +3,5 @@
 --network=default-vpc
 --gcp_service_account=xds-k8s-interop-tests@grpc-testing.iam.gserviceaccount.com
 --private_api_key_secret_name=projects/830293263384/secrets/xds-interop-tests-private-api-access-key
+# Randomize xds port.
+--server_xds_port=0
diff --git a/tools/run_tests/xds_k8s_test_driver/config/local-dev.cfg.example b/tools/run_tests/xds_k8s_test_driver/config/local-dev.cfg.example
index 9a7f908..c42d81e 100644
--- a/tools/run_tests/xds_k8s_test_driver/config/local-dev.cfg.example
+++ b/tools/run_tests/xds_k8s_test_driver/config/local-dev.cfg.example
@@ -11,6 +11,9 @@
 # Uncomment to ensure the allow health check firewall exists before test case runs
 # --ensure_firewall
 
+# Use predictable resource suffix to simplify debugging
+--resource_suffix=dev
+
 # The name of kube context to use. See `gcloud container clusters get-credentials` and `kubectl config`
 --kube_context=context_name
 
diff --git a/tools/run_tests/xds_k8s_test_driver/framework/helpers/datetime.py b/tools/run_tests/xds_k8s_test_driver/framework/helpers/datetime.py
new file mode 100644
index 0000000..ef88e3d
--- /dev/null
+++ b/tools/run_tests/xds_k8s_test_driver/framework/helpers/datetime.py
@@ -0,0 +1,39 @@
+# Copyright 2021 gRPC authors.
+#
+# 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.
+"""This contains common helpers for working with dates and time."""
+import datetime
+import re
+from typing import Pattern
+
+RE_ZERO_OFFSET: Pattern[str] = re.compile(r'[+\-]00:?00$')
+
+
+def utc_now() -> datetime.datetime:
+    """Construct a datetime from current time in UTC timezone."""
+    return datetime.datetime.now(datetime.timezone.utc)
+
+
+def datetime_suffix(*, seconds: bool = False) -> str:
+    """Return current UTC date, and time in a format useful for resource naming.
+
+    Examples:
+        - 20210626-1859   (seconds=False)
+        - 20210626-185942 (seconds=True)
+    Use in resources names incompatible with ISO 8601, e.g. some GCP resources
+    that only allow lowercase alphanumeric chars and dashes.
+
+    Hours and minutes are joined together for better readability, so time is
+    visually distinct from dash-separated date.
+    """
+    return utc_now().strftime('%Y%m%d-%H%M' + ('%S' if seconds else ''))
diff --git a/tools/run_tests/xds_k8s_test_driver/framework/helpers/rand.py b/tools/run_tests/xds_k8s_test_driver/framework/helpers/rand.py
new file mode 100644
index 0000000..3a593e9
--- /dev/null
+++ b/tools/run_tests/xds_k8s_test_driver/framework/helpers/rand.py
@@ -0,0 +1,33 @@
+# Copyright 2021 gRPC authors.
+#
+# 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.
+"""This contains common helpers for generating randomized data."""
+import random
+import string
+
+# Alphanumeric characters, similar to regex [:alnum:] class, [a-zA-Z0-9]
+ALPHANUM = string.ascii_letters + string.digits
+# Lowercase alphanumeric characters: [a-z0-9]
+# Use ALPHANUM_LOWERCASE alphabet when case-sensitivity is a concern.
+ALPHANUM_LOWERCASE = string.ascii_lowercase + string.digits
+
+
+def rand_string(length: int = 8, *, lowercase: bool = False) -> str:
+    """Return random alphanumeric string of given length.
+
+    Space for default arguments: alphabet^length
+       lowercase and uppercase = (26*2 + 10)^8 = 2.18e14 = 218 trillion.
+       lowercase only = (26 + 10)^8 = 2.8e12 = 2.8 trillion.
+    """
+    alphabet = ALPHANUM_LOWERCASE if lowercase else ALPHANUM
+    return ''.join(random.choices(population=alphabet, k=length))
diff --git a/tools/run_tests/xds_k8s_test_driver/framework/infrastructure/gcp/compute.py b/tools/run_tests/xds_k8s_test_driver/framework/infrastructure/gcp/compute.py
index ac6eef6..72db8f7 100644
--- a/tools/run_tests/xds_k8s_test_driver/framework/infrastructure/gcp/compute.py
+++ b/tools/run_tests/xds_k8s_test_driver/framework/infrastructure/gcp/compute.py
@@ -235,6 +235,17 @@
                 'target': target_proxy.url,
             })
 
+    def exists_forwarding_rule(self, src_port) -> bool:
+        # TODO(sergiitk): Better approach for confirming the port is available.
+        #   It's possible a rule allocates actual port range, e.g 8000-9000,
+        #   and this wouldn't catch it. For now, we assume there's no
+        #   port ranges used in the project.
+        filter_str = (f'(portRange eq "{src_port}-{src_port}") '
+                      f'(IPAddress eq "0.0.0.0")'
+                      f'(loadBalancingScheme eq "INTERNAL_SELF_MANAGED")')
+        return self._exists_resource(self.api.globalForwardingRules(),
+                                     filter=filter_str)
+
     def delete_forwarding_rule(self, name):
         self._delete_resource(self.api.globalForwardingRules(),
                               'forwardingRule', name)
@@ -329,6 +340,16 @@
                     self.resource_pretty_format(resp))
         return self.GcpResource(resp['name'], resp['selfLink'])
 
+    def _exists_resource(self, collection: discovery.Resource,
+                         filter: str) -> bool:
+        resp = collection.list(
+            project=self.project, filter=filter,
+            maxResults=1).execute(num_retries=self._GCP_API_RETRIES)
+        if 'kind' not in resp:
+            # TODO(sergiitk): better error
+            raise ValueError('List response "kind" is missing')
+        return 'items' in resp and resp['items']
+
     def _insert_resource(self, collection: discovery.Resource,
                          body: Dict[str, Any]) -> GcpResource:
         logger.info('Creating compute resource:\n%s',
diff --git a/tools/run_tests/xds_k8s_test_driver/framework/infrastructure/traffic_director.py b/tools/run_tests/xds_k8s_test_driver/framework/infrastructure/traffic_director.py
index 871f3c0..ec4f1fc 100644
--- a/tools/run_tests/xds_k8s_test_driver/framework/infrastructure/traffic_director.py
+++ b/tools/run_tests/xds_k8s_test_driver/framework/infrastructure/traffic_director.py
@@ -11,7 +11,9 @@
 # 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.
+import functools
 import logging
+import random
 from typing import Any, List, Optional, Set
 
 from framework import xds_flags
@@ -41,8 +43,11 @@
 
 class TrafficDirectorManager:
     compute: _ComputeV1
+    resource_prefix: str
+    resource_suffix: str
+
     BACKEND_SERVICE_NAME = "backend-service"
-    ALTERNATIVE_BACKEND_SERVICE_NAME = "alternative-backend-service"
+    ALTERNATIVE_BACKEND_SERVICE_NAME = "backend-service-alt"
     HEALTH_CHECK_NAME = "health-check"
     URL_MAP_NAME = "url-map"
     URL_MAP_PATH_MATCHER_NAME = "path-matcher"
@@ -56,6 +61,7 @@
         project: str,
         *,
         resource_prefix: str,
+        resource_suffix: str,
         network: str = 'default',
     ):
         # API
@@ -65,6 +71,7 @@
         self.project: str = project
         self.network: str = network
         self.resource_prefix: str = resource_prefix
+        self.resource_suffix: str = resource_suffix
 
         # Managed resources
         self.health_check: Optional[GcpResource] = None
@@ -124,8 +131,14 @@
         self.delete_alternative_backend_service(force=force)
         self.delete_health_check(force=force)
 
-    def _ns_name(self, name):
-        return f'{self.resource_prefix}-{name}'
+    @functools.lru_cache(None)
+    def _make_resource_name(self, name: str) -> str:
+        """Make dash-separated resource name with resource prefix and suffix."""
+        parts = [self.resource_prefix, name]
+        # Avoid trailing dash when the suffix is empty.
+        if self.resource_suffix:
+            parts.append(self.resource_suffix)
+        return '-'.join(parts)
 
     def create_health_check(
             self,
@@ -138,14 +151,14 @@
         if protocol is None:
             protocol = _HealthCheckGRPC
 
-        name = self._ns_name(self.HEALTH_CHECK_NAME)
+        name = self._make_resource_name(self.HEALTH_CHECK_NAME)
         logger.info('Creating %s Health Check "%s"', protocol.name, name)
         resource = self.compute.create_health_check(name, protocol, port=port)
         self.health_check = resource
 
     def delete_health_check(self, force=False):
         if force:
-            name = self._ns_name(self.HEALTH_CHECK_NAME)
+            name = self._make_resource_name(self.HEALTH_CHECK_NAME)
         elif self.health_check:
             name = self.health_check.name
         else:
@@ -159,7 +172,7 @@
         if protocol is None:
             protocol = _BackendGRPC
 
-        name = self._ns_name(self.BACKEND_SERVICE_NAME)
+        name = self._make_resource_name(self.BACKEND_SERVICE_NAME)
         logger.info('Creating %s Backend Service "%s"', protocol.name, name)
         resource = self.compute.create_backend_service_traffic_director(
             name, health_check=self.health_check, protocol=protocol)
@@ -167,13 +180,13 @@
         self.backend_service_protocol = protocol
 
     def load_backend_service(self):
-        name = self._ns_name(self.BACKEND_SERVICE_NAME)
+        name = self._make_resource_name(self.BACKEND_SERVICE_NAME)
         resource = self.compute.get_backend_service_traffic_director(name)
         self.backend_service = resource
 
     def delete_backend_service(self, force=False):
         if force:
-            name = self._ns_name(self.BACKEND_SERVICE_NAME)
+            name = self._make_resource_name(self.BACKEND_SERVICE_NAME)
         elif self.backend_service:
             name = self.backend_service.name
         else:
@@ -213,7 +226,7 @@
             self, protocol: Optional[BackendServiceProtocol] = _BackendGRPC):
         if protocol is None:
             protocol = _BackendGRPC
-        name = self._ns_name(self.ALTERNATIVE_BACKEND_SERVICE_NAME)
+        name = self._make_resource_name(self.ALTERNATIVE_BACKEND_SERVICE_NAME)
         logger.info('Creating %s Alternative Backend Service "%s"',
                     protocol.name, name)
         resource = self.compute.create_backend_service_traffic_director(
@@ -222,13 +235,14 @@
         self.alternative_backend_service_protocol = protocol
 
     def load_alternative_backend_service(self):
-        name = self._ns_name(self.ALTERNATIVE_BACKEND_SERVICE_NAME)
+        name = self._make_resource_name(self.ALTERNATIVE_BACKEND_SERVICE_NAME)
         resource = self.compute.get_backend_service_traffic_director(name)
         self.alternative_backend_service = resource
 
     def delete_alternative_backend_service(self, force=False):
         if force:
-            name = self._ns_name(self.ALTERNATIVE_BACKEND_SERVICE_NAME)
+            name = self._make_resource_name(
+                self.ALTERNATIVE_BACKEND_SERVICE_NAME)
         elif self.alternative_backend_service:
             name = self.alternative_backend_service.name
         else:
@@ -272,8 +286,8 @@
         src_port: int,
     ) -> GcpResource:
         src_address = f'{src_host}:{src_port}'
-        name = self._ns_name(self.URL_MAP_NAME)
-        matcher_name = self._ns_name(self.URL_MAP_PATH_MATCHER_NAME)
+        name = self._make_resource_name(self.URL_MAP_NAME)
+        matcher_name = self._make_resource_name(self.URL_MAP_PATH_MATCHER_NAME)
         logger.info('Creating URL map "%s": %s -> %s', name, src_address,
                     self.backend_service.name)
         resource = self.compute.create_url_map(name, matcher_name,
@@ -290,7 +304,7 @@
 
     def delete_url_map(self, force=False):
         if force:
-            name = self._ns_name(self.URL_MAP_NAME)
+            name = self._make_resource_name(self.URL_MAP_NAME)
         elif self.url_map:
             name = self.url_map.name
         else:
@@ -300,7 +314,7 @@
         self.url_map = None
 
     def create_target_proxy(self):
-        name = self._ns_name(self.TARGET_PROXY_NAME)
+        name = self._make_resource_name(self.TARGET_PROXY_NAME)
         if self.backend_service_protocol is BackendServiceProtocol.GRPC:
             target_proxy_type = 'GRPC'
             create_proxy_fn = self.compute.create_target_grpc_proxy
@@ -318,7 +332,7 @@
 
     def delete_target_grpc_proxy(self, force=False):
         if force:
-            name = self._ns_name(self.TARGET_PROXY_NAME)
+            name = self._make_resource_name(self.TARGET_PROXY_NAME)
         elif self.target_proxy:
             name = self.target_proxy.name
         else:
@@ -330,7 +344,7 @@
 
     def delete_target_http_proxy(self, force=False):
         if force:
-            name = self._ns_name(self.TARGET_PROXY_NAME)
+            name = self._make_resource_name(self.TARGET_PROXY_NAME)
         elif self.target_proxy:
             name = self.target_proxy.name
         else:
@@ -340,8 +354,21 @@
         self.target_proxy = None
         self.target_proxy_is_http = False
 
+    def find_unused_forwarding_rule_port(
+            self,
+            *,
+            lo: int = 1024,  # To avoid confusion, skip well-known ports.
+            hi: int = 65535,
+            attempts: int = 25) -> int:
+        for attempts in range(attempts):
+            src_port = random.randint(lo, hi)
+            if not (self.compute.exists_forwarding_rule(src_port)):
+                return src_port
+        # TODO(sergiitk): custom exception
+        raise RuntimeError("Couldn't find unused forwarding rule port")
+
     def create_forwarding_rule(self, src_port: int):
-        name = self._ns_name(self.FORWARDING_RULE_NAME)
+        name = self._make_resource_name(self.FORWARDING_RULE_NAME)
         src_port = int(src_port)
         logging.info(
             'Creating forwarding rule "%s" in network "%s": 0.0.0.0:%s -> %s',
@@ -354,7 +381,7 @@
 
     def delete_forwarding_rule(self, force=False):
         if force:
-            name = self._ns_name(self.FORWARDING_RULE_NAME)
+            name = self._make_resource_name(self.FORWARDING_RULE_NAME)
         elif self.forwarding_rule:
             name = self.forwarding_rule.name
         else:
@@ -364,7 +391,7 @@
         self.forwarding_rule = None
 
     def create_firewall_rule(self, allowed_ports: List[str]):
-        name = self._ns_name(self.FIREWALL_RULE_NAME)
+        name = self._make_resource_name(self.FIREWALL_RULE_NAME)
         logging.info(
             'Creating firewall rule "%s" in network "%s" with allowed ports %s',
             name, self.network, allowed_ports)
@@ -376,7 +403,7 @@
     def delete_firewall_rule(self, force=False):
         """The firewall rule won't be automatically removed."""
         if force:
-            name = self._ns_name(self.FIREWALL_RULE_NAME)
+            name = self._make_resource_name(self.FIREWALL_RULE_NAME)
         elif self.firewall_rule:
             name = self.firewall_rule.name
         else:
@@ -390,7 +417,8 @@
     netsec: Optional[_NetworkSecurityV1Alpha1]
     SERVER_TLS_POLICY_NAME = "server-tls-policy"
     CLIENT_TLS_POLICY_NAME = "client-tls-policy"
-    ENDPOINT_CONFIG_SELECTOR_NAME = "endpoint-config-selector"
+    # TODO(sergiitk): Rename to ENDPOINT_POLICY_NAME when upgraded to v1beta
+    ENDPOINT_CONFIG_SELECTOR_NAME = "endpoint-policy"
     CERTIFICATE_PROVIDER_INSTANCE = "google_cloud_private_spiffe"
 
     def __init__(
@@ -399,11 +427,13 @@
         project: str,
         *,
         resource_prefix: str,
+        resource_suffix: Optional[str] = None,
         network: str = 'default',
     ):
         super().__init__(gcp_api_manager,
                          project,
                          resource_prefix=resource_prefix,
+                         resource_suffix=resource_suffix,
                          network=network)
 
         # API
@@ -445,7 +475,7 @@
         self.delete_client_tls_policy(force=force)
 
     def create_server_tls_policy(self, *, tls, mtls):
-        name = self._ns_name(self.SERVER_TLS_POLICY_NAME)
+        name = self._make_resource_name(self.SERVER_TLS_POLICY_NAME)
         logger.info('Creating Server TLS Policy %s', name)
         if not tls and not mtls:
             logger.warning(
@@ -468,7 +498,7 @@
 
     def delete_server_tls_policy(self, force=False):
         if force:
-            name = self._ns_name(self.SERVER_TLS_POLICY_NAME)
+            name = self._make_resource_name(self.SERVER_TLS_POLICY_NAME)
         elif self.server_tls_policy:
             name = self.server_tls_policy.name
         else:
@@ -479,7 +509,7 @@
 
     def create_endpoint_config_selector(self, server_namespace, server_name,
                                         server_port):
-        name = self._ns_name(self.ENDPOINT_CONFIG_SELECTOR_NAME)
+        name = self._make_resource_name(self.ENDPOINT_CONFIG_SELECTOR_NAME)
         logger.info('Creating Endpoint Config Selector %s', name)
         endpoint_matcher_labels = [{
             "labelName": "app",
@@ -511,7 +541,7 @@
 
     def delete_endpoint_config_selector(self, force=False):
         if force:
-            name = self._ns_name(self.ENDPOINT_CONFIG_SELECTOR_NAME)
+            name = self._make_resource_name(self.ENDPOINT_CONFIG_SELECTOR_NAME)
         elif self.ecs:
             name = self.ecs.name
         else:
@@ -521,7 +551,7 @@
         self.ecs = None
 
     def create_client_tls_policy(self, *, tls, mtls):
-        name = self._ns_name(self.CLIENT_TLS_POLICY_NAME)
+        name = self._make_resource_name(self.CLIENT_TLS_POLICY_NAME)
         logger.info('Creating Client TLS Policy %s', name)
         if not tls and not mtls:
             logger.warning(
@@ -542,7 +572,7 @@
 
     def delete_client_tls_policy(self, force=False):
         if force:
-            name = self._ns_name(self.CLIENT_TLS_POLICY_NAME)
+            name = self._make_resource_name(self.CLIENT_TLS_POLICY_NAME)
         elif self.client_tls_policy:
             name = self.client_tls_policy.name
         else:
diff --git a/tools/run_tests/xds_k8s_test_driver/framework/test_app/base_runner.py b/tools/run_tests/xds_k8s_test_driver/framework/test_app/base_runner.py
index 504dc18..6d585e1 100644
--- a/tools/run_tests/xds_k8s_test_driver/framework/test_app/base_runner.py
+++ b/tools/run_tests/xds_k8s_test_driver/framework/test_app/base_runner.py
@@ -286,3 +286,14 @@
             name, service_port)
         logger.info("Service %s: detected NEG=%s in zones=%s", name, neg_name,
                     neg_zones)
+
+    @classmethod
+    def _make_namespace_name(cls, resource_prefix: str, resource_suffix: str,
+                             name: str) -> str:
+        """A helper to make consistent test app kubernetes namespace name
+        for given resource prefix and suffix."""
+        parts = [resource_prefix, name]
+        # Avoid trailing dash when the suffix is empty.
+        if resource_suffix:
+            parts.append(resource_suffix)
+        return '-'.join(parts)
diff --git a/tools/run_tests/xds_k8s_test_driver/framework/test_app/client_app.py b/tools/run_tests/xds_k8s_test_driver/framework/test_app/client_app.py
index b0b5602..47dd6c8 100644
--- a/tools/run_tests/xds_k8s_test_driver/framework/test_app/client_app.py
+++ b/tools/run_tests/xds_k8s_test_driver/framework/test_app/client_app.py
@@ -338,3 +338,17 @@
             self._delete_service_account(self.service_account_name)
             self.service_account = None
         super().cleanup(force=force_namespace and force)
+
+    @classmethod
+    def make_namespace_name(cls,
+                            resource_prefix: str,
+                            resource_suffix: str,
+                            name: str = 'client') -> str:
+        """A helper to make consistent XdsTestClient kubernetes namespace name
+        for given resource prefix and suffix.
+
+        Note: the idea is to intentionally produce different namespace name for
+        the test server, and the test client, as that closely mimics real-world
+        deployments.
+        """
+        return cls._make_namespace_name(resource_prefix, resource_suffix, name)
diff --git a/tools/run_tests/xds_k8s_test_driver/framework/test_app/server_app.py b/tools/run_tests/xds_k8s_test_driver/framework/test_app/server_app.py
index aba2bb1..8869796 100644
--- a/tools/run_tests/xds_k8s_test_driver/framework/test_app/server_app.py
+++ b/tools/run_tests/xds_k8s_test_driver/framework/test_app/server_app.py
@@ -307,3 +307,18 @@
             self._delete_service_account(self.service_account_name)
             self.service_account = None
         super().cleanup(force=(force_namespace and force))
+
+    @classmethod
+    def make_namespace_name(cls,
+                            resource_prefix: str,
+                            resource_suffix: str,
+                            name: str = 'server') -> str:
+        """A helper to make consistent XdsTestServer kubernetes namespace name
+        for given resource prefix and suffix.
+
+        Note: the idea is to intentionally produce different namespace name for
+        the test server, and the test client, as that closely mimics real-world
+        deployments.
+        :rtype: object
+        """
+        return cls._make_namespace_name(resource_prefix, resource_suffix, name)
diff --git a/tools/run_tests/xds_k8s_test_driver/framework/xds_flags.py b/tools/run_tests/xds_k8s_test_driver/framework/xds_flags.py
index 6b1ecdc..6d13a79 100644
--- a/tools/run_tests/xds_k8s_test_driver/framework/xds_flags.py
+++ b/tools/run_tests/xds_k8s_test_driver/framework/xds_flags.py
@@ -17,11 +17,27 @@
 # GCP
 PROJECT = flags.DEFINE_string("project",
                               default=None,
-                              help="GCP Project ID. Required")
+                              help="(required) GCP Project ID.")
+RESOURCE_PREFIX = flags.DEFINE_string(
+    "resource_prefix",
+    default=None,
+    help=("(required) The prefix used to name GCP resources.\n"
+          "Together with `resource_suffix` used to create unique "
+          "resource names."))
+# TODO(sergiitk): remove after all migration to --resource_prefix completed.
+#   Known migration work: url map, staging flagfiles.
 NAMESPACE = flags.DEFINE_string(
     "namespace",
     default=None,
-    help="Isolate GCP resources using given namespace / name prefix. Required")
+    help="Deprecated. Use --resource_prefix instead.")
+RESOURCE_SUFFIX = flags.DEFINE_string(
+    "resource_suffix",
+    default=None,
+    help=("The suffix used to name GCP resources.\n"
+          "Together with `resource_prefix` used to create unique "
+          "resource names.\n"
+          "(default: test suite will generate a random suffix, based on suite "
+          "resource management preferences)"))
 NETWORK = flags.DEFINE_string("network",
                               default="default",
                               help="GCP Network ID")
@@ -29,7 +45,7 @@
 XDS_SERVER_URI = flags.DEFINE_string(
     "xds_server_uri",
     default=None,
-    help="Override Traffic Director server uri, for testing")
+    help="Override Traffic Director server URI.")
 ENSURE_FIREWALL = flags.DEFINE_bool(
     "ensure_firewall",
     default=False,
@@ -44,37 +60,62 @@
     help="Update the allowed ports of the firewall rule.")
 
 # Test server
-SERVER_NAME = flags.DEFINE_string("server_name",
-                                  default="psm-grpc-server",
-                                  help="Server deployment and service name")
-SERVER_PORT = flags.DEFINE_integer("server_port",
-                                   default=8080,
-                                   lower_bound=0,
-                                   upper_bound=65535,
-                                   help="Server test port")
+SERVER_NAME = flags.DEFINE_string(
+    "server_name",
+    default="psm-grpc-server",
+    help="The name to use for test server deployments.")
+SERVER_PORT = flags.DEFINE_integer(
+    "server_port",
+    default=8080,
+    lower_bound=1,
+    upper_bound=65535,
+    help="Server test port.\nMust be within --firewall_allowed_ports.")
 SERVER_MAINTENANCE_PORT = flags.DEFINE_integer(
     "server_maintenance_port",
+    default=None,
+    lower_bound=1,
+    upper_bound=65535,
+    help=("Server port running maintenance services: Channelz, CSDS, Health, "
+          "XdsUpdateHealth, and ProtoReflection (optional).\n"
+          "Must be within --firewall_allowed_ports.\n"
+          "(default: the port is chosen automatically based on "
+          "the security configuration)"))
+SERVER_XDS_HOST = flags.DEFINE_string(
+    "server_xds_host",
+    default="xds-test-server",
+    help=("The xDS hostname of the test server.\n"
+          "Together with `server_xds_port` makes test server target URI, "
+          "xds:///hostname:port"))
+# Note: port 0 known to represent a request for dynamically-allocated port
+# https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Well-known_ports
+SERVER_XDS_PORT = flags.DEFINE_integer(
+    "server_xds_port",
+    default=8080,
     lower_bound=0,
     upper_bound=65535,
-    default=None,
-    help="Server port running maintenance services: health check, channelz, etc"
-)
-SERVER_XDS_HOST = flags.DEFINE_string("server_xds_host",
-                                      default='xds-test-server',
-                                      help="Test server xDS hostname")
-SERVER_XDS_PORT = flags.DEFINE_integer("server_xds_port",
-                                       default=8000,
-                                       help="Test server xDS port")
+    help=("The xDS port of the test server.\n"
+          "Together with `server_xds_host` makes test server target URI, "
+          "xds:///hostname:port\n"
+          "Must be unique within a GCP project.\n"
+          "Set to 0 to select any unused port."))
 
 # Test client
-CLIENT_NAME = flags.DEFINE_string("client_name",
-                                  default="psm-grpc-client",
-                                  help="Client deployment and service name")
-CLIENT_PORT = flags.DEFINE_integer("client_port",
-                                   default=8079,
-                                   help="Client test port")
+CLIENT_NAME = flags.DEFINE_string(
+    "client_name",
+    default="psm-grpc-client",
+    help="The name to use for test client deployments")
+CLIENT_PORT = flags.DEFINE_integer(
+    "client_port",
+    default=8079,
+    lower_bound=1,
+    upper_bound=65535,
+    help=(
+        "The port test client uses to run gRPC services: Channelz, CSDS, "
+        "XdsStats, XdsUpdateClientConfigure, and ProtoReflection (optional).\n"
+        "Doesn't have to be within --firewall_allowed_ports."))
 
 flags.mark_flags_as_required([
     "project",
-    "namespace",
+    # TODO(sergiitk): Make required when --namespace is removed.
+    # "resource_prefix",
 ])
diff --git a/tools/run_tests/xds_k8s_test_driver/framework/xds_k8s_testcase.py b/tools/run_tests/xds_k8s_test_driver/framework/xds_k8s_testcase.py
index 909ecd8..337d8ed 100644
--- a/tools/run_tests/xds_k8s_test_driver/framework/xds_k8s_testcase.py
+++ b/tools/run_tests/xds_k8s_test_driver/framework/xds_k8s_testcase.py
@@ -11,6 +11,7 @@
 # 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.
+import abc
 import datetime
 import enum
 import hashlib
@@ -25,6 +26,8 @@
 from framework import xds_flags
 from framework import xds_k8s_flags
 from framework.helpers import retryers
+import framework.helpers.datetime
+import framework.helpers.rand
 from framework.infrastructure import gcp
 from framework.infrastructure import k8s
 from framework.infrastructure import traffic_director
@@ -48,21 +51,35 @@
 flags.adopt_module_key_flags(xds_k8s_flags)
 
 # Type aliases
+TrafficDirectorManager = traffic_director.TrafficDirectorManager
+TrafficDirectorSecureManager = traffic_director.TrafficDirectorSecureManager
 XdsTestServer = server_app.XdsTestServer
 XdsTestClient = client_app.XdsTestClient
+KubernetesServerRunner = server_app.KubernetesServerRunner
+KubernetesClientRunner = client_app.KubernetesClientRunner
 LoadBalancerStatsResponse = grpc_testing.LoadBalancerStatsResponse
 _ChannelState = grpc_channelz.ChannelState
 _timedelta = datetime.timedelta
-_DEFAULT_SECURE_MODE_MAINTENANCE_PORT = \
-    server_app.KubernetesServerRunner.DEFAULT_SECURE_MODE_MAINTENANCE_PORT
 
 
-class XdsKubernetesTestCase(absltest.TestCase):
-    k8s_api_manager: k8s.KubernetesApiManager
+class XdsKubernetesTestCase(absltest.TestCase, metaclass=abc.ABCMeta):
+    _resource_suffix_randomize: bool = True
+    client_namespace: str
+    client_runner: KubernetesClientRunner
     gcp_api_manager: gcp.api.GcpApiManager
+    k8s_api_manager: k8s.KubernetesApiManager
+    resource_prefix: str
+    resource_suffix: str = ''
+    server_namespace: str
+    server_runner: KubernetesServerRunner
+    server_xds_port: int
+    td: TrafficDirectorManager
 
     @classmethod
     def setUpClass(cls):
+        """Hook method for setting up class fixture before running tests in
+        the class.
+        """
         # GCP
         cls.project: str = xds_flags.PROJECT.value
         cls.network: str = xds_flags.NETWORK.value
@@ -72,9 +89,17 @@
         cls.ensure_firewall = xds_flags.ENSURE_FIREWALL.value
         cls.firewall_allowed_ports = xds_flags.FIREWALL_ALLOWED_PORTS.value
 
-        # Base namespace
-        # TODO(sergiitk): generate for each test
-        cls.namespace: str = xds_flags.NAMESPACE.value
+        # Resource names.
+        # TODO(sergiitk): Drop namespace parsing when --namespace is removed.
+        cls.resource_prefix = (xds_flags.RESOURCE_PREFIX.value or
+                               xds_flags.NAMESPACE.value)
+        if not cls.resource_prefix:
+            raise flags.IllegalFlagValueError(
+                'Required one of the flags: --resource_prefix or --namespace')
+
+        if xds_flags.RESOURCE_SUFFIX.value is not None:
+            cls._resource_suffix_randomize = False
+            cls.resource_suffix = xds_flags.RESOURCE_SUFFIX.value
 
         # Test server
         cls.server_image = xds_k8s_flags.SERVER_IMAGE.value
@@ -101,15 +126,53 @@
         cls.gcp_api_manager = gcp.api.GcpApiManager()
 
     def setUp(self):
-        # TODO(sergiitk): generate namespace with run id for each test
-        self.server_namespace = self.namespace
-        self.client_namespace = self.namespace
+        """Hook method for setting up the test fixture before exercising it."""
+        super().setUp()
 
-        # Init this in child class
-        # TODO(sergiitk): consider making a method to be less error-prone
-        self.server_runner = None
-        self.client_runner = None
-        self.td = None
+        if self._resource_suffix_randomize:
+            self.resource_suffix = self._random_resource_suffix()
+        logger.info('Test run resource prefix: %s, suffix: %s',
+                    self.resource_prefix, self.resource_suffix)
+
+        # TD Manager
+        self.td = self.initTrafficDirectorManager()
+
+        # Test Server runner
+        self.server_namespace = KubernetesServerRunner.make_namespace_name(
+            self.resource_prefix, self.resource_suffix)
+        self.server_runner = self.initKubernetesServerRunner()
+
+        # Test Client runner
+        self.client_namespace = KubernetesClientRunner.make_namespace_name(
+            self.resource_prefix, self.resource_suffix)
+        self.client_runner = self.initKubernetesClientRunner()
+
+        # Ensures the firewall exist
+        if self.ensure_firewall:
+            self.td.create_firewall_rule(
+                allowed_ports=self.firewall_allowed_ports)
+
+        # Randomize xds port, when it's set to 0
+        if self.server_xds_port == 0:
+            # TODO(sergiitk): this is prone to race conditions:
+            #  The port might not me taken now, but there's not guarantee
+            #  it won't be taken until the tests get to creating
+            #  forwarding rule. This check is better than nothing,
+            #  but we should find a better approach.
+            self.server_xds_port = self.td.find_unused_forwarding_rule_port()
+            logger.info('Found unused xds port: %s', self.server_xds_port)
+
+    @abc.abstractmethod
+    def initTrafficDirectorManager(self) -> TrafficDirectorManager:
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def initKubernetesServerRunner(self) -> KubernetesServerRunner:
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def initKubernetesClientRunner(self) -> KubernetesClientRunner:
+        raise NotImplementedError
 
     @classmethod
     def tearDownClass(cls):
@@ -132,6 +195,19 @@
         self.server_runner.cleanup(force=self.force_cleanup,
                                    force_namespace=self.force_cleanup)
 
+    @staticmethod
+    def _random_resource_suffix() -> str:
+        # Date and time suffix for debugging. Seconds skipped, not as relevant
+        # Format example: 20210626-1859
+        datetime_suffix: str = framework.helpers.datetime.datetime_suffix()
+        # Use lowercase chars because some resource names won't allow uppercase.
+        # For len 5, total (26 + 10)^5 = 60,466,176 combinations.
+        # Approx. number of test runs needed to start at the same minute to
+        # produce a collision: math.sqrt(math.pi/2 * (26+10)**5) ≈ 9745.
+        # https://en.wikipedia.org/wiki/Birthday_attack#Mathematics
+        unique_hash: str = framework.helpers.rand.rand_string(5, lowercase=True)
+        return f'{datetime_suffix}-{unique_hash}'
+
     def setupTrafficDirectorGrpc(self):
         self.td.setup_for_grpc(self.server_xds_host,
                                self.server_xds_port,
@@ -206,28 +282,22 @@
 
     @classmethod
     def setUpClass(cls):
+        """Hook method for setting up class fixture before running tests in
+        the class.
+        """
         super().setUpClass()
         if cls.server_maintenance_port is None:
-            cls.server_maintenance_port = \
-                server_app.KubernetesServerRunner.DEFAULT_MAINTENANCE_PORT
+            cls.server_maintenance_port = KubernetesServerRunner.DEFAULT_MAINTENANCE_PORT
 
-    def setUp(self):
-        super().setUp()
+    def initTrafficDirectorManager(self) -> TrafficDirectorManager:
+        return TrafficDirectorManager(self.gcp_api_manager,
+                                      project=self.project,
+                                      resource_prefix=self.resource_prefix,
+                                      resource_suffix=self.resource_suffix,
+                                      network=self.network)
 
-        # Traffic Director Configuration
-        self.td = traffic_director.TrafficDirectorManager(
-            self.gcp_api_manager,
-            project=self.project,
-            resource_prefix=self.namespace,
-            network=self.network)
-
-        # Ensures the firewall exist
-        if self.ensure_firewall:
-            self.td.create_firewall_rule(
-                allowed_ports=self.firewall_allowed_ports)
-
-        # Test Server Runner
-        self.server_runner = server_app.KubernetesServerRunner(
+    def initKubernetesServerRunner(self) -> KubernetesServerRunner:
+        return KubernetesServerRunner(
             k8s.KubernetesNamespace(self.k8s_api_manager,
                                     self.server_namespace),
             deployment_name=self.server_name,
@@ -239,8 +309,8 @@
             xds_server_uri=self.xds_server_uri,
             network=self.network)
 
-        # Test Client Runner
-        self.client_runner = client_app.KubernetesClientRunner(
+    def initKubernetesClientRunner(self) -> KubernetesClientRunner:
+        return KubernetesClientRunner(
             k8s.KubernetesNamespace(self.k8s_api_manager,
                                     self.client_namespace),
             deployment_name=self.client_name,
@@ -273,6 +343,7 @@
 
 
 class SecurityXdsKubernetesTestCase(XdsKubernetesTestCase):
+    td: TrafficDirectorSecureManager
 
     class SecurityMode(enum.Enum):
         MTLS = enum.auto()
@@ -281,6 +352,9 @@
 
     @classmethod
     def setUpClass(cls):
+        """Hook method for setting up class fixture before running tests in
+        the class.
+        """
         super().setUpClass()
         if cls.server_maintenance_port is None:
             # In secure mode, the maintenance port is different from
@@ -288,25 +362,18 @@
             # Health Checks and Channelz tests available.
             # When not provided, use explicit numeric port value, so
             # Backend Health Checks are created on a fixed port.
-            cls.server_maintenance_port = _DEFAULT_SECURE_MODE_MAINTENANCE_PORT
+            cls.server_maintenance_port = KubernetesServerRunner.DEFAULT_SECURE_MODE_MAINTENANCE_PORT
 
-    def setUp(self):
-        super().setUp()
-
-        # Traffic Director Configuration
-        self.td = traffic_director.TrafficDirectorSecureManager(
+    def initTrafficDirectorManager(self) -> TrafficDirectorSecureManager:
+        return TrafficDirectorSecureManager(
             self.gcp_api_manager,
             project=self.project,
-            resource_prefix=self.namespace,
+            resource_prefix=self.resource_prefix,
+            resource_suffix=self.resource_suffix,
             network=self.network)
 
-        # Ensures the firewall exist
-        if self.ensure_firewall:
-            self.td.create_firewall_rule(
-                allowed_ports=self.firewall_allowed_ports)
-
-        # Test Server Runner
-        self.server_runner = server_app.KubernetesServerRunner(
+    def initKubernetesServerRunner(self) -> KubernetesServerRunner:
+        return KubernetesServerRunner(
             k8s.KubernetesNamespace(self.k8s_api_manager,
                                     self.server_namespace),
             deployment_name=self.server_name,
@@ -320,8 +387,8 @@
             deployment_template='server-secure.deployment.yaml',
             debug_use_port_forwarding=self.debug_use_port_forwarding)
 
-        # Test Client Runner
-        self.client_runner = client_app.KubernetesClientRunner(
+    def initKubernetesClientRunner(self) -> KubernetesClientRunner:
+        return KubernetesClientRunner(
             k8s.KubernetesNamespace(self.k8s_api_manager,
                                     self.client_namespace),
             deployment_name=self.client_name,
diff --git a/tools/run_tests/xds_k8s_test_driver/run.sh b/tools/run_tests/xds_k8s_test_driver/run.sh
index f549a48..a4dde33 100755
--- a/tools/run_tests/xds_k8s_test_driver/run.sh
+++ b/tools/run_tests/xds_k8s_test_driver/run.sh
@@ -42,7 +42,7 @@
 EXAMPLES:
 $0 bin/run_td_setup.py --help      # list script-specific options
 $0 bin/run_td_setup.py --helpfull  # list all available options
-XDS_K8S_CONFIG=./path-to-flagfile.cfg $0 bin/run_td_setup.py --namespace=override-namespace
+XDS_K8S_CONFIG=./path-to-flagfile.cfg ./run.sh bin/run_td_setup.py --resource_suffix=override-suffix
 $0 tests/baseline_test.py
 $0 tests/security_test.py --verbosity=1 --logger_levels=__main__:DEBUG,framework:DEBUG
 $0 tests/security_test.py SecurityTest.test_mtls --nocheck_local_certs
diff --git a/tools/run_tests/xds_k8s_test_driver/tests/security_test.py b/tools/run_tests/xds_k8s_test_driver/tests/security_test.py
index 2c19d33..a4c7149 100644
--- a/tools/run_tests/xds_k8s_test_driver/tests/security_test.py
+++ b/tools/run_tests/xds_k8s_test_driver/tests/security_test.py
@@ -12,12 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import logging
-import uuid
 
 from absl import flags
 from absl.testing import absltest
 
 from framework import xds_k8s_testcase
+from framework.helpers import rand
 
 logger = logging.getLogger(__name__)
 flags.adopt_module_key_flags(xds_k8s_testcase)
@@ -161,7 +161,7 @@
                                       server_port=self.server_port,
                                       tls=True,
                                       mtls=False)
-        incorrect_namespace = f'incorrect-namespace-{uuid.uuid4().hex}'
+        incorrect_namespace = f'incorrect-namespace-{rand.rand_string()}'
         self.td.setup_client_security(server_namespace=incorrect_namespace,
                                       server_name=self.server_name,
                                       tls=True,