Skip to content

Commit 0f2afad

Browse files
committed
feat(postgresql): add TLS related options
This adds `sslCert`, `sslKey` and `sslRootCert` options to the PostgreSQL `ProviderConfig` needed to establish a connection to a database that either uses non-public certificates or requires client certificate authentication. I've opted to create the `Options` struct to hold these and the existing `sslMode` parameter, this will allow adding other options[1] easier in the future. [1] https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS Signed-off-by: Zoran Regvart <[email protected]>
1 parent c742efe commit 0f2afad

File tree

15 files changed

+151
-33
lines changed

15 files changed

+151
-33
lines changed

apis/postgresql/v1alpha1/provider_types.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package v1alpha1
1919
import (
2020
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2121

22+
"github.com/crossplane-contrib/provider-sql/pkg/clients"
23+
"github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql"
2224
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
2325
)
2426

@@ -36,6 +38,24 @@ type ProviderConfigSpec struct {
3638
// +kubebuilder:default=verify-full
3739
// +kubebuilder:validation:Optional
3840
SSLMode *string `json:"sslMode,omitempty"`
41+
// Path to the certificate used for client authentication
42+
// +kubebuilder:validation:Optional
43+
SSLCert *string `json:"sslCert,omitempty"`
44+
// Path to the key used for client authentication
45+
// +kubebuilder:validation:Optional
46+
SSLKey *string `json:"sslKey,omitempty"`
47+
// Path to the CA certificate(s) used for verifying the server certificate
48+
// +kubebuilder:validation:Optional
49+
SSLRootCert *string `json:"sslRootCert,omitempty"`
50+
}
51+
52+
func (s ProviderConfigSpec) Options() postgresql.Options {
53+
return postgresql.Options{
54+
SSLMode: clients.ToString(s.SSLMode),
55+
SSLCert: clients.ToString(s.SSLCert),
56+
SSLKey: clients.ToString(s.SSLKey),
57+
SSLRootCert: clients.ToString(s.SSLRootCert),
58+
}
3959
}
4060

