Skip to content

Commit 04b478a

Browse files
committed
fix permissions in operator cert, add basic e2e test
On-behalf-of: @SAP [email protected]
1 parent 9256ac0 commit 04b478a

File tree

9 files changed

+309
-13
lines changed

9 files changed

+309
-13
lines changed

hack/ci/run-e2e-tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,6 @@ WHAT="${WHAT:-./test/e2e/...}"
109109
TEST_ARGS="${TEST_ARGS:--timeout 2h -v}"
110110
E2E_PARALLELISM=${E2E_PARALLELISM:-2}
111111

112-
(set -x; go test -tags e2e -p $E2E_PARALLELISM $TEST_ARGS "$WHAT")
112+
(set -x; go test -tags e2e -parallel $E2E_PARALLELISM $TEST_ARGS "$WHAT")
113113

114114
echo "Done. :-)"

hack/run-e2e-tests.sh

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ set -euo pipefail
1919
KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-e2e}"
2020
DATA_DIR=".e2e-$KIND_CLUSTER_NAME"
2121
OPERATOR_PID=0
22+
PROTOKOL_PID=0
23+
NO_TEARDOWN=${NO_TEARDOWN:-false}
2224

2325
mkdir -p "$DATA_DIR"
26+
rm -rf "$DATA_DIR/kind-logs"
2427
echo "Logs are stored in $DATA_DIR/."
2528
DATA_DIR="$(realpath "$DATA_DIR")"
2629

@@ -32,14 +35,26 @@ kind create cluster --name "$KIND_CLUSTER_NAME"
3235
chmod 600 "$KUBECONFIG"
3336

3437
teardown_kind() {
35-
echo "Stopping kcp-operator…"
36-
kill -TERM $OPERATOR_PID
37-
wait $OPERATOR_PID
38+
if [[ $OPERATOR_PID -gt 0 ]]; then
39+
echo "Stopping kcp-operator…"
40+
kill -TERM $OPERATOR_PID
41+
wait $OPERATOR_PID
42+
fi
43+
44+
if [[ $PROTOKOL_PID -gt 0 ]]; then
45+
echo "Stopping protokol…"
46+
kill -TERM $PROTOKOL_PID
47+
# no wait because protokol ends quickly and wait would fail
48+
fi
3849

3950
kind delete cluster --name "$KIND_CLUSTER_NAME"
4051
rm "$KUBECONFIG"
4152
}
42-
trap teardown_kind EXIT
53+
54+
if ! $NO_TEARDOWN; then
55+
echo "Will tear down kind cluster once the script has finished."
56+
trap teardown_kind EXIT
57+
fi
4358

4459
echo "Kubeconfig is in $KUBECONFIG."
4560

@@ -75,6 +90,14 @@ _build/manager \
7590
OPERATOR_PID=$!
7691
echo "Running as process $OPERATOR_PID."
7792

93+
if command -v protokol &> /dev/null; then
94+
protokol --namespace 'e2e-*' --output "$DATA_DIR/kind-logs" 2>/dev/null &
95+
PROTOKOL_PID=$!
96+
else
97+
echo "Install https://codeberg.org/xrstf/protokol to automatically"
98+
echo "collect logs from the kind cluster."
99+
fi
100+
78101
echo "Running e2e tests…"
79102

80103
export HELM_BINARY="$(realpath _tools/helm)"
@@ -84,4 +107,4 @@ WHAT="${WHAT:-./test/e2e/...}"
84107
TEST_ARGS="${TEST_ARGS:--timeout 2h -v}"
85108
E2E_PARALLELISM=${E2E_PARALLELISM:-2}
86109

87-
(set -x; go test -tags e2e -p $E2E_PARALLELISM $TEST_ARGS "$WHAT")
110+
(set -x; go test -tags e2e -parallel $E2E_PARALLELISM $TEST_ARGS "$WHAT")

internal/controller/rootshard/controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ func (r *RootShardReconciler) reconcile(ctx context.Context, rootShard *operator
144144
rootshard.VirtualWorkspacesCertificateReconciler(rootShard),
145145
rootshard.LogicalClusterAdminCertificateReconciler(rootShard),
146146
rootshard.ExternalLogicalClusterAdminCertificateReconciler(rootShard),
147+
rootshard.OperatorClientCertificateReconciler(rootShard),
147148
}
148149

149150
// Intermediate CAs that we need to generate a certificate and an issuer for.

