Compare commits

...

25 Commits

Author SHA1 Message Date
Kubernetes Publisher
68d28fb0b4 Update dependencies to v0.37.0-alpha.2 tag 2026-06-25 05:07:16 +00:00
Kubernetes Publisher
68262a1cf9 Merge pull request #139907 from ania-borowiec/remove_misleading_comments
Remove mentions about admission controllers from doc comments for  PodGroupTemplate.Priority and  PodGroupTemplate.PriorityClassName

Kubernetes-commit: 2a419be78ae256ef719071c6ae3bd46cb39b5b81
2026-06-23 17:05:47 +00:00
Ania Borowiec
fc42561c17 Remove mentions about admission controllers in PodGroupTemplate doc comments for Priority and PriorityClassName fields
Kubernetes-commit: f6fabaeb1e4c8607846402b52b6f6f4ba60c074b
2026-06-22 12:19:27 +00:00
Kubernetes Publisher
d04ac3067f Merge pull request #139867 from liggitt/json-v2-kube-openapi
Update kube-openapi to isolate from stdlib json/v2 prerelease

Kubernetes-commit: 2a799b273281aa952f457553e865050aba08bfe3
2026-06-19 01:04:47 +00:00
Jordan Liggitt
f04563ced1 Update kube-openapi to isolate from stdlib json/v2 prerelease
Kubernetes-commit: f45bf7803fa2fa86ea924a5cfe3503cb224fcbd1
2026-06-18 18:54:52 -04:00
Kubernetes Publisher
848305dfba Merge pull request #139825 from michaelasp/fixMutationTest
Fix dangling goroutine in mutation detector edge case

Kubernetes-commit: 55ac7c75fc03e69cff68075da901540c0f22b90e
2026-06-19 01:04:43 +00:00
Kubernetes Publisher
c18a3de8a3 Merge pull request #139755 from michaelasp/shutdownWait
Expose a way to wait for a controller listener to be fully shutdown

Kubernetes-commit: 7bdae3ff06a86acdf2b3cf9407b75b93c505e51f
2026-06-18 01:04:04 +00:00
Michael Aspinwall
29ceea8774 Fix timeout with dangling goroutine in mutation detector test
Kubernetes-commit: a332504c1abb8bedc564df94df424b8e6f11bc55
2026-06-17 17:49:48 +00:00
Kubernetes Publisher
f226f2f6e6 Merge pull request #139743 from antekjb/renamePodGroupScheduled
Rename PodGroupScheduled condition to PodGroupInitiallyScheduled

Kubernetes-commit: 47d52eabda121e12b5bedc59c8f3efef820f96c6
2026-06-17 15:34:44 +00:00
Kubernetes Publisher
cba7557c26 Merge pull request #139279 from antekjb/AllowForMinCountMutability
Make minCount mutable in Workload and PodGroup APIs

Kubernetes-commit: 176451642a3c25f18eaacba03417610aaa69a84b
2026-06-17 15:34:41 +00:00
Antoni Basista
f4285d806d Rename PodGroupScheduled condition to PodGroupInitiallyScheduled
Kubernetes-commit: 0d59683459ab25f1430b0740989ca63935bdaa47
2026-06-17 12:49:22 +00:00
Antoni Basista
c949b958f1 Make minCount mutable in Workload and PodGroup APIs
Kubernetes-commit: 61dc7df681ff4ad36809c10e5233ac97806a7780
2026-06-17 12:44:49 +00:00
Kubernetes Publisher
1e5bbe8452 Merge pull request #139520 from macsko/merge_featuregates
Merge GangScheduling and WorkloadAwarePreemption feature gates into GenericWorkload

Kubernetes-commit: 05f19de8edd6557dbe35403f0501ac3e97f600b3
2026-06-17 07:34:34 +00:00
Kubernetes Publisher
9f2f0bbab6 Merge pull request #139779 from PatrickLaabs/133935-clientgo
Replacing deprecated sets.NewString with generic implementation

Kubernetes-commit: c7381f90594a77851d051e4e18fcfbfb6e474b97
2026-06-16 23:34:35 +00:00
Kubernetes Publisher
23b19597fe Merge pull request #139586 from ania-borowiec/update_comment
Update doc comment for PodSpec.PreemptionPolicy to reflect the actual logic

Kubernetes-commit: 1436bc703dda6414a5b595f4b51ea6846535299d
2026-06-16 23:34:34 +00:00
Kubernetes Publisher
81deda607d Merge pull request #138999 from neolit123/1.37-client-go-generate-key-field-in-config
client-go: add Config.GenerateKey in certificate_manager.go

Kubernetes-commit: 0b19f2fae00634492409cf63e3c1998e77a5480f
2026-06-16 23:34:30 +00:00
PatrickLaabs
bb74650ccf Replacing deprecated sets.NewString with generic implementation
Kubernetes-commit: f3fc35faf035e6a7ad90d68a237d43b44de82280
2026-06-16 13:44:52 +02:00
Kubernetes Publisher
9fb400dccd Merge pull request #138080 from Lidang-Jiang/feat/dv-statefulset-selector-immutable
Migrate StatefulSet immutable fields to declarative validation

