Skip to content

Commit 99f7ce4

Browse files
committed
WIP
On-behalf-of: @SAP [email protected]
1 parent 18ba324 commit 99f7ce4

File tree

8 files changed

+209
-20
lines changed

8 files changed

+209
-20
lines changed

hack/run-e2e-tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export IMG="ghcr.io/kcp-dev/kcp-operator:e2e"
8585
make --no-print-directory docker-build kind-load deploy
8686

8787
if command -v protokol &> /dev/null; then
88-
protokol --namespace 'e2e-*' --output "$DATA_DIR/kind-logs" 2>/dev/null &
88+
protokol --namespace 'e2e-*' --namespace kcp-operator-system --output "$DATA_DIR/kind-logs" 2>/dev/null &
8989
PROTOKOL_PID=$!
9090
else
9191
echo "Install https://codeberg.org/xrstf/protokol to automatically"

internal/resources/kubeconfig/certificate.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package kubeconfig
1919
import (
2020
certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
2121
certmanagermetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
22+
"k8s.io/apimachinery/pkg/util/sets"
2223

2324
"github.com/kcp-dev/kcp-operator/internal/reconciling"
2425
"github.com/kcp-dev/kcp-operator/internal/resources"
@@ -27,6 +28,9 @@ import (
2728
)
2829

2930
func ClientCertificateReconciler(kubeConfig *operatorv1alpha1.Kubeconfig, issuerName string) reconciling.NamedCertificateReconcilerFactory {
31+
orgs := sets.New(kubeConfig.Spec.Groups...)
32+
orgs.Insert(KubeconfigGroup(kubeConfig))
33+
3034
return func() (string, reconciling.CertificateReconciler) {
3135
return kubeConfig.GetCertificateName(), func(cert *certmanagerv1.Certificate) (*certmanagerv1.Certificate, error) {
3236
cert.SetLabels(kubeConfig.Labels)
@@ -50,7 +54,7 @@ func ClientCertificateReconciler(kubeConfig *operatorv1alpha1.Kubeconfig, issuer
5054

5155
CommonName: kubeConfig.Spec.Username,
5256
Subject: &certmanagerv1.X509Subject{
53-
Organizations: kubeConfig.Spec.Groups,
57+
Organizations: sets.List(orgs),
5458
},
5559

5660
IssuerRef: certmanagermetav1.ObjectReference{

test/e2e/frontproxies/frontproxies_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333

3434
operatorv1alpha1 "github.com/kcp-dev/kcp-operator/sdk/apis/operator/v1alpha1"
3535
"github.com/kcp-dev/kcp-operator/test/utils"
36+
"github.com/kcp-dev/logicalcluster/v3"
3637
)
3738

3839
func TestCreateFrontProxy(t *testing.T) {
@@ -81,7 +82,7 @@ func TestCreateFrontProxy(t *testing.T) {
8182

8283
// verify that we can use frontproxy kubeconfig to access rootshard workspaces
8384
t.Log("Connecting to FrontProxy...")
84-
kcpClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, fpConfig.Name)
85+
kcpClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, fpConfig.Name, logicalcluster.None)
8586
// proof of life: list something every logicalcluster in kcp has
8687
t.Log("Should be able to list Secrets.")
8788
secrets := &corev1.SecretList{}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//go:build e2e
2+
3+
/*
4+
Copyright 2025 The KCP Authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package kubeconfigrbac
20+
21+
import (
22+
"context"
23+
"fmt"
24+
"testing"
25+
"time"
26+
27+
"github.com/go-logr/logr"
28+
29+
kcptenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
30+
"github.com/kcp-dev/logicalcluster/v3"
31+
corev1 "k8s.io/api/core/v1"
32+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
"k8s.io/apimachinery/pkg/types"
34+
"k8s.io/apimachinery/pkg/util/wait"
35+
ctrlruntime "sigs.k8s.io/controller-runtime"
36+
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
37+
38+
operatorv1alpha1 "github.com/kcp-dev/kcp-operator/sdk/apis/operator/v1alpha1"
39+
"github.com/kcp-dev/kcp-operator/test/utils"
40+
)
41+
42+
func TestProvisionFrontProxyRBAC(t *testing.T) {
43+
ctrlruntime.SetLogger(logr.Discard())
44+
45+
client := utils.GetKubeClient(t)
46+
ctx := context.Background()
47+
48+
rootCluster := logicalcluster.NewPath("root")
49+
namespace := utils.CreateSelfDestructingNamespace(t, ctx, client, "provision-frontproxy-rbac")
50+
externalHostname := fmt.Sprintf("front-proxy-front-proxy.%s.svc.cluster.local", namespace.Name)
51+
52+
// deploy rootshard
53+
rootShard := utils.DeployRootShard(ctx, t, client, namespace.Name, externalHostname)
54+
55+
// deploy front-proxy
56+
frontProxy := utils.DeployFrontProxy(ctx, t, client, namespace.Name, rootShard.Name, externalHostname)
57+
58+
// create a dummy workspace where we later want to provision RBAC in
59+
t.Log("Creating dummy workspace…")
60+
workspace := &kcptenancyv1alpha1.Workspace{
61+
ObjectMeta: metav1.ObjectMeta{
62+
Name: "test",
63+
},
64+
Spec: kcptenancyv1alpha1.WorkspaceSpec{
65+
Type: kcptenancyv1alpha1.WorkspaceTypeReference{
66+
Name: "universal",
67+
},
68+
},
69+
}
70+
71+
dummyCluster := rootCluster.Join(workspace.Name)
72+
proxyClient := utils.ConnectWithRootShardProxy(t, ctx, client, &rootShard, rootCluster)
73+
if err := proxyClient.Create(ctx, workspace); err != nil {
74+
t.Fatalf("Failed to create workspace: %v", err)
75+
}
76+
77+
// wait for workspace to be ready
78+
t.Log("Waiting for workspace to be ready…")
79+
dummyClient := utils.ConnectWithRootShardProxy(t, ctx, client, &rootShard, dummyCluster)
80+
81+
err := wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
82+
return dummyClient.List(ctx, &corev1.SecretList{}) == nil, nil
83+
})
84+
if err != nil {
85+
t.Fatalf("Failed to wait for workspace to become available: %v", err)
86+
}
87+
88+
// create my-config kubeconfig
89+
configSecretName := "kubeconfig-my-config-e2e"
90+
91+
// as of now, this Kubeconfig will not grant any permissions yet
92+
fpConfig := operatorv1alpha1.Kubeconfig{}
93+
fpConfig.Name = "my-config"
94+
fpConfig.Namespace = namespace.Name
95+
fpConfig.Spec = operatorv1alpha1.KubeconfigSpec{
96+
Target: operatorv1alpha1.KubeconfigTarget{
97+
FrontProxyRef: &corev1.LocalObjectReference{
98+
Name: frontProxy.Name,
99+
},
100+
},
101+
Username: "e2e",
102+
Validity: metav1.Duration{Duration: 2 * time.Hour},
103+
SecretRef: corev1.LocalObjectReference{
104+
Name: configSecretName,
105+
},
106+
}
107+
108+
t.Log("Creating kubeconfig with no permissions attached…")
109+
if err := client.Create(ctx, &fpConfig); err != nil {
110+
t.Fatal(err)
111+
}
112+
utils.WaitForObject(t, ctx, client, &corev1.Secret{}, types.NamespacedName{Namespace: fpConfig.Namespace, Name: fpConfig.Spec.SecretRef.Name})
113+
114+
t.Log("Connecting to FrontProxy…")
115+
kcpClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, fpConfig.Name, dummyCluster)
116+
117+
// This should not work yet.
118+
t.Logf("Should not be able to list Secrets in %v.", dummyCluster)
119+
if err := kcpClient.List(ctx, &corev1.SecretList{}); err == nil {
120+
t.Fatal("Should not have been able to list Secrets, but was. Where have my permissions come from?")
121+
}
122+
123+
// Now we extend the Kubeconfig with additional permissions.
124+
fpConfig.Spec.Authorization = &operatorv1alpha1.KubeconfigAuthorization{
125+
ClusterRoleBindings: operatorv1alpha1.KubeconfigClusterRoleBindings{
126+
WorkspacePath: dummyCluster.String(),
127+
ClusterRoles: []string{"cluster-admin"},
128+
},
129+
}
130+
131+
t.Log("Updating kubeconfig with permissions attached…")
132+
if err := client.Update(ctx, &fpConfig); err != nil {
133+
t.Fatal(err)
134+
}
135+
136+
t.Logf("Should now be able to list Secrets in %v.", dummyCluster)
137+
err = wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
138+
return kcpClient.List(ctx, &corev1.SecretList{}) == nil, nil
139+
})
140+
if err != nil {
141+
t.Fatalf("Failed to list Secrets in dummy workspace: %v", err)
142+
}
143+
144+
// And now we remove the permissions again.
145+
t.Log("Updating kubeconfig to remove the attached permissions…")
146+
if err := client.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(&fpConfig), &fpConfig); err != nil {
147+
t.Fatal(err)
148+
}
149+
150+
fpConfig.Spec.Authorization = nil
151+
152+
if err := client.Update(ctx, &fpConfig); err != nil {
153+
t.Fatal(err)
154+
}
155+
156+
t.Logf("Should no longer be able to list Secrets in %v.", dummyCluster)
157+
err = wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
158+
return kcpClient.List(ctx, &corev1.SecretList{}) != nil, nil
159+
})
160+
if err != nil {
161+
t.Fatalf("Failed to wait for permissions to be gone: %v", err)
162+
}
163+
}

test/e2e/rootshards/proxy_test.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,9 @@ func TestRootShardProxy(t *testing.T) {
4545

4646
client := utils.GetKubeClient(t)
4747
ctx := context.Background()
48-
namespaceSuffix := "rootshard-proxy"
4948

50-
namespace := utils.CreateSelfDestructingNamespace(t, ctx, client, namespaceSuffix)
51-
externalHostname := fmt.Sprintf("front-proxy-front-proxy.e2e-%s.svc.cluster.local", namespaceSuffix)
49+
namespace := utils.CreateSelfDestructingNamespace(t, ctx, client, "rootshard-proxy")
50+
externalHostname := fmt.Sprintf("front-proxy-front-proxy.e2e-%s.svc.cluster.local", namespace.Name)
5251

5352
// deploy a root shard incl. etcd
5453
rootShard := utils.DeployRootShard(ctx, t, client, namespace.Name, externalHostname)
@@ -87,7 +86,7 @@ func TestRootShardProxy(t *testing.T) {
8786
utils.WaitForObject(t, ctx, client, &corev1.Secret{}, types.NamespacedName{Namespace: rsConfig.Namespace, Name: rsConfig.Spec.SecretRef.Name})
8887

8988
t.Log("Connecting to RootShard...")
90-
rootShardClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, rsConfig.Name)
89+
rootShardClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, rsConfig.Name, logicalcluster.None)
9190

9291
// wait until the 2nd shard has registered itself successfully at the root shard
9392
shardKey := types.NamespacedName{Name: shardName}
@@ -131,9 +130,6 @@ func TestRootShardProxy(t *testing.T) {
131130

132131
// build a client through the proxy to the new workspace
133132
proxyClient := utils.ConnectWithRootShardProxy(t, ctx, client, &rootShard, logicalcluster.NewPath("root").Join(workspace.Name))
134-
if err != nil {
135-
t.Fatalf("Failed to create root shard proxy client: %v", err)
136-
}
137133

138134
// proof of life: list something every logicalcluster in kcp has
139135
t.Log("Should be able to list Secrets in the new workspace.")

test/e2e/rootshards/rootshards_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232

3333
operatorv1alpha1 "github.com/kcp-dev/kcp-operator/sdk/apis/operator/v1alpha1"
3434
"github.com/kcp-dev/kcp-operator/test/utils"
35+
"github.com/kcp-dev/logicalcluster/v3"
3536
)
3637

3738
func TestCreateRootShard(t *testing.T) {
@@ -70,7 +71,7 @@ func TestCreateRootShard(t *testing.T) {
7071
utils.WaitForObject(t, ctx, client, &corev1.Secret{}, types.NamespacedName{Namespace: rsConfig.Namespace, Name: rsConfig.Spec.SecretRef.Name})
7172

7273
t.Log("Connecting to RootShard...")
73-
kcpClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, rsConfig.Name)
74+
kcpClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, rsConfig.Name, logicalcluster.None)
7475

7576
// proof of life: list something every logicalcluster in kcp has
7677
t.Log("Should be able to list Secrets.")

test/e2e/shards/shards_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"github.com/go-logr/logr"
2828
kcpcorev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
29+
"github.com/kcp-dev/logicalcluster/v3"
2930

3031
corev1 "k8s.io/api/core/v1"
3132
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -80,7 +81,7 @@ func TestCreateShard(t *testing.T) {
8081
utils.WaitForObject(t, ctx, client, &corev1.Secret{}, types.NamespacedName{Namespace: rsConfig.Namespace, Name: rsConfig.Spec.SecretRef.Name})
8182

8283
t.Log("Connecting to RootShard...")
83-
rootShardClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, rsConfig.Name)
84+
rootShardClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, rsConfig.Name, logicalcluster.None)
8485

8586
// wait until the 2nd shard has registered itself successfully at the root shard
8687
shardKey := types.NamespacedName{Name: shardName}
@@ -115,7 +116,7 @@ func TestCreateShard(t *testing.T) {
115116
utils.WaitForObject(t, ctx, client, &corev1.Secret{}, types.NamespacedName{Namespace: shardConfig.Namespace, Name: shardConfig.Spec.SecretRef.Name})
116117

117118
t.Log("Connecting to Shard...")
118-
kcpClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, shardConfig.Name)
119+
kcpClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, shardConfig.Name, logicalcluster.None)
119120

120121
// proof of life: list something every logicalcluster in kcp has
121122
t.Log("Should be able to list Secrets.")

test/utils/utils.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"net"
2323
"net/url"
2424
"os/exec"
25+
"regexp"
2526
"strconv"
2627
"strings"
2728
"testing"
@@ -110,12 +111,12 @@ func CreateSelfDestructingNamespace(t *testing.T, ctx context.Context, client ct
110111
t.Fatal(err)
111112
}
112113

113-
t.Cleanup(func() {
114-
t.Logf("Deleting namespace %s...", name)
115-
if err := client.Delete(ctx, &ns); err != nil {
116-
t.Fatal(err)
117-
}
118-
})
114+
// t.Cleanup(func() {
115+
// t.Logf("Deleting namespace %s...", name)
116+
// if err := client.Delete(ctx, &ns); err != nil {
117+
// t.Fatal(err)
118+
// }
119+
// })
119120

120121
return &ns
121122
}
@@ -169,6 +170,7 @@ func ConnectWithKubeconfig(
169170
client ctrlruntimeclient.Client,
170171
namespace string,
171172
kubeconfigName string,
173+
cluster logicalcluster.Path,
172174
) ctrlruntimeclient.Client {
173175
t.Helper()
174176

@@ -219,6 +221,11 @@ func ConnectWithKubeconfig(
219221
parsed.Host = net.JoinHostPort("localhost", fmt.Sprintf("%d", localPort))
220222
clientConfig.Host = parsed.String()
221223

224+
// switch to another workspace is desired
225+
if !cluster.Empty() {
226+
clientConfig.Host = changeClusterInURL(clientConfig.Host, cluster)
227+
}
228+
222229
// create a client through the tunnel
223230
kcpClient, err := ctrlruntimeclient.New(clientConfig, ctrlruntimeclient.Options{Scheme: NewScheme(t)})
224231
if err != nil {
@@ -259,7 +266,7 @@ func ConnectWithRootShardProxy(
259266
proxyUrl := fmt.Sprintf("https://%s", net.JoinHostPort("localhost", fmt.Sprintf("%d", localPort)))
260267

261268
if !cluster.Empty() {
262-
proxyUrl = fmt.Sprintf("%s/clusters/%s", proxyUrl, cluster.String())
269+
proxyUrl = changeClusterInURL(proxyUrl, cluster)
263270
}
264271

265272
cfg := &rest.Config{
@@ -279,3 +286,19 @@ func ConnectWithRootShardProxy(
279286

280287
return kcpClient
281288
}
289+
290+
var clusterRegexp = regexp.MustCompile(`/clusters/([^/]+)`)
291+
292+
func changeClusterInURL(u string, newCluster logicalcluster.Path) string {
293+
newPath := fmt.Sprintf("/clusters/%s", newCluster)
294+
295+
matches := clusterRegexp.FindAllString(u, 1)
296+
if len(matches) == 0 {
297+
return u + newPath
298+
}
299+
300+
// make sure that if a URL is "/clusters/root/apis/example.com/v1/namespaces/bla/clusters/mycluster",
301+
// we only replace the first match, especially important if the URL was "/clusters/X/apis/example.com/v1/clusters/X"
302+
// (i.e. accessing the cluster resource X in the kcp cluster also called X)
303+
return strings.Replace(u, matches[0], newPath, 1)
304+
}

0 commit comments

Comments
 (0)