Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/docs/guides/parent-gcp-gke-autopilot.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ resources:
externalEgressIp:
enabled: true # Enables CloudNAT with static IP
# existing: "projects/my-project/regions/europe-west3/addresses/my-static-ip" # Optional: use existing IP
# Cloud NAT port tuning (all optional; defaults: 64 min ports, EIM on, DPA off).
# Raise these when pods open many concurrent outbound connections and you see
# source-port exhaustion (dropped SYNs / dial i/o timeouts):
# minPortsPerVm: 1024
# dynamicPortAllocation: true # requires endpointIndependentMapping: false
# endpointIndependentMapping: false
```

### **What Private VPC Does**
Expand Down
12 changes: 12 additions & 0 deletions docs/schemas/gcp/gkeautopilotresource.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,23 @@
"externalEgressIp": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"dynamicPortAllocation": {
"type": "boolean"
},
"enabled": {
"type": "boolean"
},
"endpointIndependentMapping": {
"type": "boolean"
},
"existing": {
"type": "string"
},
"maxPortsPerVm": {
"type": "integer"
},
"minPortsPerVm": {
"type": "integer"
}
},
"required": [
Expand Down
6 changes: 6 additions & 0 deletions docs/schemas/gcp/postgresgcpcloudsqlconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,15 @@
"pointInTimeRecoveryEnabled": {
"type": "boolean"
},
"privateNetwork": {
"type": "string"
},
"project": {
"type": "string"
},
"publicIpEnabled": {
"type": "boolean"
},
"queryInsightsEnabled": {
"type": "boolean"
},
Expand Down
14 changes: 7 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ require (
github.com/disgoorg/snowflake/v2 v2.0.3
github.com/dustin/go-humanize v1.0.1
github.com/fatih/color v1.19.0
github.com/go-delve/delve v1.26.3
github.com/go-delve/delve v1.27.0
github.com/go-git/go-billy/v5 v5.9.0
github.com/go-git/go-git/v5 v5.19.1
github.com/golangci/golangci-lint v1.64.8
Expand Down Expand Up @@ -61,9 +61,9 @@ require (
gocloud.dev v0.46.0
golang.org/x/crypto v0.53.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.21.0
golang.org/x/sync v0.22.0
golang.org/x/term v0.44.0
golang.org/x/text v0.38.0
golang.org/x/text v0.40.0
google.golang.org/api v0.284.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -464,12 +464,12 @@ require (
golang.org/x/arch v0.11.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect
golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.45.0 // indirect
golang.org/x/tools v0.47.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
Expand Down
28 changes: 14 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,8 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w=
github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w=
github.com/go-delve/delve v1.26.3 h1:uCWPnLLYmVRXLt0yhw305sCi5lQLHzYB2fZ0FB3KLUI=
github.com/go-delve/delve v1.26.3/go.mod h1:Ua/k2AAu4cLrUXGSRVH1b2Nzq2aCK188b9EYlAojlz4=
github.com/go-delve/delve v1.27.0 h1:i66Einw/sQhm0hlbjLNUNxrwCmKdTcIqpyHVqSWAbd0=
github.com/go-delve/delve v1.27.0/go.mod h1:l6Xb1ype6VEKoKZCaOg7mGN5B8Zi3Sn+K7qs5Pqqc8k=
github.com/go-delve/liner v1.2.3-0.20231231155935-4726ab1d7f62 h1:IGtvsNyIuRjl04XAOFGACozgUD7A82UffYxZt4DWbvA=
github.com/go-delve/liner v1.2.3-0.20231231155935-4726ab1d7f62/go.mod h1:biJCRbqp51wS+I92HMqn5H8/A0PAhxn2vyOT+JqhiGI=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
Expand Down Expand Up @@ -1094,8 +1094,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
Expand All @@ -1113,8 +1113,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -1126,8 +1126,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down Expand Up @@ -1160,8 +1160,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc=
golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
Expand All @@ -1183,8 +1183,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand All @@ -1209,8 +1209,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
Expand Down
90 changes: 90 additions & 0 deletions pkg/clouds/gcloud/config_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: MIT
// Copyright (c) Simple Container

package gcloud

import (
"testing"

. "github.com/onsi/gomega"
"github.com/samber/lo"
)

// Cloud NAT port tuning is validated against the EFFECTIVE values (defaults
// applied) and GCP's real bounds, so misconfigurations fail here instead of late
// at pulumi up.
func TestExternalEgressIpConfig_NatTuningValidate(t *testing.T) {
tests := []struct {
name string
cfg ExternalEgressIpConfig
errSubstr string
}{
{name: "valid dynamic port allocation", cfg: ExternalEgressIpConfig{Enabled: true, MinPortsPerVm: lo.ToPtr(1024), MaxPortsPerVm: lo.ToPtr(8192), DynamicPortAllocation: lo.ToPtr(true), EndpointIndependentMapping: lo.ToPtr(false)}},
{name: "valid raised min without dpa", cfg: ExternalEgressIpConfig{Enabled: true, MinPortsPerVm: lo.ToPtr(1024)}},
{name: "min below floor", cfg: ExternalEgressIpConfig{Enabled: true, MinPortsPerVm: lo.ToPtr(16)}, errSubstr: "minPortsPerVm must be between"},
{name: "min above ceiling", cfg: ExternalEgressIpConfig{Enabled: true, MinPortsPerVm: lo.ToPtr(70000)}, errSubstr: "minPortsPerVm must be between"},
{name: "max below floor", cfg: ExternalEgressIpConfig{Enabled: true, MaxPortsPerVm: lo.ToPtr(16)}, errSubstr: "maxPortsPerVm must be between"},
{name: "max above ceiling", cfg: ExternalEgressIpConfig{Enabled: true, MaxPortsPerVm: lo.ToPtr(70000)}, errSubstr: "maxPortsPerVm must be between"},
{name: "lone max below default min", cfg: ExternalEgressIpConfig{Enabled: true, MaxPortsPerVm: lo.ToPtr(48)}, errSubstr: "must be >= minPortsPerVm"},
{name: "explicit max below min", cfg: ExternalEgressIpConfig{Enabled: true, MinPortsPerVm: lo.ToPtr(4096), MaxPortsPerVm: lo.ToPtr(1024)}, errSubstr: "must be >= minPortsPerVm"},
{name: "dpa requires eim off when unset", cfg: ExternalEgressIpConfig{Enabled: true, DynamicPortAllocation: lo.ToPtr(true)}, errSubstr: "endpointIndependentMapping: false"},
{name: "dpa requires eim off when explicitly true", cfg: ExternalEgressIpConfig{Enabled: true, DynamicPortAllocation: lo.ToPtr(true), EndpointIndependentMapping: lo.ToPtr(true)}, errSubstr: "endpointIndependentMapping: false"},
{name: "dpa min not power of two", cfg: ExternalEgressIpConfig{Enabled: true, DynamicPortAllocation: lo.ToPtr(true), EndpointIndependentMapping: lo.ToPtr(false), MinPortsPerVm: lo.ToPtr(1000)}, errSubstr: "minPortsPerVm must be a power of two"},
{name: "dpa max not power of two", cfg: ExternalEgressIpConfig{Enabled: true, DynamicPortAllocation: lo.ToPtr(true), EndpointIndependentMapping: lo.ToPtr(false), MaxPortsPerVm: lo.ToPtr(3000)}, errSubstr: "maxPortsPerVm must be a power of two"},
{name: "dpa max equals min", cfg: ExternalEgressIpConfig{Enabled: true, DynamicPortAllocation: lo.ToPtr(true), EndpointIndependentMapping: lo.ToPtr(false), MinPortsPerVm: lo.ToPtr(1024), MaxPortsPerVm: lo.ToPtr(1024)}, errSubstr: "must be > minPortsPerVm"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
RegisterTestingT(t)
err := tc.cfg.Validate()
if tc.errSubstr == "" {
Expect(err).ToNot(HaveOccurred())
} else {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(tc.errSubstr))
}
})
}
}

func TestPostgresGcpCloudsqlConfig_Validate(t *testing.T) {
tests := []struct {
name string
cfg PostgresGcpCloudsqlConfig
errSubstr string
}{
{name: "empty is valid", cfg: PostgresGcpCloudsqlConfig{}},
{name: "availabilityType invalid", cfg: PostgresGcpCloudsqlConfig{AvailabilityType: lo.ToPtr("HA")}, errSubstr: "availabilityType must be"},
{name: "availabilityType regional ok", cfg: PostgresGcpCloudsqlConfig{AvailabilityType: lo.ToPtr("REGIONAL")}},
{name: "privateNetwork bad format", cfg: PostgresGcpCloudsqlConfig{PrivateNetwork: lo.ToPtr("my-vpc")}, errSubstr: "privateNetwork must be a full"},
{name: "privateNetwork ok", cfg: PostgresGcpCloudsqlConfig{PrivateNetwork: lo.ToPtr("projects/p/global/networks/vpc")}},
{name: "public off without network", cfg: PostgresGcpCloudsqlConfig{PublicIpEnabled: lo.ToPtr(false)}, errSubstr: "requires privateNetwork"},
{name: "public off with empty network", cfg: PostgresGcpCloudsqlConfig{PublicIpEnabled: lo.ToPtr(false), PrivateNetwork: lo.ToPtr("")}, errSubstr: "requires privateNetwork"},
{name: "public off with network ok", cfg: PostgresGcpCloudsqlConfig{PublicIpEnabled: lo.ToPtr(false), PrivateNetwork: lo.ToPtr("projects/p/global/networks/vpc")}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
RegisterTestingT(t)
err := tc.cfg.Validate()
if tc.errSubstr == "" {
Expect(err).ToNot(HaveOccurred())
} else {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(tc.errSubstr))
}
})
}
}

func TestPostgresGcpCloudsqlConfig_ProxyAndNetworkHelpers(t *testing.T) {
RegisterTestingT(t)
Expect((&PostgresGcpCloudsqlConfig{}).HasPrivateNetwork()).To(BeFalse())
Expect((&PostgresGcpCloudsqlConfig{PrivateNetwork: lo.ToPtr("")}).HasPrivateNetwork()).To(BeFalse())
Expect((&PostgresGcpCloudsqlConfig{PrivateNetwork: lo.ToPtr("projects/p/global/networks/vpc")}).HasPrivateNetwork()).To(BeTrue())

// Proxy stays on the public endpoint until the public IP is disabled.
Expect((&PostgresGcpCloudsqlConfig{}).UsesPrivateIpProxy()).To(BeFalse())
Expect((&PostgresGcpCloudsqlConfig{PublicIpEnabled: lo.ToPtr(true)}).UsesPrivateIpProxy()).To(BeFalse())
Expect((&PostgresGcpCloudsqlConfig{PrivateNetwork: lo.ToPtr("projects/p/global/networks/vpc")}).UsesPrivateIpProxy()).To(BeFalse())
Expect((&PostgresGcpCloudsqlConfig{PublicIpEnabled: lo.ToPtr(false)}).UsesPrivateIpProxy()).To(BeTrue())
}
68 changes: 68 additions & 0 deletions pkg/clouds/gcloud/gke_autopilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ const (
TemplateTypeGkeAutopilot = "gcp-gke-autopilot"
)

// Cloud NAT port-allocation defaults and bounds. Defaults preserve prior
// behaviour; the floor/ceiling match GCP's accepted range for ports per VM.
const (
DefaultMinPortsPerVm = 64
DefaultMaxPortsPerVm = 65536
PortsPerVmFloor = 32
PortsPerVmCeiling = 65536
)

type GkeAutopilotResource struct {
Credentials `json:",inline" yaml:",inline"`
GkeMinVersion string `json:"gkeMinVersion" yaml:"gkeMinVersion"`
Expand Down Expand Up @@ -48,6 +57,23 @@ type Timeouts struct {
type ExternalEgressIpConfig struct {
Enabled bool `json:"enabled" yaml:"enabled"`
Existing string `json:"existing,omitempty" yaml:"existing,omitempty"`

// Cloud NAT port/mapping tuning. All optional; when unset the previous
// defaults are preserved (64 min ports, endpoint-independent mapping on,
// dynamic port allocation off), so existing clusters are unaffected until
// they opt in. Raising the port budget and enabling dynamic port
// allocation avoids source-port exhaustion for pods that open many
// concurrent outbound connections (dropped SYNs surface downstream as
// dial i/o timeouts).
MinPortsPerVm *int `json:"minPortsPerVm,omitempty" yaml:"minPortsPerVm,omitempty"`
MaxPortsPerVm *int `json:"maxPortsPerVm,omitempty" yaml:"maxPortsPerVm,omitempty"`
// DynamicPortAllocation lets a VM scale its NAT ports between min and max
// on demand. GCP requires endpoint-independent mapping to be off when it is
// enabled, and both port bounds to be powers of two.
DynamicPortAllocation *bool `json:"dynamicPortAllocation,omitempty" yaml:"dynamicPortAllocation,omitempty"`
// EndpointIndependentMapping toggles NAT EIM (default true). Must be false
// to use dynamic port allocation.
EndpointIndependentMapping *bool `json:"endpointIndependentMapping,omitempty" yaml:"endpointIndependentMapping,omitempty"`
}

type GkeAutopilotTemplate struct {
Expand Down Expand Up @@ -194,5 +220,47 @@ func (c *ExternalEgressIpConfig) Validate() error {
}
}

// Validate the effective port bounds (applying the same defaults as
// resolveNatPortSettings) so a lone maxPortsPerVm below the default minimum
// is caught here rather than failing late at the GCP API.
minPorts, maxPorts := DefaultMinPortsPerVm, DefaultMaxPortsPerVm
if c.MinPortsPerVm != nil {
minPorts = *c.MinPortsPerVm
if minPorts < PortsPerVmFloor || minPorts > PortsPerVmCeiling {
return errors.Errorf("minPortsPerVm must be between %d and %d, got %d", PortsPerVmFloor, PortsPerVmCeiling, minPorts)
}
}
if c.MaxPortsPerVm != nil {
maxPorts = *c.MaxPortsPerVm
if maxPorts < PortsPerVmFloor || maxPorts > PortsPerVmCeiling {
return errors.Errorf("maxPortsPerVm must be between %d and %d, got %d", PortsPerVmFloor, PortsPerVmCeiling, maxPorts)
}
}
if maxPorts < minPorts {
return errors.Errorf("effective maxPortsPerVm (%d) must be >= minPortsPerVm (%d)", maxPorts, minPorts)
}

if c.DynamicPortAllocation != nil && *c.DynamicPortAllocation {
// GCP rejects dynamic port allocation together with endpoint-independent
// mapping, needs both bounds to be powers of two, and needs a real range
// (max strictly greater than min).
if c.EndpointIndependentMapping == nil || *c.EndpointIndependentMapping {
return errors.New("dynamicPortAllocation requires endpointIndependentMapping: false")
}
if !isPowerOfTwo(minPorts) {
return errors.Errorf("minPortsPerVm must be a power of two when dynamicPortAllocation is enabled, got %d", minPorts)
}
if !isPowerOfTwo(maxPorts) {
return errors.Errorf("maxPortsPerVm must be a power of two when dynamicPortAllocation is enabled, got %d", maxPorts)
}
if maxPorts <= minPorts {
return errors.Errorf("effective maxPortsPerVm (%d) must be > minPortsPerVm (%d) when dynamicPortAllocation is enabled", maxPorts, minPorts)
}
}

return nil
}

func isPowerOfTwo(n int) bool {
return n > 0 && n&(n-1) == 0
}
Loading
Loading