Kubernetes-commit: c7699a3b4b1f09a31ca0a985eaedce6ae502652c
2026-06-15 23:34:08 +00:00
Michael Aspinwall
ebfbc6844a Expose a way to wait for a controller listener to be fully shutdown
Kubernetes-commit: 338db9dde41f09aa094b61ef128e2d11327960ca
2026-06-15 17:52:00 +00:00
Lidang-Jiang
d8fe9715b9 Migrate StatefulSet immutable fields to declarative validation
Kubernetes-commit: 91341fc879394357dd24a7aaca3331cae1c2d6f6
2026-06-10 09:04:35 +08:00
Kubernetes Publisher
394ed525a6 Merge pull request #139101 from lalitc375/conditions
Setup metav1.Condition for declarative valdiations.

Kubernetes-commit: 8f8aa9aae157b88db6ba02836c57596496d3f684
2026-06-11 04:14:23 +00:00
Ania Borowiec
6a08ddec11 update doc comment for PreemptionPolicy to reflect the actual logic
Kubernetes-commit: 312919c2278f0d419b15f688578ae721966aa607
2026-06-09 11:11:54 +00:00
Maciej Skoczeń
c3667cb92d Merge GangScheduling and WorkloadAwarePreemption feature gates into GenericWorkload
Kubernetes-commit: 54ca619d4b3c31665d92868439118a4740c994a6
2026-06-05 12:48:52 +00:00
Lalit Chauhan
b5201bbace Generate Protos
Kubernetes-commit: c7576703f081b7658735871b78afadc7ef083444
2026-05-15 17:20:52 +00:00
Lubomir I. Ivanov
b48f101b05 client-go: add Config.GenerateKey in certificate_manager.go
Add a new field GenerateKey in the Config struct that allows
the user to set a custom function that would generate
a private key of their choice.

If the field is not set, the default remains:
  ecdsa.GenerateKey(elliptic.P256(), rand.Reader)

Add unit tests for this code path, with key fixtures
and function overloading to avoid additional key generation.

Enforce minimum bits on the generated keys to ensure
they are secure with the function validateKeyStrength().
For RSA the minimum key size is 2048, for ECDSA the minimum
curve bits are 256. Unit test this function too.

Kubernetes-commit: dec94de30f90f7e7e2859701ffce79ef8b137e3d
2026-05-12 15:57:12 +02:00
18 changed files with 569 additions and 61 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.
// If empty, defaulted to labels on the pod template.
// It must match the pod template's labels.
// 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,6 +172,8 @@ 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,6 +36,9 @@ 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,7 +25,15 @@ 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.
// 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.
MinCount *int32 `json:"minCount,omitempty"`
}

View File