4161
const (

apis/postgresql/v1alpha1/zz_generated.deepcopy.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package/crds/postgresql.sql.crossplane.io_providerconfigs.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ spec:
8484
Defines the database name used to set up a connection to the provided
8585
PostgreSQL instance. Same as PGDATABASE environment variable.
8686
type: string
87+
sslCert:
88+
description: Path to the certificate used for client authentication
89+
type: string
90+
sslKey:
91+
description: Path to the key used for client authentication
92+
type: string
8793
sslMode:
8894
default: verify-full
8995
description: |-
@@ -95,6 +101,10 @@ spec:
95101
- verify-ca
96102
- verify-full
97103
type: string
104+
sslRootCert:
105+
description: Path to the CA certificate(s) used for verifying the
106+
server certificate
107+
type: string
98108
required:
99109
- credentials
100110
type: object

pkg/clients/postgresql/postgresql.go

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,62 @@ type postgresDB struct {
2323
dsn string
2424
endpoint string
2525
port string
26-
sslmode string
26+
options Options
27+
}
28+
29+
type Options struct {
30+
SSLMode string
31+
SSLCert string
32+
SSLKey string
33+
SSLRootCert string
34+
}
35+
36+
func (o Options) queryString() string {
37+
values := url.Values{}
38+
39+
if o.SSLMode != "" {
40+
values.Add("sslmode", o.SSLMode)
41+
}
42+
43+
if o.SSLCert != "" {
44+
values.Add("sslcert", o.SSLCert)
45+
}
46+
47+
if o.SSLKey != "" {
48+
values.Add("sslkey", o.SSLKey)
49+
}
50+
51+
if o.SSLRootCert != "" {
52+
values.Add("sslrootcert", o.SSLRootCert)
53+
}
54+
55+
return values.Encode()
2756
}
2857

2958
// New returns a new PostgreSQL database client. The default database name is
3059
// an empty string. The underlying pq library will default to either using the
3160
// value of PGDATABASE, or if unset, the hardcoded string 'postgres'.
32-
// The sslmode defines the mode used to set up the connection for the provider.
33-
func New(creds map[string][]byte, database, sslmode string) xsql.DB {
61+
// The options provide additional settings to set up the connection for the
62+
// provider.
63+
func New(creds map[string][]byte, database string, options Options) xsql.DB {
3464
// TODO(negz): Support alternative connection secret formats?
3565
endpoint := string(creds[xpv1.ResourceCredentialsSecretEndpointKey])
3666
port := string(creds[xpv1.ResourceCredentialsSecretPortKey])
3767
username := string(creds[xpv1.ResourceCredentialsSecretUserKey])
3868
password := string(creds[xpv1.ResourceCredentialsSecretPasswordKey])
39-
dsn := DSN(username, password, endpoint, port, database, sslmode)
69+
70+
dsn := DSN(username, password, endpoint, port, database, options.queryString())
4071

4172
return postgresDB{
4273
dsn: dsn,
4374
endpoint: endpoint,
4475
port: port,
45-
sslmode: sslmode,
76+
options: options,
4677
}
4778
}
4879

4980
// DSN returns the DSN URL
50-
func DSN(username, password, endpoint, port, database, sslmode string) string {
81+
func DSN(username, password, endpoint, port, database, options string) string {
5182
// Use net/url UserPassword to encode the username and password
5283
// This will ensure that any special characters in the username or password
5384
// are percent-encoded for use in the user info portion of the DSN URL
@@ -57,7 +88,8 @@ func DSN(username, password, endpoint, port, database, sslmode string) string {
5788
endpoint + ":" +
5889
port + "/" +
5990
database +
60-
"?sslmode=" + sslmode
91+
"?" + options
92+
6193
}
6294

6395
// ExecTx executes an array of queries, committing if all are successful and
@@ -130,10 +162,13 @@ func (c postgresDB) Scan(ctx context.Context, q xsql.Query, dest ...interface{})
130162
// GetConnectionDetails returns the connection details for a user of this DB
131163
func (c postgresDB) GetConnectionDetails(username, password string) managed.ConnectionDetails {
132164
return managed.ConnectionDetails{
133-
xpv1.ResourceCredentialsSecretUserKey: []byte(username),
134-
xpv1.ResourceCredentialsSecretPasswordKey: []byte(password),
135-
xpv1.ResourceCredentialsSecretEndpointKey: []byte(c.endpoint),
136-
xpv1.ResourceCredentialsSecretPortKey: []byte(c.port),
165+
xpv1.ResourceCredentialsSecretUserKey: []byte(username),
166+
xpv1.ResourceCredentialsSecretPasswordKey: []byte(password),
167+
xpv1.ResourceCredentialsSecretEndpointKey: []byte(c.endpoint),
168+
xpv1.ResourceCredentialsSecretPortKey: []byte(c.port),
169+
xpv1.ResourceCredentialsSecretClientCertKey: []byte(c.options.SSLCert),
170+
xpv1.ResourceCredentialsSecretClientKeyKey: []byte(c.options.SSLKey),
171+
xpv1.ResourceCredentialsSecretCAKey: []byte(c.options.SSLRootCert),
137172
}
138173
}
139174

pkg/clients/postgresql/postgresql_test.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,44 @@ func TestDSNURLEscaping(t *testing.T) {
1212
rawPass := "password^"
1313
encPass := "password%5E"
1414
sslmode := "require"
15-
dsn := DSN(user, rawPass, endpoint, port, db, sslmode)
15+
options := Options{
16+
SSLMode: sslmode,
17+
}
18+
dsn := DSN(user, rawPass, endpoint, port, db, options.queryString())
1619
if dsn != "postgres://"+user+":"+encPass+"@"+endpoint+":"+port+"/"+db+"?sslmode="+sslmode {
1720
t.Errorf("DSN string did not match expected output with userinfo URL encoded")
1821
}
1922
}
23+
24+
func TestOptionsToQueryString(t *testing.T) {
25+
cases := []struct {
26+
name string
27+
given Options
28+
expected string
29+
}{
30+
{
31+
name: "empty",
32+
given: Options{},
33+
expected: "",
34+
},
35+
{
36+
name: "everything",
37+
given: Options{
38+
SSLMode: "require",
39+
SSLCert: "/path/to/ssl.crt",
40+
SSLKey: "/path/to/ssl.key",
41+
SSLRootCert: "/path/to/ca.crt",
42+
},
43+
expected: "sslcert=%2Fpath%2Fto%2Fssl.crt&sslkey=%2Fpath%2Fto%2Fssl.key&sslmode=require&sslrootcert=%2Fpath%2Fto%2Fca.crt",
44+
},
45+
}
46+
47+
for _, c := range cases {
48+
t.Run(c.name, func(t *testing.T) {
49+
got := c.given.queryString()
50+
if c.expected != got {
51+
t.Errorf("Expected query string to be %q, but it was %q", c.expected, got)
52+
}
53+
})
54+
}
55+
}

pkg/controller/postgresql/database/reconciler.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import (
4040
"github.com/crossplane/crossplane-runtime/pkg/resource"
4141

4242
"github.com/crossplane-contrib/provider-sql/apis/postgresql/v1alpha1"
43-
"github.com/crossplane-contrib/provider-sql/pkg/clients"
4443
"github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql"
4544
"github.com/crossplane-contrib/provider-sql/pkg/clients/xsql"
4645
)
@@ -93,7 +92,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
9392
type connector struct {
9493
kube client.Client
9594
usage resource.Tracker
96-
newDB func(creds map[string][]byte, database string, sslmode string) xsql.DB
95+
newDB func(creds map[string][]byte, database string, options postgresql.Options) xsql.DB
9796
}
9897

9998
func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) {
@@ -126,7 +125,7 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E
126125
return nil, errors.Wrap(err, errGetSecret)
127126
}
128127

129-
return &external{db: c.newDB(s.Data, pc.Spec.DefaultDatabase, clients.ToString(pc.Spec.SSLMode))}, nil
128+
return &external{db: c.newDB(s.Data, pc.Spec.DefaultDatabase, pc.Spec.Options())}, nil
130129
}
131130

132131
type external struct{ db xsql.DB }

pkg/controller/postgresql/database/reconciler_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/crossplane/crossplane-runtime/pkg/resource"
3333
"github.com/crossplane/crossplane-runtime/pkg/test"
3434

35+
"github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql"
3536
"github.com/crossplane-contrib/provider-sql/pkg/clients/xsql"
3637
)
3738

@@ -64,7 +65,7 @@ func TestConnect(t *testing.T) {
6465
type fields struct {
6566
kube client.Client
6667
usage resource.Tracker
67-
newDB func(creds map[string][]byte, database string, sslmode string) xsql.DB
68+
newDB func(creds map[string][]byte, database string, options postgresql.Options) xsql.DB
6869
}
6970

7071
type args struct {

pkg/controller/postgresql/extension/reconciler.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import (
3636
"github.com/crossplane/crossplane-runtime/pkg/resource"
3737

3838
"github.com/crossplane-contrib/provider-sql/apis/postgresql/v1alpha1"
39-
"github.com/crossplane-contrib/provider-sql/pkg/clients"
4039
"github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql"
4140
"github.com/crossplane-contrib/provider-sql/pkg/clients/xsql"
4241
)
@@ -85,7 +84,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
8584
type connector struct {
8685
kube client.Client
8786
usage resource.Tracker
88-
newDB func(creds map[string][]byte, database string, sslmode string) xsql.DB
87+
newDB func(creds map[string][]byte, database string, options postgresql.Options) xsql.DB
8988
}
9089

9190
func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) {
@@ -118,13 +117,15 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E
118117
return nil, errors.Wrap(err, errGetSecret)
119118
}
120119

120+
options := pc.Spec.Options()
121+
121122
// We do not want to create an extension on the default DB
122123
// if the user was expecting a database name to be resolved.
123124
if cr.Spec.ForProvider.Database != nil {
124-
return &external{db: c.newDB(s.Data, *cr.Spec.ForProvider.Database, clients.ToString(pc.Spec.SSLMode))}, nil
125+
return &external{db: c.newDB(s.Data, *cr.Spec.ForProvider.Database, options)}, nil
125126
}
126127

127-
return &external{db: c.newDB(s.Data, pc.Spec.DefaultDatabase, clients.ToString(pc.Spec.SSLMode))}, nil
128+
return &external{db: c.newDB(s.Data, pc.Spec.DefaultDatabase, options)}, nil
128129
}
129130

130131
type external struct{ db xsql.DB }

pkg/controller/postgresql/extension/reconciler_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/crossplane/crossplane-runtime/pkg/resource"
3333
"github.com/crossplane/crossplane-runtime/pkg/test"
3434

35+
"github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql"
3536
"github.com/crossplane-contrib/provider-sql/pkg/clients/xsql"
3637
)
3738

