Skip to content

Commit a369b9d

Browse files
committed
fix: reuse http client for externaldata requests
Signed-off-by: Maneesh Singh <[email protected]>
1 parent c2efb00 commit a369b9d

File tree

6 files changed

+155
-135
lines changed

6 files changed

+155
-135
lines changed

constraint/pkg/client/drivers/rego/builtin.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package rego
22

33
import (
4+
"crypto/tls"
5+
"crypto/x509"
6+
"encoding/base64"
7+
"fmt"
48
"net/http"
9+
"net/url"
510
"time"
611

12+
"github.com/open-policy-agent/frameworks/constraint/pkg/apis/externaldata/unversioned"
713
"github.com/open-policy-agent/frameworks/constraint/pkg/externaldata"
814
"github.com/open-policy-agent/opa/ast"
915
"github.com/open-policy-agent/opa/rego"
@@ -12,6 +18,9 @@ import (
1218
const (
1319
providerResponseAPIVersion = "externaldata.gatekeeper.sh/v1beta1"
1420
providerResponseKind = "ProviderResponse"
21+
HTTPSScheme = "https"
22+
idleConnTimeout = 90 * time.Second
23+
maxIdleConnsPerHost = 100
1524
)
1625

1726
func externalDataBuiltin(d *Driver) func(bctx rego.BuiltinContext, regorequest *ast.Term) (*ast.Term, error) {
@@ -31,6 +40,12 @@ func externalDataBuiltin(d *Driver) func(bctx rego.BuiltinContext, regorequest *
3140
return externaldata.HandleError(http.StatusBadRequest, err)
3241
}
3342

43+
client, err := getClient(&provider, clientCert)
44+
if err != nil {
45+
return externaldata.HandleError(http.StatusInternalServerError,
46+
fmt.Errorf("failed to get HTTP client: %w", err))
47+
}
48+
3449
// check provider response cache
3550
var providerRequestKeys []string
3651
var providerResponseStatusCode int
@@ -71,7 +86,7 @@ func externalDataBuiltin(d *Driver) func(bctx rego.BuiltinContext, regorequest *
7186
}
7287

7388
if len(providerRequestKeys) > 0 {
74-
externaldataResponse, statusCode, err := d.sendRequestToProvider(bctx.Context, &provider, providerRequestKeys, clientCert)
89+
externaldataResponse, statusCode, err := d.sendRequestToProvider(bctx.Context, &provider, providerRequestKeys, client)
7590
if err != nil {
7691
return externaldata.HandleError(statusCode, err)
7792
}
@@ -115,3 +130,49 @@ func externalDataBuiltin(d *Driver) func(bctx rego.BuiltinContext, regorequest *
115130
return externaldata.PrepareRegoResponse(regoResponse)
116131
}
117132
}
133+
134+
// getClient returns a new HTTP client, and set up its TLS configuration.
135+
func getClient(provider *unversioned.Provider, clientCert *tls.Certificate) (*http.Client, error) {
136+
u, err := url.Parse(provider.Spec.URL)
137+
if err != nil {
138+
return nil, fmt.Errorf("failed to parse provider URL %s: %w", provider.Spec.URL, err)
139+
}
140+
141+
if u.Scheme != HTTPSScheme {
142+
return nil, fmt.Errorf("only HTTPS scheme is supported")
143+
}
144+
145+
client := &http.Client{
146+
Timeout: time.Duration(provider.Spec.Timeout) * time.Second,
147+
}
148+
149+
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS13}
150+
151+
// present our client cert to the server
152+
// in case provider wants to verify it
153+
if clientCert != nil {
154+
tlsConfig.Certificates = []tls.Certificate{*clientCert}
155+
}
156+
157+
// if the provider presents its own CA bundle,
158+
// we will use it to verify the server's certificate
159+
caBundleData, err := base64.StdEncoding.DecodeString(provider.Spec.CABundle)
160+
if err != nil {
161+
return nil, fmt.Errorf("failed to decode CA bundle: %w", err)
162+
}
163+
164+
providerCertPool := x509.NewCertPool()
165+
if ok := providerCertPool.AppendCertsFromPEM(caBundleData); !ok {
166+
return nil, fmt.Errorf("failed to append provider's CA bundle to certificate pool")
167+
}
168+
169+
tlsConfig.RootCAs = providerCertPool
170+
171+
client.Transport = &http.Transport{
172+
TLSClientConfig: tlsConfig,
173+
IdleConnTimeout: idleConnTimeout,
174+
MaxIdleConnsPerHost: maxIdleConnsPerHost,
175+
}
176+
177+
return client, nil
178+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package rego
2+
3+
import (
4+
"crypto/tls"
5+
"testing"
6+
7+
"github.com/open-policy-agent/frameworks/constraint/pkg/apis/externaldata/unversioned"
8+
)
9+
10+
const (
11+
validCABundle = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUIwekNDQVgyZ0F3SUJBZ0lKQUkvTTdCWWp3Qit1TUEwR0NTcUdTSWIzRFFFQkJRVUFNRVV4Q3pBSkJnTlYKQkFZVEFrRlZNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWAphV1JuYVhSeklGQjBlU0JNZEdRd0hoY05NVEl3T1RFeU1qRTFNakF5V2hjTk1UVXdPVEV5TWpFMU1qQXlXakJGCk1Rc3dDUVlEVlFRR0V3SkJWVEVUTUJFR0ExVUVDQXdLVTI5dFpTMVRkR0YwWlRFaE1COEdBMVVFQ2d3WVNXNTAKWlhKdVpYUWdWMmxrWjJsMGN5QlFkSGtnVEhSa01Gd3dEUVlKS29aSWh2Y05BUUVCQlFBRFN3QXdTQUpCQU5MSgpoUEhoSVRxUWJQa2xHM2liQ1Z4d0dNUmZwL3Y0WHFoZmRRSGRjVmZIYXA2TlE1V29rLzR4SUErdWkzNS9NbU5hCnJ0TnVDK0JkWjF0TXVWQ1BGWmNDQXdFQUFhTlFNRTR3SFFZRFZSME9CQllFRkp2S3M4UmZKYVhUSDA4VytTR3YKelF5S24wSDhNQjhHQTFVZEl3UVlNQmFBRkp2S3M4UmZKYVhUSDA4VytTR3Z6UXlLbjBIOE1Bd0dBMVVkRXdRRgpNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEUVFCSmxmZkpIeWJqREd4Uk1xYVJtRGhYMCs2djAyVFVLWnNXCnI1UXVWYnBRaEg2dSswVWdjVzBqcDlRd3B4b1BUTFRXR1hFV0JCQnVyeEZ3aUNCaGtRK1YKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
12+
badCABundle = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCmhlbGxvCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
13+
)
14+
15+
func Test_getClient(t *testing.T) {
16+
type args struct {
17+
provider *unversioned.Provider
18+
clientCert *tls.Certificate
19+
}
20+
tests := []struct {
21+
name string
22+
args args
23+
wantErr bool
24+
}{
25+
{
26+
name: "invalid http url",
27+
args: args{
28+
provider: &unversioned.Provider{
29+
Spec: unversioned.ProviderSpec{
30+
URL: "http://foo",
31+
},
32+
},
33+
clientCert: nil,
34+
},
35+
wantErr: true,
36+
},
37+
{
38+
name: "no CA bundle",
39+
args: args{
40+
provider: &unversioned.Provider{
41+
Spec: unversioned.ProviderSpec{
42+
URL: "https://foo",
43+
},
44+
},
45+
clientCert: nil,
46+
},
47+
wantErr: true,
48+
},
49+
{
50+
name: "invalid CA bundle",
51+
args: args{
52+
provider: &unversioned.Provider{
53+
Spec: unversioned.ProviderSpec{
54+
URL: "https://foo",
55+
CABundle: badCABundle,
56+
},
57+
},
58+
clientCert: nil,
59+
},
60+
wantErr: true,
61+
},
62+
{
63+
name: "valid CA bundle",
64+
args: args{
65+
provider: &unversioned.Provider{
66+
Spec: unversioned.ProviderSpec{
67+
URL: "https://foo",
68+
CABundle: validCABundle,
69+
},
70+
},
71+
clientCert: nil,
72+
},
73+
wantErr: false,
74+
},
75+
}
76+
for _, tt := range tests {
77+
t.Run(tt.name, func(t *testing.T) {
78+
_, err := getClient(tt.args.provider, tt.args.clientCert)
79+
if (err != nil) != tt.wantErr {
80+
t.Errorf("getClient() error = %v, wantErr %v", err, tt.wantErr)
81+
return
82+
}
83+
})
84+
}
85+
}

constraint/pkg/client/drivers/rego/driver_unit_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package rego
22

33
import (
44
"context"
5-
"crypto/tls"
65
"errors"
76
"fmt"
87
"net/http"
@@ -676,7 +675,7 @@ func TestDriver_ExternalData(t *testing.T) {
676675
},
677676
clientCertContent: clientCert,
678677
clientKeyContent: clientKey,
679-
sendRequestToProvider: func(_ context.Context, _ *unversioned.Provider, _ []string, _ *tls.Certificate) (*externaldata.ProviderResponse, int, error) {
678+
sendRequestToProvider: func(_ context.Context, _ *unversioned.Provider, _ []string, _ *http.Client) (*externaldata.ProviderResponse, int, error) {
680679
return nil, http.StatusBadRequest, errors.New("error from SendRequestToProvider")
681680
},
682681
errorExpected: true,
@@ -695,7 +694,7 @@ func TestDriver_ExternalData(t *testing.T) {
695694
},
696695
clientCertContent: clientCert,
697696
clientKeyContent: clientKey,
698-
sendRequestToProvider: func(_ context.Context, _ *unversioned.Provider, _ []string, _ *tls.Certificate) (*externaldata.ProviderResponse, int, error) {
697+
sendRequestToProvider: func(_ context.Context, _ *unversioned.Provider, _ []string, _ *http.Client) (*externaldata.ProviderResponse, int, error) {
699698
return &externaldata.ProviderResponse{
700699
APIVersion: "v1beta1",
701700
Kind: "Provider",

constraint/pkg/externaldata/cache.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import (
1414
"k8s.io/apimachinery/pkg/util/wait"
1515
)
1616

17+
const (
18+
HTTPSScheme = "https"
19+
)
20+
1721
type ProviderCache struct {
1822
cache map[string]unversioned.Provider
1923
mux sync.RWMutex

constraint/pkg/externaldata/request.go

Lines changed: 2 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,15 @@ package externaldata
33
import (
44
"bytes"
55
"context"
6-
"crypto/tls"
7-
"crypto/x509"
8-
"encoding/base64"
96
"encoding/json"
107
"fmt"
118
"io"
129
"net/http"
13-
"net/url"
1410
"time"
1511

1612
"github.com/open-policy-agent/frameworks/constraint/pkg/apis/externaldata/unversioned"
1713
)
1814

19-
const (
20-
HTTPScheme = "http"
21-
HTTPSScheme = "https"
22-
)
23-
2415
// RegoRequest is the request for external_data rego function.
2516
type RegoRequest struct {
2617
// ProviderName is the name of the external data provider.
@@ -57,17 +48,16 @@ func NewProviderRequest(keys []string) *ProviderRequest {
5748
}
5849

5950
// SendRequestToProvider is a function that sends a request to the external data provider.
60-
type SendRequestToProvider func(ctx context.Context, provider *unversioned.Provider, keys []string, clientCert *tls.Certificate) (*ProviderResponse, int, error)
51+
type SendRequestToProvider func(ctx context.Context, provider *unversioned.Provider, keys []string, client *http.Client) (*ProviderResponse, int, error)
6152

6253
// DefaultSendRequestToProvider is the default function to send the request to the external data provider.
63-
func DefaultSendRequestToProvider(ctx context.Context, provider *unversioned.Provider, keys []string, clientCert *tls.Certificate) (*ProviderResponse, int, error) {
54+
func DefaultSendRequestToProvider(ctx context.Context, provider *unversioned.Provider, keys []string, client *http.Client) (*ProviderResponse, int, error) {
6455
externaldataRequest := NewProviderRequest(keys)
6556
body, err := json.Marshal(externaldataRequest)
6657
if err != nil {
6758
return nil, http.StatusInternalServerError, fmt.Errorf("failed to marshal external data request: %w", err)
6859
}
6960

70-
client, err := getClient(provider, clientCert)
7161
if err != nil {
7262
return nil, http.StatusInternalServerError, fmt.Errorf("failed to get HTTP client: %w", err)
7363
}
@@ -100,50 +90,6 @@ func DefaultSendRequestToProvider(ctx context.Context, provider *unversioned.Pro
10090
return &externaldataResponse, resp.StatusCode, nil
10191
}
10292

103-
// getClient returns a new HTTP client, and set up its TLS configuration.
104-
func getClient(provider *unversioned.Provider, clientCert *tls.Certificate) (*http.Client, error) {
105-
u, err := url.Parse(provider.Spec.URL)
106-
if err != nil {
107-
return nil, fmt.Errorf("failed to parse provider URL %s: %w", provider.Spec.URL, err)
108-
}
109-
110-
if u.Scheme != HTTPSScheme {
111-
return nil, fmt.Errorf("only HTTPS scheme is supported")
112-
}
113-
114-
client := &http.Client{
115-
Timeout: time.Duration(provider.Spec.Timeout) * time.Second,
116-
}
117-
118-
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS13}
119-
120-
// present our client cert to the server
121-
// in case provider wants to verify it
122-
if clientCert != nil {
123-
tlsConfig.Certificates = []tls.Certificate{*clientCert}
124-
}
125-
126-
// if the provider presents its own CA bundle,
127-
// we will use it to verify the server's certificate
128-
caBundleData, err := base64.StdEncoding.DecodeString(provider.Spec.CABundle)
129-
if err != nil {
130-
return nil, fmt.Errorf("failed to decode CA bundle: %w", err)
131-
}
132-
133-
providerCertPool := x509.NewCertPool()
134-
if ok := providerCertPool.AppendCertsFromPEM(caBundleData); !ok {
135-
return nil, fmt.Errorf("failed to append provider's CA bundle to certificate pool")
136-
}
137-
138-
tlsConfig.RootCAs = providerCertPool
139-
140-
client.Transport = &http.Transport{
141-
TLSClientConfig: tlsConfig,
142-
}
143-
144-
return client, nil
145-
}
146-
14793
// ProviderKind strings are special string constants for Providers.
14894
// +kubebuilder:validation:Enum=ProviderRequestKind;ProviderResponseKind
14995
type ProviderKind string

0 commit comments

Comments
 (0)