@@ -2,6 +2,7 @@ package infra
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
67 "fmt"
78 "net/http"
@@ -14,6 +15,7 @@ import (
1415
1516 "github.com/gruntwork-io/terratest/modules/k8s"
1617 "github.com/gruntwork-io/terratest/modules/random"
18+ "github.com/gruntwork-io/terratest/modules/retry"
1719 "github.com/stretchr/testify/require"
1820 "google.golang.org/api/compute/v1"
1921 "google.golang.org/api/container/v1"
@@ -44,6 +46,12 @@ const (
4446// Default project ID to use if not specified in the environment.
4547const defaultProjectID = "helm-testing"
4648
49+ // getNodeServiceAccount returns the GKE node service account from the environment.
50+ // If empty, gcloud falls back to the project's default Compute Engine service account.
51+ func getNodeServiceAccount () string {
52+ return os .Getenv ("GCP_NODE_SERVICE_ACCOUNT" )
53+ }
54+
4755// getProjectID returns the GCP project ID from the environment variable or falls back to default.
4856func getProjectID () string {
4957 if projectID := os .Getenv ("GCP_PROJECT_ID" ); projectID != "" {
@@ -227,6 +235,72 @@ func (r *GcpRegion) SetUpInfra(t *testing.T) {
227235 require .NoError (t , err )
228236 err = r .deployAndConfigureCoreDNS (t , kubeConfigPath )
229237 require .NoError (t , err , "failed to deploy and configure CoreDNS" )
238+
239+ if r .IsMultiRegion {
240+ for _ , clusterName := range r .Clusters {
241+ err = patchKubeDNSForCustomDomains (t , clusterName , kubeConfigPath )
242+ require .NoError (t , err , "failed to patch kube-dns for custom domains on cluster %s" , clusterName )
243+ }
244+ }
245+ }
246+
247+ // patchKubeDNSForCustomDomains patches the kube-dns ConfigMap with stubDomains for
248+ // custom cluster domains and restarts node-local-dns to pick up the new config.
249+ func patchKubeDNSForCustomDomains (t * testing.T , clusterName , kubeConfigPath string ) error {
250+ kubectlOpts := k8s .NewKubectlOptions (clusterName , kubeConfigPath , "kube-system" )
251+
252+ clusterIP , err := k8s .RunKubectlAndGetOutputE (t , kubectlOpts ,
253+ "get" , "service" , "kube-dns-upstream" , "-o" , "jsonpath={.spec.clusterIP}" )
254+ if err != nil {
255+ return fmt .Errorf ("failed to get kube-dns-upstream ClusterIP on cluster %s: %w" , clusterName , err )
256+ }
257+ clusterIP = strings .TrimSpace (clusterIP )
258+ if clusterIP == "" {
259+ return fmt .Errorf ("kube-dns-upstream service has no ClusterIP on cluster %s" , clusterName )
260+ }
261+ t .Logf ("[gcp] kube-dns-upstream ClusterIP on cluster %s: %s" , clusterName , clusterIP )
262+
263+ stubDomains := make (map [string ][]string )
264+ for _ , domain := range operator .CustomDomains {
265+ stubDomains [domain ] = []string {clusterIP }
266+ }
267+ stubDomainsJSON , err := json .Marshal (stubDomains )
268+ if err != nil {
269+ return fmt .Errorf ("failed to marshal stubDomains: %w" , err )
270+ }
271+
272+ type configMapPatch struct {
273+ Data map [string ]string `json:"data"`
274+ }
275+ patchJSON , err := json .Marshal (configMapPatch {Data : map [string ]string {
276+ "stubDomains" : string (stubDomainsJSON ),
277+ }})
278+ if err != nil {
279+ return fmt .Errorf ("failed to marshal ConfigMap patch: %w" , err )
280+ }
281+
282+ if err := k8s .RunKubectlE (t , kubectlOpts , "patch" , "configmap" , "kube-dns" ,
283+ "--type=merge" , "-p" , string (patchJSON )); err != nil {
284+ return fmt .Errorf ("failed to patch kube-dns ConfigMap on cluster %s: %w" , clusterName , err )
285+ }
286+ t .Logf ("[gcp] Patched kube-dns ConfigMap with stubDomains on cluster %s" , clusterName )
287+
288+ if err := k8s .RunKubectlE (t , kubectlOpts , "delete" , "pods" ,
289+ "-l" , "k8s-app=node-local-dns" , "--grace-period=0" , "--force" ); err != nil {
290+ t .Logf ("[gcp] Warning: force-delete of node-local-dns pods failed (may be harmless): %v" , err )
291+ }
292+
293+ _ , err = retry .DoWithRetryE (t , "wait for node-local-dns rollout" , defaultRetries , defaultRetryInterval ,
294+ func () (string , error ) {
295+ return k8s .RunKubectlAndGetOutputE (t , kubectlOpts ,
296+ "rollout" , "status" , "daemonset/node-local-dns" , "--timeout=10s" )
297+ })
298+ if err != nil {
299+ return fmt .Errorf ("node-local-dns did not become ready after patching on cluster %s: %w" , clusterName , err )
300+ }
301+
302+ t .Logf ("[gcp] node-local-dns ready with stub domains for custom cluster domains on cluster %s" , clusterName )
303+ return nil
230304}
231305
232306// TeardownInfra deletes all GCP resources created by SetUpInfra.
@@ -627,16 +701,20 @@ func createGKERegionalCluster(ctx context.Context, client *container.Service, se
627701 "--tags" , strings .Join ([]string {defaultNodeTag }, "," ), // Join tags if there are multiple
628702 "--enable-master-authorized-networks" ,
629703 "--master-authorized-networks" , strings .Join ([]string {"0.0.0.0/0" }, "," ),
630- "--num-nodes" , fmt .Sprint (defaultNodesPerZone ),
704+ "--num-nodes" , fmt .Sprint (defaultNodesPerZone + 1 ), // 2 nodes/zone avoids autoscaling during TestClusterScaleUp
631705 "--min-nodes" , fmt .Sprint (defaultNodesPerZone ),
632- "--max-nodes" , fmt .Sprint (defaultNodesPerZone + 1 ), // Needed for scaling cluster
706+ "--max-nodes" , fmt .Sprint (defaultNodesPerZone + 2 ), // headroom above initial count
633707 "--enable-autoscaling" , // Enable autoscaling
634708 "--autoprovisioning-network-tags" , strings .Join ([]string {autoprovisioningNodeTag }, "," ),
635709 "--machine-type" , gcpDefaultMachineType ,
636710 "--disk-size" , "30GB" , // Limit disk size to 30GB
637711 "--quiet" , // Suppress interactive prompts
638712 }
639713
714+ if sa := getNodeServiceAccount (); sa != "" {
715+ args = append (args , "--service-account" , sa )
716+ }
717+
640718 cmd := exec .Command ("gcloud" , args ... )
641719
642720 // Stream gcloud's stdout/stderr directly for real-time visibility into the long-running creation process.
0 commit comments