Compare commits

..

1 Commits

Author SHA1 Message Date
Kubernetes Publisher
81042e1c53 Update dependencies to v0.37.0-alpha.1 tag 2026-06-11 04:15:20 +00:00
18 changed files with 60 additions and 568 deletions

View File

@@ -36,7 +36,7 @@ type StatefulSetSpecApplyConfiguration struct {
// TODO: Consider a rename of this field.
Replicas *int32 `json:"replicas,omitempty"`
// selector is a label query over pods that should match the replica count.
// It must match the pod template's labels.
// If empty, defaulted to labels on the pod template.
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
Selector *v1.LabelSelectorApplyConfiguration `json:"selector,omitempty"`
// template is the object that describes the pod that will be created if

View File

@@ -172,8 +172,6 @@ type PodSpecApplyConfiguration struct {
EnableServiceLinks *bool `json:"enableServiceLinks,omitempty"`
// PreemptionPolicy is the Policy for preempting pods with lower priority.
// One of Never, PreemptLowerPriority.
// When Priority Admission Controller is enabled, it prevents users from setting
// this field. The admission controller populates this field from PriorityClassName.
// Defaults to PreemptLowerPriority if unset.
PreemptionPolicy *corev1.PreemptionPolicy `json:"preemptionPolicy,omitempty"`
// Overhead represents the resource overhead associated with running a pod for a given RuntimeClass.

View File

@@ -36,9 +36,6 @@ import (
// // +patchStrategy=merge
// // +listType=map
// // +listMapKey=type
// // +k8s:alpha(since: "1.37")=+k8s:optional
// // +k8s:alpha(since: "1.37")=+k8s:listType=map
// // +k8s:alpha(since: "1.37")=+k8s:listMapKey=type
// Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
//
// // other fields

View File

@@ -25,15 +25,7 @@ package v1alpha3
type GangSchedulingPolicyApplyConfiguration struct {
// MinCount is the minimum number of pods that must be schedulable or scheduled
// at the same time for the scheduler to admit the entire group.
// It must be a positive integer. This field is mutable to support workload scaling.
//
// Note that the scheduler operates on an eventually consistent model. Updates
// to minCount may not be immediately reflected in scheduling decisions due to
// propagation delays. If minCount is updated while a scheduling cycle is in
// progress for that group, the new value may not take effect until the next
// cycle. Moreover, minCount is only enforced during scheduling, meaning that
// modifications to this field do not affect already-scheduled pods, applying
// only to those evaluated in future cycles.
// It must be a positive integer.
MinCount *int32 `json:"minCount,omitempty"`
}

View File

@@ -26,18 +26,13 @@ import (
// with apply.
//
// PodGroupSchedulingPolicy defines the scheduling configuration for a PodGroup.
// Exactly one policy must be set. The policy is chosen at creation time by setting either the
// Basic or Gang field. The PodGroup may not change policy after creation.
// Fields within chosen policy may be updated after creation when their individual fields allow it.
// Exactly one policy must be set.
type PodGroupSchedulingPolicyApplyConfiguration struct {
// Basic specifies that the pods in this group should be scheduled using
// standard Kubernetes scheduling behavior. Setting this field at group creation time
// opts this group to basic scheduling; this field cannot be changed afterward.
// standard Kubernetes scheduling behavior.
Basic *schedulingv1alpha3.BasicSchedulingPolicy `json:"basic,omitempty"`
// Gang specifies that the pods in this group should be scheduled using
// all-or-nothing semantics. Setting this field at group creation time
// opts this group to gang scheduling; this field cannot be set or unset afterward.
// The minCount field within Gang scheduling policy remains mutable after group creation.
// all-or-nothing semantics.
Gang *GangSchedulingPolicyApplyConfiguration `json:"gang,omitempty"`
}

View File

@@ -28,6 +28,7 @@ type PodGroupSpecApplyConfiguration struct {
PodGroupTemplateRef *PodGroupTemplateReferenceApplyConfiguration `json:"podGroupTemplateRef,omitempty"`
// SchedulingPolicy defines the scheduling policy for this instance of the PodGroup.
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
// This field is immutable.
SchedulingPolicy *PodGroupSchedulingPolicyApplyConfiguration `json:"schedulingPolicy,omitempty"`
// SchedulingConstraints defines optional scheduling constraints (e.g. topology) for this PodGroup.
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
@@ -49,6 +50,8 @@ type PodGroupSpecApplyConfiguration struct {
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
// One of Single, All. Defaults to Single if unset.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
DisruptionMode *DisruptionModeApplyConfiguration `json:"disruptionMode,omitempty"`
// PriorityClassName defines the priority that should be considered when scheduling this pod group.
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
@@ -56,6 +59,8 @@ type PodGroupSpecApplyConfiguration struct {
// (i.e. if no priority class is specified, admission control can set this to the global default
// priority class if it exists. Otherwise, the pod group's priority will be zero).
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
PriorityClassName *string `json:"priorityClassName,omitempty"`
// Priority is the value of priority of this pod group. Various system components
// use this field to find the priority of the pod group. When Priority Admission
@@ -63,6 +68,8 @@ type PodGroupSpecApplyConfiguration struct {
// controller populates this field from PriorityClassName.
// The higher the value, the higher the priority.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
Priority *int32 `json:"priority,omitempty"`
}

View File

@@ -30,13 +30,11 @@ type PodGroupStatusApplyConfiguration struct {
// Conditions represent the latest observations of the PodGroup's state.
//
// Known condition types:
// - "PodGroupInitiallyScheduled": Indicates whether the scheduling requirement has been satisfied.
// Once this condition transitions to True, it serves as a terminal state and will never revert to False,
// even if pods are subsequently evicted and group constraints are no longer met.
// - "PodGroupScheduled": Indicates whether the scheduling requirement has been satisfied.
// - "DisruptionTarget": Indicates whether the PodGroup is about to be terminated
// due to disruption such as preemption.
//
// Known reasons for the PodGroupInitiallyScheduled condition:
// Known reasons for the PodGroupScheduled condition:
// - "Unschedulable": The PodGroup cannot be scheduled due to resource constraints,
// affinity/anti-affinity rules, or insufficient capacity for the gang.
// - "SchedulerError": The PodGroup cannot be scheduled due to some internal error

View File

@@ -30,7 +30,6 @@ type PodGroupTemplateApplyConfiguration struct {
SchedulingPolicy *PodGroupSchedulingPolicyApplyConfiguration `json:"schedulingPolicy,omitempty"`
// SchedulingConstraints defines optional scheduling constraints (e.g. topology) for this PodGroupTemplate.
// This field is only available when the TopologyAwareWorkloadScheduling feature gate is enabled.
// This field is immutable.
SchedulingConstraints *PodGroupSchedulingConstraintsApplyConfiguration `json:"schedulingConstraints,omitempty"`
// ResourceClaims defines which ResourceClaims may be shared among Pods in
// the group. Pods consume the devices allocated to a PodGroup's claim by
@@ -45,16 +44,23 @@ type PodGroupTemplateApplyConfiguration struct {
ResourceClaims []PodGroupResourceClaimApplyConfiguration `json:"resourceClaims,omitempty"`
// DisruptionMode defines the mode in which a given PodGroup can be disrupted.
// One of Single, All.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
DisruptionMode *DisruptionModeApplyConfiguration `json:"disruptionMode,omitempty"`
// PriorityClassName indicates the priority that should be considered when scheduling
// a pod group created from this template.
// This field is immutable.
// a pod group created from this template. If no priority class is specified, admission
// control can set this to the global default priority class if it exists. Otherwise,
// pod groups created from this template will have the priority set to zero.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
PriorityClassName *string `json:"priorityClassName,omitempty"`
// Priority is the value of priority of pod groups created from this template. Various
// system components use this field to find the priority of the pod group.
// system components use this field to find the priority of the pod group. When
// Priority Admission Controller is enabled, it prevents users from setting this field.
// The admission controller populates this field from PriorityClassName.
// The higher the value, the higher the priority.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
Priority *int32 `json:"priority,omitempty"`
}

View File

@@ -29,8 +29,7 @@ type WorkloadSpecApplyConfiguration struct {
// This field is immutable.
ControllerRef *TypedLocalObjectReferenceApplyConfiguration `json:"controllerRef,omitempty"`
// PodGroupTemplates is the list of templates that make up the Workload.
// The maximum number of templates is 8. Templates cannot be added or removed after the workload is created.
// Existing templates may still be updated where their individual fields allow it.
// The maximum number of templates is 8. This field is immutable.
PodGroupTemplates []PodGroupTemplateApplyConfiguration `json:"podGroupTemplates,omitempty"`
}

View File

@@ -71,7 +71,7 @@ func ServerSupportsVersion(client DiscoveryInterface, requiredGV schema.GroupVer
return err
}
versions := metav1.ExtractGroupVersions(groups)
serverVersions := sets.Set[string]{}
serverVersions := sets.String{}
for _, v := range versions {
serverVersions.Insert(v)
}

8
go.mod
View File

@@ -23,11 +23,11 @@ require (
golang.org/x/time v0.15.0
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
gopkg.in/evanphx/json-patch.v4 v4.13.0
k8s.io/api v0.37.0-alpha.2
k8s.io/apimachinery v0.37.0-alpha.2
k8s.io/api v0.37.0-alpha.1
k8s.io/apimachinery v0.37.0-alpha.1
k8s.io/klog/v2 v2.140.0
k8s.io/kube-openapi v0.0.0-20260618221249-bc653b64f974
k8s.io/streaming v0.37.0-alpha.2
k8s.io/kube-openapi v0.0.0-20260519202549-bbf5c5577288
k8s.io/streaming v0.37.0-alpha.1
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730
sigs.k8s.io/randfill v1.0.0

16
go.sum
View File

@@ -118,16 +118,16 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.37.0-alpha.2 h1:QlFfNpLR8u4REgMKPJW6stZut+Ig694IAk9v5pmL+7c=
k8s.io/api v0.37.0-alpha.2/go.mod h1:QvG6GVNM/dqApHJv1BidtiWpviZA3QbztsHgTUmgmbk=
k8s.io/apimachinery v0.37.0-alpha.2 h1:nVMCXf4rfXUIuCkpTOnhcDzZ+p5QS98XTGvfHgTW/Bk=
k8s.io/apimachinery v0.37.0-alpha.2/go.mod h1:S4uhVCP6dikhTM/sZqN4bWD53ZJZKAKN+kTZ522D/rA=
k8s.io/api v0.37.0-alpha.1 h1:fTnRCeg6apyW4NEMAzh4I0QKmhLrfBn/Wd7ofdiWrWs=
k8s.io/api v0.37.0-alpha.1/go.mod h1:Zb4KyckZ/m8i6rABBg74uJhc6A4crNQo9epNeXftsc0=
k8s.io/apimachinery v0.37.0-alpha.1 h1:1h6p2YVjU618lo3rFTTKELLzWNu/rmOQSY6Yn5VBnSI=
k8s.io/apimachinery v0.37.0-alpha.1/go.mod h1:NCrGZAB1OmwASDtTJ2J0mNYwcgFJSH6FimUsgzQ53d0=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260618221249-bc653b64f974 h1:JVogoTvOj6gutlx8bUwGh0e8o8L4X8nDbTLyONmoVvk=
k8s.io/kube-openapi v0.0.0-20260618221249-bc653b64f974/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
k8s.io/streaming v0.37.0-alpha.2 h1:j1g81dEBrCTHq+hW5Q9D5/FcJJoGa/MaUmBSMHnE0ZY=
k8s.io/streaming v0.37.0-alpha.2/go.mod h1:QN1+yCAfxcSvo0908Z3rFKkA5Xlr3KgUZAGwKuR6qQk=
k8s.io/kube-openapi v0.0.0-20260519202549-bbf5c5577288 h1:A7Lby6ekC6nv+6oO38huCMFBRP0Os+tIeq1GkwxOQes=
k8s.io/kube-openapi v0.0.0-20260519202549-bbf5c5577288/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
k8s.io/streaming v0.37.0-alpha.1 h1:8/IDL5B3WI2l7cnO3na104t9oCuQYEXZ6H3095GpfLo=
k8s.io/streaming v0.37.0-alpha.1/go.mod h1:QN1+yCAfxcSvo0908Z3rFKkA5Xlr3KgUZAGwKuR6qQk=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=

View File

@@ -33,7 +33,7 @@ func TestDaemonSetLister(t *testing.T) {
testCases := []struct {
inDSs []*extensions.DaemonSet
list func() ([]*extensions.DaemonSet, error)
outDaemonSetNames sets.Set[string]
outDaemonSetNames sets.String
expectErr bool
}{
// Basic listing
@@ -44,7 +44,7 @@ func TestDaemonSetLister(t *testing.T) {
list: func() ([]*extensions.DaemonSet, error) {
return lister.List(labels.Everything())
},
outDaemonSetNames: sets.New[string]("basic"),
outDaemonSetNames: sets.NewString("basic"),
},
// Listing multiple daemon sets
{
@@ -56,7 +56,7 @@ func TestDaemonSetLister(t *testing.T) {
list: func() ([]*extensions.DaemonSet, error) {
return lister.List(labels.Everything())
},
outDaemonSetNames: sets.New[string]("basic", "complex", "complex2"),
outDaemonSetNames: sets.NewString("basic", "complex", "complex2"),
},
// No pod labels
{
@@ -74,7 +74,7 @@ func TestDaemonSetLister(t *testing.T) {
}
return lister.GetPodDaemonSets(pod)
},
outDaemonSetNames: sets.New[string](),
outDaemonSetNames: sets.NewString(),
expectErr: true,
},
// No DS selectors
@@ -94,7 +94,7 @@ func TestDaemonSetLister(t *testing.T) {
}
return lister.GetPodDaemonSets(pod)
},
outDaemonSetNames: sets.New[string](),
outDaemonSetNames: sets.NewString(),
expectErr: true,
},
// Matching labels to selectors and namespace
@@ -123,7 +123,7 @@ func TestDaemonSetLister(t *testing.T) {
}
return lister.GetPodDaemonSets(pod)
},
outDaemonSetNames: sets.New[string]("bar"),
outDaemonSetNames: sets.NewString("bar"),
},
}
for _, c := range testCases {

View File

@@ -53,10 +53,7 @@ func TestMutationDetector(t *testing.T) {
period: 1 * time.Second,
retainDuration: 2 * time.Minute,
failureFunc: func(message string) {
select {
case mutationFound <- true:
case <-stopCh:
}
mutationFound <- true
},
}
informer.cacheMutationDetector = detector
@@ -66,14 +63,8 @@ func TestMutationDetector(t *testing.T) {
wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
detector.addedObjsLock.Lock()
addedLen := len(detector.addedObjs)
detector.addedObjsLock.Unlock()
detector.compareObjectsLock.Lock()
cachedLen := len(detector.cachedObjs)
detector.compareObjectsLock.Unlock()
return addedLen+cachedLen > 0, nil
defer detector.addedObjsLock.Unlock()
return len(detector.addedObjs) > 0, nil
})
detector.compareObjectsLock.Lock()

View File

@@ -177,15 +177,6 @@ type SharedInformer interface {
// RemoveEventHandler removes a formerly added event handler given by
// its registration handle.
// This function is guaranteed to be idempotent, and thread-safe.
//
// Note: RemoveEventHandler is asynchronous. It stops queueing new events
// but does not wait for already-queued events to finish executing.
// Goroutines processing the remaining events may still be running and
// invoking callbacks after this function returns.
//
// If the caller needs to wait for all handlers to finish executing (for
// example, to safely close channels or release resources used by the handler),
// they should use [ShutDownEventHandler].
RemoveEventHandler(handle ResourceEventHandlerRegistration) error
// GetStore returns the informer's local cache as a Store.
GetStore() Store
@@ -1023,24 +1014,6 @@ func (s *sharedIndexInformer) RemoveEventHandler(handle ResourceEventHandlerRegi
return s.processor.removeListener(handle)
}
// ShutDownEventHandler removes the event handler and blocks until it has fully
// stopped processing events.
//
// Like RemoveEventHandler, it is idempotent and thread-safe. However, it MUST NOT
// be called from within the event handler's own callbacks, as that will result
// in a deadlock.
func ShutDownEventHandler(informer SharedInformer, handle ResourceEventHandlerRegistration) error {
if err := informer.RemoveEventHandler(handle); err != nil {
return err
}
if s, ok := handle.(interface{ ShutdownChan() <-chan struct{} }); ok {
<-s.ShutdownChan()
} else {
return fmt.Errorf("handle does not support ShutdownChan()")
}
return nil
}
// sharedProcessor has a collection of processorListener and can
// distribute a notification object to its listeners. There are two
// kinds of distribute operations. The sync distributions go to a
@@ -1113,8 +1086,6 @@ func (p *sharedProcessor) removeListener(handle ResourceEventHandlerRegistration
if p.listenersStarted {
close(listener.addCh)
} else {
close(listener.runFinished)
}
return nil
@@ -1238,11 +1209,10 @@ func (p *sharedProcessor) resyncCheckPeriodChanged(logger klog.Logger, resyncChe
// processorListener also keeps track of the adjusted requested resync
// period of the listener.
type processorListener struct {
logger klog.Logger
nextCh chan interface{}
addCh chan interface{}
done chan struct{}
runFinished chan struct{}
logger klog.Logger
nextCh chan interface{}
addCh chan interface{}
done chan struct{}
handler ResourceEventHandler
handlerName string
@@ -1295,10 +1265,6 @@ func (p *processorListener) HasSyncedChecker() DoneChecker {
return p.syncTracker
}
func (p *processorListener) ShutdownChan() <-chan struct{} {
return p.runFinished
}
func newProcessListener(logger klog.Logger, handler ResourceEventHandler, requestedResyncPeriod, resyncPeriod time.Duration, now time.Time, bufferSize int, hasSynced DoneChecker) *processorListener {
handlerName := nameForHandler(handler)
ret := &processorListener{
@@ -1306,7 +1272,6 @@ func newProcessListener(logger klog.Logger, handler ResourceEventHandler, reques
nextCh: make(chan interface{}),
addCh: make(chan interface{}),
done: make(chan struct{}),
runFinished: make(chan struct{}),
upstreamHasSynced: hasSynced,
handler: handler,
handlerName: handlerName,
@@ -1363,7 +1328,6 @@ func (p *processorListener) pop() {
}
func (p *processorListener) run() {
defer close(p.runFinished)
// this call blocks until the channel is closed. When a panic happens during the notification
// we will catch it, **the offending item will be skipped!**, and after a short delay (one second)
// the next notification will be attempted. This is usually better than the alternative of never

View File

@@ -1385,221 +1385,3 @@ func numOccurrences(hay, needle string) int {
hay = hay[index+len(needle):]
}
}
func TestRemoveEventHandler_AsynchronousPanic(t *testing.T) {
source := newFakeControllerSource(t)
informer := NewSharedInformer(source, &v1.Pod{}, 0).(*sharedIndexInformer)
stop := make(chan struct{})
var wg wait.Group
wg.StartWithChannel(stop, informer.Run)
defer func() {
close(stop)
wg.Wait()
}()
scheduledPods := make(chan *v1.Pod, 1)
blockChan := make(chan struct{})
handlerStarted := make(chan struct{})
handlerFinished := make(chan struct{})
var panicVal any
var panicked bool
handler := ResourceEventHandlerFuncs{
AddFunc: func(obj any) {
close(handlerStarted)
<-blockChan
defer close(handlerFinished)
defer func() {
if r := recover(); r != nil {
panicVal = r
panicked = true
}
}()
scheduledPods <- obj.(*v1.Pod)
},
}
handle, err := informer.AddEventHandler(handler)
if err != nil {
t.Fatalf("Failed to add event handler: %v", err)
}
// Trigger the event
source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}})
// Wait for handler to start and block
<-handlerStarted
// Now remove the handler. This should return immediately.
err = informer.RemoveEventHandler(handle)
if err != nil {
t.Fatalf("Failed to remove event handler: %v", err)
}
// Close the channel that the handler writes to
close(scheduledPods)
// Unblock the handler
close(blockChan)
// Wait for handler to finish
<-handlerFinished
if !panicked {
t.Fatalf("Expected handler to panic (send on closed channel), but it did not")
}
t.Logf("Caught expected panic: %v", panicVal)
}
func TestRemoveEventHandler_SynchronousShutdown(t *testing.T) {
source := newFakeControllerSource(t)
informer := NewSharedInformer(source, &v1.Pod{}, 0).(*sharedIndexInformer)
stop := make(chan struct{})
var wg wait.Group
wg.StartWithChannel(stop, informer.Run)
defer func() {
close(stop)
wg.Wait()
}()
scheduledPods := make(chan *v1.Pod, 1)
blockChan := make(chan struct{})
handlerStarted := make(chan struct{})
handlerFinished := make(chan struct{})
var panicVal any
var panicked bool
handler := ResourceEventHandlerFuncs{
AddFunc: func(obj any) {
close(handlerStarted)
<-blockChan
defer close(handlerFinished)
defer func() {
if r := recover(); r != nil {
panicVal = r
panicked = true
}
}()
scheduledPods <- obj.(*v1.Pod)
},
}
handle, err := informer.AddEventHandler(handler)
if err != nil {
t.Fatalf("Failed to add event handler: %v", err)
}
// Trigger the event
source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}})
// Wait for handler to start and block
<-handlerStarted
// Unblock the handler so it can run and exit
close(blockChan)
// Now remove the handler and wait for it to fully stop.
err = ShutDownEventHandler(informer, handle)
if err != nil {
t.Fatalf("Failed to shutdown event handler: %v", err)
}
// NOW it is safe to close the channel
close(scheduledPods)
// Wait for handler function to finish (should already be done)
<-handlerFinished
if panicked {
t.Fatalf("Handler panicked unexpectedly: %v", panicVal)
}
}
func TestShutDownEventHandler_Lifecycles(t *testing.T) {
t.Run("InformerNeverStarted", func(t *testing.T) {
source := newFakeControllerSource(t)
informer := NewSharedInformer(source, &v1.Pod{}, 0).(*sharedIndexInformer)
handler := ResourceEventHandlerFuncs{
AddFunc: func(obj any) {},
}
handle, err := informer.AddEventHandler(handler)
if err != nil {
t.Fatalf("Failed to add event handler: %v", err)
}
// Shutdown should return immediately because the informer was never started.
err = ShutDownEventHandler(informer, handle)
if err != nil {
t.Fatalf("Failed to shutdown event handler: %v", err)
}
})
t.Run("StartedThenAddedThenShutDown", func(t *testing.T) {
source := newFakeControllerSource(t)
informer := NewSharedInformer(source, &v1.Pod{}, 0).(*sharedIndexInformer)
stop := make(chan struct{})
var wg wait.Group
wg.StartWithChannel(stop, informer.Run)
defer func() {
close(stop)
wg.Wait()
}()
// Wait deterministically for the informer to sync (proving it has started)
syncCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if !WaitFor(syncCtx, "informer sync", informer.HasSyncedChecker()) {
t.Fatalf("Informer did not sync")
}
handler := ResourceEventHandlerFuncs{
AddFunc: func(obj any) {},
}
handle, err := informer.AddEventHandler(handler)
if err != nil {
t.Fatalf("Failed to add event handler: %v", err)
}
err = ShutDownEventHandler(informer, handle)
if err != nil {
t.Fatalf("Failed to shutdown event handler: %v", err)
}
})
t.Run("AddedThenStartedThenShutDown", func(t *testing.T) {
source := newFakeControllerSource(t)
informer := NewSharedInformer(source, &v1.Pod{}, 0).(*sharedIndexInformer)
handler := ResourceEventHandlerFuncs{
AddFunc: func(obj any) {},
}
handle, err := informer.AddEventHandler(handler)
if err != nil {
t.Fatalf("Failed to add event handler: %v", err)
}
stop := make(chan struct{})
var wg wait.Group
wg.StartWithChannel(stop, informer.Run)
defer func() {
close(stop)
wg.Wait()
}()
// Wait deterministically for the informer to sync (proving it has started)
syncCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if !WaitFor(syncCtx, "informer sync", informer.HasSyncedChecker()) {
t.Fatalf("Informer did not sync")
}
err = ShutDownEventHandler(informer, handle)
if err != nil {
t.Fatalf("Failed to shutdown event handler: %v", err)
}
})
}

View File

@@ -18,13 +18,13 @@ package certificate
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"reflect"
@@ -202,13 +202,6 @@ type Config struct {
// CertifcateRenewFailure will record a metric that keeps track of
// certificate renewal failures.
CertificateRenewFailure Counter
// GenerateKey is an optional function to generate the private key for a new
// certificate signing request. If not set, an ECDSA P-256 key is generated.
// Currently only *ecdsa.PrivateKey and *rsa.PrivateKey are supported.
// The custom key must be strong enough or an error will be returned
// when attempting to generate a CSR. For RSA the minimum bits must be 2048,
// for ECDSA the minimum curve size must be 256 bits.
GenerateKey func() (crypto.Signer, error)
// Name is an optional string that will be used when writing log output
// via logger.WithName or returning errors from manager methods.
//
@@ -276,7 +269,6 @@ type manager struct {
signerName string
requestedCertificateLifetime *time.Duration
getUsages func(privateKey interface{}) []certificates.KeyUsage
generateKey func() (crypto.Signer, error)
forceRotation bool
certStore Store
@@ -330,7 +322,6 @@ func NewManager(config *Config) (Manager, error) {
signerName: config.SignerName,
requestedCertificateLifetime: config.RequestedCertificateLifetime,
getUsages: getUsages,
generateKey: config.GenerateKey,
certStore: config.CertificateStore,
certificateRotation: config.CertificateRotation,
certificateRenewFailure: config.CertificateRenewFailure,
@@ -771,29 +762,19 @@ func (m *manager) updateServerError(err error) error {
return nil
}
var generateKeyFunc = generateKeyFuncImpl
func generateKeyFuncImpl() (crypto.Signer, error) {
return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
}
func (m *manager) generateCSR() (template *x509.CertificateRequest, csrPEM []byte, keyPEM []byte, key interface{}, err error) {
generateKey := m.generateKey
if generateKey == nil {
generateKey = generateKeyFunc
}
// Generate a new private key.
privateKey, err := generateKey()
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("unable to generate a new private key: %w", err)
}
if err = validateKeyStrength(privateKey); err != nil {
return nil, nil, nil, nil, fmt.Errorf("the new key is insecure: %w", err)
}
keyPEM, err = keyutil.MarshalPrivateKeyToPEM(privateKey)
der, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("unable to marshal the new key to PEM: %w", err)
return nil, nil, nil, nil, fmt.Errorf("unable to marshal the new key to DER: %w", err)
}
keyPEM = pem.EncodeToMemory(&pem.Block{Type: keyutil.ECPrivateKeyBlockType, Bytes: der})
template = m.getTemplate()
if template == nil {
return nil, nil, nil, nil, errors.New("unable to create a csr, no template available")
@@ -826,25 +807,3 @@ func hasKeyUsage(usages []certificates.KeyUsage, usage certificates.KeyUsage) bo
}
return false
}
// validateKeyStrength checks that the key is strong enough to be used for a certificate.
// For RSA the minimum bits must be 2048, for ECDSA the minimum curve size must be 256 bits.
func validateKeyStrength(key crypto.Signer) error {
const (
minRSAKeyBits = 2048
minECDSAKeyBits = 256
)
switch k := key.(type) {
case *rsa.PrivateKey:
if bits := k.N.BitLen(); bits < minRSAKeyBits {
return fmt.Errorf("RSA key size %d bits is below the minimum of %d",
bits, minRSAKeyBits)
}
case *ecdsa.PrivateKey:
if bits := k.Curve.Params().BitSize; bits < minECDSAKeyBits {
return fmt.Errorf("ECDSA key curve %s (%d bits) is below the minimum of %d",
k.Curve.Params().Name, bits, minECDSAKeyBits)
}
}
return nil
}

View File

@@ -19,7 +19,6 @@ package certificate
import (
"bytes"
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
@@ -44,7 +43,6 @@ import (
"k8s.io/client-go/kubernetes/fake"
certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
clienttesting "k8s.io/client-go/testing"
"k8s.io/client-go/util/keyutil"
"k8s.io/klog/v2"
"k8s.io/klog/v2/ktesting"
netutils "k8s.io/utils/net"
@@ -1188,200 +1186,6 @@ func TestContext(t *testing.T) {
require.Contains(t, logger.GetSink().(ktesting.Underlier).GetBuffer().String(), "certificate: Rotating certificates", "contextual log output from manager.rotateCerts")
}
// testGenerateECKeyPEM1 is a EC P-256 private key fixture.
// This key is for testing purposes only and is not considered secure.
const testGenerateECKeyPEM1 = `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIHb7bL0ccx8oLfPycKQT/R2sKrPH8LU5CnDz/65jUjWooAoGCCqGSM49
AwEHoUQDQgAExNsMY0QTw83e3eFOZMLqpT6vqEAvpSMo5+9TSU/faJScYeSsHxlK
tO96nbPcQbWCMGjhrpBWYZcn07iu125DpA==
-----END EC PRIVATE KEY-----
`
// testGenerateECKeyPEM2 is a EC P-256 private key fixture.
// This key is for testing purposes only and is not considered secure.
const testGenerateECKeyPEM2 = `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIGy34cJkuN1N5chK9kLnf/Y5OT1rulnzyz6qignGpOJvoAoGCCqGSM49
AwEHoUQDQgAE5CqbCF3D+r/QNUv8yrr/+kqOMTP6PGUe2G4AFvisUqGx0KoRj5dq
F3qmQC6E+3zzI7+uhpZ3Ju/+696ZQ6GrJQ==
-----END EC PRIVATE KEY-----
`
// testGenerateRSAKeyPEM1 is a RSA 2048 private key fixture.
// This key is for testing purposes only and is not considered secure.
const testGenerateRSAKeyPEM1 = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDFav2tigP5v/EM
x3ktZ+me1/h+L8zfA/djb58M2GiBZMMfyCpHrLxiBrrj5eRH6HUuETHk8cMymmg1
HfzJ7P03/1W99vSiZMe1jsScZUeXZMiqJjF2UkiaJ73I/8xyzz+M/+E1UfZctjqm
gV7R8ATivtKXhOW0GPgrYShZ+62pAaZfYixVNxs9LhNmv0BGGLtPsJ4bdcc0lD1b
e/n0wFhAN59QwOaTUvRnQtYK8hYXSG/v5sFTsMkc1ZXwyqA360UX3lcYogDx510R
SraUAu3Z+Lf4xbokczdEewZJ5jtAG85IwiFQLm8+hkWUx5PWYZH4yyTZhKqKebd3
CirlHBzBAgMBAAECggEADjbPNQ4u4xiS09y444VhKMgGuESGImM4DgzHYuFiBORV
uEAX6zk2Bx4nmVPAGqf+EpG3tJ2Y9FfHEQFWZiOOHS4L5QrsP5UKBrnU0NM/UwM1
VNWTJ3XH4cbiv0og/6iJvC6K7zqLhnldwlymOxoQ//0apJYzrm1DJmcZxKt+Vstg
gVUV7umoIsfE8YpFH4qECXAuixQ0vWi99BGuyLlnaeYP65QD6sRpkn9TZisMTpZX
GTP/PsRdKJ2rohO3/VjsZytBYsr6ibBk+FZ/+EmRYLKF5m+kROefHYyKpUgKDV+v
d5UwREsfs4MxU1IXjEROOWsdAHa+98S74+A5PBiiUQKBgQDwUAVtzODkdBb9LGUF
428pumAgox16UFG6Qh0LNNmp77VztolXKRUg40EKIm3qD7BmlR2fjI7YMbjjNlFE
XfCDWifBU+sOtHyVa+3rrjeBj13noAq3nXKEOagO6FowGgrD0eC9QTur09yG4dun
fYKXjvRlTFm1MVBKDy7noJq+6wKBgQDSTiL8q4PD9pheIcz0VmJ8B4W7JCPwNn+r
2mKcls23nIbYiweg14ZKEACby6py1L3+h2Trzy7ktnEsjqxRA/UqC7X56obqLJ7c
34zOuZXytwsDEzQZgDWBEP3Hd65eCtiSkdOVKsI5wubS9idyCtPZC4LsWdKFc5S4
rNwSD6ugAwKBgEyYpPJXgEMxAXbW5KhY0sDRJ/yfITEwUqx0kD9XLB2vSv3D68i9
Tn+6D6wER1Z4g7hexR9qtMkSKCU71fFdo+CqJsvHTL/WJXOXADHDyOth4AOJDoFy
DOM6YWfHBaAZXN8HkYOhPDzLfZn8eX/MUIiwRxPWny1St42zgzbPCSPbAoGAXrCX
yDRhi6ZITHnjklAi371zVSOcmteu/G3D4MV1sqpjfLR8psrjyA0UeRFmmXV4ZlYH
9rS+ZHRQ2MMUixXBGUFUmkYioOWeUczF1X5yKWqJJsVKvACiFo7T9S/J7sXrZXML
VSp/cQp0a6AxeoOthxhLxqdaxoOX/t6159vuZokCgYEA0VcQlf0nTABk6UaJptJf
CUOQPW3bNxHoiv5q0ZbVPIOGIRNlK4CsORdeh+tQiCuWYk1YzWk0zrKlEnPhjsml
N9Yi/TjuQRxDA97qeRMq2i35g/UHO2LZVdc96owTojs5Snxop32nbEV9zsrBjybG
T4kkiBn7MJZZreDWZUZWpC4=
-----END PRIVATE KEY-----
`
// testGenerateRSAKeyPEMInsecure is a RSA 1024 private key fixture.
// This key is for testing purposes only and is not considered secure.
const testGenerateRSAKeyPEMInsecure = `-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALB6f6vwyY0IZWcE
Fv8ViUi5kZ64sWUAiWnQhfK0lrAtICJCqy2p95cloaAVNpqvX6uLK3odT0aA7vRZ
PXtYhDtpYTSHvvbAXbHUPl9/u3EPSQ1ByOtdAPio8DmMdWD6xRK4JjfHhH6bLsI+
VHblKik/+BK+xgVo1VuNoRWC8NYRAgMBAAECgYALCs0mgGFSE2CJ5LP+J7YwcFkD
1AVYfyM59VfOPv2vPhGUxzRf/fKtmMePRUiGfvrm2EVDBaa2T/6zmApcZ4ZVeSf8
izNQilInGkCyOQhEbIJ+CE9IUXQG9h5qwdsM9Ehb6+ZkdF/JatkzKtceC0PYlSw8
ZouKqMKMylvEXHLr6QJBAOSY7MRdsip/lAaNJOk/1JCqblV15DS9WpxzDZ1+JBhp
NYrnEy7qe/p1tUKBQMt3nM2Kt1N2HXyPhWVhMvpitkkCQQDFoi2T9T31nVm/4HFV
ALK5wt3FU0MLLn+E+km/++SUjVebtjCC5Gxz+ByAsj963jhCdh+2EzgyPkOKGK5a
LwGJAkBBjuXgDuroqzvdgR8D0a15a5dG5Q90XJWe5pQSBbn+UjXrxwdGXjL+CkHY
d88ISx5qCA05X1dngJWGFJEVI7gZAkAbWHFOA6TrEzaT4g5MYKhaI6hj4T1pkql6
UNdbhRL/qv7wQKk9szV+ZlorRH6cFZtbNtT0cHxaF1tpBDk7qT1hAkEAs8WLPccN
N2BAYjQjJanWfMAIyHPdwenmKhlvkWFvnwZWQecHXDmfHZlEREqpkd9IxOIGNii7
OiCuooK0UDdbPw==
-----END PRIVATE KEY-----
`
// testGenerateECKeyPEMInsecure is a EC P-224 private key fixture.
// This key is for testing purposes only and is not considered secure.
const testGenerateECKeyPEMInsecure = `-----BEGIN PRIVATE KEY-----
MHgCAQAwEAYHKoZIzj0CAQYFK4EEACEEYTBfAgEBBByl2nOZXdac4dWx0UvrHn8G
P3p+oI+mjH6Znno3oTwDOgAE5/S2xcvEyJWSOjNilulwy9YnPOPubuZ4ztOh95kU
eaVq8eqxqzGSxwanMrwbD/1PQ97kNdi53No=
-----END PRIVATE KEY-----
`
func TestGenerateKeyConfig(t *testing.T) {
parsedECKey1 := parsePEMKey(t, testGenerateECKeyPEM1)
parsedECKey2 := parsePEMKey(t, testGenerateECKeyPEM2)
parsedRSAKey1 := parsePEMKey(t, testGenerateRSAKeyPEM1)
fixedErr := errors.New("some error")
testCases := []struct {
name string
generateKey func() (crypto.Signer, error)
wantKey crypto.Signer
wantErr error
}{
{
name: "nil returns default EC key (key1)",
generateKey: nil,
wantKey: parsedECKey1,
},
{
name: "returns custom EC key (key2)",
generateKey: func() (crypto.Signer, error) { return parsedECKey2, nil },
wantKey: parsedECKey2,
},
{
name: "returns custom RSA key",
generateKey: func() (crypto.Signer, error) { return parsedRSAKey1, nil },
wantKey: parsedRSAKey1,
},
{
name: "simulate parsing error",
generateKey: func() (crypto.Signer, error) { return nil, fixedErr },
wantErr: fixedErr,
},
}
// Setup default overload.
orig := generateKeyFunc
generateKeyFunc = func() (crypto.Signer, error) { return parsedECKey1, nil }
defer func() { generateKeyFunc = orig }()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, ctx := ktesting.NewTestContext(t)
m := manager{
getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} },
generateKey: tc.generateKey,
now: time.Now,
ctx: ctx,
}
_, _, _, key, err := m.generateCSR()
if tc.wantErr != nil {
if !errors.Is(err, tc.wantErr) {
t.Errorf("expected error %v, got %v", tc.wantErr, err)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if key != tc.wantKey {
t.Errorf("expected key: %T, got: %T", tc.wantKey, key)
}
})
}
}
func TestValidateKeyStrength(t *testing.T) {
testCases := []struct {
name string
key crypto.Signer
wantErr bool
}{
{
name: "RSA 1024 is insecure",
key: parsePEMKey(t, testGenerateRSAKeyPEMInsecure),
wantErr: true,
},
{
name: "ECDSA P-224 is insecure",
key: parsePEMKey(t, testGenerateECKeyPEMInsecure),
wantErr: true,
},
{
name: "RSA 2048 is secure",
key: parsePEMKey(t, testGenerateRSAKeyPEM1),
wantErr: false,
},
{
name: "ECDSA P-256 is secure",
key: parsePEMKey(t, testGenerateECKeyPEM1),
wantErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := validateKeyStrength(tc.key)
if tc.wantErr != (err != nil) {
t.Errorf("expected error: %v, got: %v, error: %v",
tc.wantErr, err != nil, err)
}
})
}
}
func parsePEMKey(t *testing.T, pem string) crypto.Signer {
t.Helper()
k, err := keyutil.ParsePrivateKeyPEM([]byte(pem))
if err != nil {
t.Fatalf("failed to parse key fixture: %v", err)
}
return k.(crypto.Signer)
}
type fakeClientFailureType int
const (