This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 611487cd test(e2e): prewarm environment pool to hide per-spec deploy 
latency (#2790)
611487cd is described below

commit 611487cd2db0996acc59f80c41817f50e2ea8364
Author: AlinsRan <[email protected]>
AuthorDate: Wed Jul 8 14:20:52 2026 +0800

    test(e2e): prewarm environment pool to hide per-spec deploy latency (#2790)
---
 test/e2e/apisix/e2e_test.go          |   5 +
 test/e2e/framework/k8s.go            |  19 ++-
 test/e2e/scaffold/apisix_deployer.go |  28 ++++
 test/e2e/scaffold/apisix_prewarm.go  | 274 +++++++++++++++++++++++++++++++++++
 test/e2e/scaffold/envpool.go         | 210 +++++++++++++++++++++++++++
 test/e2e/scaffold/scaffold.go        |   2 +-
 6 files changed, 534 insertions(+), 4 deletions(-)

diff --git a/test/e2e/apisix/e2e_test.go b/test/e2e/apisix/e2e_test.go
index fde91636..d6657b63 100644
--- a/test/e2e/apisix/e2e_test.go
+++ b/test/e2e/apisix/e2e_test.go
@@ -45,3 +45,8 @@ func TestAPISIXE2E(t *testing.T) {
        _, _ = fmt.Fprintf(GinkgoWriter, "Starting APISIX standalone e2e 
suite\n")
        RunSpecs(t, "apisix standalone e2e suite")
 }
+
+// Tear down any prewarmed environments left in the pools when the suite ends.
+var _ = AfterSuite(func() {
+       scaffold.ShutdownAllPools()
+})
diff --git a/test/e2e/framework/k8s.go b/test/e2e/framework/k8s.go
index a943a1b0..1d959cc7 100644
--- a/test/e2e/framework/k8s.go
+++ b/test/e2e/framework/k8s.go
@@ -74,6 +74,14 @@ func (f *Framework) ensureService(name, namespace string, 
desiredEndpoints int)
        return f.ensureServiceWithTimeout(name, namespace, desiredEndpoints, 
120)
 }
 
+// EnsureServiceReadyE waits for the named service in the given namespace to 
have
+// the desired number of ready endpoints, returning an error instead of failing
+// the spec. It is safe to call from background goroutines (e.g. the prewarm
+// pool) because it does not use Ginkgo/Gomega assertions.
+func (f *Framework) EnsureServiceReadyE(namespace, name string, 
desiredEndpoints int) error {
+       return f.ensureService(name, namespace, desiredEndpoints)
+}
+
 func (f *Framework) ensureServiceWithTimeout(name, namespace string, 
desiredEndpoints, timeout int) error {
        backoff := wait.Backoff{
                Duration: 6 * time.Second,
@@ -285,10 +293,15 @@ func WaitPodsAvailable(t testing.TestingT, kubeOps 
*k8s.KubectlOptions, opts met
 }
 
 func waitExponentialBackoff(condFunc func() (bool, error)) error {
+       // Poll at a fixed short interval instead of an exponential backoff.
+       // Pods typically become ready in the 10-30s window; an exponential
+       // backoff (0.5,1,2,4,8,16,32,64s) polls very sparsely exactly there, 
so a
+       // pod ready at t=16s would not be detected until t=31.5s. A constant 2s
+       // interval detects readiness promptly and keeps the same ~180s ceiling.
        backoff := wait.Backoff{
-               Duration: 500 * time.Millisecond,
-               Factor:   2,
-               Steps:    8,
+               Duration: 2 * time.Second,
+               Factor:   1,
+               Steps:    90,
        }
        return wait.ExponentialBackoff(backoff, condFunc)
 }
diff --git a/test/e2e/scaffold/apisix_deployer.go 
b/test/e2e/scaffold/apisix_deployer.go
index 5786710b..7c4b1954 100644
--- a/test/e2e/scaffold/apisix_deployer.go
+++ b/test/e2e/scaffold/apisix_deployer.go
@@ -59,6 +59,34 @@ func NewAPISIXDeployer(s *Scaffold) Deployer {
 }
 
 func (s *APISIXDeployer) BeforeEach() {
+       // Fast path: pick up a prewarmed environment so the deploy/readiness
+       // latency is paid by a background worker (overlapping the previous 
spec)
+       // instead of on this spec's critical path. Only the default profile is
+       // pooled; anything else, or any provisioning failure, falls back to the
+       // synchronous deploy below.
+       if prewarmEnabled() && isPoolable(s.opts) && specPrewarmable() {
+               fw, opts := s.Framework, s.opts
+               pool := getOrStartPool(profileKey(opts), prewarmDepth(), func() 
*pooledEnv {
+                       return provisionAPISIXEnv(fw, opts)
+               })
+               env := pool.acquire()
+               if env != nil && env.err == nil {
+                       if err := s.loadPooledEnv(env); err != nil {
+                               s.Logf("prewarm tunnel setup failed, falling 
back to synchronous deploy: %v", err)
+                               destroyPooledEnv(env)
+                       } else {
+                               return
+                       }
+               } else if env != nil && env.err != nil {
+                       s.Logf("prewarm provision failed, falling back to 
synchronous deploy: %v", env.err)
+                       destroyPooledEnv(env)
+               }
+       }
+
+       s.beforeEachSync()
+}
+
+func (s *APISIXDeployer) beforeEachSync() {
        s.runtimeOpts = s.opts
        s.namespace = fmt.Sprintf("ingress-apisix-e2e-tests-%s-%d", 
s.runtimeOpts.Name, time.Now().Nanosecond())
        s.kubectlOptions = &k8s.KubectlOptions{
diff --git a/test/e2e/scaffold/apisix_prewarm.go 
b/test/e2e/scaffold/apisix_prewarm.go
new file mode 100644
index 00000000..2574da68
--- /dev/null
+++ b/test/e2e/scaffold/apisix_prewarm.go
@@ -0,0 +1,274 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 scaffold
+
+import (
+       "bytes"
+       "fmt"
+       "os"
+       "strings"
+       "sync/atomic"
+       "time"
+
+       "github.com/gruntwork-io/terratest/modules/k8s"
+       . "github.com/onsi/ginkgo/v2" //nolint:staticcheck
+       corev1 "k8s.io/api/core/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/utils/ptr"
+
+       "github.com/apache/apisix-ingress-controller/test/e2e/framework"
+)
+
+var nsCounter int64
+
+// defaultProfileName is the scaffold Options.Name used by the default profile.
+const defaultProfileName = "default"
+
+// streamRouteLabels marks Gateway API stream-route specs (TCP/TLS/UDP).
+var streamRouteLabels = map[string]struct{}{
+       "tcproute": {},
+       "tlsroute": {},
+       "udproute": {},
+}
+
+// specPrewarmable reports whether the currently running spec may be served 
from
+// the prewarm pool. Stream-route specs (Gateway API TCP/TLS/UDP routes and the
+// ApisixRoute stream-route suite) are excluded and fall back to synchronous
+// deployment: under apisix-standalone a stream route served from a prewarmed
+// data plane is flaky, because the data plane's stream subsystem races with 
the
+// concurrent background provisioning of the next pooled environment, whereas
+// HTTP routes tolerate that contention.
+func specPrewarmable() bool {
+       report := CurrentSpecReport()
+       for _, label := range report.Labels() {
+               if _, ok := streamRouteLabels[label]; ok {
+                       return false
+               }
+       }
+       for _, text := range report.ContainerHierarchyTexts {
+               if strings.Contains(text, "StreamRoute") {
+                       return false
+               }
+       }
+       return true
+}
+
+// isPoolable reports whether an environment with these options can be served
+// from the prewarm pool. Only the default profile is pooled; webhook-enabled
+// and custom-keyed environments fall back to synchronous deployment.
+func isPoolable(o Options) bool {
+       return !o.SkipHooks &&
+               !o.EnableWebhook &&
+               o.ControllerName == "" &&
+               o.APISIXAdminAPIKey == ""
+}
+
+// profileKey identifies the pool an environment belongs to. Within a process
+// all default scaffolds share one pool.
+func profileKey(o Options) string {
+       name := o.Name
+       if name == "" {
+               name = defaultProfileName
+       }
+       return "name=" + name
+}
+
+func formatRegistry(workloadTemplate string) string {
+       if customRegistry, ok := os.LookupEnv("REGISTRY"); ok {
+               return strings.ReplaceAll(workloadTemplate, "127.0.0.1:5000", 
customRegistry)
+       }
+       return workloadTemplate
+}
+
+// provisionAPISIXEnv builds a complete default-profile environment using
+// error-returning primitives only, so it is safe to run in a background
+// goroutine. Any failure is captured in pooledEnv.err for the caller to 
handle.
+func provisionAPISIXEnv(fw *framework.Framework, opts Options) *pooledEnv {
+       t := &bgTestingT{}
+       env := &pooledEnv{}
+
+       name := opts.Name
+       if name == "" {
+               name = defaultProfileName
+       }
+       ns := fmt.Sprintf("ingress-apisix-e2e-tests-%s-p%d-%d",
+               name, GinkgoParallelProcess(), atomic.AddInt64(&nsCounter, 1))
+       env.namespace = ns
+       env.kubectlOptions = &k8s.KubectlOptions{
+               ConfigPath: GetKubeconfig(),
+               Namespace:  ns,
+       }
+       env.adminKey = getEnvOrDefault("APISIX_ADMIN_KEY", 
"edd1c9f034335f136f87ad84b625c8f1")
+       env.controllerName = fmt.Sprintf("%s/%s", DefaultControllerName, ns)
+
+       if err := k8s.CreateNamespaceE(t, env.kubectlOptions, ns); err != nil {
+               env.err = fmt.Errorf("creating namespace: %w", err)
+               return env
+       }
+
+       // 1) Data plane (APISIX, plus etcd when the provider needs it).
+       svc, err := provisionDataplane(t, env, opts)
+       if err != nil {
+               env.err = err
+               return env
+       }
+       env.dataplaneService = svc
+
+       // 2) Ingress controller.
+       if err := provisionIngress(t, env); err != nil {
+               env.err = err
+               return env
+       }
+
+       // 3) httpbin test backend.
+       httpbinSvc, err := provisionHTTPBIN(fw, t, env)
+       if err != nil {
+               env.err = err
+               return env
+       }
+       env.httpbinService = httpbinSvc
+
+       return env
+}
+
+func provisionDataplane(t *bgTestingT, env *pooledEnv, _ Options) 
(*corev1.Service, error) {
+       serviceName := framework.ProviderType
+       configProvider := framework.ConfigProviderTypeYaml
+       if framework.ProviderType == framework.ProviderTypeAPISIX {
+               configProvider = framework.ConfigProviderTypeEtcd
+       }
+
+       if configProvider == framework.ConfigProviderTypeEtcd {
+               if err := k8s.KubectlApplyFromStringE(t, env.kubectlOptions, 
framework.EtcdSpec); err != nil {
+                       return nil, fmt.Errorf("applying etcd: %w", err)
+               }
+               if err := framework.WaitPodsAvailable(t, env.kubectlOptions, 
metav1.ListOptions{
+                       LabelSelector: "app=etcd",
+               }); err != nil {
+                       return nil, fmt.Errorf("waiting for etcd pod: %w", err)
+               }
+       }
+
+       deployOpts := APISIXDeployOptions{
+               Namespace:        env.namespace,
+               AdminKey:         env.adminKey,
+               ServiceName:      serviceName,
+               ServiceHTTPPort:  9080,
+               ServiceHTTPSPort: 9443,
+               ConfigProvider:   configProvider,
+               Replicas:         ptr.To(1),
+       }
+       buf := bytes.NewBuffer(nil)
+       if err := framework.APISIXStandaloneTpl.Execute(buf, &deployOpts); err 
!= nil {
+               return nil, fmt.Errorf("rendering apisix template: %w", err)
+       }
+       if err := k8s.KubectlApplyFromStringE(t, env.kubectlOptions, 
buf.String()); err != nil {
+               return nil, fmt.Errorf("applying apisix: %w", err)
+       }
+       if err := framework.WaitPodsAvailable(t, env.kubectlOptions, 
metav1.ListOptions{
+               LabelSelector: "app.kubernetes.io/name=apisix",
+       }); err != nil {
+               return nil, fmt.Errorf("waiting for apisix pod: %w", err)
+       }
+
+       svc, err := k8s.GetServiceE(t, env.kubectlOptions, serviceName)
+       if err != nil {
+               return nil, fmt.Errorf("getting dataplane service: %w", err)
+       }
+       return svc, nil
+}
+
+func provisionIngress(t *bgTestingT, env *pooledEnv) error {
+       opts := framework.IngressDeployOpts{
+               ControllerName:     env.controllerName,
+               ProviderType:       framework.ProviderType,
+               ProviderSyncPeriod: 1 * time.Hour,
+               Namespace:          env.namespace,
+               Replicas:           ptr.To(1),
+               WebhookEnable:      false,
+               DisableGatewayAPI:  framework.DisableGatewayAPI,
+       }
+       buf := bytes.NewBuffer(nil)
+       if err := framework.IngressSpecTpl.Execute(buf, opts); err != nil {
+               return fmt.Errorf("rendering ingress template: %w", err)
+       }
+       if err := k8s.KubectlApplyFromStringE(t, env.kubectlOptions, 
buf.String()); err != nil {
+               return fmt.Errorf("applying ingress controller: %w", err)
+       }
+       if err := framework.WaitPodsAvailable(t, env.kubectlOptions, 
metav1.ListOptions{
+               LabelSelector: "control-plane=controller-manager",
+       }); err != nil {
+               return fmt.Errorf("waiting for controller pod: %w", err)
+       }
+       return nil
+}
+
+func provisionHTTPBIN(fw *framework.Framework, t *bgTestingT, env *pooledEnv) 
(*corev1.Service, error) {
+       deployment := fmt.Sprintf(formatRegistry(_httpbinDeploymentTemplate), 1)
+       if err := k8s.KubectlApplyFromStringE(t, env.kubectlOptions, 
deployment); err != nil {
+               return nil, fmt.Errorf("applying httpbin deployment: %w", err)
+       }
+       if err := k8s.KubectlApplyFromStringE(t, env.kubectlOptions, 
_httpService); err != nil {
+               return nil, fmt.Errorf("applying httpbin service: %w", err)
+       }
+       if err := fw.EnsureServiceReadyE(env.namespace, HTTPBinServiceName, 1); 
err != nil {
+               return nil, fmt.Errorf("waiting for httpbin endpoints: %w", err)
+       }
+       svc, err := k8s.GetServiceE(t, env.kubectlOptions, HTTPBinServiceName)
+       if err != nil {
+               return nil, fmt.Errorf("getting httpbin service: %w", err)
+       }
+       return svc, nil
+}
+
+// loadPooledEnv installs a prewarmed environment onto the deployer's scaffold 
so
+// the rest of the spec behaves exactly as if it had been deployed 
synchronously.
+//
+// Port-forward tunnels are (re)created here, on the spec's critical path, 
rather
+// than reused from the background prewarm worker. A port-forward opened during
+// prewarm can be left in a broken state when it is established before the data
+// plane's listener is fully serving (e.g. the TLS stream listener) and then 
sits
+// idle in the pool buffer until a spec picks it up, surfacing as an EOF on 
first
+// use. Recreating them here against a fully-provisioned data plane matches the
+// synchronous path exactly; tunnel setup is cheap (~1-2s) relative to the
+// deploy/readiness latency that prewarm hides.
+func (s *APISIXDeployer) loadPooledEnv(env *pooledEnv) error {
+       s.runtimeOpts = s.opts
+       s.namespace = env.namespace
+       s.kubectlOptions = env.kubectlOptions
+       s.runtimeOpts.ControllerName = env.controllerName
+       s.runtimeOpts.APISIXAdminAPIKey = env.adminKey
+       s.dataplaneService = env.dataplaneService
+       s.httpbinService = env.httpbinService
+       s.finalizers = nil
+       s.additionalGateways = make(map[string]*GatewayResources)
+
+       apisixTunnels, err := s.createDataplaneTunnels(env.dataplaneService, 
s.kubectlOptions, env.dataplaneService.Name)
+       if err != nil {
+               return fmt.Errorf("creating dataplane tunnels: %w", err)
+       }
+       s.apisixTunnels = apisixTunnels
+
+       adminTunnel, err := s.createAdminTunnel(env.dataplaneService)
+       if err != nil {
+               return fmt.Errorf("creating admin tunnel: %w", err)
+       }
+       s.adminTunnel = adminTunnel
+
+       return nil
+}
diff --git a/test/e2e/scaffold/envpool.go b/test/e2e/scaffold/envpool.go
new file mode 100644
index 00000000..5517cd7f
--- /dev/null
+++ b/test/e2e/scaffold/envpool.go
@@ -0,0 +1,210 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 scaffold
+
+import (
+       "fmt"
+       "os"
+       "strconv"
+       "sync"
+
+       "github.com/gruntwork-io/terratest/modules/k8s"
+       corev1 "k8s.io/api/core/v1"
+)
+
+// pooledEnv is a fully provisioned, ready-to-use test environment produced by
+// the prewarm pool ahead of time, so a spec's BeforeEach can pick it up 
without
+// paying the deploy/readiness latency on its critical path.
+type pooledEnv struct {
+       namespace        string
+       kubectlOptions   *k8s.KubectlOptions
+       controllerName   string
+       adminKey         string
+       dataplaneService *corev1.Service
+       httpbinService   *corev1.Service
+
+       // err is non-nil if provisioning failed; the consumer falls back to a
+       // synchronous deploy and discards this env.
+       err error
+}
+
+// bgTestingT is a minimal terratest testing.TestingT implementation usable 
from
+// background goroutines (the prewarm workers run outside any Ginkgo spec, so
+// Ginkgo's GinkgoT/Expect must not be used there). Fatal/FailNow abort the
+// current provision via panic, which safeProvision recovers into 
pooledEnv.err.
+type bgTestingT struct{}
+
+type bgAbort struct{ msg string }
+
+func (bgAbort) Error() string { return "prewarm provision aborted" }
+
+func (t *bgTestingT) Fail()                        {}
+func (t *bgTestingT) FailNow()                     { panic(bgAbort{msg: 
"FailNow"}) }
+func (t *bgTestingT) Fatal(args ...any)            { panic(bgAbort{msg: 
fmt.Sprint(args...)}) }
+func (t *bgTestingT) Fatalf(f string, args ...any) { panic(bgAbort{msg: 
fmt.Sprintf(f, args...)}) }
+func (t *bgTestingT) Error(args ...any)            {}
+func (t *bgTestingT) Errorf(f string, args ...any) {}
+func (t *bgTestingT) Name() string                 { return "prewarm" }
+
+// envPool maintains a small set of prewarmed environments for one profile.
+// A buffered channel of capacity `depth` holds ready environments; `depth`
+// worker goroutines keep refilling it. With depth=1 this is a double buffer:
+// one env is ready while the next is being built concurrently with the
+// currently running spec.
+type envPool struct {
+       ch        chan *pooledEnv
+       stop      chan struct{}
+       stopOnce  sync.Once
+       wg        sync.WaitGroup
+       provision func() *pooledEnv
+}
+
+func newEnvPool(depth int, provision func() *pooledEnv) *envPool {
+       if depth < 1 {
+               depth = 1
+       }
+       p := &envPool{
+               ch:        make(chan *pooledEnv, depth),
+               stop:      make(chan struct{}),
+               provision: provision,
+       }
+       for i := 0; i < depth; i++ {
+               p.wg.Add(1)
+               go p.worker()
+       }
+       return p
+}
+
+func (p *envPool) worker() {
+       defer p.wg.Done()
+       for {
+               select {
+               case <-p.stop:
+                       return
+               default:
+               }
+               env := safeProvision(p.provision)
+               select {
+               case p.ch <- env:
+               case <-p.stop:
+                       // Shutting down before this env was consumed: clean it 
up.
+                       destroyPooledEnv(env)
+                       return
+               }
+       }
+}
+
+func (p *envPool) acquire() *pooledEnv {
+       select {
+       case env := <-p.ch:
+               return env
+       case <-p.stop:
+               return nil
+       }
+}
+
+func (p *envPool) shutdown() {
+       p.stopOnce.Do(func() { close(p.stop) })
+       p.wg.Wait()
+       // Drain and destroy any environments still buffered.
+       for {
+               select {
+               case env := <-p.ch:
+                       destroyPooledEnv(env)
+               default:
+                       return
+               }
+       }
+}
+
+// safeProvision runs a provision function, converting any panic (including a
+// bgTestingT Fatal/FailNow) into a pooledEnv carrying the error.
+func safeProvision(provision func() *pooledEnv) (env *pooledEnv) {
+       defer func() {
+               if r := recover(); r != nil {
+                       if env == nil {
+                               env = &pooledEnv{}
+                       }
+                       env.err = fmt.Errorf("panic during prewarm provision: 
%v", r)
+               }
+       }()
+       return provision()
+}
+
+// destroyPooledEnv tears down an environment that will not be used by a spec 
by
+// deleting its namespace. Tunnels are only created once an env is handed to a
+// spec (see loadPooledEnv), so an unused pooled env owns no port-forwards.
+func destroyPooledEnv(env *pooledEnv) {
+       if env == nil {
+               return
+       }
+       if env.namespace != "" && env.kubectlOptions != nil {
+               _ = k8s.RunKubectlE(&bgTestingT{}, env.kubectlOptions, 
"delete", "namespace", env.namespace, "--wait=false")
+       }
+}
+
+// --- process-global pool registry, keyed by profile -------------------------
+
+var (
+       poolsMu sync.Mutex
+       pools   = map[string]*envPool{}
+)
+
+func getOrStartPool(key string, depth int, provision func() *pooledEnv) 
*envPool {
+       poolsMu.Lock()
+       defer poolsMu.Unlock()
+       if p, ok := pools[key]; ok {
+               return p
+       }
+       p := newEnvPool(depth, provision)
+       pools[key] = p
+       return p
+}
+
+// ShutdownAllPools stops every prewarm pool and tears down any environments
+// still buffered. It is registered as an AfterSuite by the e2e suite root, not
+// here, so that suites which merely import this package (e.g. the benchmark
+// suite, which declares its own AfterSuite) are not given a duplicate node.
+func ShutdownAllPools() {
+       poolsMu.Lock()
+       ps := pools
+       pools = map[string]*envPool{}
+       poolsMu.Unlock()
+       for _, p := range ps {
+               p.shutdown()
+       }
+}
+
+// --- knobs ------------------------------------------------------------------
+
+// prewarmEnabled reports whether the prewarm pool is active. It is on by
+// default and can be disabled with E2E_PREWARM=false.
+func prewarmEnabled() bool {
+       return getEnvOrDefault("E2E_PREWARM", "true") != "false"
+}
+
+// prewarmDepth is the pool depth (ready + in-flight environments) per profile
+// per process. Default 1 (double buffer). Override with E2E_PREWARM_DEPTH.
+func prewarmDepth() int {
+       if v := os.Getenv("E2E_PREWARM_DEPTH"); v != "" {
+               if n, err := strconv.Atoi(v); err == nil && n > 0 {
+                       return n
+               }
+       }
+       return 1
+}
diff --git a/test/e2e/scaffold/scaffold.go b/test/e2e/scaffold/scaffold.go
index d4498d02..7a6234ca 100644
--- a/test/e2e/scaffold/scaffold.go
+++ b/test/e2e/scaffold/scaffold.go
@@ -140,7 +140,7 @@ func (s *Scaffold) AdminKey() string {
 // NewScaffold creates an e2e test scaffold.
 func NewScaffold(o Options) *Scaffold {
        if o.Name == "" {
-               o.Name = "default"
+               o.Name = defaultProfileName
        }
        if o.Kubeconfig == "" {
                o.Kubeconfig = GetKubeconfig()

Reply via email to