@@ -26,13 +26,18 @@ import (
// with apply.
//
// PodGroupSchedulingPolicy defines the scheduling configuration for a PodGroup.
// Exactly one policy must be set.
// 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.
type PodGroupSchedulingPolicyApplyConfiguration struct {
// Basic specifies that the pods in this group should be scheduled using
// standard Kubernetes scheduling behavior.
// standard Kubernetes scheduling behavior. Setting this field at group creation time
// opts this group to basic scheduling; this field cannot be changed afterward.
Basic *schedulingv1alpha3.BasicSchedulingPolicy `json:"basic,omitempty"`
// Gang specifies that the pods in this group should be scheduled using
// all-or-nothing semantics.
// 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.
Gang *GangSchedulingPolicyApplyConfiguration `json:"gang,omitempty"`
}

View File

@@ -28,7 +28,6 @@ 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.
@@ -50,8 +49,6 @@ 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.
@@ -59,8 +56,6 @@ 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
@@ -68,8 +63,6 @@ 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,11 +30,13 @@ type PodGroupStatusApplyConfiguration struct {
// Conditions represent the latest observations of the PodGroup's state.
//
// Known condition types:
// - "PodGroupScheduled": Indicates whether the scheduling requirement has been satisfied.
// - "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.
// - "DisruptionTarget": Indicates whether the PodGroup is about to be terminated
// due to disruption such as preemption.
//
// Known reasons for the PodGroupScheduled condition:
// Known reasons for the PodGroupInitiallyScheduled 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,6 +30,7 @@ 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
@@ -44,23 +45,16 @@ 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 available only when the WorkloadAwarePreemption feature gate
// is enabled.
// This field is immutable.
DisruptionMode *DisruptionModeApplyConfiguration `json:"disruptionMode,omitempty"`
// PriorityClassName indicates the priority that should be considered when scheduling
// 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.
// a pod group created from this template.
// This field is immutable.
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. When
// Priority Admission Controller is enabled, it prevents users from setting this field.
// The admission controller populates this field from PriorityClassName.
// system components use this field to find the priority of the pod group.
// The higher the value, the higher the priority.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
// This field is immutable.
Priority *int32 `json:"priority,omitempty"`
}

View File

@@ -29,7 +29,8 @@ 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. This field is immutable.
// 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.
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.String{}
serverVersions := sets.Set[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.0.0-20260609035027-e126b8a6418e
k8s.io/apimachinery v0.0.0-20260609072944-ff574143e0ad
k8s.io/api v0.37.0-alpha.2
k8s.io/apimachinery v0.37.0-alpha.2
k8s.io/klog/v2 v2.140.0
k8s.io/kube-openapi v0.0.0-20260519202549-bbf5c5577288
k8s.io/streaming v0.0.0-20260602192112-0a84e25f8e5e
k8s.io/kube-openapi v0.0.0-20260618221249-bc653b64f974
k8s.io/streaming v0.37.0-alpha.2
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.0.0-20260609035027-e126b8a6418e h1:/mUSCvKhPW3DBo2fMLXRRfVg5GeAhwRPZ3jDpssZovE=
k8s.io/api v0.0.0-20260609035027-e126b8a6418e/go.mod h1:Bm3GRuK6YwYUVfVny2zq6C5dDgOF+xZ+YWuSEJPNTh4=
k8s.io/apimachinery v0.0.0-20260609072944-ff574143e0ad h1:EWDIHVYyygxlfcRf0lZefe+yTkSyEbPtWoTuoRaeEjE=
k8s.io/apimachinery v0.0.0-20260609072944-ff574143e0ad/go.mod h1:fimydsj7+VrAUXJvVoRBL9TgvZfVdrtsBIEHiLUQC4I=
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/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-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.0.0-20260602192112-0a84e25f8e5e h1:hk+SAa1pedjgwlCSfpcY0gHgGddPjfy0hX/ZQglt7js=
k8s.io/streaming v0.0.0-20260602192112-0a84e25f8e5e/go.mod h1:QN1+yCAfxcSvo0908Z3rFKkA5Xlr3KgUZAGwKuR6qQk=
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/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.String
outDaemonSetNames sets.Set[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.NewString("basic"),
outDaemonSetNames: sets.New[string]("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.NewString("basic", "complex", "complex2"),
outDaemonSetNames: sets.New[string]("basic", "complex", "complex2"),
},
// No pod labels
{
@@ -74,7 +74,7 @@ func TestDaemonSetLister(t *testing.T) {
}
return lister.GetPodDaemonSets(pod)
},
outDaemonSetNames: sets.NewString(),
outDaemonSetNames: sets.New[string](),
expectErr: true,
},
// No DS selectors
@@ -94,7 +94,7 @@ func TestDaemonSetLister(t *testing.T) {
}
return lister.GetPodDaemonSets(pod)
},
outDaemonSetNames: sets.NewString(),
outDaemonSetNames: sets.New[string](),
expectErr: true,
},
// Matching labels to selectors and namespace
@@ -123,7 +123,7 @@ func TestDaemonSetLister(t *testing.T) {
}
return lister.GetPodDaemonSets(pod)
},
outDaemonSetNames: sets.NewString("bar"),
outDaemonSetNames: sets.New[string]("bar"),
},
}
for _, c := range testCases {

View File

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

View File

@@ -177,6 +177,15 @@ 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
@@ -1014,6 +1023,24 @@ 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
@@ -1086,6 +1113,8 @@ func (p *sharedProcessor) removeListener(handle ResourceEventHandlerRegistration
if p.listenersStarted {
close(listener.addCh)
} else {
close(listener.runFinished)
}
return nil
@@ -1209,10 +1238,11 @@ 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{}
logger klog.Logger
nextCh chan interface{}
addCh chan interface{}
done chan struct{}
runFinished chan struct{}
handler ResourceEventHandler
handlerName string
@@ -1265,6 +1295,10 @@ 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{
@@ -1272,6 +1306,7 @@ 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,
@@ -1328,6 +1363,7 @@ 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,3 +1385,221 @@ 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,6 +202,13 @@ 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.
//
@@ -269,6 +276,7 @@ type manager struct {
signerName string
requestedCertificateLifetime *time.Duration
getUsages func(privateKey interface{}) []certificates.KeyUsage
generateKey func() (crypto.Signer, error)
forceRotation bool
certStore Store
@@ -322,6 +330,7 @@ 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,
@@ -762,19 +771,29 @@ 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 := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
privateKey, err := generateKey()
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("unable to generate a new private key: %w", err)
}
der, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("unable to marshal the new key to DER: %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)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("unable to marshal the new key to PEM: %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")
@@ -807,3 +826,25 @@ 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,6 +19,7 @@ package certificate
import (
"bytes"
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
@@ -43,6 +44,7 @@ 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"
@@ -1186,6 +1188,200 @@ 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 (