apiextensions: make conversion webhook test server more compositional

This commit is contained in:
Dr. Stefan Schimanski 2019-05-25 17:08:04 +02:00
parent 0cafec6608
commit 5b5fea0502
3 changed files with 43 additions and 64 deletions

View File

@ -22,6 +22,7 @@ import (
"net/http"
"reflect"
"strings"
"sync"
"testing"
"time"
@ -36,6 +37,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/apimachinery/pkg/util/wait"
etcd3watcher "k8s.io/apiserver/pkg/storage/etcd3"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/dynamic"
@ -131,7 +133,8 @@ func TestWebhookConverter(t *testing.T) {
for _, test := range tests {
t.Run(test.group, func(t *testing.T) {
tearDown, webhookClientConfig, webhookWaitReady, err := convert.StartConversionWebhookServerWithWaitReady(test.handler)
upCh, handler := closeOnCall(test.handler)
tearDown, webhookClientConfig, err := convert.StartConversionWebhookServer(handler)
if err != nil {
t.Fatal(err)
}
@ -140,12 +143,17 @@ func TestWebhookConverter(t *testing.T) {
ctc.setConversionWebhook(t, webhookClientConfig)
defer ctc.removeConversionWebhook(t)
err = webhookWaitReady(30*time.Second, func() error {
// the marker's storage version is v1beta2, so a v1beta1 read always triggers conversion
// wait until new webhook is called the first time
if err := wait.PollImmediate(time.Millisecond*100, wait.ForeverTestTimeout, func() (bool, error) {
_, err := ctc.versionedClient(marker.GetNamespace(), "v1beta1").Get(marker.GetName(), metav1.GetOptions{})
return err
})
if err != nil {
select {
case <-upCh:
return true, nil
default:
t.Logf("Waiting for webhook to become effective, getting marker object: %v", err)
return false, nil
}
}); err != nil {
t.Fatal(err)
}
@ -562,3 +570,14 @@ func newConversionMultiVersionFixture(namespace, name, version string) *unstruct
},
}
}
func closeOnCall(h http.Handler) (chan struct{}, http.Handler) {
ch := make(chan struct{})
once := sync.Once{}
return ch, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
once.Do(func() {
close(ch)
})
h.ServeHTTP(w, r)
})
}

View File

@ -10,7 +10,7 @@ go_library(
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
],
)

View File

@ -25,91 +25,51 @@ import (
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"k8s.io/apimachinery/pkg/util/wait"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/uuid"
)
// WaitReadyFunc calls triggerConversionFn periodically and waits until it detects that the webhook
// conversion server has handled at least 1 conversion request or the timeout is exceeded, in which
// case an error is returned.
type WaitReadyFunc func(timeout time.Duration, triggerConversionFn func() error) error
// StartConversionWebhookServerWithWaitReady starts an http server with the provided handler and returns the WebhookClientConfig
// needed to configure a CRD to use this conversion webhook as its converter.
// It also returns a WaitReadyFunc to be called after the CRD is configured to wait until the conversion webhook handler
// accepts at least one conversion request. If the server fails to start, an error is returned.
// WaitReady is useful when changing the conversion webhook config of an existing CRD because the update does not take effect immediately.
func StartConversionWebhookServerWithWaitReady(handler http.Handler) (func(), *apiextensionsv1beta1.WebhookClientConfig, WaitReadyFunc, error) {
var once sync.Once
handlerReadyC := make(chan struct{})
readyNotifyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
once.Do(func() {
close(handlerReadyC)
})
handler.ServeHTTP(w, r)
})
tearDown, webhookConfig, err := StartConversionWebhookServer(readyNotifyHandler)
if err != nil {
return nil, nil, nil, fmt.Errorf("error starting webhook server: %v", err)
}
waitReady := func(timeout time.Duration, triggerConversionFn func() error) error {
var err error
for {
select {
case <-handlerReadyC:
return nil
case <-time.After(timeout):
return fmt.Errorf("Timed out waiting for CRD webhook converter update, last trigger conversion error: %v", err)
case <-time.After(100 * time.Millisecond):
err = triggerConversionFn()
}
}
}
return tearDown, webhookConfig, waitReady, err
}
// StartConversionWebhookServer starts an http server with the provided handler and returns the WebhookClientConfig
// needed to configure a CRD to use this conversion webhook as its converter.
func StartConversionWebhookServer(handler http.Handler) (func(), *apiextensionsv1beta1.WebhookClientConfig, error) {
// Use a unique path for each webhook server. This ensures that all conversion requests
// received by the handler are intended for it; if a WebhookClientConfig other than this one
// is applied in the api server, conversion requests will not reach the handler (if they
// reach the server they will be returned at 404). This helps prevent tests that require a
// specific conversion webhook from accidentally using a different one, which could otherwise
// cause a test to flake or pass when it should fail. Since updating the conversion client
// config of a custom resource definition does not take effect immediately, this is needed
// by the WaitReady returned StartConversionWebhookServerWithWaitReady to detect when a
// conversion client config change in the api server has taken effect.
path := fmt.Sprintf("/conversionwebhook-%s", uuid.NewUUID())
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(localhostCert) {
return nil, nil, fmt.Errorf("Failed to append Cert from PEM")
return nil, nil, fmt.Errorf("failed to append Cert from PEM")
}
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
if err != nil {
return nil, nil, fmt.Errorf("Failed to build cert with error: %+v", err)
return nil, nil, fmt.Errorf("failed to build cert with error: %+v", err)
}
webhookMux := http.NewServeMux()
webhookMux.Handle(path, handler)
webhookMux.Handle("/convert", handler)
webhookServer := httptest.NewUnstartedServer(webhookMux)
webhookServer.TLS = &tls.Config{
RootCAs: roots,
Certificates: []tls.Certificate{cert},
}
webhookServer.StartTLS()
endpoint := webhookServer.URL + path
endpoint := webhookServer.URL + "/convert"
webhookConfig := &apiextensionsv1beta1.WebhookClientConfig{
CABundle: localhostCert,
URL: &endpoint,
}
// StartTLS returns immediately, there is a small chance of a race to avoid.
if err := wait.PollImmediate(time.Millisecond*100, wait.ForeverTestTimeout, func() (bool, error) {
_, err := webhookServer.Client().Get(webhookServer.URL) // even a 404 is fine
return err == nil, nil
}); err != nil {
webhookServer.Close()
return nil, nil, err
}
return webhookServer.Close, webhookConfig, nil
}