Skip to content
Closed
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 Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ ifdef SUITES
SUITES_ARG = --suites $(SUITES)
COMPLETE_SUITES_ARG = -args $(SUITES_ARG)
endif
TEST_FEATURE_GATES ?= WorkspaceMounts=true,CacheAPIs=true,WorkspaceAuthentication=true
TEST_FEATURE_GATES ?= WorkspaceMounts=true,CacheAPIs=true,WorkspaceAuthentication=true,KcpNativeGarbageCollector=true
PROXY_FEATURE_GATES ?= $(TEST_FEATURE_GATES)

.PHONY: test-e2e
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
github.com/kcp-dev/sdk v0.0.0
github.com/martinlindhe/base36 v1.1.1
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
github.com/ntnn/go-ntnn v0.0.0-20251229015853-51cc0da7e08f
github.com/prometheus/client_golang v1.22.0
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/ntnn/go-ntnn v0.0.0-20251229015853-51cc0da7e08f h1:w1DgvbCztYvtdvzXcF6I+QMjyMoSKrxQU9W1yRJQ4dU=
github.com/ntnn/go-ntnn v0.0.0-20251229015853-51cc0da7e08f/go.mod h1:6NY9sWL6eKDrvfT2JcweMfakeTbkIANaAf9qXtlfNrQ=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
Expand Down
8 changes: 8 additions & 0 deletions pkg/features/kcp_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ const (
// users into workspaces from foreign OIDC issuers. This feature can be individually enabled on each shard and
// the front-proxy.
WorkspaceAuthentication featuregate.Feature = "WorkspaceAuthentication"

// owner: @ntnn
// alpha: v0.1
// Enables the kcp-native garbage collector. When disabled kcp relies on a modified version of the upstream garbage collector.
KcpNativeGarbageCollector featuregate.Feature = "KcpNativeGarbageCollector"
)

// DefaultFeatureGate exposes the upstream feature gate, but with our gate setting applied.
Expand Down Expand Up @@ -141,6 +146,9 @@ var defaultVersionedGenericControlPlaneFeatureGates = map[featuregate.Feature]fe
WorkspaceAuthentication: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
KcpNativeGarbageCollector: {
{Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.Alpha},
},
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side:
genericfeatures.APIResponseCompression: {
Expand Down
125 changes: 125 additions & 0 deletions pkg/reconciler/garbagecollector/gc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
Copyright 2025 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 garbagecollector

import (
"context"

"github.com/go-logr/logr"

"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/util/workqueue"

kcpapiextensionsv1 "github.com/kcp-dev/client-go/apiextensions/informers/apiextensions/v1"
kcpkubernetesclient "github.com/kcp-dev/client-go/kubernetes"
kcpmetadataclient "github.com/kcp-dev/client-go/metadata"
corev1alpha1informers "github.com/kcp-dev/sdk/client/informers/externalversions/core/v1alpha1"

"github.com/kcp-dev/kcp/pkg/informer"
"github.com/kcp-dev/kcp/pkg/logging"
"github.com/kcp-dev/kcp/pkg/reconciler/dynamicrestmapper"
)

const NativeControllerName = "kcp-native-garbage-collector"

type Options struct {
LogicalClusterInformer corev1alpha1informers.LogicalClusterClusterInformer
CRDInformer kcpapiextensionsv1.CustomResourceDefinitionClusterInformer
DynRESTMapper *dynamicrestmapper.DynamicRESTMapper
Logger logr.Logger
KubeClusterClient kcpkubernetesclient.ClusterInterface
MetadataClusterClient kcpmetadataclient.ClusterInterface
SharedInformerFactory *informer.DiscoveringDynamicSharedInformerFactory
InformersSynced chan struct{}

DeletionWorkers int
}

// GarbageCollector is a kcp-native garbage collector that cascades and
// orphans resources based on their relationships.
type GarbageCollector struct {
options Options

log logr.Logger

graph *Graph

handlerCancels map[schema.GroupVersionResource]func()

deletionQueue workqueue.TypedRateLimitingInterface[*deletionItem]
}

func NewGarbageCollector(options Options) *GarbageCollector {
gc := &GarbageCollector{}

gc.options = options
if gc.options.DeletionWorkers <= 0 {
gc.options.DeletionWorkers = 2
}

gc.log = logging.WithReconciler(options.Logger, NativeControllerName)

gc.graph = NewGraph()
gc.handlerCancels = make(map[schema.GroupVersionResource]func())
gc.deletionQueue = workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[*deletionItem](),
workqueue.TypedRateLimitingQueueConfig[*deletionItem]{
Name: ControllerName,
},
)

return gc
}

// Start starts the garbage collector.
//
// The GC uses cluster-aware informers to watch builtin Kubernetes
// resources, as well as CRDs to dynamically start and stop watchers for
// dynamic resources across all logical clusters the shard is
// responsible for.
//
// Whne resources are changed, the GC updates an internal graph to track
// the relationships between objects.
func (gc *GarbageCollector) Start(ctx context.Context) {
defer utilruntime.HandleCrash()
defer gc.deletionQueue.ShutDown()

// Wait for informers to be started and synced.
//
// TODO(ntnn): Without waiting the GC will fail. Specifically
// builtin APIs will work and the CRD handlers will register new
// monitors for new resources _but_ the handlers for these resources
// then do not fire.
// That doesn't make a lot of sense to me because registering the
// handlers and the caches being started and synced should be
// independent.
// I suspect that somewhere something in the informer factory is
// swapped out without carrying the existing registrations over,
// causing handlers registered before the swapping to not be
// notified once the informers are started.
<-gc.options.InformersSynced

// Register handlers for builtin APIs and CRDs.
deregister := gc.registerHandlers(ctx)
defer deregister()

// Run deletion workers.
gc.startDeletion(ctx)

<-ctx.Done()
}
Loading