-
Notifications
You must be signed in to change notification settings - Fork 23
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
test: Use Pebble as the ACME server in integration tests #339
Changes from 9 commits
1ee52d4
5261057
976f568
c4bd12e
9a4b83b
5247220
1f74efd
302b707
4b5c3c3
60c891f
7d7e9f1
4e54849
11f0c63
b3fc789
c0c9f48
4ef2bd7
ff75f19
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 |
---|---|---|
|
@@ -16,7 +16,7 @@ import ( | |
certclient "github.com/gardener/cert-management/pkg/cert/client" | ||
ctrl "github.com/gardener/cert-management/pkg/controller" | ||
_ "github.com/gardener/cert-management/pkg/controller/issuer" | ||
|
||
testutils "github.com/gardener/cert-management/test/utils" | ||
"github.com/gardener/controller-manager-library/pkg/controllermanager" | ||
"github.com/gardener/controller-manager-library/pkg/controllermanager/cluster" | ||
"github.com/gardener/controller-manager-library/pkg/controllermanager/controller/mappings" | ||
|
@@ -47,19 +47,34 @@ var ( | |
ctx context.Context | ||
log logr.Logger | ||
|
||
restConfig *rest.Config | ||
testEnv *envtest.Environment | ||
testClient client.Client | ||
kubeconfigFile string | ||
restConfig *rest.Config | ||
testEnv *envtest.Environment | ||
testClient client.Client | ||
acmeDirectoryAddress string | ||
kubeconfigFile string | ||
|
||
scheme *runtime.Scheme | ||
) | ||
|
||
var _ = BeforeSuite(func() { | ||
|
||
logf.SetLogger(logger.MustNewZapLogger(logger.DebugLevel, logger.FormatJSON, zap.WriteTo(GinkgoWriter))) | ||
log = logf.Log.WithName(testID) | ||
|
||
By("Start Pebble ACME server") | ||
certificatePath, directoryAddress, err := testutils.RunPebble(log.WithName("pebble")) | ||
Expect(err).NotTo(HaveOccurred()) | ||
acmeDirectoryAddress = directoryAddress | ||
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. Sets the package scoped variable that tests will use to create |
||
|
||
// The go-acme/lego library needs to trust the TLS certificate of the Pebble ACME server. | ||
// See: https://github.com/go-acme/lego/blob/f2f5550d3a55ec1118f73346cce7a984b4d530f6/lego/client_config.go#L19-L24 | ||
Expect(os.Setenv("LEGO_CA_CERTIFICATES", certificatePath)).To(Succeed()) | ||
|
||
// Starting the Pebble TLS server is a blocking function call that runs in a separate goroutine. | ||
// As the ACME directory endpoint might not be available immediately, we wait until it is reachable. | ||
Eventually(func() error { | ||
return testutils.CheckPebbleAvailability(certificatePath, acmeDirectoryAddress) | ||
}).Should(Succeed()) | ||
|
||
By("Start test environment") | ||
testEnv = &envtest.Environment{ | ||
CRDInstallOptions: envtest.CRDInstallOptions{ | ||
|
@@ -73,8 +88,7 @@ var _ = BeforeSuite(func() { | |
ErrorIfCRDPathMissing: true, | ||
} | ||
|
||
var err error | ||
restConfig, err = testEnv.Start() | ||
restConfig, err := testEnv.Start() | ||
Expect(err).NotTo(HaveOccurred()) | ||
Expect(restConfig).NotTo(BeNil()) | ||
|
||
|
@@ -86,6 +100,7 @@ var _ = BeforeSuite(func() { | |
DeferCleanup(func() { | ||
By("Stop test environment") | ||
Expect(testEnv.Stop()).To(Succeed()) | ||
_ = os.RemoveAll(filepath.Dir(certificatePath)) | ||
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. The code responsible for starting the Pebble server generates a certificate and private key on the fly in a temporary OS directory. |
||
_ = os.Remove(kubeconfigFile) | ||
}) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,8 @@ var _ = Describe("Issuer controller tests", func() { | |
) | ||
|
||
BeforeEach(func() { | ||
Expect(acmeDirectoryAddress).NotTo(BeEmpty()) | ||
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. During development, I messed up initializing the package level variable in the test suite. Tests should assert early on that the variable has been set. |
||
|
||
ctxLocal := context.Background() | ||
ctx0 := ctxutil.CancelContext(ctxutil.WaitGroupContext(context.Background(), "main")) | ||
ctx = ctxutil.TickContext(ctx0, controllermanager.DeletionActivity) | ||
|
@@ -78,7 +80,7 @@ var _ = Describe("Issuer controller tests", func() { | |
Spec: v1alpha1.IssuerSpec{ | ||
ACME: &v1alpha1.ACMESpec{ | ||
Email: "foo@somewhere-foo-123456.com", | ||
Server: "https://acme-staging-v02.api.letsencrypt.org/directory", | ||
Server: acmeDirectoryAddress, | ||
AutoRegistration: true, | ||
}, | ||
}, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package testutils | ||
|
||
import ( | ||
"github.com/go-logr/logr" | ||
"log" | ||
"strings" | ||
) | ||
|
||
type logBridge struct { | ||
logr logr.Logger | ||
} | ||
|
||
func (logBridge *logBridge) Write(p []byte) (n int, err error) { | ||
message := strings.TrimSpace(string(p)) | ||
|
||
logBridge.logr.Info(message) | ||
|
||
return len(p), nil | ||
} | ||
|
||
// NewLogBridge creates a new log.Logger that forwards all log messages to the given logr.Logger. | ||
func NewLogBridge(logr logr.Logger) *log.Logger { | ||
writer := &logBridge{logr} | ||
|
||
return log.New(writer, "", 0) | ||
} | ||
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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
package testutils | ||
|
||
import ( | ||
"crypto/tls" | ||
"crypto/x509" | ||
"fmt" | ||
"net/http" | ||
"os" | ||
"os/exec" | ||
"runtime" | ||
|
||
"github.com/go-logr/logr" | ||
"github.com/letsencrypt/pebble/v2/ca" | ||
"github.com/letsencrypt/pebble/v2/cmd" | ||
"github.com/letsencrypt/pebble/v2/db" | ||
"github.com/letsencrypt/pebble/v2/va" | ||
"github.com/letsencrypt/pebble/v2/wfe" | ||
) | ||
|
||
// The default values for the Pebble config have been taken from: https://github.com/letsencrypt/pebble/blob/main/test/config/pebble-config.json | ||
const ( | ||
listenAddress = "localhost:14000" | ||
ocspResponderURL = "" | ||
alternateRoots = 0 | ||
chainLength = 1 | ||
certificateValidityPeriod = 0 | ||
httpPort = 5002 | ||
tlsPort = 5001 | ||
strict = true | ||
customResolverAddr = "" | ||
requireEAB = false | ||
retryAfterAuthz = 3 | ||
retryAfterOrder = 5 | ||
) | ||
|
||
// RunPebble runs a pebble server with the given configuration. | ||
// The code is copied, shortened, and adapted from: https://github.com/letsencrypt/pebble/blob/main/cmd/pebble/main.go | ||
func RunPebble(logr logr.Logger) (certificatePath, directoryAddress string, err error) { | ||
// We don't want to go through DNS-01 challenges in the integration tests as we would have to spin up a local, authoritative DNS server. | ||
// Setting the environment variable PEBBLE_VA_ALWAYS_VALID to 1 makes the Pebble server always return a valid response for the validation authority. | ||
// Testing the DNS-01 challenge is covered by the functional E2E tests. | ||
// See the Pebble documentation: https://github.com/letsencrypt/pebble#skipping-validation | ||
err = os.Setenv("PEBBLE_VA_ALWAYS_VALID", "1") | ||
if err != nil { | ||
return "", "", fmt.Errorf("failed to set environment variable: %v", err) | ||
} | ||
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. This could also be done in the test suite but as we never want to pass DNS-01 challenges in the integration tests I figured the responsibility could be kept in the Pebble utility code. |
||
|
||
certificatePath, privateKeyPath, err := generateCertificate() | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
log := NewLogBridge(logr) | ||
|
||
database := db.NewMemoryStore() | ||
certificateAuthority := ca.New(log, database, ocspResponderURL, alternateRoots, chainLength, certificateValidityPeriod) | ||
validationAuthority := va.New(log, httpPort, tlsPort, strict, customResolverAddr, database) | ||
|
||
wfeImpl := wfe.New(log, database, validationAuthority, certificateAuthority, strict, requireEAB, retryAfterAuthz, retryAfterOrder) | ||
muxHandler := wfeImpl.Handler() | ||
|
||
directoryAddress = fmt.Sprintf("https://%s%s", listenAddress, wfe.DirectoryPath) | ||
|
||
log.Printf("Listening on: %s", listenAddress) | ||
log.Printf("ACME directory available at: %s", | ||
directoryAddress) | ||
|
||
go func() { | ||
err := http.ListenAndServeTLS( | ||
listenAddress, | ||
certificatePath, | ||
privateKeyPath, | ||
muxHandler) | ||
marc1404 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
cmd.FailOnError(err, "Calling ListenAndServeTLS()") | ||
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. http.ListenAndServeTLS is a blocking function that only returns in case of an error (e.g. address already in use). cmd.FailOnError is implemented by Pebble and effectively exits the process with the error. |
||
}() | ||
|
||
return certificatePath, directoryAddress, nil | ||
} | ||
|
||
// CheckPebbleAvailability checks if the Pebble ACME server is available at the given address. | ||
func CheckPebbleAvailability(certificatePath string, listenAddress string) error { | ||
rootCAs, err := loadCertPool(certificatePath) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
customTransport := http.DefaultTransport.(*http.Transport).Clone() | ||
customTransport.TLSClientConfig = &tls.Config{RootCAs: rootCAs} | ||
marc1404 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
http.DefaultTransport = customTransport | ||
client := &http.Client{Transport: customTransport} | ||
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. The ACME protocol enforces TLS and the utility code generates a temporary certificate on the fly. |
||
|
||
response, err := client.Get(listenAddress) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
defer response.Body.Close() | ||
|
||
if response.StatusCode != http.StatusOK { | ||
return fmt.Errorf("expected status 200 from %s, got %d", listenAddress, response.StatusCode) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// generateCertificate generates a certificate and private key for the Pebble server in a temporary OS directory. | ||
// Inspired from: https://github.com/gardener/cert-management/blob/584014befb02a80a063184c9b765db7e12e66f52/hack/kind/pebble/pebble-up.sh#L15-L23 | ||
func generateCertificate() (certificatePath, privateKeyPath string, err error) { | ||
tempDirectoryPath, err := os.MkdirTemp("", "pebble") | ||
if err != nil { | ||
return "", "", fmt.Errorf("failed to create temporary directory: %v", err) | ||
} | ||
|
||
goRoot := runtime.GOROOT() | ||
goFile := fmt.Sprintf("%s/src/crypto/tls/generate_cert.go", goRoot) | ||
command := exec.Command("go", "run", goFile, "--host=localhost", "--ecdsa-curve=P256") | ||
marc1404 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
command.Dir = tempDirectoryPath | ||
err = command.Run() | ||
if err != nil { | ||
return "", "", fmt.Errorf("failed to generate certificate: %v", err) | ||
} | ||
|
||
certificatePath = fmt.Sprintf("%s/cert.pem", tempDirectoryPath) | ||
privateKeyPath = fmt.Sprintf("%s/key.pem", tempDirectoryPath) | ||
|
||
return certificatePath, privateKeyPath, nil | ||
} | ||
|
||
func loadCertPool(certificatePath string) (*x509.CertPool, error) { | ||
certData, err := os.ReadFile(certificatePath) | ||
marc1404 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to read certificate file: %v", err) | ||
} | ||
|
||
certPool := x509.NewCertPool() | ||
ok := certPool.AppendCertsFromPEM(certData) | ||
if !ok { | ||
return nil, fmt.Errorf("failed to parse certificates from PEM") | ||
} | ||
|
||
return certPool, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
acmeDirectoryAddress
has been added. This variable holds the directory address of the local Pebble ACME server that will be used to createIssuer
resources in tests.The test asserts that this variable has been set before the test starts.