Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ presubmits:
- name: KCP_TAG
value: main
- name: KCP_RELEASE
value: '0.33'
value: '0.34'
resources:
requests:
memory: 8Gi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11510,6 +11510,28 @@ spec:
- name
- spec
type: object
shardPeers:
description: |-
Optional: ShardPeers are the root shard and all Shards belonging to it, used to
seed shard discovery through the Admin workspace.
items:
description: |-
ShardPeer is a shard of the installation that serves the Admin workspace
(/services/admin), which front-proxies use to discover all shards.
properties:
name:
description: Name is the name of the RootShard or Shard object.
type: string
url:
description: |-
URL is the base URL of the server serving the shard's Admin workspace:
the shard itself, or its external VirtualWorkspace server if one is configured.
type: string
required:
- name
- url
type: object
type: array
shards:
description: 'Optional: Shards are the names of all Shards belonging
to the root shard.'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7634,6 +7634,28 @@ spec:
- etcd
- external
type: object
shardPeers:
description: |-
Optional: ShardPeers are the root shard and all Shards belonging to it, used to
seed shard discovery through the Admin workspace.
items:
description: |-
ShardPeer is a shard of the installation that serves the Admin workspace
(/services/admin), which front-proxies use to discover all shards.
properties:
name:
description: Name is the name of the RootShard or Shard object.
type: string
url:
description: |-
URL is the base URL of the server serving the shard's Admin workspace:
the shard itself, or its external VirtualWorkspace server if one is configured.
type: string
required:
- name
- url
type: object
type: array
shards:
description: 'Optional: Shards are the names of all Shards belonging
to the root shard.'
Expand Down
22 changes: 22 additions & 0 deletions internal/resources/compiledfrontproxy/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ func (r *reconciler) deploymentReconciler() reconciling.NamedDeploymentReconcile
mountSecret(secretName, frontProxyBasepath+"/kubeconfig", false)
}

if len(r.shardPeers()) > 0 && supportsShardPeers(version) {
mountSecret(r.peersKubeconfigSecretName(), peersKubeconfigPath, true)
}

// front-proxy kubeconfig client cert
mountSecret(r.certName(operatorv1alpha1.KubeconfigCertificate), frontProxyBasepath+"/kubeconfig-client-cert", true)

Expand Down Expand Up @@ -249,6 +253,20 @@ var defaultArgs = []string{
"--mapping-file=/etc/kcp-front-proxy/config/path-mapping.yaml",
}

const peersKubeconfigPath = frontProxyBasepath + "/peers-kubeconfig"

// supportsShardPeers returns whether the front-proxy discovers shards through the Admin
// workspace and accepts --shard-peer-kubeconfig.
func supportsShardPeers(version *semver.Version) bool {
if version == nil {
return true
}

constraint, _ := semver.NewConstraint(">=0.34.0-0")

return constraint.Check(version)
}

