mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-10 10:03:43 +00:00
f1ccdcc713
This is the start of an integration/e2e test suite for the tailscale operator. It currently only tests two major features, ingress proxy and API server proxy, but we intend to expand it to cover more features over time. It also only supports manual runs for now. We intend to integrate it into CI checks in a separate update when we have planned how to securely provide CI with the secrets required for connecting to a test tailnet. Updates #12622 Change-Id: I31e464bb49719348b62a563790f2bc2ba165a11b Co-authored-by: Irbe Krumina <irbe@tailscale.com> Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
157 lines
3.9 KiB
Go
157 lines
3.9 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/client-go/rest"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
|
"tailscale.com/client/tailscale"
|
|
"tailscale.com/tsnet"
|
|
"tailscale.com/tstest"
|
|
)
|
|
|
|
// See [TestMain] for test requirements.
|
|
func TestProxy(t *testing.T) {
|
|
if tsClient == nil {
|
|
t.Skip("TestProxy requires credentials for a tailscale client")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cfg := config.GetConfigOrDie()
|
|
cl, err := client.New(cfg, client.Options{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create role and role binding to allow a group we'll impersonate to do stuff.
|
|
createAndCleanup(t, ctx, cl, &rbacv1.Role{
|
|
ObjectMeta: objectMeta("tailscale", "read-secrets"),
|
|
Rules: []rbacv1.PolicyRule{{
|
|
APIGroups: []string{""},
|
|
Verbs: []string{"get"},
|
|
Resources: []string{"secrets"},
|
|
}},
|
|
})
|
|
createAndCleanup(t, ctx, cl, &rbacv1.RoleBinding{
|
|
ObjectMeta: objectMeta("tailscale", "read-secrets"),
|
|
Subjects: []rbacv1.Subject{{
|
|
Kind: "Group",
|
|
Name: "ts:e2e-test-proxy",
|
|
}},
|
|
RoleRef: rbacv1.RoleRef{
|
|
Kind: "Role",
|
|
Name: "read-secrets",
|
|
},
|
|
})
|
|
|
|
// Get operator host name from kube secret.
|
|
operatorSecret := corev1.Secret{
|
|
ObjectMeta: objectMeta("tailscale", "operator"),
|
|
}
|
|
if err := get(ctx, cl, &operatorSecret); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Connect to tailnet with test-specific tag so we can use the
|
|
// [testGrants] ACLs when connecting to the API server proxy
|
|
ts := tsnetServerWithTag(t, ctx, "tag:e2e-test-proxy")
|
|
proxyCfg := &rest.Config{
|
|
Host: fmt.Sprintf("https://%s:443", hostNameFromOperatorSecret(t, operatorSecret)),
|
|
Dial: ts.Dial,
|
|
}
|
|
proxyCl, err := client.New(proxyCfg, client.Options{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Expect success.
|
|
allowedSecret := corev1.Secret{
|
|
ObjectMeta: objectMeta("tailscale", "operator"),
|
|
}
|
|
// Wait for up to a minute the first time we use the proxy, to give it time
|
|
// to provision the TLS certs.
|
|
if err := tstest.WaitFor(time.Second*60, func() error {
|
|
return get(ctx, proxyCl, &allowedSecret)
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Expect forbidden.
|
|
forbiddenSecret := corev1.Secret{
|
|
ObjectMeta: objectMeta("default", "operator"),
|
|
}
|
|
if err := get(ctx, proxyCl, &forbiddenSecret); err == nil || !apierrors.IsForbidden(err) {
|
|
t.Fatalf("expected forbidden error fetching secret from default namespace: %s", err)
|
|
}
|
|
}
|
|
|
|
func tsnetServerWithTag(t *testing.T, ctx context.Context, tag string) *tsnet.Server {
|
|
caps := tailscale.KeyCapabilities{
|
|
Devices: tailscale.KeyDeviceCapabilities{
|
|
Create: tailscale.KeyDeviceCreateCapabilities{
|
|
Reusable: false,
|
|
Preauthorized: true,
|
|
Ephemeral: true,
|
|
Tags: []string{tag},
|
|
},
|
|
},
|
|
}
|
|
|
|
authKey, authKeyMeta, err := tsClient.CreateKey(ctx, caps)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := tsClient.DeleteKey(ctx, authKeyMeta.ID); err != nil {
|
|
t.Errorf("error deleting auth key: %s", err)
|
|
}
|
|
})
|
|
|
|
ts := &tsnet.Server{
|
|
Hostname: "test-proxy",
|
|
Ephemeral: true,
|
|
Dir: t.TempDir(),
|
|
AuthKey: authKey,
|
|
}
|
|
_, err = ts.Up(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := ts.Close(); err != nil {
|
|
t.Errorf("error shutting down tsnet.Server: %s", err)
|
|
}
|
|
})
|
|
|
|
return ts
|
|
}
|
|
|
|
func hostNameFromOperatorSecret(t *testing.T, s corev1.Secret) string {
|
|
profiles := map[string]any{}
|
|
if err := json.Unmarshal(s.Data["_profiles"], &profiles); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, ok := strings.CutPrefix(string(s.Data["_current-profile"]), "profile-")
|
|
if !ok {
|
|
t.Fatal(string(s.Data["_current-profile"]))
|
|
}
|
|
profile, ok := profiles[key]
|
|
if !ok {
|
|
t.Fatal(profiles)
|
|
}
|
|
|
|
return ((profile.(map[string]any))["Name"]).(string)
|
|
}
|