-
-
Notifications
You must be signed in to change notification settings - Fork 512
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TLS wait strategy that makes it easy to construct a TLS config from details stored in a PEM formatted files in the container, allowing clients to connect securely even when using a self signed certificate. Use embed to simplify wait test loading of certs.
- Loading branch information
Showing
14 changed files
with
379 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# TLS Strategy | ||
|
||
TLS Strategy waits for one or more files to exist in the container and uses them | ||
and other details to construct a `tls.Config` which can be used to create secure | ||
connections. | ||
|
||
It supports: | ||
|
||
- x509 PEM Certificate loaded from a certificate / key file pair. | ||
- Root Certificate Authorities aka RootCAs loaded from PEM encoded files. | ||
- Server name. | ||
- Startup timeout to be used in seconds, default is 60 seconds. | ||
- Poll interval to be used in milliseconds, default is 100 milliseconds. | ||
|
||
## Waiting for certificate pair | ||
|
||
The following snippets show how to configure a request to wait for certificate | ||
pair to exist once started and then read the | ||
[tls.Config](https://pkg.go.dev/crypto/tls#Config), alongside how to copy a test | ||
certificate pair into a container image using a `Dockerfile`. | ||
|
||
It should be noted that copying certificate pairs into an images is only an | ||
example which might be useful for testing with testcontainers-go and should not | ||
be done with production images as that could expose your certificates if your | ||
images become public. | ||
|
||
<!--codeinclude--> | ||
[Wait for certificate](../../../wait/tls_test.go) inside_block:waitForTLSCert | ||
[Read TLS Config](../../../wait/tls_test.go) inside_block:waitTLSConfig | ||
[Dockerfile with certificate](../../../wait/testdata/http/Dockerfile) | ||
<!--/codeinclude--> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
-----BEGIN EC PRIVATE KEY----- | ||
MHcCAQEEIM8HuDwcZyVqBBy2C6db6zNb/dAJ69bq5ejAEz7qGOIQoAoGCCqGSM49 | ||
AwEHoUQDQgAEBL2ioRmfTc70WT0vyx+amSQOGbMeoMRAfF2qaPzpzOqpKTk0aLOG | ||
0735iy9Fz16PX4vqnLMiM/ZupugAhB//yA== | ||
-----END EC PRIVATE KEY----- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
-----BEGIN CERTIFICATE----- | ||
MIIBxTCCAWugAwIBAgIUWBLNpiF1o4r+5ZXwawzPOfBM1F8wCgYIKoZIzj0EAwIw | ||
ADAeFw0yMDA4MTkxMzM4MDBaFw0zMDA4MTcxMzM4MDBaMBkxFzAVBgNVBAMTDnRl | ||
c3Rjb250YWluZXJzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBL2ioRmfTc70 | ||
WT0vyx+amSQOGbMeoMRAfF2qaPzpzOqpKTk0aLOG0735iy9Fz16PX4vqnLMiM/Zu | ||
pugAhB//yKOBqTCBpjAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUH | ||
AwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUTMdz5PIZ+Gix4jYUzRIHfByrW+Yw | ||
HwYDVR0jBBgwFoAUFdfV6PSYUlHs+lSQNouRwSfR2ZgwMQYDVR0RBCowKIIVdGVz | ||
dGNvbnRhaW5lci5nby50ZXN0gglsb2NhbGhvc3SHBH8AAAEwCgYIKoZIzj0EAwID | ||
SAAwRQIhAJznPNumi2Plf0GsP9DpC+8WukT/jUhnhcDWCfZ6Ini2AiBLhnhFebZX | ||
XWfSsdSNxIo20OWvy6z3wqdybZtRUfdU+g== | ||
-----END CERTIFICATE----- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package wait | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"fmt" | ||
"io" | ||
"time" | ||
) | ||
|
||
// Validate we implement interface. | ||
var _ Strategy = (*TLSStrategy)(nil) | ||
|
||
// TLSStrategy is a strategy for handling TLS. | ||
type TLSStrategy struct { | ||
// General Settings. | ||
timeout *time.Duration | ||
pollInterval time.Duration | ||
|
||
// Custom Settings. | ||
certFiles *x509KeyPair | ||
rootFiles []string | ||
|
||
// State. | ||
tlsConfig *tls.Config | ||
} | ||
|
||
// x509KeyPair is a pair of certificate and key files. | ||
type x509KeyPair struct { | ||
certPEMFile string | ||
keyPEMFile string | ||
} | ||
|
||
// ForTLSCert returns a CertStrategy that will add a Certificate to the [tls.Config] | ||
// constructed from PEM formatted certificate key file pair in the container. | ||
func ForTLSCert(certPEMFile, keyPEMFile string) *TLSStrategy { | ||
return &TLSStrategy{ | ||
certFiles: &x509KeyPair{ | ||
certPEMFile: certPEMFile, | ||
keyPEMFile: keyPEMFile, | ||
}, | ||
tlsConfig: &tls.Config{}, | ||
pollInterval: defaultPollInterval(), | ||
} | ||
} | ||
|
||
// ForTLSRootCAs returns a CertStrategy that sets the root CAs for the [tls.Config] | ||
// using the given PEM formatted files from the container. | ||
func ForTLSRootCAs(pemFiles ...string) *TLSStrategy { | ||
return &TLSStrategy{ | ||
rootFiles: pemFiles, | ||
tlsConfig: &tls.Config{}, | ||
pollInterval: defaultPollInterval(), | ||
} | ||
} | ||
|
||
// WithRootCAs sets the root CAs for the [tls.Config] using the given files from | ||
// the container. | ||
func (ws *TLSStrategy) WithRootCAs(files ...string) *TLSStrategy { | ||
ws.rootFiles = files | ||
return ws | ||
} | ||
|
||
// WithCert sets the [tls.Config] Certificates using the given files from the container. | ||
func (ws *TLSStrategy) WithCert(certPEMFile, keyPEMFile string) *TLSStrategy { | ||
ws.certFiles = &x509KeyPair{ | ||
certPEMFile: certPEMFile, | ||
keyPEMFile: keyPEMFile, | ||
} | ||
return ws | ||
} | ||
|
||
// WithServerName sets the server for the [tls.Config]. | ||
func (ws *TLSStrategy) WithServerName(serverName string) *TLSStrategy { | ||
ws.tlsConfig.ServerName = serverName | ||
return ws | ||
} | ||
|
||
// WithStartupTimeout can be used to change the default startup timeout. | ||
func (ws *TLSStrategy) WithStartupTimeout(startupTimeout time.Duration) *TLSStrategy { | ||
ws.timeout = &startupTimeout | ||
return ws | ||
} | ||
|
||
// WithPollInterval can be used to override the default polling interval of 100 milliseconds. | ||
func (ws *TLSStrategy) WithPollInterval(pollInterval time.Duration) *TLSStrategy { | ||
ws.pollInterval = pollInterval | ||
return ws | ||
} | ||
|
||
// TLSConfig returns the TLS config once the strategy is ready. | ||
// If the strategy is nil, it returns nil. | ||
func (ws *TLSStrategy) TLSConfig() *tls.Config { | ||
if ws == nil { | ||
return nil | ||
} | ||
|
||
return ws.tlsConfig | ||
} | ||
|
||
// WaitUntilReady implements the [Strategy] interface. | ||
// It waits for the CA, client cert and key files to be available in the container and | ||
// uses them to setup the TLS config. | ||
func (ws *TLSStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { | ||
size := len(ws.rootFiles) | ||
if ws.certFiles != nil { | ||
size += 2 | ||
} | ||
strategies := make([]Strategy, 0, size) | ||
for _, file := range ws.rootFiles { | ||
strategies = append(strategies, | ||
ForFile(file).WithMatcher(func(r io.Reader) error { | ||
buf, err := io.ReadAll(r) | ||
if err != nil { | ||
return fmt.Errorf("read CA cert file %q: %w", file, err) | ||
} | ||
|
||
if ws.tlsConfig.RootCAs == nil { | ||
ws.tlsConfig.RootCAs = x509.NewCertPool() | ||
} | ||
|
||
if !ws.tlsConfig.RootCAs.AppendCertsFromPEM(buf) { | ||
return fmt.Errorf("invalid CA cert file %q", file) | ||
} | ||
|
||
return nil | ||
}).WithPollInterval(ws.pollInterval), | ||
) | ||
} | ||
|
||
if ws.certFiles != nil { | ||
var certPEMBlock []byte | ||
strategies = append(strategies, | ||
ForFile(ws.certFiles.certPEMFile).WithMatcher(func(r io.Reader) error { | ||
var err error | ||
if certPEMBlock, err = io.ReadAll(r); err != nil { | ||
return fmt.Errorf("read certificate cert %q: %w", ws.certFiles.certPEMFile, err) | ||
} | ||
|
||
return nil | ||
}).WithPollInterval(ws.pollInterval), | ||
ForFile(ws.certFiles.keyPEMFile).WithMatcher(func(r io.Reader) error { | ||
keyPEMBlock, err := io.ReadAll(r) | ||
if err != nil { | ||
return fmt.Errorf("read certificate key %q: %w", ws.certFiles.keyPEMFile, err) | ||
} | ||
|
||
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) | ||
if err != nil { | ||
return fmt.Errorf("x509 key pair %q %q: %w", ws.certFiles.certPEMFile, ws.certFiles.keyPEMFile, err) | ||
} | ||
|
||
ws.tlsConfig.Certificates = []tls.Certificate{cert} | ||
|
||
return nil | ||
}).WithPollInterval(ws.pollInterval), | ||
) | ||
} | ||
|
||
strategy := ForAll(strategies...) | ||
if ws.timeout != nil { | ||
strategy.WithStartupTimeout(*ws.timeout) | ||
} | ||
|
||
return strategy.WaitUntilReady(ctx, target) | ||
} |
Oops, something went wrong.