func supportsMountProxy(version *semver.Version) bool {
if version == nil {
return true
Expand All @@ -264,6 +282,10 @@ func (r *reconciler) getArgs(version *semver.Version) []string {

args = append(args, fmt.Sprintf("--client-ca-file=%s/client-ca/tls.crt", frontProxyBasepath))

if len(r.shardPeers()) > 0 && supportsShardPeers(version) {
args = append(args, fmt.Sprintf("--shard-peer-kubeconfig=%s/kubeconfig", peersKubeconfigPath))
}

if supportsMountProxy(version) {
args = append(args,
fmt.Sprintf("--requestheader-client-ca-file=%s/tls.crt", getCAMountPath(operatorv1alpha1.RequestHeaderClientCA)),
Expand Down
20 changes: 20 additions & 0 deletions internal/resources/compiledfrontproxy/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,22 @@ func (r *reconciler) rootShardCAName(caName operatorv1alpha1.CA) string {
return resources.GetCompiledRootShardCAName(r.rootShard, caName)
}

// shardPeers returns the shards seeding the proxy's shard discovery, in either mode.
func (r *reconciler) shardPeers() []deployv1alpha1.ShardPeer {
if r.frontProxy != nil {
return r.frontProxy.Spec.ShardPeers
}
return r.rootShard.Spec.ShardPeers
}

// peersKubeconfigSecretName is the Secret holding the kubeconfig for --shard-peer-kubeconfig.
func (r *reconciler) peersKubeconfigSecretName() string {
if r.frontProxy != nil {
return resources.GetCompiledFrontProxyPeersKubeconfigName(r.frontProxy)
}
return resources.GetCompiledRootShardProxyPeersKubeconfigName(r.rootShard)
}

// certName returns the name of one of the proxy's own certificates.
func (r *reconciler) certName(certKind operatorv1alpha1.Certificate) string {
if r.frontProxy != nil {
Expand Down Expand Up @@ -154,6 +170,10 @@ func (r *reconciler) Reconcile(ctx context.Context, client ctrlruntimeclient.Cli
r.dynamicKubeconfigSecretReconciler(),
}

if len(r.shardPeers()) > 0 {
secretReconcilers = append(secretReconcilers, r.peersKubeconfigSecretReconciler())
}

deploymentReconcilers := []k8creconciling.NamedDeploymentReconcilerFactory{
r.deploymentReconciler(),
}
Expand Down
60 changes: 60 additions & 0 deletions internal/resources/compiledfrontproxy/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,63 @@ func (r *reconciler) dynamicKubeconfig() ([]byte, error) {

return yaml.Marshal(kubeconfig)
}

func (r *reconciler) peersKubeconfigSecretReconciler() reconciling.NamedSecretReconcilerFactory {
return func() (string, reconciling.SecretReconciler) {
return r.peersKubeconfigSecretName(), func(obj *corev1.Secret) (*corev1.Secret, error) {
obj.SetLabels(r.resourceLabels)

kubeconfig, err := r.peersKubeconfig()
if err != nil {
return nil, err
}

obj.Data = map[string][]byte{
"kubeconfig": kubeconfig,
}

return obj, nil
}
}
}

// peersKubeconfig lists every shard as a named cluster. kcp discovers shards through the
// Admin workspace of these seed peers; the current context supplies the credentials and
// TLS settings used for all of them.
func (r *reconciler) peersKubeconfig() ([]byte, error) {
peers := r.shardPeers()

kubeconfig := clientcmdv1.Config{
Contexts: []clientcmdv1.NamedContext{
{
Name: "peers",
Context: clientcmdv1.Context{
Cluster: peers[0].Name,
AuthInfo: "admin",
},
},
},
CurrentContext: "peers",
AuthInfos: []clientcmdv1.NamedAuthInfo{
{
Name: "admin",
AuthInfo: clientcmdv1.AuthInfo{
ClientCertificate: clientCertificatePath,
ClientKey: clientKeyPath,
},
},
},
}

for _, peer := range peers {
kubeconfig.Clusters = append(kubeconfig.Clusters, clientcmdv1.NamedCluster{
Name: peer.Name,
Cluster: clientcmdv1.Cluster{
CertificateAuthority: kubeconfigCAPath,
Server: peer.URL,
},
})
}

return yaml.Marshal(kubeconfig)
}
117 changes: 117 additions & 0 deletions internal/resources/compiledfrontproxy/secrets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
Copyright 2026 The kcp Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package compiledfrontproxy

import (
"slices"
"testing"

"github.com/Masterminds/semver/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"k8s.io/client-go/tools/clientcmd"

deployv1alpha1 "github.com/kcp-dev/kcp-operator/sdk/apis/deploy/v1alpha1"
)

var testPeers = []deployv1alpha1.ShardPeer{
{Name: "r00t", URL: "https://r00t-kcp.ns.svc.cluster.local:6443"},
{Name: "shard1", URL: "https://shard1-shard-kcp.ns.svc.cluster.local:6443"},
}

func TestPeersKubeconfig(t *testing.T) {
rec := NewFrontProxy(&deployv1alpha1.CompiledFrontProxy{
Spec: deployv1alpha1.CompiledFrontProxySpec{ShardPeers: testPeers},
})

data, err := rec.peersKubeconfig()
require.NoError(t, err)

config, err := clientcmd.Load(data)
require.NoError(t, err)

require.Len(t, config.Clusters, len(testPeers))
for _, peer := range testPeers {
cluster, ok := config.Clusters[peer.Name]
require.True(t, ok, "missing cluster for peer %q", peer.Name)
assert.Equal(t, peer.URL, cluster.Server)
assert.Equal(t, kubeconfigCAPath, cluster.CertificateAuthority)
}

current := config.Contexts[config.CurrentContext]
require.NotNil(t, current)
assert.Equal(t, testPeers[0].Name, current.Cluster)

authInfo := config.AuthInfos[current.AuthInfo]
require.NotNil(t, authInfo)
assert.Equal(t, clientCertificatePath, authInfo.ClientCertificate)
assert.Equal(t, clientKeyPath, authInfo.ClientKey)
}

func TestShardPeerArgs(t *testing.T) {
const flag = "--shard-peer-kubeconfig=/etc/kcp-front-proxy/peers-kubeconfig/kubeconfig"

tests := []struct {
name string
peers []deployv1alpha1.ShardPeer
version *semver.Version
expected bool
}{
{
name: "unknown version with peers",
peers: testPeers,
expected: true,
},
{
name: "supported version with peers",
peers: testPeers,
version: semver.MustParse("0.34.0"),
expected: true,
},
{
name: "prerelease of supported version",
peers: testPeers,
version: semver.MustParse("0.34.0-alpha.1"),
expected: true,
},
{
name: "unsupported version",
peers: testPeers,
version: semver.MustParse("0.33.1"),
expected: false,
},
{
name: "no peers",
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
frontProxy := NewFrontProxy(&deployv1alpha1.CompiledFrontProxy{
Spec: deployv1alpha1.CompiledFrontProxySpec{ShardPeers: tt.peers},
})
assert.Equal(t, tt.expected, slices.Contains(frontProxy.getArgs(tt.version), flag))

rootShardProxy := NewRootShardProxy(&deployv1alpha1.CompiledRootShard{
Spec: deployv1alpha1.CompiledRootShardSpec{ShardPeers: tt.peers},
})
assert.Equal(t, tt.expected, slices.Contains(rootShardProxy.getArgs(tt.version), flag))
})
}
}
3 changes: 2 additions & 1 deletion internal/resources/frontproxy/compiled.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (

// CompiledFrontProxyReconciler resolves a FrontProxy and everything it references into the
// render input the CompiledFrontProxy controller consumes.
func CompiledFrontProxyReconciler(frontProxy *operatorv1alpha1.FrontProxy, rootShard *operatorv1alpha1.RootShard, shards []operatorv1alpha1.Shard, revisions map[string]string) reconciling.NamedCompiledFrontProxyReconcilerFactory {
func CompiledFrontProxyReconciler(frontProxy *operatorv1alpha1.FrontProxy, rootShard *operatorv1alpha1.RootShard, shards []operatorv1alpha1.Shard, peers []deployv1alpha1.ShardPeer, revisions map[string]string) reconciling.NamedCompiledFrontProxyReconcilerFactory {
return func() (string, reconciling.CompiledFrontProxyReconciler) {
return frontProxy.Name, func(obj *deployv1alpha1.CompiledFrontProxy) (*deployv1alpha1.CompiledFrontProxy, error) {
obj.Labels = maps.Clone(frontProxy.Labels)
Expand All @@ -56,6 +56,7 @@ func CompiledFrontProxyReconciler(frontProxy *operatorv1alpha1.FrontProxy, rootS
}

obj.Spec.Shards = utils.ShardNames(shards)
obj.Spec.ShardPeers = peers

return obj, nil
}
Expand Down
8 changes: 8 additions & 0 deletions internal/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,14 @@ func GetCompiledFrontProxyDynamicKubeconfigName(f *deployv1alpha1.CompiledFrontP
return fmt.Sprintf("%s-%s-dynamic-kubeconfig", f.Spec.RootShard.Name, f.Name)
}

func GetCompiledRootShardProxyPeersKubeconfigName(r *deployv1alpha1.CompiledRootShard) string {
return fmt.Sprintf("%s-proxy-peers-kubeconfig", r.Name)
}

func GetCompiledFrontProxyPeersKubeconfigName(f *deployv1alpha1.CompiledFrontProxy) string {
return fmt.Sprintf("%s-%s-peers-kubeconfig", f.Spec.RootShard.Name, f.Name)
}

func GetCacheServerClientCertificateName(s *operatorv1alpha1.CacheServer) string {
return fmt.Sprintf("%s-client-certificate", s.Name)
}
Expand Down
3 changes: 2 additions & 1 deletion internal/resources/rootshard/compiled.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (

// CompiledRootShardReconciler resolves a RootShard and everything it references into the
// render input the CompiledRootShard controller consumes.
func CompiledRootShardReconciler(rootShard *operatorv1alpha1.RootShard, kcpVW *operatorv1alpha1.VirtualWorkspace, shards []operatorv1alpha1.Shard, revisions map[string]string) reconciling.NamedCompiledRootShardReconcilerFactory {
func CompiledRootShardReconciler(rootShard *operatorv1alpha1.RootShard, kcpVW *operatorv1alpha1.VirtualWorkspace, shards []operatorv1alpha1.Shard, peers []deployv1alpha1.ShardPeer, revisions map[string]string) reconciling.NamedCompiledRootShardReconcilerFactory {
return func() (string, reconciling.CompiledRootShardReconciler) {
return rootShard.Name, func(obj *deployv1alpha1.CompiledRootShard) (*deployv1alpha1.CompiledRootShard, error) {
obj.Labels = maps.Clone(rootShard.Labels)
Expand Down Expand Up @@ -58,6 +58,7 @@ func CompiledRootShardReconciler(rootShard *operatorv1alpha1.RootShard, kcpVW *o
}

obj.Spec.Shards = utils.ShardNames(shards)
obj.Spec.ShardPeers = peers

return obj, nil
}
Expand Down
Loading
Loading