Skip to content
File

Blob: acme/pebble_test.go

go135 lines
1package acme
2 
3import (
4 "bytes"
5 "context"
6 "crypto/tls"
7 "crypto/x509"
8 "fmt"
9 "os"
10 "path"
11 "testing"
12 "time"
13 
14 "github.com/testcontainers/testcontainers-go"
15 "github.com/testcontainers/testcontainers-go/wait"
16 
17 "github.com/stretchr/testify/require"
18)
19 
20const (
21 pebbleImage = "ghcr.io/letsencrypt/pebble:latest"
22 acmePort = "14000/tcp"
23 mgmtPort = "15000/tcp"
24)
25 
26// pebbleEnv holds the configuration for connecting to a Pebble ACME server
27type pebbleEnv struct {
28 ACMEURL string
29 TrustedRoots *x509.CertPool
30 container testcontainers.Container
31}
32 
33// Stop terminates the Pebble container
34func (p *pebbleEnv) Stop(ctx context.Context) error {
35 if p.container != nil {
36 return testcontainers.TerminateContainer(p.container)
37 }
38 return nil
39}
40 
41// StartPebble starts a Pebble ACME test server in a container.
42// It requires the following files to exist (run `make certs` first):
43// - dev/pebble/config.json
44// - dev/pebble/certs/cert.pem (Pebble CA)
45// - certs/pebble.pem (Pebble server cert)
46// - certs/pebble.key (Pebble server key)
47func StartPebble(t *testing.T) *pebbleEnv {
48 t.Helper()
49 as := require.New(t)
50 ctx := t.Context()
51 
52 // Resolve paths relative to this test file
53 devPebblePath := path.Join(basepath, "..", "dev", "pebble")
54 certsPath := path.Join(basepath, "..", "certs")
55 
56 // Read Pebble CA for trusted roots
57 pebbleCaPath := path.Join(devPebblePath, "certs", "cert.pem")
58 pebbleCa, err := os.ReadFile(pebbleCaPath)
59 as.NoError(err, "failed to read Pebble CA; run 'make certs' first")
60 
61 trustedRoots := x509.NewCertPool()
62 ok := trustedRoots.AppendCertsFromPEM(pebbleCa)
63 as.True(ok, "failed to parse Pebble CA certificate")
64 
65 // Read files to copy into the container
66 configPath := path.Join(devPebblePath, "config.json")
67 configData, err := os.ReadFile(configPath)
68 as.NoError(err, "failed to read Pebble config.json")
69 
70 pebblePemPath := path.Join(certsPath, "pebble.pem")
71 pebblePem, err := os.ReadFile(pebblePemPath)
72 as.NoError(err, "failed to read certs/pebble.pem; run 'make certs' first")
73 
74 pebbleKeyPath := path.Join(certsPath, "pebble.key")
75 pebbleKey, err := os.ReadFile(pebbleKeyPath)
76 as.NoError(err, "failed to read certs/pebble.key; run 'make certs' first")
77 
78 // Create a TLS config that trusts the Pebble CA for the wait strategy
79 tlsConfig := &tls.Config{
80 RootCAs: trustedRoots,
81 }
82 
83 // Start Pebble container
84 ctr, err := testcontainers.Run(ctx, pebbleImage,
85 testcontainers.WithFiles(
86 testcontainers.ContainerFile{
87 Reader: bytes.NewReader(configData),
88 ContainerFilePath: "/pebble/config.json",
89 FileMode: 0o644,
90 },
91 testcontainers.ContainerFile{
92 Reader: bytes.NewReader(pebblePem),
93 ContainerFilePath: "/certs/pebble.pem",
94 FileMode: 0o644,
95 },
96 testcontainers.ContainerFile{
97 Reader: bytes.NewReader(pebbleKey),
98 ContainerFilePath: "/certs/pebble.key",
99 FileMode: 0o600,
100 },
101 ),
102 testcontainers.WithExposedPorts(acmePort, mgmtPort),
103 testcontainers.WithEnv(map[string]string{
104 "PEBBLE_VA_NOSLEEP": "1",
105 // reduce test flakes due to certmagic waiting at least 1 minute before retrying
106 "PEBBLE_VA_ALWAYS_VALID": "1",
107 "PEBBLE_WFE_NONCEREJECT": "0",
108 }),
109 testcontainers.WithCmd("-config", "/pebble/config.json"),
110 testcontainers.WithWaitStrategy(
111 wait.ForHTTP("/dir").
112 WithTLS(true, tlsConfig).
113 WithPort(acmePort).
114 WithStartupTimeout(30*time.Second),
115 ),
116 )
117 as.NoError(err, "failed to start Pebble container")
118 
119 // Get the mapped port
120 host, err := ctr.Host(ctx)
121 as.NoError(err)
122 
123 mappedPort, err := ctr.MappedPort(ctx, acmePort)
124 as.NoError(err)
125 
126 acmeURL := fmt.Sprintf("https://%s:%s/dir", host, mappedPort.Port())
127 t.Logf("Pebble ACME server started at %s", acmeURL)
128 
129 return &pebbleEnv{
130 ACMEURL: acmeURL,
131 TrustedRoots: trustedRoots,
132 container: ctr,
133 }
134}