internal/resources/resources.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ const (
3939
ShardLabel = "operator.kcp.io/shard"
4040
FrontProxyLabel = "operator.kcp.io/front-proxy"
4141
KubeconfigLabel = "operator.kcp.io/kubeconfig"
42+
43+
// OperatorUsername is the common name embedded in the operator's admin certificate
44+
// that is created for each RootShard. This name alone has no special meaning, as
45+
// the certificate also has system:masters as an organization, which is what ultimately
46+
// grants the operator its permissions.
47+
OperatorUsername = "system:kcp-operator"
4248
)
4349

4450
func GetImageSettings(imageSpec *operatorv1alpha1.ImageSpec) (string, []corev1.LocalObjectReference) {

internal/resources/rootshard/certificates.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,43 @@ func ExternalLogicalClusterAdminCertificateReconciler(rootShard *operatorv1alpha
231231
}
232232
}
233233
}
234+
235+
func OperatorClientCertificateReconciler(rootShard *operatorv1alpha1.RootShard) reconciling.NamedCertificateReconcilerFactory {
236+
const certKind = operatorv1alpha1.OperatorCertificate
237+
238+
name := resources.GetRootShardCertificateName(rootShard, certKind)
239+
template := rootShard.Spec.CertificateTemplates.CertificateTemplate(certKind)
240+
241+
return func() (string, reconciling.CertificateReconciler) {
242+
return name, func(cert *certmanagerv1.Certificate) (*certmanagerv1.Certificate, error) {
243+
cert.SetLabels(resources.GetRootShardResourceLabels(rootShard))
244+
cert.Spec = certmanagerv1.CertificateSpec{
245+
CommonName: resources.OperatorUsername,
246+
SecretName: name,
247+
Duration: &operatorv1alpha1.DefaultCertificateDuration,
248+
RenewBefore: &operatorv1alpha1.DefaultCertificateRenewal,
249+
250+
PrivateKey: &certmanagerv1.CertificatePrivateKey{
251+
Algorithm: certmanagerv1.RSAKeyAlgorithm,
252+
Size: 4096,
253+
},
254+
255+
Subject: &certmanagerv1.X509Subject{
256+
Organizations: []string{"system:kcp:admin"},
257+
},
258+
259+
Usages: []certmanagerv1.KeyUsage{
260+
certmanagerv1.UsageClientAuth,
261+
},
262+
263+
IssuerRef: certmanagermetav1.ObjectReference{
264+
Name: resources.GetRootShardCAName(rootShard, operatorv1alpha1.FrontProxyClientCA),
265+
Kind: "Issuer",
266+
Group: "cert-manager.io",
267+
},
268+
}
269+
270+
return utils.ApplyCertificateTemplate(cert, &template), nil
271+
}
272+
}
273+
}

sdk/apis/operator/v1alpha1/common.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ const (
8585
AdminKubeconfigClientCertificate Certificate = "admin-kubeconfig"
8686
LogicalClusterAdminCertificate Certificate = "logical-cluster-admin"
8787
ExternalLogicalClusterAdminCertificate Certificate = "external-logical-cluster-admin"
88+
89+
// OperatorCertificate is created for a RootShard and used by the operator to
90+
// connect
91+
OperatorCertificate Certificate = "kcp-operator"
8892
)
8993

9094
type CA string

