Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update JDBC URL when rotating password #366

Merged
merged 6 commits into from
May 3, 2024
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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module github.com/nais/cli

go 1.22

toolchain go1.22.2

require (
cloud.google.com/go/cloudsqlconn v1.9.0
github.com/GoogleCloudPlatform/cloudsql-proxy v1.35.1
Expand Down Expand Up @@ -45,12 +47,14 @@ require (
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
Expand All @@ -65,6 +69,8 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.17.1 // indirect
github.com/onsi/gomega v1.33.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
Expand Down Expand Up @@ -112,6 +115,7 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
Expand Down Expand Up @@ -174,8 +178,12 @@ github.com/nais/liberator v0.0.0-20231027133155-2fd0a0affcd1 h1:GDNEgBlmxfid7B6v
github.com/nais/liberator v0.0.0-20231027133155-2fd0a0affcd1/go.mod h1:cWThp1WBBbkRFhMI2DQMvBTTEN+6GPzmmh+Xjv8vffE=
github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY=
github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw=
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -204,6 +212,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
Expand Down Expand Up @@ -280,6 +289,7 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
Expand Down
45 changes: 36 additions & 9 deletions pkg/postgres/dbinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,21 @@ func (i *DBInfo) dbConnectionMultiDB(ctx context.Context) (*ConnectionInfo, erro
}

func createConnectionInfo(secret corev1.Secret, instance string) *ConnectionInfo {
u, err := url.Parse(getSecretDataValue(secret, "_URL"))
if err != nil {
panic(err)
var pgUrl *url.URL
var jdbcUrl *url.URL
var err error
for name, val := range secret.Data {
if strings.HasSuffix(name, "_URL") {
value := string(val)
if strings.HasSuffix(name, "_JDBC_URL") {
jdbcUrl, err = url.Parse(value)
} else {
pgUrl, err = url.Parse(value)
}
if err != nil {
panic(err)
}
}
}

return &ConnectionInfo{
Expand All @@ -155,7 +167,8 @@ func createConnectionInfo(secret corev1.Secret, instance string) *ConnectionInfo
dbName: getSecretDataValue(secret, "_DATABASE"),
port: getSecretDataValue(secret, "_PORT"),
host: getSecretDataValue(secret, "_HOST"),
url: u,
url: pgUrl,
jdbcUrl: jdbcUrl,
instance: instance,
}
}
Expand Down Expand Up @@ -279,19 +292,33 @@ type ConnectionInfo struct {
port string
host string
url *url.URL
jdbcUrl *url.URL
}

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

func (c *ConnectionInfo) JDBCURL() string {
return c.url.String()
}

func (c *ConnectionInfo) SetPassword(password string) {
c.password = password
c.url.User = url.UserPassword(c.username, password)
if c.url != nil {
c.url.User = url.UserPassword(c.username, password)
}
if c.jdbcUrl != nil {
queries := c.jdbcUrl.Query()
queries.Set("password", password)
c.jdbcUrl.RawQuery = queries.Encode()
} else if c.url != nil {
queries := c.url.Query()
queries.Set("password", password)
queries.Set("user", c.username)
c.jdbcUrl = &url.URL{
Scheme: "jdbc:postgresql",
Host: c.url.Host,
Path: c.dbName,
RawQuery: queries.Encode(),
}
}
}

func getSecretDataValue(secret corev1.Secret, suffix string) string {
Expand Down
15 changes: 14 additions & 1 deletion pkg/postgres/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,28 @@ func updateKubernetesSecret(ctx context.Context, dbInfo *DBInfo, dbConnectionInf
return fmt.Errorf("unable to the k8s secret %q in %q: %w", "google-sql-"+dbInfo.appName, dbInfo.namespace, err)
}

jdbcUrlSet := false
prefix := ""
for key := range secret.Data {
if strings.HasSuffix(key, "_PASSWORD") {
secret.Data[key] = []byte(dbConnectionInfo.password)
}
if strings.HasSuffix(key, "_URL") {
secret.Data[key] = []byte(dbConnectionInfo.JDBCURL())
if strings.HasSuffix(key, "_JDBC_URL") && dbConnectionInfo.jdbcUrl != nil {
secret.Data[key] = []byte(dbConnectionInfo.jdbcUrl.String())
jdbcUrlSet = true
} else if dbConnectionInfo.url != nil {
secret.Data[key] = []byte(dbConnectionInfo.url.String())
prefix = strings.TrimSuffix(key, "_URL")
}
}
}

if !jdbcUrlSet && dbConnectionInfo.jdbcUrl != nil && len(prefix) > 0 {
key := prefix + "_JDBC_URL"
secret.Data[key] = []byte(dbConnectionInfo.jdbcUrl.String())
}

_, err = dbInfo.k8sClient.CoreV1().Secrets(dbInfo.namespace).Update(ctx, secret, v1.UpdateOptions{})
if err != nil {
return fmt.Errorf("failed updating k8s secret %q in %q with new password: %w", "google-sql-"+dbInfo.appName, dbInfo.namespace, err)
Expand Down
215 changes: 215 additions & 0 deletions pkg/postgres/password_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package postgres

import (
"context"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/types"
core_v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
"net/url"
"strings"
)

const (
namespace = "password-ns"
secretName = "google-sql-password-app"
appName = "password-app"
newPassword = "new-password"
oldPassword = "old-password"

jdbcUrlTmpl = "jdbc:postgresql://localhost:5432/my-database?user=my-user&password=%s"
pgUrlTmpl = "postgresql://my-user:%s@localhost:5432/my-database"
)

var newJdbcUrl *url.URL
var newPgUrl *url.URL

func init() {
var err error
newJdbcUrl, err = url.Parse(fmt.Sprintf(jdbcUrlTmpl, newPassword))
if err != nil {
panic(err)
}

newPgUrl, err = url.Parse(fmt.Sprintf(pgUrlTmpl, newPassword))
if err != nil {
panic(err)
}
}

type test struct {
secretPrep []SecretPrep
assertSecret []AssertSecret
}

var _ = Describe("Password", func() {
var k8sClient *fake.Clientset
var secret *core_v1.Secret

BeforeEach(func() {
k8sClient = fake.NewSimpleClientset()
secret = &core_v1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Data: map[string][]byte{
"DB_HOST": []byte("localhost"),
"DB_PORT": []byte("5432"),
"DB_DATABASE": []byte("my-database"),
"DB_USERNAME": []byte("my-user"),
},
}
})

DescribeTableSubtree("",
func(test test) {
var dbInfo *DBInfo
var dbConnectionInfo *ConnectionInfo

BeforeEach(func() {
for _, prep := range test.secretPrep {
prep(secret)
}

err := k8sClient.Tracker().Add(secret)
Expect(err).To(BeNil())

dbInfo = createDbInfo(k8sClient)
dbConnectionInfo = createConnectionInfo(*secret, dbInfo.instanceName)
})

It("rotating password", func(ctx context.Context) {
dbConnectionInfo.SetPassword(newPassword)

err := updateKubernetesSecret(ctx, dbInfo, dbConnectionInfo)
Expect(err).To(BeNil())

gvr, _ := meta.UnsafeGuessKindToResource(secret.GroupVersionKind())
actual, err := k8sClient.Tracker().Get(gvr, secret.Namespace, secret.Name)
Expect(err).To(BeNil())

actualSecret, ok := actual.(*core_v1.Secret)
Expect(ok).To(BeTrue())

for _, assert := range test.assertSecret {
assert(actualSecret)
}
})
},
Entry("has only password", test{
secretPrep: []SecretPrep{AddPassword},
assertSecret: []AssertSecret{HasPassword, HasNoUrl, HasNoJdbcUrl},
}),
Entry("has password and url", test{
secretPrep: []SecretPrep{AddPassword, AddUrl},
assertSecret: []AssertSecret{HasPassword, HasUrl, HasJdbcUrl},
}),
Entry("has all", test{
secretPrep: []SecretPrep{AddPassword, AddUrl, AddJdbcUrl},
assertSecret: []AssertSecret{HasPassword, HasUrl, HasJdbcUrl},
}),
Entry("has password and jdbc url", test{
secretPrep: []SecretPrep{AddPassword, AddJdbcUrl},
assertSecret: []AssertSecret{HasPassword, HasNoUrl, HasJdbcUrl},
}),
)
})

func createDbInfo(k8sClient kubernetes.Interface) *DBInfo {
return &DBInfo{
k8sClient: k8sClient,
dynamicClient: nil,
config: nil,
namespace: namespace,
appName: appName,
projectID: "project-id",
connectionName: "connection:name",
multiDB: false,
instanceName: "my-instance",
databaseName: "my-database",
user: "my-user",
}
}

type SecretPrep func(secret *core_v1.Secret)

func AddPassword(secret *core_v1.Secret) {
secret.Data["DB_PASSWORD"] = []byte(oldPassword)
}

func AddUrl(secret *core_v1.Secret) {
secret.Data["DB_URL"] = []byte(fmt.Sprintf(pgUrlTmpl, oldPassword))
}

func AddJdbcUrl(secret *core_v1.Secret) {
secret.Data["DB_JDBC_URL"] = []byte(fmt.Sprintf(jdbcUrlTmpl, oldPassword))
}

type AssertSecret func(actual *core_v1.Secret)

func EqualUrlNoQuery(expected *url.URL) types.GomegaMatcher {
expectedNoQuery, _, _ := strings.Cut(expected.String(), "?")
return WithTransform(func(actual *url.URL) string {
actualNoQuery, _, _ := strings.Cut(actual.String(), "?")
return actualNoQuery
}, Equal(expectedNoQuery))
}

func EqualQuery(expected *url.URL) types.GomegaMatcher {
expectedQuery := expected.Query()
return WithTransform(func(actual *url.URL) url.Values {
return actual.Query()
}, Equal(expectedQuery))
}

func HasPassword(actual *core_v1.Secret) {
By("should have new password in DB_PASSWORD", func() {
Expect(actual.Data["DB_PASSWORD"]).To(Equal([]byte(newPassword)))
})
}

func HasUrl(actual *core_v1.Secret) {
By("should have new password in DB_URL", func() {
u, err := url.Parse(string(actual.Data["DB_URL"]))
Expect(err).To(BeNil())
Expect(u).To(SatisfyAll(
EqualUrlNoQuery(newPgUrl),
EqualQuery(newPgUrl),
))
})
}

func HasNoUrl(actual *core_v1.Secret) {
By("should not have DB_URL", func() {
_, ok := actual.Data["DB_URL"]
Expect(ok).To(BeFalse())
})
}

func HasJdbcUrl(actual *core_v1.Secret) {
By("should have new password in DB_JDBC_URL", func() {
u, err := url.Parse(string(actual.Data["DB_JDBC_URL"]))
Expect(err).To(BeNil())
Expect(u).To(SatisfyAll(
EqualUrlNoQuery(newJdbcUrl),
EqualQuery(newJdbcUrl),
))
})
}

func HasNoJdbcUrl(actual *core_v1.Secret) {
By("should not have DB_JDBC_URL", func() {
_, ok := actual.Data["DB_JDBC_URL"]
Expect(ok).To(BeFalse())
})
}
Loading