-
-
Notifications
You must be signed in to change notification settings - Fork 586
feat(wait): tls strategy #2896
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
feat(wait): tls strategy #2896
Changes from all commits
0bf53e2
a5c2c0e
27489b5
77cc34a
58ba86c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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--> |
| 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----- |
| 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----- |
| 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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: do you think it would be interesting to provide a way for consumers to forget about generating certs and the library build them on the fly? Something like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep love that idea, something for a follow up PR. |
||
| 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) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.