Skip to content

Commit

Permalink
verify that kubectl watch calls work against proxy (#753)
Browse files Browse the repository at this point in the history
  • Loading branch information
MatousJobanek authored Jun 26, 2023
1 parent 7ae6502 commit d25f9cb
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 7 deletions.
76 changes: 76 additions & 0 deletions test/e2e/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
. "github.com/codeready-toolchain/toolchain-e2e/testsupport"
appstudiov1 "github.com/codeready-toolchain/toolchain-e2e/testsupport/appstudio/api/v1alpha1"
"github.com/codeready-toolchain/toolchain-e2e/testsupport/wait"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/gofrs/uuid"
Expand Down Expand Up @@ -129,6 +130,12 @@ func TestProxyFlow(t *testing.T) {

t.Logf("Proxy URL: %s", hostAwait.APIProxyURL)

waitForWatcher := runWatcher(t, awaitilities)
defer func() {
t.Log("wait until the watcher is stopped")
waitForWatcher.Wait()
}()

users := []*proxyUser{
{
expectedMemberCluster: memberAwait,
Expand Down Expand Up @@ -552,6 +559,75 @@ func TestProxyFlow(t *testing.T) {
assert.NoError(t, err)
}

// this test will:
// 1. provision a watcher user
// 2. run a goroutine which will:
// I. create a long-running GET call with a watch=true parameter
// II. the call will be terminated via a context timeout
// III. check the expected error that it was terminated via a context and not on the server side
func runWatcher(t *testing.T, awaitilities wait.Awaitilities) *sync.WaitGroup {

// ======================================================
// let's define two timeouts

// contextTimeout defines the time after which the GET (watch) call will be terminated (via the context)
// this one is the expected timeout and should be bigger than the default one that was originally set
// for OpenShift Route and the RoundTripper inside proxy to make sure that the call is terminated
// via the context and not by the server.
contextTimeout := 40 * time.Second

// this timeout will be set when initializing the go client - just to be sure that
// there is no other value set by default and is bigger than the contextTimeout.
clientConfigTimeout := 50 * time.Second
// ======================================================

t.Log("provisioning the watcher")
watchUser := &proxyUser{
expectedMemberCluster: awaitilities.Member1(),
username: "watcher",
identityID: uuid.Must(uuid.NewV4()),
}
createAppStudioUser(t, awaitilities, watchUser)

proxyConfig := awaitilities.Host().CreateAPIProxyConfig(t, watchUser.token, awaitilities.Host().APIProxyURL)
proxyConfig.Timeout = clientConfigTimeout
watcherClient, err := kubernetes.NewForConfig(proxyConfig)
require.NoError(t, err)

// we need to get a list of ConfigMaps, so we can use the resourceVersion
// of the list resource in the watch call
t.Log("getting the first list of ConfigMaps")
list, err := watcherClient.CoreV1().
ConfigMaps(tenantNsName(watchUser.compliantUsername)).
List(context.Background(), metav1.ListOptions{})
require.NoError(t, err)

var waitForWatcher sync.WaitGroup
waitForWatcher.Add(1)
// run the watch in a goroutine because it will take 40 seconds until the call is terminated
go func() {
t.Run("running the watcher", func(t *testing.T) {
defer waitForWatcher.Done()
withTimeout, cancelFunc := context.WithTimeout(context.Background(), contextTimeout)
defer cancelFunc()

started := time.Now()
t.Log("starting the watch call")
_, err := watcherClient.RESTClient().Get().
AbsPath(fmt.Sprintf("/api/v1/namespaces/%s/configmaps", tenantNsName(watchUser.compliantUsername))).
Param("resourceVersion", list.GetResourceVersion()).
Param("watch", "true").
Do(withTimeout).
Get()
t.Logf("stopping the watch after %s", time.Since(started))

assert.EqualError(t, err, "unexpected error when reading response body. Please retry. Original error: context deadline exceeded", "The call should be terminated by the context timeout")
assert.NotContains(t, err.Error(), "unexpected EOF", "If it contains 'unexpected EOF' then the call was terminated on the server side, which is not expected.")
})
}()
return &waitForWatcher
}

func TestSpaceLister(t *testing.T) {
// given
awaitilities := WaitForDeployments(t)
Expand Down
2 changes: 2 additions & 0 deletions testsupport/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/codeready-toolchain/toolchain-common/pkg/cluster"
appstudiov1 "github.com/codeready-toolchain/toolchain-e2e/testsupport/appstudio/api/v1alpha1"
"github.com/codeready-toolchain/toolchain-e2e/testsupport/wait"
"github.com/stretchr/testify/assert"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/kubectl/pkg/scheme"

Expand Down Expand Up @@ -81,6 +82,7 @@ func WaitForDeployments(t *testing.T) wait.Awaitilities {
// set api proxy values
apiRoute, err := initHostAwait.WaitForRouteToBeAvailable(t, registrationServiceNs, "api", "/proxyhealth")
require.NoError(t, err)
assert.Equal(t, "24h", apiRoute.Annotations["haproxy.router.openshift.io/timeout"])
initHostAwait.APIProxyURL = strings.TrimSuffix(fmt.Sprintf("https://%s/%s", apiRoute.Spec.Host, apiRoute.Spec.Path), "/")

// wait for member operators to be ready
Expand Down
19 changes: 12 additions & 7 deletions testsupport/wait/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -1660,22 +1660,27 @@ func (a *HostAwaitility) GetHostOperatorPod() (corev1.Pod, error) {
return pods.Items[0], nil
}

// CreateAPIProxyClient creates a client to the appstudio api proxy using the given user token
func (a *HostAwaitility) CreateAPIProxyClient(t *testing.T, usertoken, proxyURL string) (client.Client, error) {
// CreateAPIProxyConfig creates a config for the proxy API using the given user token
func (a *HostAwaitility) CreateAPIProxyConfig(t *testing.T, usertoken, proxyURL string) *rest.Config {
apiConfig, err := clientcmd.NewDefaultClientConfigLoadingRules().Load()
require.NoError(t, err)
defaultConfig, err := clientcmd.NewDefaultClientConfig(*apiConfig, &clientcmd.ConfigOverrides{}).ClientConfig()
require.NoError(t, err)

s := scheme.Scheme
builder := append(runtime.SchemeBuilder{}, corev1.AddToScheme)
require.NoError(t, builder.AddToScheme(s))

proxyKubeConfig := &rest.Config{
return &rest.Config{
Host: proxyURL,
TLSClientConfig: defaultConfig.TLSClientConfig,
BearerToken: usertoken,
}
}

// CreateAPIProxyClient creates a client to the appstudio api proxy using the given user token
func (a *HostAwaitility) CreateAPIProxyClient(t *testing.T, userToken, proxyURL string) (client.Client, error) {
proxyKubeConfig := a.CreateAPIProxyConfig(t, userToken, proxyURL)

s := scheme.Scheme
builder := append(runtime.SchemeBuilder{}, corev1.AddToScheme)
require.NoError(t, builder.AddToScheme(s))

// Getting the proxy client can fail from time to time if the proxy's informer cache has not been
// updated yet and we try to create the client too quickly so retry to reduce flakiness.
Expand Down

0 comments on commit d25f9cb

Please sign in to comment.