test/e2e/rootshards/proxy_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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 rootshards
20+
21+
import (
22+
"context"
23+
"fmt"
24+
"testing"
25+
"time"
26+
27+
"github.com/go-logr/logr"
28+
kcpcorev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
29+
kcptenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
30+
"github.com/kcp-dev/logicalcluster/v3"
31+
32+
corev1 "k8s.io/api/core/v1"
33+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34+
"k8s.io/apimachinery/pkg/types"
35+
"k8s.io/apimachinery/pkg/util/wait"
36+
ctrlruntime "sigs.k8s.io/controller-runtime"
37+
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
38+
39+
operatorv1alpha1 "github.com/kcp-dev/kcp-operator/sdk/apis/operator/v1alpha1"
40+
"github.com/kcp-dev/kcp-operator/test/utils"
41+
)
42+
43+
func TestRootShardProxy(t *testing.T) {
44+
ctrlruntime.SetLogger(logr.Discard())
45+
46+
client := utils.GetKubeClient(t)
47+
ctx := context.Background()
48+
namespaceSuffix := "rootshard-proxy"
49+
50+
namespace := utils.CreateSelfDestructingNamespace(t, ctx, client, namespaceSuffix)
51+
externalHostname := fmt.Sprintf("front-proxy-front-proxy.e2e-%s.svc.cluster.local", namespaceSuffix)
52+
53+
// deploy a root shard incl. etcd
54+
rootShard := utils.DeployRootShard(ctx, t, client, namespace.Name, externalHostname)
55+
56+
// deploy a 2nd shard incl. etcd
57+
shardName := "aadvark"
58+
utils.DeployShard(ctx, t, client, namespace.Name, shardName, rootShard.Name)
59+
60+
// deploy front-proxy
61+
utils.DeployFrontProxy(ctx, t, client, namespace.Name, rootShard.Name, externalHostname)
62+
63+
configSecretName := "kubeconfig"
64+
65+
rsConfig := operatorv1alpha1.Kubeconfig{}
66+
rsConfig.Name = "test"
67+
rsConfig.Namespace = namespace.Name
68+
69+
rsConfig.Spec = operatorv1alpha1.KubeconfigSpec{
70+
Target: operatorv1alpha1.KubeconfigTarget{
71+
RootShardRef: &corev1.LocalObjectReference{
72+
Name: rootShard.Name,
73+
},
74+
},
75+
Username: "e2e",
76+
Validity: metav1.Duration{Duration: 2 * time.Hour},
77+
SecretRef: corev1.LocalObjectReference{
78+
Name: configSecretName,
79+
},
80+
Groups: []string{"system:kcp:admin"},
81+
}
82+
83+
t.Log("Creating kubeconfig for RootShard…")
84+
if err := client.Create(ctx, &rsConfig); err != nil {
85+
t.Fatal(err)
86+
}
87+
utils.WaitForObject(t, ctx, client, &corev1.Secret{}, types.NamespacedName{Namespace: rsConfig.Namespace, Name: rsConfig.Spec.SecretRef.Name})
88+
89+
t.Log("Connecting to RootShard…")
90+
rootShardClient := utils.ConnectWithKubeconfig(t, ctx, client, namespace.Name, rsConfig.Name)
91+
92+
// wait until the 2nd shard has registered itself successfully at the root shard
93+
shardKey := types.NamespacedName{Name: shardName}
94+
t.Log("Waiting for Shard to register itself on the RootShard…")
95+
utils.WaitForObject(t, ctx, rootShardClient, &kcpcorev1alpha1.Shard{}, shardKey)
96+
97+
// create workspace that we want to have scheduled onto the 2nd shard
98+
t.Log("Creating workspace with its logicalcluster on the 2nd Shard…")
99+
workspace := &kcptenancyv1alpha1.Workspace{
100+
ObjectMeta: metav1.ObjectMeta{
101+
Name: "test",
102+
},
103+
Spec: kcptenancyv1alpha1.WorkspaceSpec{
104+
Type: kcptenancyv1alpha1.WorkspaceTypeReference{
105+
Name: "universal",
106+
},
107+
Location: &kcptenancyv1alpha1.WorkspaceLocation{
108+
Selector: &metav1.LabelSelector{
109+
MatchLabels: map[string]string{
110+
"name": shardName,
111+
},
112+
},
113+
},
114+
},
115+
}
116+
if err := rootShardClient.Create(ctx, workspace); err != nil {
117+
t.Fatalf("Failed to create workspace: %v", err)
118+
}
119+
120+
err := wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
121+
err = rootShardClient.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(workspace), workspace)
122+
if err != nil {
123+
return false, err
124+
}
125+
126+
return workspace.Status.Phase == kcpcorev1alpha1.LogicalClusterPhaseReady, nil
127+
})
128+
if err != nil {
129+
t.Fatalf("Failed to wait for workspace to become ready: %v", err)
130+
}
131+
132+
// build a client through the proxy to the new workspace
133+
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+
}
137+
138+
// proof of life: list something every logicalcluster in kcp has
139+
t.Log("Should be able to list Secrets in the new workspace.")
140+
secrets := &corev1.SecretList{}
141+
if err := proxyClient.List(ctx, secrets); err != nil {
142+
t.Fatalf("Failed to list secrets in workspace: %v", err)
143+
}
144+
}

test/utils/deploy.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,21 @@ func DeployRootShard(ctx context.Context, t *testing.T, client ctrlruntimeclient
159159
},
160160
},
161161
}),
162+
Proxy: &operatorv1alpha1.RootShardProxySpec{
163+
CertificateTemplates: operatorv1alpha1.CertificateTemplateMap{
164+
string(operatorv1alpha1.ServerCertificate): operatorv1alpha1.CertificateTemplate{
165+
Spec: &operatorv1alpha1.CertificateSpecTemplate{
166+
DNSNames: []string{"localhost"},
167+
},
168+
},
169+
},
170+
},
171+
}
172+
173+
if tag := getKcpTag(); tag != "" {
174+
rootShard.Spec.Proxy.Image = &operatorv1alpha1.ImageSpec{
175+
Tag: tag,
176+
}
162177
}
163178

164179
for _, patch := range patches {

0 commit comments

Comments
 (0)