-
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 all 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 |
---|---|---|
|
@@ -8,6 +8,7 @@ import ( | |
"context" | ||
"encoding/base64" | ||
"fmt" | ||
"io" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
@@ -16,7 +17,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 +48,39 @@ 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() { | ||
var ( | ||
certificatePath string | ||
pebbleHTTPServer io.Closer | ||
err error | ||
) | ||
|
||
logf.SetLogger(logger.MustNewZapLogger(logger.DebugLevel, logger.FormatJSON, zap.WriteTo(GinkgoWriter))) | ||
log = logf.Log.WithName(testID) | ||
|
||
By("Start Pebble ACME server") | ||
pebbleHTTPServer, certificatePath, acmeDirectoryAddress, err = testutils.RunPebble(log.WithName("pebble")) | ||
Expect(err).NotTo(HaveOccurred()) | ||
|
||
// 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,7 +94,6 @@ var _ = BeforeSuite(func() { | |
ErrorIfCRDPathMissing: true, | ||
} | ||
|
||
var err error | ||
restConfig, err = testEnv.Start() | ||
Expect(err).NotTo(HaveOccurred()) | ||
Expect(restConfig).NotTo(BeNil()) | ||
|
@@ -86,7 +106,11 @@ 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) | ||
if pebbleHTTPServer != nil { | ||
_ = pebbleHTTPServer.Close() | ||
} | ||
}) | ||
|
||
By("Create test client") | ||
|
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,218 @@ | ||
// Adapted from the Go standard library generate_cert.go | ||
// Source: https://github.com/golang/go/blob/master/src/crypto/tls/generate_cert.go | ||
|
||
// Generate a self-signed X.509 certificate for a TLS server. Outputs to | ||
// 'cert.pem' and 'key.pem' and will overwrite existing files. | ||
|
||
package testutils | ||
|
||
import ( | ||
"crypto/ecdsa" | ||
"crypto/ed25519" | ||
"crypto/elliptic" | ||
"crypto/rand" | ||
"crypto/rsa" | ||
"crypto/x509" | ||
"crypto/x509/pkix" | ||
"encoding/pem" | ||
"fmt" | ||
"log" | ||
"math/big" | ||
"net" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
) | ||
|
||
type certFlags struct { | ||
// Comma-separated hostnames and IPs to generate a certificate for | ||
host *string | ||
|
||
//Creation date formatted as Jan 1 15:04:05 2011 | ||
validFrom *string | ||
|
||
// Duration that certificate is valid for | ||
validFor *time.Duration | ||
|
||
// Whether this cert should be its own Certificate Authority | ||
isCA *bool | ||
|
||
// Size of RSA key to generate. Ignored if ecdsaCurve is set | ||
rsaBits *int | ||
|
||
// ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521 | ||
ecdsaCurve *string | ||
|
||
// Generate an Ed25519 key | ||
ed25519Key *bool | ||
} | ||
|
||
func generateCert(certFlags certFlags, certPath, keyPath string) error { | ||
setDefaults(&certFlags) | ||
|
||
if len(*certFlags.host) == 0 { | ||
log.Fatalf("Missing required --host parameter") | ||
} | ||
|
||
var priv any | ||
var err error | ||
switch *certFlags.ecdsaCurve { | ||
case "": | ||
if *certFlags.ed25519Key { | ||
_, priv, err = ed25519.GenerateKey(rand.Reader) | ||
} else { | ||
priv, err = rsa.GenerateKey(rand.Reader, *certFlags.rsaBits) | ||
} | ||
case "P224": | ||
priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) | ||
case "P256": | ||
priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
case "P384": | ||
priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) | ||
case "P521": | ||
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) | ||
default: | ||
log.Fatalf("Unrecognized elliptic curve: %q", *certFlags.ecdsaCurve) | ||
} | ||
if err != nil { | ||
log.Fatalf("Failed to generate private key: %v", err) | ||
} | ||
|
||
// ECDSA, ED25519 and RSA subject keys should have the DigitalSignature | ||
// KeyUsage bits set in the x509.Certificate template | ||
keyUsage := x509.KeyUsageDigitalSignature | ||
// Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In | ||
// the context of TLS this KeyUsage is particular to RSA key exchange and | ||
// authentication. | ||
if _, isRSA := priv.(*rsa.PrivateKey); isRSA { | ||
keyUsage |= x509.KeyUsageKeyEncipherment | ||
} | ||
|
||
var notBefore time.Time | ||
if len(*certFlags.validFrom) == 0 { | ||
notBefore = time.Now() | ||
} else { | ||
notBefore, err = time.Parse("Jan 2 15:04:05 2006", *certFlags.validFrom) | ||
if err != nil { | ||
return fmt.Errorf("failed to parse creation date: %v", err) | ||
} | ||
} | ||
|
||
notAfter := notBefore.Add(*certFlags.validFor) | ||
|
||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) | ||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) | ||
if err != nil { | ||
return fmt.Errorf("failed to generate serial number: %v", err) | ||
} | ||
|
||
template := x509.Certificate{ | ||
SerialNumber: serialNumber, | ||
Subject: pkix.Name{ | ||
Organization: []string{"Acme Co"}, | ||
}, | ||
NotBefore: notBefore, | ||
NotAfter: notAfter, | ||
|
||
KeyUsage: keyUsage, | ||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | ||
BasicConstraintsValid: true, | ||
} | ||
|
||
hosts := strings.Split(*certFlags.host, ",") | ||
for _, h := range hosts { | ||
if ip := net.ParseIP(h); ip != nil { | ||
template.IPAddresses = append(template.IPAddresses, ip) | ||
} else { | ||
template.DNSNames = append(template.DNSNames, h) | ||
} | ||
} | ||
|
||
if *certFlags.isCA { | ||
template.IsCA = true | ||
template.KeyUsage |= x509.KeyUsageCertSign | ||
} | ||
|
||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) | ||
if err != nil { | ||
return fmt.Errorf("failed to create certificate: %v", err) | ||
} | ||
|
||
certOut, err := os.Create(filepath.Clean(certPath)) | ||
if err != nil { | ||
return fmt.Errorf("failed to open cert.pem for writing: %v", err) | ||
} | ||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { | ||
return fmt.Errorf("failed to write data to cert.pem: %v", err) | ||
} | ||
if err := certOut.Close(); err != nil { | ||
return fmt.Errorf("error closing cert.pem: %v", err) | ||
} | ||
|
||
keyOut, err := os.OpenFile(filepath.Clean(keyPath), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) | ||
if err != nil { | ||
return fmt.Errorf("failed to open key.pem for writing: %v", err) | ||
} | ||
privBytes, err := x509.MarshalPKCS8PrivateKey(priv) | ||
if err != nil { | ||
return fmt.Errorf("unable to marshal private key: %v", err) | ||
} | ||
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { | ||
return fmt.Errorf("failed to write data to key.pem: %v", err) | ||
} | ||
if err := keyOut.Close(); err != nil { | ||
return fmt.Errorf("error closing key.pem: %v", err) | ||
} | ||
return nil | ||
} | ||
|
||
func setDefaults(certFlags *certFlags) { | ||
if certFlags.host == nil { | ||
certFlags.host = new(string) | ||
*certFlags.host = "" | ||
} | ||
|
||
if certFlags.validFrom == nil { | ||
certFlags.validFrom = new(string) | ||
*certFlags.validFrom = "" | ||
} | ||
|
||
if certFlags.validFor == nil { | ||
certFlags.validFor = new(time.Duration) | ||
*certFlags.validFor = 365 * 24 * time.Hour | ||
} | ||
|
||
if certFlags.isCA == nil { | ||
certFlags.isCA = new(bool) | ||
*certFlags.isCA = false | ||
} | ||
|
||
if certFlags.rsaBits == nil { | ||
certFlags.rsaBits = new(int) | ||
*certFlags.rsaBits = 2048 | ||
} | ||
|
||
if certFlags.ecdsaCurve == nil { | ||
certFlags.ecdsaCurve = new(string) | ||
*certFlags.ecdsaCurve = "" | ||
} | ||
|
||
if certFlags.ed25519Key == nil { | ||
certFlags.ed25519Key = new(bool) | ||
*certFlags.ed25519Key = false | ||
} | ||
} | ||
|
||
func publicKey(priv any) any { | ||
switch k := priv.(type) { | ||
case *rsa.PrivateKey: | ||
return &k.PublicKey | ||
case *ecdsa.PrivateKey: | ||
return &k.PublicKey | ||
case ed25519.PrivateKey: | ||
return k.Public().(ed25519.PublicKey) | ||
default: | ||
return nil | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package testutils | ||
|
||
import ( | ||
"log" | ||
"strings" | ||
|
||
"github.com/go-logr/logr" | ||
) | ||
|
||
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 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.