Skip to content

Commit 6ae3157

Browse files
authoredMay 3, 2024··
Merge pull request #366 from nais/rotate-jdbc-url
Update JDBC URL when rotating password
2 parents 450d27d + da21b8b commit 6ae3157

File tree

6 files changed

+294
-10
lines changed

6 files changed

+294
-10
lines changed
 

‎go.mod

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module github.com/nais/cli
22

33
go 1.22
44

5+
toolchain go1.22.2
6+
57
require (
68
cloud.google.com/go/cloudsqlconn v1.9.0
79
github.com/GoogleCloudPlatform/cloudsql-proxy v1.35.1
@@ -45,12 +47,14 @@ require (
4547
github.com/go-openapi/jsonpointer v0.20.0 // indirect
4648
github.com/go-openapi/jsonreference v0.20.2 // indirect
4749
github.com/go-openapi/swag v0.22.4 // indirect
50+
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
4851
github.com/gogo/protobuf v1.3.2 // indirect
4952
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
5053
github.com/golang/protobuf v1.5.4 // indirect
5154
github.com/google/gnostic-models v0.6.8 // indirect
5255
github.com/google/go-cmp v0.6.0 // indirect
5356
github.com/google/gofuzz v1.2.0 // indirect
57+
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
5458
github.com/google/s2a-go v0.1.7 // indirect
5559
github.com/google/uuid v1.6.0 // indirect
5660
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
@@ -65,6 +69,8 @@ require (
6569
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
6670
github.com/modern-go/reflect2 v1.0.2 // indirect
6771
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
72+
github.com/onsi/ginkgo/v2 v2.17.1 // indirect
73+
github.com/onsi/gomega v1.33.0 // indirect
6874
github.com/pkg/errors v0.9.1 // indirect
6975
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
7076
github.com/prometheus/client_golang v1.18.0 // indirect

‎go.sum

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
1717
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
1818
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
1919
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
20+
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
21+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
22+
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
2023
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
2124
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
2225
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
@@ -112,6 +115,7 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF
112115
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
113116
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
114117
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
118+
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
115119
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
116120
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
117121
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
@@ -174,8 +178,12 @@ github.com/nais/liberator v0.0.0-20231027133155-2fd0a0affcd1 h1:GDNEgBlmxfid7B6v
174178
github.com/nais/liberator v0.0.0-20231027133155-2fd0a0affcd1/go.mod h1:cWThp1WBBbkRFhMI2DQMvBTTEN+6GPzmmh+Xjv8vffE=
175179
github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY=
176180
github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw=
181+
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
182+
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
177183
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
178184
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
185+
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
186+
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
179187
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
180188
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
181189
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -204,6 +212,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
204212
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
205213
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
206214
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
215+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
207216
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
208217
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
209218
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -280,6 +289,7 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
280289
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
281290
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
282291
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
292+
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
283293
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
284294
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
285295
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=

‎pkg/postgres/dbinfo.go

+36-9
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,21 @@ func (i *DBInfo) dbConnectionMultiDB(ctx context.Context) (*ConnectionInfo, erro
144144
}
145145

146146
func createConnectionInfo(secret corev1.Secret, instance string) *ConnectionInfo {
147-
u, err := url.Parse(getSecretDataValue(secret, "_URL"))
148-
if err != nil {
149-
panic(err)
147+
var pgUrl *url.URL
148+
var jdbcUrl *url.URL
149+
var err error
150+
for name, val := range secret.Data {
151+
if strings.HasSuffix(name, "_URL") {
152+
value := string(val)
153+
if strings.HasSuffix(name, "_JDBC_URL") {
154+
jdbcUrl, err = url.Parse(value)
155+
} else {
156+
pgUrl, err = url.Parse(value)
157+
}
158+
if err != nil {
159+
panic(err)
160+
}
161+
}
150162
}
151163

152164
return &ConnectionInfo{
@@ -155,7 +167,8 @@ func createConnectionInfo(secret corev1.Secret, instance string) *ConnectionInfo
155167
dbName: getSecretDataValue(secret, "_DATABASE"),
156168
port: getSecretDataValue(secret, "_PORT"),
157169
host: getSecretDataValue(secret, "_HOST"),
158-
url: u,
170+
url: pgUrl,
171+
jdbcUrl: jdbcUrl,
159172
instance: instance,
160173
}
161174
}
@@ -279,19 +292,33 @@ type ConnectionInfo struct {
279292
port string
280293
host string
281294
url *url.URL
295+
jdbcUrl *url.URL
282296
}
283297

284298
func (c *ConnectionInfo) ProxyConnectionString() string {
285299
return fmt.Sprintf("host=%v user=%v dbname=%v password=%v sslmode=disable", c.instance, c.username, c.dbName, c.password)
286300
}
287301

288-
func (c *ConnectionInfo) JDBCURL() string {
289-
return c.url.String()
290-
}
291-
292302
func (c *ConnectionInfo) SetPassword(password string) {
293303
c.password = password
294-
c.url.User = url.UserPassword(c.username, password)
304+
if c.url != nil {
305+
c.url.User = url.UserPassword(c.username, password)
306+
}
307+
if c.jdbcUrl != nil {
308+
queries := c.jdbcUrl.Query()
309+
queries.Set("password", password)
310+
c.jdbcUrl.RawQuery = queries.Encode()
311+
} else if c.url != nil {
312+
queries := c.url.Query()
313+
queries.Set("password", password)
314+
queries.Set("user", c.username)
315+
c.jdbcUrl = &url.URL{
316+
Scheme: "jdbc:postgresql",
317+
Host: c.url.Host,
318+
Path: c.dbName,
319+
RawQuery: queries.Encode(),
320+
}
321+
}
295322
}
296323

297324
func getSecretDataValue(secret corev1.Secret, suffix string) string {

‎pkg/postgres/password.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,28 @@ func updateKubernetesSecret(ctx context.Context, dbInfo *DBInfo, dbConnectionInf
6868
return fmt.Errorf("unable to the k8s secret %q in %q: %w", "google-sql-"+dbInfo.appName, dbInfo.namespace, err)
6969
}
7070

71+
jdbcUrlSet := false
72+
prefix := ""
7173
for key := range secret.Data {
7274
if strings.HasSuffix(key, "_PASSWORD") {
7375
secret.Data[key] = []byte(dbConnectionInfo.password)
7476
}
7577
if strings.HasSuffix(key, "_URL") {
76-
secret.Data[key] = []byte(dbConnectionInfo.JDBCURL())
78+
if strings.HasSuffix(key, "_JDBC_URL") && dbConnectionInfo.jdbcUrl != nil {
79+
secret.Data[key] = []byte(dbConnectionInfo.jdbcUrl.String())
80+
jdbcUrlSet = true
81+
} else if dbConnectionInfo.url != nil {
82+
secret.Data[key] = []byte(dbConnectionInfo.url.String())
83+
prefix = strings.TrimSuffix(key, "_URL")
84+
}
7785
}
7886
}
7987

88+
if !jdbcUrlSet && dbConnectionInfo.jdbcUrl != nil && len(prefix) > 0 {
89+
key := prefix + "_JDBC_URL"
90+
secret.Data[key] = []byte(dbConnectionInfo.jdbcUrl.String())
91+
}
92+
8093
_, err = dbInfo.k8sClient.CoreV1().Secrets(dbInfo.namespace).Update(ctx, secret, v1.UpdateOptions{})
8194
if err != nil {
8295
return fmt.Errorf("failed updating k8s secret %q in %q with new password: %w", "google-sql-"+dbInfo.appName, dbInfo.namespace, err)

‎pkg/postgres/password_test.go

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package postgres
2+
3+
import (
4+
"context"
5+
"fmt"
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
"github.com/onsi/gomega/types"
9+
core_v1 "k8s.io/api/core/v1"
10+
"k8s.io/apimachinery/pkg/api/meta"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/client-go/kubernetes"
13+
"k8s.io/client-go/kubernetes/fake"
14+
"net/url"
15+
"strings"
16+
)
17+
18+
const (
19+
namespace = "password-ns"
20+
secretName = "google-sql-password-app"
21+
appName = "password-app"
22+
newPassword = "new-password"
23+
oldPassword = "old-password"
24+
25+
jdbcUrlTmpl = "jdbc:postgresql://localhost:5432/my-database?user=my-user&password=%s"
26+
pgUrlTmpl = "postgresql://my-user:%s@localhost:5432/my-database"
27+
)
28+
29+
var newJdbcUrl *url.URL
30+
var newPgUrl *url.URL
31+
32+
func init() {
33+
var err error
34+
newJdbcUrl, err = url.Parse(fmt.Sprintf(jdbcUrlTmpl, newPassword))
35+
if err != nil {
36+
panic(err)
37+
}
38+
39+
newPgUrl, err = url.Parse(fmt.Sprintf(pgUrlTmpl, newPassword))
40+
if err != nil {
41+
panic(err)
42+
}
43+
}
44+
45+
type test struct {
46+
secretPrep []SecretPrep
47+
assertSecret []AssertSecret
48+
}
49+
50+
var _ = Describe("Password", func() {
51+
var k8sClient *fake.Clientset
52+
var secret *core_v1.Secret
53+
54+
BeforeEach(func() {
55+
k8sClient = fake.NewSimpleClientset()
56+
secret = &core_v1.Secret{
57+
TypeMeta: metav1.TypeMeta{
58+
Kind: "Secret",
59+
APIVersion: "v1",
60+
},
61+
ObjectMeta: metav1.ObjectMeta{
62+
Name: secretName,
63+
Namespace: namespace,
64+
},
65+
Data: map[string][]byte{
66+
"DB_HOST": []byte("localhost"),
67+
"DB_PORT": []byte("5432"),
68+
"DB_DATABASE": []byte("my-database"),
69+
"DB_USERNAME": []byte("my-user"),
70+
},
71+
}
72+
})
73+
74+
DescribeTableSubtree("",
75+
func(test test) {
76+
var dbInfo *DBInfo
77+
var dbConnectionInfo *ConnectionInfo
78+
79+
BeforeEach(func() {
80+
for _, prep := range test.secretPrep {
81+
prep(secret)
82+
}
83+
84+
err := k8sClient.Tracker().Add(secret)
85+
Expect(err).To(BeNil())
86+
87+
dbInfo = createDbInfo(k8sClient)
88+
dbConnectionInfo = createConnectionInfo(*secret, dbInfo.instanceName)
89+
})
90+
91+
It("rotating password", func(ctx context.Context) {
92+
dbConnectionInfo.SetPassword(newPassword)
93+
94+
err := updateKubernetesSecret(ctx, dbInfo, dbConnectionInfo)
95+
Expect(err).To(BeNil())
96+
97+
gvr, _ := meta.UnsafeGuessKindToResource(secret.GroupVersionKind())
98+
actual, err := k8sClient.Tracker().Get(gvr, secret.Namespace, secret.Name)
99+
Expect(err).To(BeNil())
100+
101+
actualSecret, ok := actual.(*core_v1.Secret)
102+
Expect(ok).To(BeTrue())
103+
104+
for _, assert := range test.assertSecret {
105+
assert(actualSecret)
106+
}
107+
})
108+
},
109+
Entry("has only password", test{
110+
secretPrep: []SecretPrep{AddPassword},
111+
assertSecret: []AssertSecret{HasPassword, HasNoUrl, HasNoJdbcUrl},
112+
}),
113+
Entry("has password and url", test{
114+
secretPrep: []SecretPrep{AddPassword, AddUrl},
115+
assertSecret: []AssertSecret{HasPassword, HasUrl, HasJdbcUrl},
116+
}),
117+
Entry("has all", test{
118+
secretPrep: []SecretPrep{AddPassword, AddUrl, AddJdbcUrl},
119+
assertSecret: []AssertSecret{HasPassword, HasUrl, HasJdbcUrl},
120+
}),
121+
Entry("has password and jdbc url", test{
122+
secretPrep: []SecretPrep{AddPassword, AddJdbcUrl},
123+
assertSecret: []AssertSecret{HasPassword, HasNoUrl, HasJdbcUrl},
124+
}),
125+
)
126+
})
127+
128+
func createDbInfo(k8sClient kubernetes.Interface) *DBInfo {
129+
return &DBInfo{
130+
k8sClient: k8sClient,
131+
dynamicClient: nil,
132+
config: nil,
133+
namespace: namespace,
134+
appName: appName,
135+
projectID: "project-id",
136+
connectionName: "connection:name",
137+
multiDB: false,
138+
instanceName: "my-instance",
139+
databaseName: "my-database",
140+
user: "my-user",
141+
}
142+
}
143+
144+
type SecretPrep func(secret *core_v1.Secret)
145+
146+
func AddPassword(secret *core_v1.Secret) {
147+
secret.Data["DB_PASSWORD"] = []byte(oldPassword)
148+
}
149+
150+
func AddUrl(secret *core_v1.Secret) {
151+
secret.Data["DB_URL"] = []byte(fmt.Sprintf(pgUrlTmpl, oldPassword))
152+
}
153+
154+
func AddJdbcUrl(secret *core_v1.Secret) {
155+
secret.Data["DB_JDBC_URL"] = []byte(fmt.Sprintf(jdbcUrlTmpl, oldPassword))
156+
}
157+
158+
type AssertSecret func(actual *core_v1.Secret)
159+
160+
func EqualUrlNoQuery(expected *url.URL) types.GomegaMatcher {
161+
expectedNoQuery, _, _ := strings.Cut(expected.String(), "?")
162+
return WithTransform(func(actual *url.URL) string {
163+
actualNoQuery, _, _ := strings.Cut(actual.String(), "?")
164+
return actualNoQuery
165+
}, Equal(expectedNoQuery))
166+
}
167+
168+
func EqualQuery(expected *url.URL) types.GomegaMatcher {
169+
expectedQuery := expected.Query()
170+
return WithTransform(func(actual *url.URL) url.Values {
171+
return actual.Query()
172+
}, Equal(expectedQuery))
173+
}
174+
175+
func HasPassword(actual *core_v1.Secret) {
176+
By("should have new password in DB_PASSWORD", func() {
177+
Expect(actual.Data["DB_PASSWORD"]).To(Equal([]byte(newPassword)))
178+
})
179+
}
180+
181+
func HasUrl(actual *core_v1.Secret) {
182+
By("should have new password in DB_URL", func() {
183+
u, err := url.Parse(string(actual.Data["DB_URL"]))
184+
Expect(err).To(BeNil())
185+
Expect(u).To(SatisfyAll(
186+
EqualUrlNoQuery(newPgUrl),
187+
EqualQuery(newPgUrl),
188+
))
189+
})
190+
}
191+
192+
func HasNoUrl(actual *core_v1.Secret) {
193+
By("should not have DB_URL", func() {
194+
_, ok := actual.Data["DB_URL"]
195+
Expect(ok).To(BeFalse())
196+
})
197+
}
198+
199+
func HasJdbcUrl(actual *core_v1.Secret) {
200+
By("should have new password in DB_JDBC_URL", func() {
201+
u, err := url.Parse(string(actual.Data["DB_JDBC_URL"]))
202+
Expect(err).To(BeNil())
203+
Expect(u).To(SatisfyAll(
204+
EqualUrlNoQuery(newJdbcUrl),
205+
EqualQuery(newJdbcUrl),
206+
))
207+
})
208+
}
209+
210+
func HasNoJdbcUrl(actual *core_v1.Secret) {
211+
By("should not have DB_JDBC_URL", func() {
212+
_, ok := actual.Data["DB_JDBC_URL"]
213+
Expect(ok).To(BeFalse())
214+
})
215+
}

‎pkg/postgres/postgres_suite_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package postgres
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestPostgres(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Postgres Suite")
13+
}

0 commit comments

Comments
 (0)
Please sign in to comment.