@@ -64,7 +65,7 @@ func TestConnect(t *testing.T) {
6465
type fields struct {
6566
kube client.Client
6667
usage resource.Tracker
67-
newDB func(creds map[string][]byte, database string, sslmode string) xsql.DB
68+
newDB func(creds map[string][]byte, database string, options postgresql.Options) xsql.DB
6869
}
6970

7071
type args struct {

pkg/controller/postgresql/grant/reconciler.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import (
3737
"github.com/crossplane/crossplane-runtime/pkg/resource"
3838

3939
"github.com/crossplane-contrib/provider-sql/apis/postgresql/v1alpha1"
40-
"github.com/crossplane-contrib/provider-sql/pkg/clients"
4140
"github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql"
4241
"github.com/crossplane-contrib/provider-sql/pkg/clients/xsql"
4342
)
@@ -94,7 +93,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
9493
type connector struct {
9594
kube client.Client
9695
usage resource.Tracker
97-
newDB func(creds map[string][]byte, database string, sslmode string) xsql.DB
96+
newDB func(creds map[string][]byte, database string, options postgresql.Options) xsql.DB
9897
}
9998

10099
func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) {
@@ -127,7 +126,7 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E
127126
return nil, errors.Wrap(err, errGetSecret)
128127
}
129128
return &external{
130-
db: c.newDB(s.Data, pc.Spec.DefaultDatabase, clients.ToString(pc.Spec.SSLMode)),
129+
db: c.newDB(s.Data, pc.Spec.DefaultDatabase, pc.Spec.Options()),
131130
kube: c.kube,
132131
}, nil
133132
}

0 commit comments

Comments
 (0)