Compare commits

..

39 Commits

Author SHA1 Message Date
Kubernetes Publisher
d43aed2afb Update dependencies to v0.35.4 tag 2026-04-15 22:51:48 +00:00
Kubernetes Publisher
8ebd9bb45e Merge pull request #138356 from dims/update-moby-spdystream-v0.5.1-1.35
[1.35] Update github.com/moby/spdystream from v0.5.0 to v0.5.1

Kubernetes-commit: 93828861e0b554581ba647d48efaba1ff579afa3
2026-04-14 15:19:54 +00:00
Davanum Srinivas
00b2f2b182 Update github.com/moby/spdystream from v0.5.0 to v0.5.1
Kubernetes-commit: 7e9c2c8eef26f99aa2f94d8e09d6d32de86c7769

Kubernetes-commit: 1687aa8c94e73c3a1744cb1f4498c0fbc41abe0c
2026-04-13 14:14:40 -04:00
Kubernetes Publisher
f80003c240 Merge pull request #136903 from pohly/automated-cherry-pick-of-#136455-origin-release-1.35
Automated cherry pick of #136455: fake client-go: un-deprecate NewSimpleClientset

Kubernetes-commit: a0e5f1aba53a16db2c2cd0a2cb33c60f43e4d984
2026-03-05 00:46:20 +05:30
Patrick Ohly
8b415569d9 fake client-go: un-deprecate NewSimpleClientset
NewSimpleClientset was marked as deprecated when NewClientset was
introduced. This has caused some confusion:
- Not all packages have NewClientset (https://github.com/kubernetes/kubernetes/issues/135980).
- Tests that work with NewSimpleClientset fail when
  switched to NewClientset (https://github.com/kubernetes/kubernetes/issues/136327)
  because of missing CRD support (https://github.com/kubernetes/kubernetes/issues/126850).

It doesn't seem burdensome to keep NewSimpleClientset around forever. Some unit
tests may even prefer to use it when they don't need server-side apply (less
overhead). Therefore there is no need to deprecate it.

This avoids churn in the eco system because contributors no longer create PRs
"because the linter complains about the usage of a deprecated function".

Kubernetes-commit: bd399917375d2a1d77c04f8dfd67a67301af9721
2026-01-23 11:20:40 +01:00
Kubernetes Publisher
2d83546256 Merge remote-tracking branch 'origin/master' into release-1.35
Kubernetes-commit: b5f0a8229dd6ce88c2364257acb65d5875706b14
2025-12-05 03:34:45 +00:00
Kubernetes Publisher
56b4af2aeb Merge pull request #135591 from p0lyn0mial/upstream-watchlist-reflector-log-fallback
downgrade reflector watchlist fallback log to V(4)

Kubernetes-commit: 9293f9326d41e1e4ad53096ef180dd6ab0f9c699
2025-12-05 06:29:44 +00:00
Kubernetes Publisher
891f94c690 Merge remote-tracking branch 'origin/master' into release-1.35
Kubernetes-commit: fc0159905536cd41e3d2a3ac8fcbd905d1bc7c3f
2025-12-04 21:37:05 +00:00
Kubernetes Publisher
65ffe044e5 Merge pull request #135580 from serathius/client-go-transformer
Embed proper interface in TransformingStore to ensure DeltaFIFO and RealFIFO are implementing it

Kubernetes-commit: 04e8064bccebd04981ee0094457550c9de4f92e3
2025-12-04 22:45:12 +00:00
Lukasz Szaszkiewicz
2fe4ac239c downgrade reflector watchlist fallback log to V(4)
Kubernetes-commit: 3f42ca14011e972ee439a27d47415bc7574f2317
2025-12-04 16:14:19 +01:00
Davanum Srinivas
97256a6495 Bump golang.org/x/crypto to v0.45.0
Signed-off-by: Davanum Srinivas <davanum@gmail.com>

Kubernetes-commit: 5302b929ae55e86ad40b57d74c326529792d0439
2025-12-01 14:11:01 -05:00
Kubernetes Publisher
46360b527e Merge pull request #135131 from Dev1622/sig-storage/mock-expand-flake-fix
e2e/storage: deflake CSI Mock volume expansion quota validation

Kubernetes-commit: 4c04786f746e349a34042a0d59ec432a8a46ddcc
2025-11-26 20:44:31 +00:00
Jordan Liggitt
171ef8cd00 Use transformer in consistency checker
Kubernetes-commit: 91368adbb556286942d996c60ab6cc39306415b7
2025-11-26 15:19:00 -05:00
Dev1622
3878a6464b vendor: update vendor and license metadata after replacing BeTrue usage in csi tests
Kubernetes-commit: 216aaf76cba4b0cba05f0901d759dc017f833cd3
2025-11-13 05:11:59 +00:00
Surya Dev
9cee84c27c Resolve lint restriction on BeTrue by introducing Succeed() with contextual error messages
Kubernetes-commit: cee5b837da96f16536e048905bfe5be7103b21d2
2025-11-12 22:38:33 +05:30
Kubernetes Publisher
45e0decafa Merge pull request #132919 from ndixita/pod-level-in-place-pod-resize
Pod level in place pod resize - alpha

Kubernetes-commit: 9673a7fbf1045fa2c5786ddc5aeeb65b2f957d1c
2025-11-12 16:42:49 +00:00
Surya
990057bc99 Update vendored dependencies
Kubernetes-commit: c68b35c4c75cb5d4c016799a966808dff3d9a9a5
2025-11-12 16:17:46 +00:00
ndixita
3980f079a7 Generated files from API changes
Kubernetes-commit: b8777c32633ecfb214b02073369edadd23e8e9ec
2025-10-15 18:39:35 +00:00
Heba
5020b674f8 KEP-5471: Extend tolerations operators (#134665)
* Add numeric operations to tolerations

Signed-off-by: Heba Elayoty <heelayot@microsoft.com>

* code review feedback

Signed-off-by: Heba Elayoty <heelayot@microsoft.com>

* add default feature gate

Signed-off-by: Heba Elayoty <heelayot@microsoft.com>

* Add integration tests

Signed-off-by: Heba Elayoty <heelayot@microsoft.com>

* Add toleration value validation

Signed-off-by: Heba Elayoty <heelayot@microsoft.com>

* Add validate options for new operators

Signed-off-by: helayoty <heelayot@microsoft.com>

* Remove log

Signed-off-by: helayoty <heelayot@microsoft.com>

* Update feature gate check

Signed-off-by: helayoty <heelayot@microsoft.com>

* emove IsValidNumericString func

Signed-off-by: helayoty <heelayot@microsoft.com>

* Implement IsDecimalInteger

Signed-off-by: helayoty <heelayot@microsoft.com>

* code review feedback

Signed-off-by: helayoty <heelayot@microsoft.com>

* Add logs to v1/toleration

Signed-off-by: Heba Elayoty <heelayot@microsoft.com>
Signed-off-by: helayoty <heelayot@microsoft.com>

* Update integration tests and address code review feedback

Signed-off-by: helayoty <heelayot@microsoft.com>

* Add feature gate to the scheduler framework

Signed-off-by: helayoty <heelayot@microsoft.com>

* Remove extra test

Signed-off-by: helayoty <heelayot@microsoft.com>

* Fix integration test

Signed-off-by: helayoty <heelayot@microsoft.com>

* pass feature gate via TolerationsTolerateTaint

Signed-off-by: helayoty <heelayot@microsoft.com>

---------

Signed-off-by: Heba Elayoty <heelayot@microsoft.com>
Signed-off-by: helayoty <heelayot@microsoft.com>

Kubernetes-commit: aceb89debc2632c5c9956c8b7ef591426a485447
2025-11-11 00:32:36 +00:00
Kubernetes Publisher
6ce2c0f8c3 Merge pull request #134870 from pmengelbert/pmengelbert/kuberc/4
Add client-go credential plugin to kuberc

Kubernetes-commit: 183892b2c96aa962118ccfc2ee1c971caf72b16b
2025-11-10 04:32:36 +00:00
Kubernetes Publisher
5c322d3acd Merge pull request #134189 from mortent/NewUpdatePartitionableDevices
Updates to DRA Partitionable Devices feature

Kubernetes-commit: 0cfbf89e709e2acbc0d4cfa46954e873236c5bb9
2025-11-07 00:33:11 +00:00
Kubernetes Publisher
aa76619c41 Merge pull request #133648 from richabanker/merged-discovery
[KEP:4020] Peer-aggregated discovery

Kubernetes-commit: b214dae15d588ba9dc0bbfe8e82c82df434d89cc
2025-11-07 00:33:08 +00:00
Morten Torkildsen
99c4257e63 Run make update
Kubernetes-commit: ef3f6a250754318d7d9d94a518acccd676a47fca
2025-11-06 16:04:19 +00:00
Richa Banker
cb5e00dc36 peerproxy: Refactor into separate files and add exclusion filter
Kubernetes-commit: d6907457723a8f9a6eb5a9789424c34286d99528
2025-11-05 21:14:26 -08:00
Richa Banker
09ccc185ed discovery: Add profile-based content negotiation
Add support for 'profile=nopeer' in Accept headers to allow clients
to opt out of peer-aggregated discovery and request local-only results.

Updates discovery client to set appropriate Accept headers based on
whether peer-aggregated discovery is desired.

Part of KEP-4020: Unknown Version Interoperability Proxy

Kubernetes-commit: 6a07342d37a762230209e362d383e1fbfc325b51
2025-11-05 21:14:25 -08:00
Kubernetes Publisher
0e6fc04326 Merge pull request #134564 from macsko/gang_scheduling
KEP-4671: Add Workload API

Kubernetes-commit: 0c2aa7fee2eff82dd5e44ae30c8bcfef7a2140db
2025-11-06 12:32:56 +00:00
Peter Engelbert
c529a209c2 Add client-go credential plugin to kuberc
Remove reference to internal types in kuberc types

* Remove unserialized types from public APIs

Also remove defaulting

* Don't do conversion gen for plugin policy types

Because the plugin policy types are explicitly allowed to be empty, they
should not affect conversion. The autogenerated conversion functions for
the `Preference` type will leave those fields empty.

* Remove defaulting tests

Comments and simplifications (h/t jordan liggitt)

Signed-off-by: Peter Engelbert <pmengelbert@gmail.com>

Kubernetes-commit: fab280950dabfefabe6a8578b7a76372a9b21874
2025-10-24 16:06:33 -04:00
Maciej Skoczeń
9b2717cec1 api: Workload API and Pod WorkloadReference generated files
Kubernetes-commit: 078f462a955bb06cbf8de3e0d6fe2e190cf14e49
2025-10-10 09:44:19 +00:00
Kubernetes Publisher
6a14de8219 Merge pull request #134339 from huww98/mutable-pv-affinity
KEP-5381: mutable pv nodeAffinity

Kubernetes-commit: 326ce8b16d777edcfb3c19c2118ec2860945d4e1
2025-11-06 12:32:54 +00:00
Kubernetes Publisher
b36e6498c1 Merge pull request #134180 from p0lyn0mial/upstream-watchlist-dynamic-interface
Promote/Enable WatchListClient feature for release 1.35

Kubernetes-commit: 099ef07c9f90a7650b5a06f58fa7005d9c760761
2025-11-06 12:32:52 +00:00
Kubernetes Publisher
823f3a75f4 Merge pull request #133389 from pravk03/node-capabilities
Introduce node declared features framework

Kubernetes-commit: b869afe68d5ffb1f74ab584653451efc811698d7
2025-11-06 12:32:51 +00:00
Lukasz Szaszkiewicz
092b2fb360 client-go/features: enable WatchListClient for 1.35 release
Kubernetes-commit: ca8847149e3e19444ffec878f0002f5f9edef566
2025-11-03 20:33:48 +01:00
Praveen Krishna
d0f0666dd2 autogenerated files from "make update"
Kubernetes-commit: 3c22291a4b0bb2407630d4fa112256aae16398a6
2025-11-01 00:07:52 +00:00
Kubernetes Publisher
7d09f53b04 Merge pull request #135072 from Jefftree/graceful-queue
Gracefully shutdown typed queue

Kubernetes-commit: 26a2945d5de6c1dc0426aebe1ad36a32963aeb39
2025-11-05 16:33:11 +00:00
Jefftree
773743c372 refactor updateUnfinishedWork into separate function to use defer unlock
Kubernetes-commit: f1bd4f509ebb2f9d02971c2dff85d5933eb074df
2025-11-04 15:27:53 +00:00
Jefftree
1463bcbba2 Fix queue to gracefully shutdown
Kubernetes-commit: bdb2e37781aec3bfec477a7beddd9fa5ac4eab1b
2025-11-03 19:04:11 +00:00
胡玮文
f8b7fe98c4 mention MutablePVNodeAffinity in the API doc
Kubernetes-commit: 78a8c2e6a35b5621a7afa9066770a819d417f707
2025-10-20 21:22:02 +08:00
Valerian Roche
7cf6a05732 Add unit tests for Data Consistency Detector
Kubernetes-commit: 76da8d6de027a4bf62601d45b8d72f8fa627ab5c
2025-07-28 13:53:34 -04:00
Valerian Roche
f466f58eea [client-go #1415] Embed proper interface in TransformingStore to ensure DeltaFIFO and RealFIFO are implementing it
Signed-off-by: Valerian Roche <valerian.roche@datadoghq.com>

Kubernetes-commit: 88c20d46a4e5ca8db7519c81856a358c919ae262
2025-07-28 13:53:34 -04:00
62 changed files with 2328 additions and 251 deletions

View File

@@ -68,6 +68,8 @@ type NodeStatusApplyConfiguration struct {
RuntimeHandlers []NodeRuntimeHandlerApplyConfiguration `json:"runtimeHandlers,omitempty"`
// Features describes the set of features implemented by the CRI implementation.
Features *NodeFeaturesApplyConfiguration `json:"features,omitempty"`
// DeclaredFeatures represents the features related to feature gates that are declared by the node.
DeclaredFeatures []string `json:"declaredFeatures,omitempty"`
}
// NodeStatusApplyConfiguration constructs a declarative configuration of the NodeStatus type for use with
@@ -206,3 +208,13 @@ func (b *NodeStatusApplyConfiguration) WithFeatures(value *NodeFeaturesApplyConf
b.Features = value
return b
}
// WithDeclaredFeatures adds the given value to the DeclaredFeatures field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the DeclaredFeatures field.
func (b *NodeStatusApplyConfiguration) WithDeclaredFeatures(values ...string) *NodeStatusApplyConfiguration {
for i := range values {
b.DeclaredFeatures = append(b.DeclaredFeatures, values[i])
}
return b
}

View File

@@ -58,6 +58,7 @@ type PersistentVolumeSpecApplyConfiguration struct {
VolumeMode *corev1.PersistentVolumeMode `json:"volumeMode,omitempty"`
// nodeAffinity defines constraints that limit what nodes this volume can be accessed from.
// This field influences the scheduling of pods that use this volume.
// This field is mutable if MutablePVNodeAffinity feature gate is enabled.
NodeAffinity *VolumeNodeAffinityApplyConfiguration `json:"nodeAffinity,omitempty"`
// Name of VolumeAttributesClass to which this persistent volume belongs. Empty value
// is not allowed. When this field is not set, it indicates that this volume does not belong to any

View File

@@ -273,6 +273,14 @@ type PodSpecApplyConfiguration struct {
// This field must be a valid DNS subdomain as defined in RFC 1123 and contain at most 64 characters.
// Requires the HostnameOverride feature gate to be enabled.
HostnameOverride *string `json:"hostnameOverride,omitempty"`
// WorkloadRef provides a reference to the Workload object that this Pod belongs to.
// This field is used by the scheduler to identify the PodGroup and apply the
// correct group scheduling policies. The Workload object referenced
// by this field may not exist at the time the Pod is created.
// This field is immutable, but a Workload object with the same name
// may be recreated with different policies. Doing this during pod scheduling
// may result in the placement not conforming to the expected policies.
WorkloadRef *WorkloadReferenceApplyConfiguration `json:"workloadRef,omitempty"`
}
// PodSpecApplyConfiguration constructs a declarative configuration of the PodSpec type for use with
@@ -669,3 +677,11 @@ func (b *PodSpecApplyConfiguration) WithHostnameOverride(value string) *PodSpecA
b.HostnameOverride = &value
return b
}
// WithWorkloadRef sets the WorkloadRef field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the WorkloadRef field is set to the value of the last call.
func (b *PodSpecApplyConfiguration) WithWorkloadRef(value *WorkloadReferenceApplyConfiguration) *PodSpecApplyConfiguration {
b.WorkloadRef = value
return b
}

View File

@@ -131,6 +131,14 @@ type PodStatusApplyConfiguration struct {
ResourceClaimStatuses []PodResourceClaimStatusApplyConfiguration `json:"resourceClaimStatuses,omitempty"`
// Status of extended resource claim backed by DRA.
ExtendedResourceClaimStatus *PodExtendedResourceClaimStatusApplyConfiguration `json:"extendedResourceClaimStatus,omitempty"`
// AllocatedResources is the total requests allocated for this pod by the node.
// If pod-level requests are not set, this will be the total requests aggregated
// across containers in the pod.
AllocatedResources *corev1.ResourceList `json:"allocatedResources,omitempty"`
// Resources represents the compute resource requests and limits that have been
// applied at the pod level if pod-level requests or limits are set in
// PodSpec.Resources
Resources *ResourceRequirementsApplyConfiguration `json:"resources,omitempty"`
}
// PodStatusApplyConfiguration constructs a declarative configuration of the PodStatus type for use with
@@ -317,3 +325,19 @@ func (b *PodStatusApplyConfiguration) WithExtendedResourceClaimStatus(value *Pod
b.ExtendedResourceClaimStatus = value
return b
}
// WithAllocatedResources sets the AllocatedResources field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the AllocatedResources field is set to the value of the last call.
func (b *PodStatusApplyConfiguration) WithAllocatedResources(value corev1.ResourceList) *PodStatusApplyConfiguration {
b.AllocatedResources = &value
return b
}
// WithResources sets the Resources field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Resources field is set to the value of the last call.
func (b *PodStatusApplyConfiguration) WithResources(value *ResourceRequirementsApplyConfiguration) *PodStatusApplyConfiguration {
b.Resources = value
return b
}

View File

@@ -32,9 +32,10 @@ type TolerationApplyConfiguration struct {
// If the key is empty, operator must be Exists; this combination means to match all values and all keys.
Key *string `json:"key,omitempty"`
// Operator represents a key's relationship to the value.
// Valid operators are Exists and Equal. Defaults to Equal.
// Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
// Exists is equivalent to wildcard for value, so that a pod can
// tolerate all taints of a particular category.
// Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
Operator *corev1.TolerationOperator `json:"operator,omitempty"`
// Value is the taint value the toleration matches to.
// If the operator is Exists, the value should be empty, otherwise just a regular string.

View File

@@ -0,0 +1,74 @@
/*
Copyright The Kubernetes 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.
*/
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v1
// WorkloadReferenceApplyConfiguration represents a declarative configuration of the WorkloadReference type for use
// with apply.
//
// WorkloadReference identifies the Workload object and PodGroup membership
// that a Pod belongs to. The scheduler uses this information to apply
// workload-aware scheduling semantics.
type WorkloadReferenceApplyConfiguration struct {
// Name defines the name of the Workload object this Pod belongs to.
// Workload must be in the same namespace as the Pod.
// If it doesn't match any existing Workload, the Pod will remain unschedulable
// until a Workload object is created and observed by the kube-scheduler.
// It must be a DNS subdomain.
Name *string `json:"name,omitempty"`
// PodGroup is the name of the PodGroup within the Workload that this Pod
// belongs to. If it doesn't match any existing PodGroup within the Workload,
// the Pod will remain unschedulable until the Workload object is recreated
// and observed by the kube-scheduler. It must be a DNS label.
PodGroup *string `json:"podGroup,omitempty"`
// PodGroupReplicaKey specifies the replica key of the PodGroup to which this
// Pod belongs. It is used to distinguish pods belonging to different replicas
// of the same pod group. The pod group policy is applied separately to each replica.
// When set, it must be a DNS label.
PodGroupReplicaKey *string `json:"podGroupReplicaKey,omitempty"`
}
// WorkloadReferenceApplyConfiguration constructs a declarative configuration of the WorkloadReference type for use with
// apply.
func WorkloadReference() *WorkloadReferenceApplyConfiguration {
return &WorkloadReferenceApplyConfiguration{}
}
// WithName sets the Name field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Name field is set to the value of the last call.
func (b *WorkloadReferenceApplyConfiguration) WithName(value string) *WorkloadReferenceApplyConfiguration {
b.Name = &value
return b
}
// WithPodGroup sets the PodGroup field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the PodGroup field is set to the value of the last call.
func (b *WorkloadReferenceApplyConfiguration) WithPodGroup(value string) *WorkloadReferenceApplyConfiguration {
b.PodGroup = &value
return b
}
// WithPodGroupReplicaKey sets the PodGroupReplicaKey field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the PodGroupReplicaKey field is set to the value of the last call.
func (b *WorkloadReferenceApplyConfiguration) WithPodGroupReplicaKey(value string) *WorkloadReferenceApplyConfiguration {
b.PodGroupReplicaKey = &value
return b
}

View File

@@ -6770,6 +6770,12 @@ var schemaYAML = typed.YAMLObject(`types:
type:
namedType: io.k8s.api.core.v1.NodeDaemonEndpoints
default: {}
- name: declaredFeatures
type:
list:
elementType:
scalar: string
elementRelationship: atomic
- name: features
type:
namedType: io.k8s.api.core.v1.NodeFeatures
@@ -7643,9 +7649,17 @@ var schemaYAML = typed.YAMLObject(`types:
elementRelationship: associative
keys:
- name
- name: workloadRef
type:
namedType: io.k8s.api.core.v1.WorkloadReference
- name: io.k8s.api.core.v1.PodStatus
map:
fields:
- name: allocatedResources
type:
map:
elementType:
namedType: io.k8s.apimachinery.pkg.api.resource.Quantity
- name: conditions
type:
list:
@@ -7724,6 +7738,9 @@ var schemaYAML = typed.YAMLObject(`types:
elementRelationship: associative
keys:
- name
- name: resources
type:
namedType: io.k8s.api.core.v1.ResourceRequirements
- name: startTime
type:
namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time
@@ -9017,6 +9034,20 @@ var schemaYAML = typed.YAMLObject(`types:
- name: runAsUserName
type:
scalar: string
- name: io.k8s.api.core.v1.WorkloadReference
map:
fields:
- name: name
type:
scalar: string
default: ""
- name: podGroup
type:
scalar: string
default: ""
- name: podGroupReplicaKey
type:
scalar: string
- name: io.k8s.api.discovery.v1.Endpoint
map:
fields:
@@ -14937,6 +14968,45 @@ var schemaYAML = typed.YAMLObject(`types:
type:
scalar: numeric
default: 0
- name: io.k8s.api.scheduling.v1alpha1.BasicSchedulingPolicy
map:
elementType:
scalar: untyped
list:
elementType:
namedType: __untyped_atomic_
elementRelationship: atomic
map:
elementType:
namedType: __untyped_deduced_
elementRelationship: separable
- name: io.k8s.api.scheduling.v1alpha1.GangSchedulingPolicy
map:
fields:
- name: minCount
type:
scalar: numeric
default: 0
- name: io.k8s.api.scheduling.v1alpha1.PodGroup
map:
fields:
- name: name
type:
scalar: string
default: ""
- name: policy
type:
namedType: io.k8s.api.scheduling.v1alpha1.PodGroupPolicy
default: {}
- name: io.k8s.api.scheduling.v1alpha1.PodGroupPolicy
map:
fields:
- name: basic
type:
namedType: io.k8s.api.scheduling.v1alpha1.BasicSchedulingPolicy
- name: gang
type:
namedType: io.k8s.api.scheduling.v1alpha1.GangSchedulingPolicy
- name: io.k8s.api.scheduling.v1alpha1.PriorityClass
map:
fields:
@@ -14963,6 +15033,51 @@ var schemaYAML = typed.YAMLObject(`types:
type:
scalar: numeric
default: 0
- name: io.k8s.api.scheduling.v1alpha1.TypedLocalObjectReference
map:
fields:
- name: apiGroup
type:
scalar: string
- name: kind
type:
scalar: string
default: ""
- name: name
type:
scalar: string
default: ""
- name: io.k8s.api.scheduling.v1alpha1.Workload
map:
fields:
- name: apiVersion
type:
scalar: string
- name: kind
type:
scalar: string
- name: metadata
type:
namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta
default: {}
- name: spec
type:
namedType: io.k8s.api.scheduling.v1alpha1.WorkloadSpec
default: {}
- name: io.k8s.api.scheduling.v1alpha1.WorkloadSpec
map:
fields:
- name: controllerRef
type:
namedType: io.k8s.api.scheduling.v1alpha1.TypedLocalObjectReference
- name: podGroups
type:
list:
elementType:
namedType: io.k8s.api.scheduling.v1alpha1.PodGroup
elementRelationship: associative
keys:
- name
- name: io.k8s.api.scheduling.v1beta1.PriorityClass
map:
fields:

View File

@@ -23,7 +23,7 @@ package v1
//
// CounterSet defines a named set of counters
// that are available to be used by devices defined in the
// ResourceSlice.
// ResourcePool.
//
// The counters are not allocatable by themselves, but
// can be referenced by devices. When a device is allocated,
@@ -36,7 +36,7 @@ type CounterSetApplyConfiguration struct {
// Counters defines the set of counters for this CounterSet
// The name of each counter must be unique in that set and must be a DNS label.
//
// The maximum number of counters in all sets is 32.
// The maximum number of counters is 32.
Counters map[string]CounterApplyConfiguration `json:"counters,omitempty"`
}

View File

@@ -48,10 +48,8 @@ type DeviceApplyConfiguration struct {
//
// There can only be a single entry per counterSet.
//
// The total number of device counter consumption entries
// must be <= 32. In addition, the total number in the
// entire ResourceSlice must be <= 1024 (for example,
// 64 devices with 16 counters each).
// The maximum number of device counter consumptions per
// device is 2.
ConsumesCounters []DeviceCounterConsumptionApplyConfiguration `json:"consumesCounters,omitempty"`
// NodeName identifies the node where the device is available.
//

View File

@@ -29,10 +29,7 @@ type DeviceCounterConsumptionApplyConfiguration struct {
CounterSet *string `json:"counterSet,omitempty"`
// Counters defines the counters that will be consumed by the device.
//
// The maximum number counters in a device is 32.
// In addition, the maximum number of all counters
// in all devices is 1024 (for example, 64 devices with
// 16 counters each).
// The maximum number of counters is 32.
Counters map[string]CounterApplyConfiguration `json:"counters,omitempty"`
}

View File

@@ -62,7 +62,9 @@ type ResourceSliceSpecApplyConfiguration struct {
AllNodes *bool `json:"allNodes,omitempty"`
// Devices lists some or all of the devices in this pool.
//
// Must not have more than 128 entries. If any device uses taints the limit is 64.
// Must not have more than 128 entries. If any device uses taints or consumes counters the limit is 64.
//
// Only one of Devices and SharedCounters can be set in a ResourceSlice.
Devices []DeviceApplyConfiguration `json:"devices,omitempty"`
// PerDeviceNodeSelection defines whether the access from nodes to
// resources in the pool is set on the ResourceSlice level or on each
@@ -74,9 +76,11 @@ type ResourceSliceSpecApplyConfiguration struct {
// SharedCounters defines a list of counter sets, each of which
// has a name and a list of counters available.
//
// The names of the SharedCounters must be unique in the ResourceSlice.
// The names of the counter sets must be unique in the ResourcePool.
//
// The maximum number of counters in all sets is 32.
// Only one of Devices and SharedCounters can be set in a ResourceSlice.
//
// The maximum number of counter sets is 8.
SharedCounters []CounterSetApplyConfiguration `json:"sharedCounters,omitempty"`
}

View File

@@ -44,10 +44,8 @@ type BasicDeviceApplyConfiguration struct {
//
// There can only be a single entry per counterSet.
//
// The total number of device counter consumption entries
// must be <= 32. In addition, the total number in the
// entire ResourceSlice must be <= 1024 (for example,
// 64 devices with 16 counters each).
// The maximum number of device counter consumptions per
// device is 2.
ConsumesCounters []DeviceCounterConsumptionApplyConfiguration `json:"consumesCounters,omitempty"`
// NodeName identifies the node where the device is available.
//

View File

@@ -23,7 +23,7 @@ package v1beta1
//
// CounterSet defines a named set of counters
// that are available to be used by devices defined in the
// ResourceSlice.
// ResourcePool.
//
// The counters are not allocatable by themselves, but
// can be referenced by devices. When a device is allocated,

View File

@@ -29,10 +29,7 @@ type DeviceCounterConsumptionApplyConfiguration struct {
CounterSet *string `json:"counterSet,omitempty"`
// Counters defines the counters that will be consumed by the device.
//
// The maximum number counters in a device is 32.
// In addition, the maximum number of all counters
// in all devices is 1024 (for example, 64 devices with
// 16 counters each).
// The maximum number of counters is 32.
Counters map[string]CounterApplyConfiguration `json:"counters,omitempty"`
}

View File

@@ -62,7 +62,9 @@ type ResourceSliceSpecApplyConfiguration struct {
AllNodes *bool `json:"allNodes,omitempty"`
// Devices lists some or all of the devices in this pool.
//
// Must not have more than 128 entries. If any device uses taints the limit is 64.
// Must not have more than 128 entries. If any device uses taints or consumes counters the limit is 64.
//
// Only one of Devices and SharedCounters can be set in a ResourceSlice.
Devices []DeviceApplyConfiguration `json:"devices,omitempty"`
// PerDeviceNodeSelection defines whether the access from nodes to
// resources in the pool is set on the ResourceSlice level or on each
@@ -74,9 +76,11 @@ type ResourceSliceSpecApplyConfiguration struct {
// SharedCounters defines a list of counter sets, each of which
// has a name and a list of counters available.
//
// The names of the SharedCounters must be unique in the ResourceSlice.
// The names of the counter sets must be unique in the ResourcePool.
//
// The maximum number of SharedCounters is 32.
// Only one of Devices and SharedCounters can be set in a ResourceSlice.
//
// The maximum number of counter sets is 8.
SharedCounters []CounterSetApplyConfiguration `json:"sharedCounters,omitempty"`
}

View File

@@ -23,7 +23,7 @@ package v1beta2
//
// CounterSet defines a named set of counters
// that are available to be used by devices defined in the
// ResourceSlice.
// ResourcePool.
//
// The counters are not allocatable by themselves, but
// can be referenced by devices. When a device is allocated,
@@ -36,7 +36,7 @@ type CounterSetApplyConfiguration struct {
// Counters defines the set of counters for this CounterSet
// The name of each counter must be unique in that set and must be a DNS label.
//
// The maximum number of counters in all sets is 32.
// The maximum number of counters is 32.
Counters map[string]CounterApplyConfiguration `json:"counters,omitempty"`
}

View File

@@ -48,10 +48,8 @@ type DeviceApplyConfiguration struct {
//
// There can only be a single entry per counterSet.
//
// The total number of device counter consumption entries
// must be <= 32. In addition, the total number in the
// entire ResourceSlice must be <= 1024 (for example,
// 64 devices with 16 counters each).
// The maximum number of device counter consumptions per
// device is 2.
ConsumesCounters []DeviceCounterConsumptionApplyConfiguration `json:"consumesCounters,omitempty"`
// NodeName identifies the node where the device is available.
//

View File

@@ -29,10 +29,7 @@ type DeviceCounterConsumptionApplyConfiguration struct {
CounterSet *string `json:"counterSet,omitempty"`
// Counters defines the counters that will be consumed by the device.
//
// The maximum number counters in a device is 32.
// In addition, the maximum number of all counters
// in all devices is 1024 (for example, 64 devices with
// 16 counters each).
// The maximum number of counters is 32.
Counters map[string]CounterApplyConfiguration `json:"counters,omitempty"`
}

View File

@@ -62,7 +62,9 @@ type ResourceSliceSpecApplyConfiguration struct {
AllNodes *bool `json:"allNodes,omitempty"`
// Devices lists some or all of the devices in this pool.
//
// Must not have more than 128 entries. If any device uses taints the limit is 64.
// Must not have more than 128 entries. If any device uses taints or consumes counters the limit is 64.
//
// Only one of Devices and SharedCounters can be set in a ResourceSlice.
Devices []DeviceApplyConfiguration `json:"devices,omitempty"`
// PerDeviceNodeSelection defines whether the access from nodes to
// resources in the pool is set on the ResourceSlice level or on each
@@ -74,9 +76,11 @@ type ResourceSliceSpecApplyConfiguration struct {
// SharedCounters defines a list of counter sets, each of which
// has a name and a list of counters available.
//
// The names of the SharedCounters must be unique in the ResourceSlice.
// The names of the counter sets must be unique in the ResourcePool.
//
// The maximum number of counters in all sets is 32.
// Only one of Devices and SharedCounters can be set in a ResourceSlice.
//
// The maximum number of counter sets is 8.
SharedCounters []CounterSetApplyConfiguration `json:"sharedCounters,omitempty"`
}

View File

@@ -0,0 +1,44 @@
/*
Copyright The Kubernetes 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.
*/
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v1alpha1
// GangSchedulingPolicyApplyConfiguration represents a declarative configuration of the GangSchedulingPolicy type for use
// with apply.
//
// GangSchedulingPolicy defines the parameters for gang scheduling.
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.
MinCount *int32 `json:"minCount,omitempty"`
}
// GangSchedulingPolicyApplyConfiguration constructs a declarative configuration of the GangSchedulingPolicy type for use with
// apply.
func GangSchedulingPolicy() *GangSchedulingPolicyApplyConfiguration {
return &GangSchedulingPolicyApplyConfiguration{}
}
// WithMinCount sets the MinCount field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the MinCount field is set to the value of the last call.
func (b *GangSchedulingPolicyApplyConfiguration) WithMinCount(value int32) *GangSchedulingPolicyApplyConfiguration {
b.MinCount = &value
return b
}

View File

@@ -0,0 +1,53 @@
/*
Copyright The Kubernetes 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.
*/
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v1alpha1
// PodGroupApplyConfiguration represents a declarative configuration of the PodGroup type for use
// with apply.
//
// PodGroup represents a set of pods with a common scheduling policy.
type PodGroupApplyConfiguration struct {
// Name is a unique identifier for the PodGroup within the Workload.
// It must be a DNS label. This field is immutable.
Name *string `json:"name,omitempty"`
// Policy defines the scheduling policy for this PodGroup.
Policy *PodGroupPolicyApplyConfiguration `json:"policy,omitempty"`
}
// PodGroupApplyConfiguration constructs a declarative configuration of the PodGroup type for use with
// apply.
func PodGroup() *PodGroupApplyConfiguration {
return &PodGroupApplyConfiguration{}
}
// WithName sets the Name field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Name field is set to the value of the last call.
func (b *PodGroupApplyConfiguration) WithName(value string) *PodGroupApplyConfiguration {
b.Name = &value
return b
}
// WithPolicy sets the Policy field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Policy field is set to the value of the last call.
func (b *PodGroupApplyConfiguration) WithPolicy(value *PodGroupPolicyApplyConfiguration) *PodGroupApplyConfiguration {
b.Policy = value
return b
}

View File

@@ -0,0 +1,58 @@
/*
Copyright The Kubernetes 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.
*/
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v1alpha1
import (
schedulingv1alpha1 "k8s.io/api/scheduling/v1alpha1"
)
// PodGroupPolicyApplyConfiguration represents a declarative configuration of the PodGroupPolicy type for use
// with apply.
//
// PodGroupPolicy defines the scheduling configuration for a PodGroup.
type PodGroupPolicyApplyConfiguration struct {
// Basic specifies that the pods in this group should be scheduled using
// standard Kubernetes scheduling behavior.
Basic *schedulingv1alpha1.BasicSchedulingPolicy `json:"basic,omitempty"`
// Gang specifies that the pods in this group should be scheduled using
// all-or-nothing semantics.
Gang *GangSchedulingPolicyApplyConfiguration `json:"gang,omitempty"`
}
// PodGroupPolicyApplyConfiguration constructs a declarative configuration of the PodGroupPolicy type for use with
// apply.
func PodGroupPolicy() *PodGroupPolicyApplyConfiguration {
return &PodGroupPolicyApplyConfiguration{}
}
// WithBasic sets the Basic field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Basic field is set to the value of the last call.
func (b *PodGroupPolicyApplyConfiguration) WithBasic(value schedulingv1alpha1.BasicSchedulingPolicy) *PodGroupPolicyApplyConfiguration {
b.Basic = &value
return b
}
// WithGang sets the Gang field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Gang field is set to the value of the last call.
func (b *PodGroupPolicyApplyConfiguration) WithGang(value *GangSchedulingPolicyApplyConfiguration) *PodGroupPolicyApplyConfiguration {
b.Gang = value
return b
}

View File

@@ -0,0 +1,67 @@
/*
Copyright The Kubernetes 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.
*/
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v1alpha1
// TypedLocalObjectReferenceApplyConfiguration represents a declarative configuration of the TypedLocalObjectReference type for use
// with apply.
//
// TypedLocalObjectReference allows to reference typed object inside the same namespace.
type TypedLocalObjectReferenceApplyConfiguration struct {
// APIGroup is the group for the resource being referenced.
// If APIGroup is empty, the specified Kind must be in the core API group.
// For any other third-party types, setting APIGroup is required.
// It must be a DNS subdomain.
APIGroup *string `json:"apiGroup,omitempty"`
// Kind is the type of resource being referenced.
// It must be a path segment name.
Kind *string `json:"kind,omitempty"`
// Name is the name of resource being referenced.
// It must be a path segment name.
Name *string `json:"name,omitempty"`
}
// TypedLocalObjectReferenceApplyConfiguration constructs a declarative configuration of the TypedLocalObjectReference type for use with
// apply.
func TypedLocalObjectReference() *TypedLocalObjectReferenceApplyConfiguration {
return &TypedLocalObjectReferenceApplyConfiguration{}
}
// WithAPIGroup sets the APIGroup field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the APIGroup field is set to the value of the last call.
func (b *TypedLocalObjectReferenceApplyConfiguration) WithAPIGroup(value string) *TypedLocalObjectReferenceApplyConfiguration {
b.APIGroup = &value
return b
}
// WithKind sets the Kind field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Kind field is set to the value of the last call.
func (b *TypedLocalObjectReferenceApplyConfiguration) WithKind(value string) *TypedLocalObjectReferenceApplyConfiguration {
b.Kind = &value
return b
}
// WithName sets the Name field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Name field is set to the value of the last call.
func (b *TypedLocalObjectReferenceApplyConfiguration) WithName(value string) *TypedLocalObjectReferenceApplyConfiguration {
b.Name = &value
return b
}

View File

@@ -0,0 +1,279 @@
/*
Copyright The Kubernetes 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.
*/
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v1alpha1
import (
schedulingv1alpha1 "k8s.io/api/scheduling/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
managedfields "k8s.io/apimachinery/pkg/util/managedfields"
internal "k8s.io/client-go/applyconfigurations/internal"
v1 "k8s.io/client-go/applyconfigurations/meta/v1"
)
// WorkloadApplyConfiguration represents a declarative configuration of the Workload type for use
// with apply.
//
// Workload allows for expressing scheduling constraints that should be used
// when managing lifecycle of workloads from scheduling perspective,
// including scheduling, preemption, eviction and other phases.
type WorkloadApplyConfiguration struct {
v1.TypeMetaApplyConfiguration `json:",inline"`
// Standard object's metadata.
// Name must be a DNS subdomain.
*v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"`
// Spec defines the desired behavior of a Workload.
Spec *WorkloadSpecApplyConfiguration `json:"spec,omitempty"`
}
// Workload constructs a declarative configuration of the Workload type for use with
// apply.
func Workload(name, namespace string) *WorkloadApplyConfiguration {
b := &WorkloadApplyConfiguration{}
b.WithName(name)
b.WithNamespace(namespace)
b.WithKind("Workload")
b.WithAPIVersion("scheduling.k8s.io/v1alpha1")
return b
}
// ExtractWorkloadFrom extracts the applied configuration owned by fieldManager from
// workload for the specified subresource. Pass an empty string for subresource to extract
// the main resource. Common subresources include "status", "scale", etc.
// workload must be a unmodified Workload API object that was retrieved from the Kubernetes API.
// ExtractWorkloadFrom provides a way to perform a extract/modify-in-place/apply workflow.
// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously
// applied if another fieldManager has updated or force applied any of the previously applied fields.
func ExtractWorkloadFrom(workload *schedulingv1alpha1.Workload, fieldManager string, subresource string) (*WorkloadApplyConfiguration, error) {
b := &WorkloadApplyConfiguration{}
err := managedfields.ExtractInto(workload, internal.Parser().Type("io.k8s.api.scheduling.v1alpha1.Workload"), fieldManager, b, subresource)
if err != nil {
return nil, err
}
b.WithName(workload.Name)
b.WithNamespace(workload.Namespace)
b.WithKind("Workload")
b.WithAPIVersion("scheduling.k8s.io/v1alpha1")
return b, nil
}
// ExtractWorkload extracts the applied configuration owned by fieldManager from
// workload. If no managedFields are found in workload for fieldManager, a
// WorkloadApplyConfiguration is returned with only the Name, Namespace (if applicable),
// APIVersion and Kind populated. It is possible that no managed fields were found for because other
// field managers have taken ownership of all the fields previously owned by fieldManager, or because
// the fieldManager never owned fields any fields.
// workload must be a unmodified Workload API object that was retrieved from the Kubernetes API.
// ExtractWorkload provides a way to perform a extract/modify-in-place/apply workflow.
// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously
// applied if another fieldManager has updated or force applied any of the previously applied fields.
func ExtractWorkload(workload *schedulingv1alpha1.Workload, fieldManager string) (*WorkloadApplyConfiguration, error) {
return ExtractWorkloadFrom(workload, fieldManager, "")
}
func (b WorkloadApplyConfiguration) IsApplyConfiguration() {}
// WithKind sets the Kind field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Kind field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithKind(value string) *WorkloadApplyConfiguration {
b.TypeMetaApplyConfiguration.Kind = &value
return b
}
// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the APIVersion field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithAPIVersion(value string) *WorkloadApplyConfiguration {
b.TypeMetaApplyConfiguration.APIVersion = &value
return b
}
// WithName sets the Name field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Name field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithName(value string) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
b.ObjectMetaApplyConfiguration.Name = &value
return b
}
// WithGenerateName sets the GenerateName field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the GenerateName field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithGenerateName(value string) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
b.ObjectMetaApplyConfiguration.GenerateName = &value
return b
}
// WithNamespace sets the Namespace field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Namespace field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithNamespace(value string) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
b.ObjectMetaApplyConfiguration.Namespace = &value
return b
}
// WithUID sets the UID field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the UID field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithUID(value types.UID) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
b.ObjectMetaApplyConfiguration.UID = &value
return b
}
// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the ResourceVersion field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithResourceVersion(value string) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
b.ObjectMetaApplyConfiguration.ResourceVersion = &value
return b
}
// WithGeneration sets the Generation field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Generation field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithGeneration(value int64) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
b.ObjectMetaApplyConfiguration.Generation = &value
return b
}
// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the CreationTimestamp field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithCreationTimestamp(value metav1.Time) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
b.ObjectMetaApplyConfiguration.CreationTimestamp = &value
return b
}
// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the DeletionTimestamp field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value
return b
}
// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value
return b
}
// WithLabels puts the entries into the Labels field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, the entries provided by each call will be put on the Labels field,
// overwriting an existing map entries in Labels field with the same key.
func (b *WorkloadApplyConfiguration) WithLabels(entries map[string]string) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 {
b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries))
}
for k, v := range entries {
b.ObjectMetaApplyConfiguration.Labels[k] = v
}
return b
}
// WithAnnotations puts the entries into the Annotations field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, the entries provided by each call will be put on the Annotations field,
// overwriting an existing map entries in Annotations field with the same key.
func (b *WorkloadApplyConfiguration) WithAnnotations(entries map[string]string) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 {
b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries))
}
for k, v := range entries {
b.ObjectMetaApplyConfiguration.Annotations[k] = v
}
return b
}
// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the OwnerReferences field.
func (b *WorkloadApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
for i := range values {
if values[i] == nil {
panic("nil value passed to WithOwnerReferences")
}
b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i])
}
return b
}
// WithFinalizers adds the given value to the Finalizers field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the Finalizers field.
func (b *WorkloadApplyConfiguration) WithFinalizers(values ...string) *WorkloadApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
for i := range values {
b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i])
}
return b
}
func (b *WorkloadApplyConfiguration) ensureObjectMetaApplyConfigurationExists() {
if b.ObjectMetaApplyConfiguration == nil {
b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{}
}
}
// WithSpec sets the Spec field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Spec field is set to the value of the last call.
func (b *WorkloadApplyConfiguration) WithSpec(value *WorkloadSpecApplyConfiguration) *WorkloadApplyConfiguration {
b.Spec = value
return b
}
// GetKind retrieves the value of the Kind field in the declarative configuration.
func (b *WorkloadApplyConfiguration) GetKind() *string {
return b.TypeMetaApplyConfiguration.Kind
}
// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration.
func (b *WorkloadApplyConfiguration) GetAPIVersion() *string {
return b.TypeMetaApplyConfiguration.APIVersion
}
// GetName retrieves the value of the Name field in the declarative configuration.
func (b *WorkloadApplyConfiguration) GetName() *string {
b.ensureObjectMetaApplyConfigurationExists()
return b.ObjectMetaApplyConfiguration.Name
}
// GetNamespace retrieves the value of the Namespace field in the declarative configuration.
func (b *WorkloadApplyConfiguration) GetNamespace() *string {
b.ensureObjectMetaApplyConfigurationExists()
return b.ObjectMetaApplyConfiguration.Namespace
}

View File

@@ -0,0 +1,61 @@
/*
Copyright The Kubernetes 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.
*/
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v1alpha1
// WorkloadSpecApplyConfiguration represents a declarative configuration of the WorkloadSpec type for use
// with apply.
//
// WorkloadSpec defines the desired state of a Workload.
type WorkloadSpecApplyConfiguration struct {
// ControllerRef is an optional reference to the controlling object, such as a
// Deployment or Job. This field is intended for use by tools like CLIs
// to provide a link back to the original workload definition.
// When set, it cannot be changed.
ControllerRef *TypedLocalObjectReferenceApplyConfiguration `json:"controllerRef,omitempty"`
// PodGroups is the list of pod groups that make up the Workload.
// The maximum number of pod groups is 8. This field is immutable.
PodGroups []PodGroupApplyConfiguration `json:"podGroups,omitempty"`
}
// WorkloadSpecApplyConfiguration constructs a declarative configuration of the WorkloadSpec type for use with
// apply.
func WorkloadSpec() *WorkloadSpecApplyConfiguration {
return &WorkloadSpecApplyConfiguration{}
}
// WithControllerRef sets the ControllerRef field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the ControllerRef field is set to the value of the last call.
func (b *WorkloadSpecApplyConfiguration) WithControllerRef(value *TypedLocalObjectReferenceApplyConfiguration) *WorkloadSpecApplyConfiguration {
b.ControllerRef = value
return b
}
// WithPodGroups adds the given value to the PodGroups field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the PodGroups field.
func (b *WorkloadSpecApplyConfiguration) WithPodGroups(values ...*PodGroupApplyConfiguration) *WorkloadSpecApplyConfiguration {
for i := range values {
if values[i] == nil {
panic("nil value passed to WithPodGroups")
}
b.PodGroups = append(b.PodGroups, *values[i])
}
return b
}

View File

@@ -1084,6 +1084,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &applyconfigurationscorev1.WeightedPodAffinityTermApplyConfiguration{}
case corev1.SchemeGroupVersion.WithKind("WindowsSecurityContextOptions"):
return &applyconfigurationscorev1.WindowsSecurityContextOptionsApplyConfiguration{}
case corev1.SchemeGroupVersion.WithKind("WorkloadReference"):
return &applyconfigurationscorev1.WorkloadReferenceApplyConfiguration{}
// Group=discovery.k8s.io, Version=v1
case discoveryv1.SchemeGroupVersion.WithKind("Endpoint"):
@@ -1886,8 +1888,20 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &applyconfigurationsschedulingv1.PriorityClassApplyConfiguration{}
// Group=scheduling.k8s.io, Version=v1alpha1
case schedulingv1alpha1.SchemeGroupVersion.WithKind("GangSchedulingPolicy"):
return &applyconfigurationsschedulingv1alpha1.GangSchedulingPolicyApplyConfiguration{}
case schedulingv1alpha1.SchemeGroupVersion.WithKind("PodGroup"):
return &applyconfigurationsschedulingv1alpha1.PodGroupApplyConfiguration{}
case schedulingv1alpha1.SchemeGroupVersion.WithKind("PodGroupPolicy"):
return &applyconfigurationsschedulingv1alpha1.PodGroupPolicyApplyConfiguration{}
case schedulingv1alpha1.SchemeGroupVersion.WithKind("PriorityClass"):
return &applyconfigurationsschedulingv1alpha1.PriorityClassApplyConfiguration{}
case schedulingv1alpha1.SchemeGroupVersion.WithKind("TypedLocalObjectReference"):
return &applyconfigurationsschedulingv1alpha1.TypedLocalObjectReferenceApplyConfiguration{}
case schedulingv1alpha1.SchemeGroupVersion.WithKind("Workload"):
return &applyconfigurationsschedulingv1alpha1.WorkloadApplyConfiguration{}
case schedulingv1alpha1.SchemeGroupVersion.WithKind("WorkloadSpec"):
return &applyconfigurationsschedulingv1alpha1.WorkloadSpecApplyConfiguration{}
// Group=scheduling.k8s.io, Version=v1beta1
case schedulingv1beta1.SchemeGroupVersion.WithKind("PriorityClass"):

View File

@@ -63,8 +63,9 @@ const (
// Aggregated discovery content-type (v2beta1). NOTE: content-type parameters
// MUST be ordered (g, v, as) for server in "Accept" header (BUT we are resilient
// to ordering when comparing returned values in "Content-Type" header).
AcceptV2Beta1 = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList"
AcceptV2 = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList"
AcceptV2Beta1 = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList"
AcceptV2 = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList"
AcceptV2NoPeer = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList;profile=nopeer"
// Prioritize aggregated discovery by placing first in the order of discovery accept types.
acceptDiscoveryFormats = AcceptV2 + "," + AcceptV2Beta1 + "," + AcceptV1
)
@@ -168,6 +169,11 @@ type DiscoveryClient struct {
LegacyPrefix string
// Forces the client to request only "unaggregated" (legacy) discovery.
UseLegacyDiscovery bool
// NoPeerDiscovery will request the "nopeer" profile of aggregated discovery.
// This allows a client to get just the discovery documents served by the single apiserver
// that it is talking to. This is useful for clients that need to understand the state
// of a single apiserver, for example, to validate that the apiserver is ready to serve traffic.
NoPeerDiscovery bool
}
var _ AggregatedDiscoveryInterface = &DiscoveryClient{}
@@ -241,10 +247,7 @@ func (d *DiscoveryClient) downloadLegacy() (
map[schema.GroupVersion]*metav1.APIResourceList,
map[schema.GroupVersion]error,
error) {
accept := acceptDiscoveryFormats
if d.UseLegacyDiscovery {
accept = AcceptV1
}
accept := selectDiscoveryAcceptHeader(d.UseLegacyDiscovery, d.NoPeerDiscovery)
var responseContentType string
body, err := d.restClient.Get().
AbsPath("/api").
@@ -307,10 +310,7 @@ func (d *DiscoveryClient) downloadAPIs() (
map[schema.GroupVersion]*metav1.APIResourceList,
map[schema.GroupVersion]error,
error) {
accept := acceptDiscoveryFormats
if d.UseLegacyDiscovery {
accept = AcceptV1
}
accept := selectDiscoveryAcceptHeader(d.UseLegacyDiscovery, d.NoPeerDiscovery)
var responseContentType string
body, err := d.restClient.Get().
AbsPath("/apis").
@@ -351,6 +351,16 @@ func (d *DiscoveryClient) downloadAPIs() (
return apiGroupList, resourcesByGV, failedGVs, nil
}
func selectDiscoveryAcceptHeader(useLegacy, nopeer bool) string {
if useLegacy {
return AcceptV1
}
if nopeer {
return AcceptV2NoPeer + "," + acceptDiscoveryFormats
}
return acceptDiscoveryFormats
}
// ContentTypeIsGVK checks of the content-type string is both
// "application/json" and matches the provided GVK. An error
// is returned if the content type string is malformed.

View File

@@ -73,10 +73,6 @@ const (
// beta: v1.30
//
// Allow the client to get a stream of individual items instead of chunking from the server.
//
// NOTE:
// The feature is disabled in Beta by default because
// it will only be turned on for selected control plane component(s).
WatchListClient Feature = "WatchListClient"
)
@@ -104,5 +100,6 @@ var defaultVersionedKubernetesFeatureGates = map[Feature]VersionedSpecs{
},
WatchListClient: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: Beta},
{Version: version.MustParse("1.35"), Default: true, PreRelease: Beta},
},
}

14
go.mod
View File

@@ -18,14 +18,14 @@ require (
github.com/spf13/pflag v1.0.9
github.com/stretchr/testify v1.11.1
go.uber.org/goleak v1.3.0
golang.org/x/net v0.43.0
golang.org/x/net v0.47.0
golang.org/x/oauth2 v0.30.0
golang.org/x/term v0.36.0
golang.org/x/term v0.37.0
golang.org/x/time v0.9.0
google.golang.org/protobuf v1.36.8
gopkg.in/evanphx/json-patch.v4 v4.13.0
k8s.io/api v0.35.0-alpha.3
k8s.io/apimachinery v0.35.0-alpha.3
k8s.io/api v0.35.4
k8s.io/apimachinery v0.35.4
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
@@ -48,7 +48,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/spdystream v0.5.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
@@ -57,8 +57,8 @@ require (
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

40
go.sum
View File

@@ -50,8 +50,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y=
github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -93,24 +93,24 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -123,10 +123,10 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.35.0-alpha.3 h1:BdcXkJ4n/NKhfg06PaSDG8r8Mpe9g3KO9Fkj7B/F8/4=
k8s.io/api v0.35.0-alpha.3/go.mod h1:SArWbUwVv7VhTGGbKX0RoMPXiT6ztjjzkKpRRdl6+E0=
k8s.io/apimachinery v0.35.0-alpha.3 h1:aHqVUsi78MIDmMfmMTRMAnpxUlA7poaU1iNXN/sM6gs=
k8s.io/apimachinery v0.35.0-alpha.3/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak=
k8s.io/api v0.35.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988=
k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU=
k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds=
k8s.io/apimachinery v0.35.4/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=

View File

@@ -426,6 +426,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
// Group=scheduling.k8s.io, Version=v1alpha1
case schedulingv1alpha1.SchemeGroupVersion.WithResource("priorityclasses"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Scheduling().V1alpha1().PriorityClasses().Informer()}, nil
case schedulingv1alpha1.SchemeGroupVersion.WithResource("workloads"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Scheduling().V1alpha1().Workloads().Informer()}, nil
// Group=scheduling.k8s.io, Version=v1beta1
case schedulingv1beta1.SchemeGroupVersion.WithResource("priorityclasses"):

View File

@@ -26,6 +26,8 @@ import (
type Interface interface {
// PriorityClasses returns a PriorityClassInformer.
PriorityClasses() PriorityClassInformer
// Workloads returns a WorkloadInformer.
Workloads() WorkloadInformer
}
type version struct {
@@ -43,3 +45,8 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList
func (v *version) PriorityClasses() PriorityClassInformer {
return &priorityClassInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
}
// Workloads returns a WorkloadInformer.
func (v *version) Workloads() WorkloadInformer {
return &workloadInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
}

View File

@@ -0,0 +1,102 @@
/*
Copyright The Kubernetes 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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package v1alpha1
import (
context "context"
time "time"
apischedulingv1alpha1 "k8s.io/api/scheduling/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
watch "k8s.io/apimachinery/pkg/watch"
internalinterfaces "k8s.io/client-go/informers/internalinterfaces"
kubernetes "k8s.io/client-go/kubernetes"
schedulingv1alpha1 "k8s.io/client-go/listers/scheduling/v1alpha1"
cache "k8s.io/client-go/tools/cache"
)
// WorkloadInformer provides access to a shared informer and lister for
// Workloads.
type WorkloadInformer interface {
Informer() cache.SharedIndexInformer
Lister() schedulingv1alpha1.WorkloadLister
}
type workloadInformer struct {
factory internalinterfaces.SharedInformerFactory
tweakListOptions internalinterfaces.TweakListOptionsFunc
namespace string
}
// NewWorkloadInformer constructs a new informer for Workload type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewWorkloadInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredWorkloadInformer(client, namespace, resyncPeriod, indexers, nil)
}
// NewFilteredWorkloadInformer constructs a new informer for Workload type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredWorkloadInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.SchedulingV1alpha1().Workloads(namespace).List(context.Background(), options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.SchedulingV1alpha1().Workloads(namespace).Watch(context.Background(), options)
},
ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.SchedulingV1alpha1().Workloads(namespace).List(ctx, options)
},
WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.SchedulingV1alpha1().Workloads(namespace).Watch(ctx, options)
},
}, client),
&apischedulingv1alpha1.Workload{},
resyncPeriod,
indexers,
)
}
func (f *workloadInformer) defaultInformer(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredWorkloadInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}
func (f *workloadInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&apischedulingv1alpha1.Workload{}, f.defaultInformer)
}
func (f *workloadInformer) Lister() schedulingv1alpha1.WorkloadLister {
return schedulingv1alpha1.NewWorkloadLister(f.Informer().GetIndexer())
}

View File

@@ -143,10 +143,6 @@ import (
// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
// without applying any field management, validations and/or defaults. It shouldn't be considered a replacement
// for a real clientset and is mostly useful in simple unit tests.
//
// Deprecated: NewClientset replaces this with support for field management, which significantly improves
// server side apply testing. NewClientset is only available when apply configurations are generated (e.g.
// via --with-applyconfig).
func NewSimpleClientset(objects ...runtime.Object) *Clientset {
o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
for _, obj := range objects {
@@ -207,6 +203,10 @@ func (c *Clientset) IsWatchListSemanticsUnSupported() bool {
// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
// without applying any validations and/or defaults. It shouldn't be considered a replacement
// for a real clientset and is mostly useful in simple unit tests.
//
// Compared to NewSimpleClientset, the Clientset returned here supports field tracking and thus
// server-side apply. Beware though that support in that for CRDs is missing
// (https://github.com/kubernetes/kubernetes/issues/126850).
func NewClientset(objects ...runtime.Object) *Clientset {
o := testing.NewFieldManagedObjectTracker(
scheme,

View File

@@ -32,6 +32,10 @@ func (c *FakeSchedulingV1alpha1) PriorityClasses() v1alpha1.PriorityClassInterfa
return newFakePriorityClasses(c)
}
func (c *FakeSchedulingV1alpha1) Workloads(namespace string) v1alpha1.WorkloadInterface {
return newFakeWorkloads(c, namespace)
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *FakeSchedulingV1alpha1) RESTClient() rest.Interface {

View File

@@ -0,0 +1,51 @@
/*
Copyright The Kubernetes 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.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v1alpha1 "k8s.io/api/scheduling/v1alpha1"
schedulingv1alpha1 "k8s.io/client-go/applyconfigurations/scheduling/v1alpha1"
gentype "k8s.io/client-go/gentype"
typedschedulingv1alpha1 "k8s.io/client-go/kubernetes/typed/scheduling/v1alpha1"
)
// fakeWorkloads implements WorkloadInterface
type fakeWorkloads struct {
*gentype.FakeClientWithListAndApply[*v1alpha1.Workload, *v1alpha1.WorkloadList, *schedulingv1alpha1.WorkloadApplyConfiguration]
Fake *FakeSchedulingV1alpha1
}
func newFakeWorkloads(fake *FakeSchedulingV1alpha1, namespace string) typedschedulingv1alpha1.WorkloadInterface {
return &fakeWorkloads{
gentype.NewFakeClientWithListAndApply[*v1alpha1.Workload, *v1alpha1.WorkloadList, *schedulingv1alpha1.WorkloadApplyConfiguration](
fake.Fake,
namespace,
v1alpha1.SchemeGroupVersion.WithResource("workloads"),
v1alpha1.SchemeGroupVersion.WithKind("Workload"),
func() *v1alpha1.Workload { return &v1alpha1.Workload{} },
func() *v1alpha1.WorkloadList { return &v1alpha1.WorkloadList{} },
func(dst, src *v1alpha1.WorkloadList) { dst.ListMeta = src.ListMeta },
func(list *v1alpha1.WorkloadList) []*v1alpha1.Workload { return gentype.ToPointerSlice(list.Items) },
func(list *v1alpha1.WorkloadList, items []*v1alpha1.Workload) {
list.Items = gentype.FromPointerSlice(items)
},
),
fake,
}
}

View File

@@ -19,3 +19,5 @@ limitations under the License.
package v1alpha1
type PriorityClassExpansion interface{}
type WorkloadExpansion interface{}

View File

@@ -29,6 +29,7 @@ import (
type SchedulingV1alpha1Interface interface {
RESTClient() rest.Interface
PriorityClassesGetter
WorkloadsGetter
}
// SchedulingV1alpha1Client is used to interact with features provided by the scheduling.k8s.io group.
@@ -40,6 +41,10 @@ func (c *SchedulingV1alpha1Client) PriorityClasses() PriorityClassInterface {
return newPriorityClasses(c)
}
func (c *SchedulingV1alpha1Client) Workloads(namespace string) WorkloadInterface {
return newWorkloads(c, namespace)
}
// NewForConfig creates a new SchedulingV1alpha1Client for the given config.
// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
// where httpClient was generated with rest.HTTPClientFor(c).

View File

@@ -0,0 +1,71 @@
/*
Copyright The Kubernetes 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.
*/
// Code generated by client-gen. DO NOT EDIT.
package v1alpha1
import (
context "context"
schedulingv1alpha1 "k8s.io/api/scheduling/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
applyconfigurationsschedulingv1alpha1 "k8s.io/client-go/applyconfigurations/scheduling/v1alpha1"
gentype "k8s.io/client-go/gentype"
scheme "k8s.io/client-go/kubernetes/scheme"
)
// WorkloadsGetter has a method to return a WorkloadInterface.
// A group's client should implement this interface.
type WorkloadsGetter interface {
Workloads(namespace string) WorkloadInterface
}
// WorkloadInterface has methods to work with Workload resources.
type WorkloadInterface interface {
Create(ctx context.Context, workload *schedulingv1alpha1.Workload, opts v1.CreateOptions) (*schedulingv1alpha1.Workload, error)
Update(ctx context.Context, workload *schedulingv1alpha1.Workload, opts v1.UpdateOptions) (*schedulingv1alpha1.Workload, error)
Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
Get(ctx context.Context, name string, opts v1.GetOptions) (*schedulingv1alpha1.Workload, error)
List(ctx context.Context, opts v1.ListOptions) (*schedulingv1alpha1.WorkloadList, error)
Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *schedulingv1alpha1.Workload, err error)
Apply(ctx context.Context, workload *applyconfigurationsschedulingv1alpha1.WorkloadApplyConfiguration, opts v1.ApplyOptions) (result *schedulingv1alpha1.Workload, err error)
WorkloadExpansion
}
// workloads implements WorkloadInterface
type workloads struct {
*gentype.ClientWithListAndApply[*schedulingv1alpha1.Workload, *schedulingv1alpha1.WorkloadList, *applyconfigurationsschedulingv1alpha1.WorkloadApplyConfiguration]
}
// newWorkloads returns a Workloads
func newWorkloads(c *SchedulingV1alpha1Client, namespace string) *workloads {
return &workloads{
gentype.NewClientWithListAndApply[*schedulingv1alpha1.Workload, *schedulingv1alpha1.WorkloadList, *applyconfigurationsschedulingv1alpha1.WorkloadApplyConfiguration](
"workloads",
c.RESTClient(),
scheme.ParameterCodec,
namespace,
func() *schedulingv1alpha1.Workload { return &schedulingv1alpha1.Workload{} },
func() *schedulingv1alpha1.WorkloadList { return &schedulingv1alpha1.WorkloadList{} },
gentype.PrefersProtobuf[*schedulingv1alpha1.Workload](),
),
}
}

View File

@@ -21,3 +21,11 @@ package v1alpha1
// PriorityClassListerExpansion allows custom methods to be added to
// PriorityClassLister.
type PriorityClassListerExpansion interface{}
// WorkloadListerExpansion allows custom methods to be added to
// WorkloadLister.
type WorkloadListerExpansion interface{}
// WorkloadNamespaceListerExpansion allows custom methods to be added to
// WorkloadNamespaceLister.
type WorkloadNamespaceListerExpansion interface{}

View File

@@ -0,0 +1,70 @@
/*
Copyright The Kubernetes 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.
*/
// Code generated by lister-gen. DO NOT EDIT.
package v1alpha1
import (
schedulingv1alpha1 "k8s.io/api/scheduling/v1alpha1"
labels "k8s.io/apimachinery/pkg/labels"
listers "k8s.io/client-go/listers"
cache "k8s.io/client-go/tools/cache"
)
// WorkloadLister helps list Workloads.
// All objects returned here must be treated as read-only.
type WorkloadLister interface {
// List lists all Workloads in the indexer.
// Objects returned here must be treated as read-only.
List(selector labels.Selector) (ret []*schedulingv1alpha1.Workload, err error)
// Workloads returns an object that can list and get Workloads.
Workloads(namespace string) WorkloadNamespaceLister
WorkloadListerExpansion
}
// workloadLister implements the WorkloadLister interface.
type workloadLister struct {
listers.ResourceIndexer[*schedulingv1alpha1.Workload]
}
// NewWorkloadLister returns a new WorkloadLister.
func NewWorkloadLister(indexer cache.Indexer) WorkloadLister {
return &workloadLister{listers.New[*schedulingv1alpha1.Workload](indexer, schedulingv1alpha1.Resource("workload"))}
}
// Workloads returns an object that can list and get Workloads.
func (s *workloadLister) Workloads(namespace string) WorkloadNamespaceLister {
return workloadNamespaceLister{listers.NewNamespaced[*schedulingv1alpha1.Workload](s.ResourceIndexer, namespace)}
}
// WorkloadNamespaceLister helps list and get Workloads.
// All objects returned here must be treated as read-only.
type WorkloadNamespaceLister interface {
// List lists all Workloads in the indexer for a given namespace.
// Objects returned here must be treated as read-only.
List(selector labels.Selector) (ret []*schedulingv1alpha1.Workload, err error)
// Get retrieves the Workload from the indexer for a given namespace and name.
// Objects returned here must be treated as read-only.
Get(name string) (*schedulingv1alpha1.Workload, error)
WorkloadNamespaceListerExpansion
}
// workloadNamespaceLister implements the WorkloadNamespaceLister
// interface.
type workloadNamespaceLister struct {
listers.ResourceIndexer[*schedulingv1alpha1.Workload]
}

View File

@@ -27,6 +27,7 @@ import (
"net/http"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"sync"
@@ -39,6 +40,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/dump"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/pkg/apis/clientauthentication"
"k8s.io/client-go/pkg/apis/clientauthentication/install"
clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
@@ -177,13 +179,29 @@ func newAuthenticator(c *cache, isTerminalFunc func(int) bool, config *api.ExecC
connTracker,
)
if err := ValidatePluginPolicy(config.PluginPolicy); err != nil {
return nil, fmt.Errorf("invalid plugin policy: %w", err)
}
allowlistLookup := sets.New[string]()
for _, entry := range config.PluginPolicy.Allowlist {
if entry.Name != "" {
allowlistLookup.Insert(entry.Name)
}
}
a := &Authenticator{
cmd: config.Command,
// Clean is called to normalize the path to facilitate comparison with
// the allowlist, when present
cmd: filepath.Clean(config.Command),
args: config.Args,
group: gv,
cluster: cluster,
provideClusterInfo: config.ProvideClusterInfo,
allowlistLookup: allowlistLookup,
execPluginPolicy: config.PluginPolicy,
installHint: config.InstallHint,
sometimes: &sometimes{
threshold: 10,
@@ -250,6 +268,9 @@ type Authenticator struct {
cluster *clientauthentication.Cluster
provideClusterInfo bool
allowlistLookup sets.Set[string]
execPluginPolicy api.PluginPolicy
// Used to avoid log spew by rate limiting install hint printing. We didn't do
// this by interval based rate limiting alone since that way may have prevented
// the install hint from showing up for kubectl users.
@@ -441,6 +462,12 @@ func (a *Authenticator) refreshCredsLocked() error {
cmd.Stdin = a.stdin
}
err = a.updateCommandAndCheckAllowlistLocked(cmd)
incrementPolicyMetric(err)
if err != nil {
return err
}
err = cmd.Run()
incrementCallsMetric(err)
if err != nil {
@@ -545,3 +572,131 @@ func (a *Authenticator) wrapCmdRunErrorLocked(err error) error {
return fmt.Errorf("exec: %v", err)
}
}
// `updateCommandAndCheckAllowlistLocked` determines whether or not the specified executable may run
// according to the credential plugin policy. If the plugin is allowed, `nil`
// is returned. If the plugin is not allowed, an error must be returned
// explaining why.
func (a *Authenticator) updateCommandAndCheckAllowlistLocked(cmd *exec.Cmd) error {
switch a.execPluginPolicy.PolicyType {
case "", api.PluginPolicyAllowAll:
return nil
case api.PluginPolicyDenyAll:
return fmt.Errorf("plugin %q not allowed: policy set to %q", a.cmd, api.PluginPolicyDenyAll)
case api.PluginPolicyAllowlist:
return a.checkAllowlistLocked(cmd)
default:
return fmt.Errorf("unknown plugin policy %q", a.execPluginPolicy.PolicyType)
}
}
// `checkAllowlistLocked` checks the specified plugin against the allowlist,
// and may update the Authenticator's allowlistLookup set.
func (a *Authenticator) checkAllowlistLocked(cmd *exec.Cmd) error {
// a.cmd is the original command as specified in the configuration, then filepath.Clean().
// cmd.Path is the possibly-resolved command.
// If either are an exact match in the allowlist, return success.
if a.allowlistLookup.Has(a.cmd) || a.allowlistLookup.Has(cmd.Path) {
return nil
}
var cmdResolvedPath string
var cmdResolvedErr error
if cmd.Path != a.cmd {
// cmd.Path changed, use the already-resolved LookPath results
cmdResolvedPath = cmd.Path
cmdResolvedErr = cmd.Err
} else {
// cmd.Path is unchanged, do LookPath ourselves
cmdResolvedPath, cmdResolvedErr = exec.LookPath(cmd.Path)
// update cmd.Path to cmdResolvedPath so we only run the resolved path
if cmdResolvedPath != "" {
cmd.Path = cmdResolvedPath
}
}
if cmdResolvedErr != nil {
return fmt.Errorf("plugin path %q cannot be resolved for credential plugin allowlist check: %w", cmd.Path, cmdResolvedErr)
}
// cmdResolvedPath may have changed, and the changed value may be in the allowlist
if a.allowlistLookup.Has(cmdResolvedPath) {
return nil
}
// There is no verbatim match
a.resolveAllowListEntriesLocked(cmd.Path)
// allowlistLookup may have changed, recheck
if a.allowlistLookup.Has(cmdResolvedPath) {
return nil
}
return fmt.Errorf("plugin path %q is not permitted by the credential plugin allowlist", cmd.Path)
}
// resolveAllowListEntriesLocked tries to resolve allowlist entries with LookPath,
// and adds successfully resolved entries to allowlistLookup.
// The optional commandHint can be used to limit which entries are resolved to ones which match the hint basename.
func (a *Authenticator) resolveAllowListEntriesLocked(commandHint string) {
hintName := filepath.Base(commandHint)
for _, entry := range a.execPluginPolicy.Allowlist {
entryBasename := filepath.Base(entry.Name)
if hintName != "" && hintName != entryBasename {
// we got a hint, and this allowlist entry does not match it
continue
}
entryResolvedPath, err := exec.LookPath(entry.Name)
if err != nil {
klog.V(5).ErrorS(err, "resolving credential plugin allowlist", "name", entry.Name)
continue
}
if entryResolvedPath != "" {
a.allowlistLookup.Insert(entryResolvedPath)
}
}
}
func ValidatePluginPolicy(policy api.PluginPolicy) error {
switch policy.PolicyType {
// "" is equivalent to "AllowAll"
case "", api.PluginPolicyAllowAll, api.PluginPolicyDenyAll:
if policy.Allowlist != nil {
return fmt.Errorf("misconfigured credential plugin allowlist: plugin policy is %q but allowlist is non-nil", policy.PolicyType)
}
return nil
case api.PluginPolicyAllowlist:
return validateAllowlist(policy.Allowlist)
default:
return fmt.Errorf("unknown plugin policy: %q", policy.PolicyType)
}
}
var emptyAllowlistEntry api.AllowlistEntry
func validateAllowlist(list []api.AllowlistEntry) error {
// This will be the case if the user has misspelled the field name for the
// allowlist. Because this is a security knob, fail immediately rather than
// proceed when the user has made a mistake.
if list == nil {
return fmt.Errorf("credential plugin policy set to %q, but allowlist is unspecified", api.PluginPolicyAllowlist)
}
if len(list) == 0 {
return fmt.Errorf("credential plugin policy set to %q, but allowlist is empty; use %q policy instead", api.PluginPolicyAllowlist, api.PluginPolicyDenyAll)
}
for i, item := range list {
if item == emptyAllowlistEntry {
return fmt.Errorf("misconfigured credential plugin allowlist: empty allowlist entry #%d", i+1)
}
if cleaned := filepath.Clean(item.Name); cleaned != item.Name {
return fmt.Errorf("non-normalized file path: %q vs %q", item.Name, cleaned)
} else if item.Name == "" {
return fmt.Errorf("empty file path: %q", item.Name)
}
}
return nil
}

View File

@@ -29,8 +29,12 @@ import (
"fmt"
"io"
"math/big"
mathrand "math/rand/v2"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"reflect"
"strconv"
"strings"
@@ -845,6 +849,354 @@ func TestRefreshCreds(t *testing.T) {
}
}
type pluginPolicyTest struct {
name string
wantErr bool
wantErrSubstr string
config *api.ExecConfig
pluginExists bool
entryExists bool
usePluginAbsPath bool
useEntryAbsPath bool
allowlistLength int
policyType api.PolicyType
allowlist []api.AllowlistEntry
}
type dualBool struct{ plugin, entry bool }
type pluginPolicyTestMatrix struct {
exists []dualBool
absolute []dualBool
allowlistLengths []int
policies []api.PolicyType
// the logic of whether or not a given test configuration should produce an error
shouldErrFunc func(tt *pluginPolicyTest) (bool, string)
}
var allPermutations = []dualBool{
{plugin: false, entry: false},
{plugin: false, entry: true},
{plugin: true, entry: false},
{plugin: true, entry: true},
}
// TestPluginPolicy tests the functioning of various plugin policies, as well
// as the the validity of a wide variety of policies.
func TestPluginPolicy(t *testing.T) {
// this is inlined to make highly visible and explicit the logic of which
// test configurations should pass and which should fail
shouldErrFunc := func(test *pluginPolicyTest) (bool, string) {
switch test.policyType {
case "", api.PluginPolicyAllowAll:
if test.allowlist != nil { // invalid
return true, "allowlist is non-nil"
}
if test.pluginExists {
return false, ""
}
return true, "not found"
case api.PluginPolicyDenyAll:
if test.allowlist != nil { // invalid
return true, "allowlist is non-nil"
}
return true, `policy set to "DenyAll"`
case api.PluginPolicyAllowlist:
if test.allowlist == nil { // invalid
return true, "allowlist is unspecified"
}
if len(test.allowlist) == 0 { // invalid
return true, "allowlist is empty; use \"DenyAll\" policy instead"
}
switch {
case test.pluginExists && test.entryExists:
return false, ""
case test.pluginExists && !test.entryExists:
return true, "is not permitted by the credential plugin allowlist"
case !test.pluginExists && test.entryExists:
// error message varies depending on whether the paths are relative or absolute
// what is important is that an error arises here
return true, ""
case !test.pluginExists && !test.entryExists:
// error message varies depending on whether the paths are relative or absolute
// what is important is that an error arises here
return true, ""
}
panic("unreachable")
}
return true, "unknown plugin policy"
}
wd, err := os.Getwd()
if err != nil {
t.Fatalf("could not get working directory: %s", err)
}
testdataDir := filepath.Join(wd, "testdata")
path := os.Getenv("PATH")
defer func() {
if err = os.Setenv("PATH", path); err != nil {
t.Fatal(err)
}
}()
matrix := pluginPolicyTestMatrix{
shouldErrFunc: shouldErrFunc,
exists: allPermutations,
absolute: allPermutations,
allowlistLengths: []int{-1 /*nil*/, 0, 1, 3},
policies: []api.PolicyType{
"",
api.PluginPolicyAllowAll,
api.PluginPolicyDenyAll,
api.PluginPolicyAllowlist,
api.PolicyType("HIGHLYILLEGAL"),
},
}
tests := matrix.makeTests(t, testdataDir, path)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := test.config
a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, c, &clientauthentication.Cluster{})
if err != nil {
if !test.wantErr {
t.Fatalf("unexpected validation error: %v", err)
} else if !strings.Contains(err.Error(), test.wantErrSubstr) {
t.Fatalf("expected error with substring '%v' got '%v'", test.wantErrSubstr, err.Error())
}
return
}
stderr := &bytes.Buffer{}
a.stderr = stderr
a.environ = func() []string { return nil }
if err := a.refreshCredsLocked(); err != nil {
if !test.wantErr {
t.Fatalf("unexpected allows plugin error: %v", err)
} else if !strings.Contains(err.Error(), test.wantErrSubstr) {
t.Fatalf("expected error with substring '%v' got '%v'", test.wantErrSubstr, err.Error())
}
return
}
if test.wantErr {
t.Fatal("expected allowlist error, but error was nil")
}
})
}
}
func (m *pluginPolicyTestMatrix) makeTests(t *testing.T, testdataDir, path string) []pluginPolicyTest {
const existingPluginInPATHBasename = "test-plugin.sh"
existingPluginInPATHAbsolutePath := filepath.Join(testdataDir, existingPluginInPATHBasename)
err := os.Setenv("PATH", fmt.Sprintf("%s:%s", testdataDir, path))
if err != nil {
t.Fatalf("error setting PATH: %s", err)
}
resolved, err := exec.LookPath(existingPluginInPATHBasename)
if err != nil {
t.Fatal(err)
}
if existingPluginInPATHAbsolutePath != resolved {
t.Fatalf("test plugin basename resolved incorrectly: did not resolve to %s", existingPluginInPATHAbsolutePath)
}
resolvedAbs, err := exec.LookPath(existingPluginInPATHAbsolutePath)
if err != nil {
t.Fatal(err)
}
if existingPluginInPATHAbsolutePath != resolvedAbs {
t.Fatalf("test plugin absolute path resolved incorrectly: did not resolve to %s", existingPluginInPATHAbsolutePath)
}
tests := make([]pluginPolicyTest, 0, len(m.exists)*2+len(m.absolute)*2+len(m.allowlistLengths)+len(m.policies))
var tt *pluginPolicyTest
for _, exists := range m.exists {
tt = new(pluginPolicyTest)
tt.setExists(exists)
for _, absolute := range m.absolute {
tt.setAbsolute(absolute)
for _, length := range m.allowlistLengths {
tt.setAllowlist(length, existingPluginInPATHAbsolutePath, existingPluginInPATHBasename)
for _, p := range m.policies {
tt.policyType = p
tt.setName()
tt.setExecConfig(existingPluginInPATHAbsolutePath, existingPluginInPATHBasename)
tt.setErrDetails(m)
tests = append(tests, *tt)
}
}
}
}
return tests
}
func (tt *pluginPolicyTest) setErrDetails(m *pluginPolicyTestMatrix) {
tt.wantErr, tt.wantErrSubstr = m.shouldErrFunc(tt)
}
func (tt *pluginPolicyTest) setAbsolute(absolute dualBool) {
tt.usePluginAbsPath = absolute.plugin
tt.useEntryAbsPath = absolute.entry
}
func (tt *pluginPolicyTest) setExists(exists dualBool) {
tt.pluginExists = exists.plugin
tt.entryExists = exists.entry
}
func (tt *pluginPolicyTest) setAllowlist(l int, existingPluginInPATHAbsolutePath string, existingPluginInPATHBasename string) {
tt.allowlistLength = l
if tt.allowlistLength >= 0 {
tt.allowlist = make([]api.AllowlistEntry, 0, tt.allowlistLength)
}
if tt.allowlistLength >= 1 {
entry := tt.makeAllowlistEntry(existingPluginInPATHAbsolutePath, existingPluginInPATHBasename)
tt.allowlist = append(tt.allowlist, entry)
}
for i := 1; i < tt.allowlistLength; i++ {
tt.allowlist = append(tt.allowlist, api.AllowlistEntry{Name: fmt.Sprintf("foo-%d", i)})
}
// shuffle the allowlist to guarantee ordering doesn't matter
for i := range tt.allowlist {
j := mathrand.IntN(i + 1)
tt.allowlist[i], tt.allowlist[j] = tt.allowlist[j], tt.allowlist[i]
}
}
func (tt *pluginPolicyTest) makeAllowlistEntry(existingPluginInPATHAbsolutePath string, existingPluginInPATHBasename string) api.AllowlistEntry {
var entry api.AllowlistEntry
switch {
case tt.entryExists && tt.useEntryAbsPath:
entry.Name = existingPluginInPATHAbsolutePath
case tt.entryExists && !tt.useEntryAbsPath:
entry.Name = existingPluginInPATHBasename
case !tt.entryExists && tt.useEntryAbsPath:
entry.Name = "/this/path/does/not/exist"
case !tt.entryExists && !tt.useEntryAbsPath:
entry.Name = "does not exist"
}
return entry
}
func (tt *pluginPolicyTest) setExecConfig(existingPluginInPATHAbsolutePath string, existingPluginInPATHBasename string) {
var cmd string
switch {
case tt.pluginExists && tt.usePluginAbsPath:
cmd = existingPluginInPATHAbsolutePath
case tt.pluginExists && !tt.usePluginAbsPath:
cmd = existingPluginInPATHBasename
case !tt.pluginExists && tt.usePluginAbsPath:
cmd = "/this/path/does/not/exist"
case !tt.pluginExists && !tt.usePluginAbsPath:
cmd = "does not exist"
default: // verifiably unreachable
cmd = "does not exist"
}
config := api.ExecConfig{}
if strings.HasSuffix(cmd, "test-plugin.sh") {
config.Env = append(config.Env, api.ExecEnvVar{
Name: "TEST_OUTPUT",
Value: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1",
"status": {
"token": "foo-bar"
}
}`,
})
config.Env = append(config.Env, api.ExecEnvVar{
Name: "TEST_EXIT_CODE",
Value: strconv.Itoa(0),
})
}
config.APIVersion = "client.authentication.k8s.io/v1"
config.InteractiveMode = api.IfAvailableExecInteractiveMode
config.Command = cmd
config.PluginPolicy.PolicyType = tt.policyType
config.PluginPolicy.Allowlist = tt.allowlist
tt.config = &config
}
func makeExistsString(s string, t bool) string {
ne := "nonexistent"
if t {
ne = "existing"
}
return fmt.Sprintf("%s-%s-path", ne, s)
}
func makePathString(s string, t bool) string {
ba := "basename"
if t {
ba = "absolute-path"
}
return fmt.Sprintf("%s-%s", ba, s)
}
func (tt *pluginPolicyTest) setName() {
p := string(tt.policyType)
if string(tt.policyType) == "" {
p = "unspecified"
}
var lengthStr string
switch tt.allowlistLength {
case -1:
lengthStr = "nil-allowlist"
case 0:
lengthStr = "empty-allowlist"
case 1:
lengthStr = "single-entry-allowlist"
default:
lengthStr = "multiple-entry-allowlist"
}
pluginExistsString := makeExistsString("plugin", tt.pluginExists)
entryExistsString := makeExistsString("entry", tt.entryExists)
pluginPathString := makePathString("plugin", tt.usePluginAbsPath)
entryPathString := makePathString("entry", tt.useEntryAbsPath)
policyString := fmt.Sprintf("with-%s-policy", strings.ToLower(p))
tt.name = filepath.Join(
policyString,
lengthStr,
pluginExistsString,
entryExistsString,
pluginPathString,
entryPathString,
)
}
func TestRoundTripper(t *testing.T) {
wantToken := ""

View File

@@ -49,6 +49,13 @@ const (
// used in some failure modes (e.g., plugin not found, client internal error) so that someone
// can more easily monitor all unsuccessful invocations.
failureExitCode = 1
// pluginAllowed represents an exec plugin invocation that was allowed by
// the plugin policy and/or the allowlist
pluginAllowed = "allowed"
// pluginAllowed represents an exec plugin invocation that was denied by
// the plugin policy and/or the allowlist
pluginDenied = "denied"
)
type certificateExpirationTracker struct {
@@ -109,3 +116,12 @@ func incrementCallsMetric(err error) {
metrics.ExecPluginCalls.Increment(failureExitCode, clientInternalError)
}
}
func incrementPolicyMetric(err error) {
if err != nil {
metrics.ExecPluginPolicyCalls.Increment(pluginDenied)
return
}
metrics.ExecPluginPolicyCalls.Increment(pluginAllowed)
}

View File

@@ -196,3 +196,114 @@ func TestCallsMetric(t *testing.T) {
t.Fatalf("got unexpected metrics calls; -want, +got:\n%s", diff)
}
}
type mockPolicyCallsMetric struct {
status string
}
type mockPolicyCallsMetricCounter struct {
policyCalls []mockPolicyCallsMetric
}
func (f *mockPolicyCallsMetricCounter) Increment(status string) {
f.policyCalls = append(f.policyCalls, mockPolicyCallsMetric{status: status})
}
func TestPolicyCallsMetric(t *testing.T) {
const (
goodOutput = `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"status": {
"token": "foo-bar"
}
}`
)
policyCallsMetricCounter := &mockPolicyCallsMetricCounter{}
originalExecPluginPolicyCalls := metrics.ExecPluginPolicyCalls
t.Cleanup(func() { metrics.ExecPluginPolicyCalls = originalExecPluginPolicyCalls })
metrics.ExecPluginPolicyCalls = policyCallsMetricCounter
tests := []struct {
wantDenied bool
policy api.PluginPolicy
}{
{
wantDenied: false,
policy: api.PluginPolicy{PolicyType: api.PluginPolicyAllowAll},
},
{
wantDenied: true,
policy: api.PluginPolicy{PolicyType: api.PluginPolicyDenyAll},
},
{
wantDenied: false,
policy: api.PluginPolicy{
PolicyType: api.PluginPolicyAllowlist,
Allowlist: []api.AllowlistEntry{
{
Name: "foobar",
},
{
Name: "testdata/test-plugin.sh",
},
},
},
},
{
wantDenied: true,
policy: api.PluginPolicy{
PolicyType: api.PluginPolicyAllowlist,
Allowlist: []api.AllowlistEntry{
{Name: "foobar"},
{Name: "baz"},
},
},
},
}
var wantPolicyCallsMetrics []mockPolicyCallsMetric
for _, test := range tests {
c := api.ExecConfig{
Command: "./testdata/test-plugin.sh",
APIVersion: "client.authentication.k8s.io/v1beta1",
Env: []api.ExecEnvVar{
{Name: "TEST_EXIT_CODE", Value: fmt.Sprintf("%d", 0)},
{Name: "TEST_OUTPUT", Value: goodOutput},
},
InteractiveMode: api.IfAvailableExecInteractiveMode,
PluginPolicy: test.policy,
}
a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
if err != nil {
t.Fatal(err)
}
a.stderr = io.Discard
err = a.refreshCredsLocked()
if err != nil && !test.wantDenied {
t.Fatalf("wanted no error, but got %q", err.Error())
}
if err == nil && test.wantDenied {
t.Fatal("wanted error, but got nil")
}
mockPolicyCallsMetric := mockPolicyCallsMetric{status: "allowed"}
if test.wantDenied {
mockPolicyCallsMetric.status = "denied"
}
wantPolicyCallsMetrics = append(wantPolicyCallsMetrics, mockPolicyCallsMetric)
}
policyCallsMetricComparer := cmp.Comparer(func(a, b mockPolicyCallsMetric) bool {
return a.status == b.status
})
actualPolicyCallsMetrics := policyCallsMetricCounter.policyCalls
if diff := cmp.Diff(wantPolicyCallsMetrics, actualPolicyCallsMetrics, policyCallsMetricComparer); diff != "" {
t.Fatalf("got unexpected metrics calls; -want, +got:\n%s", diff)
}
}

View File

@@ -708,20 +708,7 @@ func newInformer(clientState Store, options InformerOptions) Controller {
// KeyLister, that way resync operations will result in the correct set
// of update/delete deltas.
var fifo Queue
if clientgofeaturegate.FeatureGates().Enabled(clientgofeaturegate.InOrderInformers) {
fifo = NewRealFIFOWithOptions(RealFIFOOptions{
KeyFunction: MetaNamespaceKeyFunc,
KnownObjects: clientState,
Transformer: options.Transform,
})
} else {
fifo = NewDeltaFIFOWithOptions(DeltaFIFOOptions{
KnownObjects: clientState,
EmitDeltaTypeReplaced: true,
Transformer: options.Transform,
})
}
fifo := newQueueFIFO(clientState, options.Transform)
cfg := &Config{
Queue: fifo,
@@ -742,3 +729,19 @@ func newInformer(clientState Store, options InformerOptions) Controller {
}
return New(cfg)
}
func newQueueFIFO(clientState Store, transform TransformFunc) Queue {
if clientgofeaturegate.FeatureGates().Enabled(clientgofeaturegate.InOrderInformers) {
return NewRealFIFOWithOptions(RealFIFOOptions{
KeyFunction: MetaNamespaceKeyFunc,
KnownObjects: clientState,
Transformer: transform,
})
} else {
return NewDeltaFIFOWithOptions(DeltaFIFOOptions{
KnownObjects: clientState,
EmitDeltaTypeReplaced: true,
Transformer: transform,
})
}
}

View File

@@ -270,7 +270,8 @@ func NewDeltaFIFOWithOptions(opts DeltaFIFOOptions) *DeltaFIFO {
}
var (
_ = Queue(&DeltaFIFO{}) // DeltaFIFO is a Queue
_ = Queue(&DeltaFIFO{}) // DeltaFIFO is a Queue
_ = TransformingStore(&DeltaFIFO{}) // DeltaFIFO implements TransformingStore to allow memory optimizations
)
var (

View File

@@ -28,7 +28,7 @@ import (
// from the most recent Delta.
// You should treat the items returned inside the deltas as immutable.
// This function was moved here because it is not consistent with normal list semantics, but is used in unit testing.
func (f *DeltaFIFO) List() []interface{} {
func (f *DeltaFIFO) list() []interface{} {
f.lock.RLock()
defer f.lock.RUnlock()
return f.listLocked()
@@ -46,7 +46,7 @@ func (f *DeltaFIFO) listLocked() []interface{} {
// ListKeys returns a list of all the keys of the objects currently
// in the FIFO.
// This function was moved here because it is not consistent with normal list semantics, but is used in unit testing.
func (f *DeltaFIFO) ListKeys() []string {
func (f *DeltaFIFO) listKeys() []string {
f.lock.RLock()
defer f.lock.RUnlock()
list := make([]string, 0, len(f.queue))
@@ -60,19 +60,19 @@ func (f *DeltaFIFO) ListKeys() []string {
// or sets exists=false.
// You should treat the items returned inside the deltas as immutable.
// This function was moved here because it is not consistent with normal list semantics, but is used in unit testing.
func (f *DeltaFIFO) Get(obj interface{}) (item interface{}, exists bool, err error) {
func (f *DeltaFIFO) get(obj interface{}) (item interface{}, exists bool, err error) {
key, err := f.KeyOf(obj)
if err != nil {
return nil, false, KeyError{obj, err}
}
return f.GetByKey(key)
return f.getByKey(key)
}
// GetByKey returns the complete list of deltas for the requested item,
// setting exists=false if that list is empty.
// You should treat the items returned inside the deltas as immutable.
// This function was moved here because it is not consistent with normal list semantics, but is used in unit testing.
func (f *DeltaFIFO) GetByKey(key string) (item interface{}, exists bool, err error) {
func (f *DeltaFIFO) getByKey(key string) (item interface{}, exists bool, err error) {
f.lock.RLock()
defer f.lock.RUnlock()
d, exists := f.items[key]
@@ -320,10 +320,10 @@ func TestDeltaFIFO_addUpdate(t *testing.T) {
f.Update(mkFifoObj("foo", 12))
f.Delete(mkFifoObj("foo", 15))
if e, a := []interface{}{mkFifoObj("foo", 15)}, f.List(); !reflect.DeepEqual(e, a) {
if e, a := []interface{}{mkFifoObj("foo", 15)}, f.list(); !reflect.DeepEqual(e, a) {
t.Errorf("Expected %+v, got %+v", e, a)
}
if e, a := []string{"foo"}, f.ListKeys(); !reflect.DeepEqual(e, a) {
if e, a := []string{"foo"}, f.listKeys(); !reflect.DeepEqual(e, a) {
t.Errorf("Expected %+v, got %+v", e, a)
}
@@ -349,7 +349,7 @@ func TestDeltaFIFO_addUpdate(t *testing.T) {
t.Errorf("Got second value %v", unexpected.val)
case <-time.After(50 * time.Millisecond):
}
_, exists, _ := f.Get(mkFifoObj("foo", ""))
_, exists, _ := f.get(mkFifoObj("foo", ""))
if exists {
t.Errorf("item did not get removed")
}
@@ -397,7 +397,7 @@ func TestDeltaFIFO_transformer(t *testing.T) {
must(f.Replace([]interface{}{}, ""))
// Should be empty
if e, a := []string{"foo", "bar"}, f.ListKeys(); !reflect.DeepEqual(e, a) {
if e, a := []string{"foo", "bar"}, f.listKeys(); !reflect.DeepEqual(e, a) {
t.Errorf("Expected %+v, got %+v", e, a)
}
@@ -507,7 +507,7 @@ func TestDeltaFIFO_addReplace(t *testing.T) {
t.Errorf("Got second value %v", unexpected.val)
case <-time.After(50 * time.Millisecond):
}
_, exists, _ := f.Get(mkFifoObj("foo", ""))
_, exists, _ := f.get(mkFifoObj("foo", ""))
if exists {
t.Errorf("item did not get removed")
}
@@ -991,7 +991,7 @@ func BenchmarkDeltaFIFOListKeys(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = f.ListKeys()
_ = f.listKeys()
}
})
b.StopTimer()

View File

@@ -79,7 +79,7 @@ type ReflectorStore interface {
// TransformingStore is an optional interface that can be implemented by the provided store.
// If implemented on the provided store reflector will use the same transformer in its internal stores.
type TransformingStore interface {
Store
ReflectorStore
Transformer() TransformFunc
}
@@ -426,7 +426,10 @@ func (r *Reflector) ListAndWatchWithContext(ctx context.Context) error {
return nil
}
if err != nil {
logger.Error(err, "The watchlist request ended with an error, falling back to the standard LIST/WATCH semantics because making progress is better than deadlocking")
logger.V(4).Info(
"Data couldn't be fetched in watchlist mode. Falling back to regular list. This is expected if watchlist is not supported or disabled in kube-apiserver.",
"err", err,
)
fallbackToList = true
// ensure that we won't accidentally pass some garbage down the watch.
w = nil
@@ -733,9 +736,11 @@ func (r *Reflector) watchList(ctx context.Context) (watch.Interface, error) {
return false
}
var transformer TransformFunc
storeOpts := []StoreOption{}
if tr, ok := r.store.(TransformingStore); ok && tr.Transformer() != nil {
storeOpts = append(storeOpts, WithTransformer(tr.Transformer()))
transformer = tr.Transformer()
storeOpts = append(storeOpts, WithTransformer(transformer))
}
initTrace := trace.New("Reflector WatchList", trace.Field{Key: "name", Value: r.name})
@@ -795,7 +800,7 @@ func (r *Reflector) watchList(ctx context.Context) (watch.Interface, error) {
// we utilize the temporaryStore to ensure independence from the current store implementation.
// as of today, the store is implemented as a queue and will be drained by the higher-level
// component as soon as it finishes replacing the content.
checkWatchListDataConsistencyIfRequested(ctx, r.name, resourceVersion, r.listerWatcher.ListWithContext, temporaryStore.List)
checkWatchListDataConsistencyIfRequested(ctx, r.name, resourceVersion, r.listerWatcher.ListWithContext, transformer, temporaryStore.List)
if err := r.store.Replace(temporaryStore.List(), resourceVersion); err != nil {
return nil, fmt.Errorf("unable to sync watch-list result: %w", err)

View File

@@ -33,11 +33,11 @@ import (
//
// Note that this function will panic when data inconsistency is detected.
// This is intentional because we want to catch it in the CI.
func checkWatchListDataConsistencyIfRequested[T runtime.Object, U any](ctx context.Context, identity string, lastSyncedResourceVersion string, listFn consistencydetector.ListFunc[T], retrieveItemsFn consistencydetector.RetrieveItemsFunc[U]) {
func checkWatchListDataConsistencyIfRequested[T runtime.Object, U any](ctx context.Context, identity string, lastSyncedResourceVersion string, listFn consistencydetector.ListFunc[T], listItemTransformFunc func(interface{}) (interface{}, error), retrieveItemsFn consistencydetector.RetrieveItemsFunc[U]) {
if !consistencydetector.IsDataConsistencyDetectionForWatchListEnabled() {
return
}
// for informers we pass an empty ListOptions because
// listFn might be wrapped for filtering during informer construction.
consistencydetector.CheckDataConsistency(ctx, identity, lastSyncedResourceVersion, listFn, metav1.ListOptions{}, retrieveItemsFn)
consistencydetector.CheckDataConsistency(ctx, identity, lastSyncedResourceVersion, listFn, listItemTransformFunc, metav1.ListOptions{}, retrieveItemsFn)
}

View File

@@ -0,0 +1,114 @@
/*
Copyright 2024 The Kubernetes 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 cache
import (
"context"
"fmt"
"testing"
"time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
clientfeatures "k8s.io/client-go/features"
clientfeaturestesting "k8s.io/client-go/features/testing"
"k8s.io/client-go/util/consistencydetector"
"k8s.io/klog/v2/ktesting"
)
func TestReflectorDataConsistencyDetector(t *testing.T) {
clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, true)
restore := consistencydetector.SetDataConsistencyDetectionForWatchListEnabledForTest(true)
defer restore()
markTransformed := func(obj interface{}) (interface{}, error) {
pod, ok := obj.(*v1.Pod)
if !ok {
return obj, nil
}
newPod := pod.DeepCopy()
if newPod.Labels == nil {
newPod.Labels = make(map[string]string)
}
newPod.Labels["transformed"] = "true"
return newPod, nil
}
for _, inOrder := range []bool{false, true} {
t.Run(fmt.Sprintf("InOrder=%v", inOrder), func(t *testing.T) {
clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.InOrderInformers, inOrder)
for _, transformerEnabled := range []bool{false, true} {
var transformer TransformFunc
if transformerEnabled {
transformer = markTransformed
}
t.Run(fmt.Sprintf("Transformer=%v", transformerEnabled), func(t *testing.T) {
runTestReflectorDataConsistencyDetector(t, transformer)
})
}
})
}
}
func runTestReflectorDataConsistencyDetector(t *testing.T, transformer TransformFunc) {
_, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
store := NewStore(MetaNamespaceKeyFunc)
fifo := newQueueFIFO(store, transformer)
lw := &ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return &v1.PodList{
ListMeta: metav1.ListMeta{ResourceVersion: "1"},
Items: []v1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod-1", ResourceVersion: "1"}},
},
}, nil
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
w := watch.NewFake()
go func() {
w.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod-1", ResourceVersion: "1"}})
w.Action(watch.Bookmark, &v1.Pod{ObjectMeta: metav1.ObjectMeta{
Name: "pod-1",
ResourceVersion: "1",
Annotations: map[string]string{metav1.InitialEventsAnnotationKey: "true"},
}})
}()
return w, nil
},
}
r := NewReflector(lw, &v1.Pod{}, fifo, 0)
go func() {
_ = wait.PollUntilContextTimeout(ctx, 10*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (bool, error) {
return r.LastSyncResourceVersion() != "", nil
})
cancel()
}()
err := r.ListAndWatchWithContext(ctx)
if err != nil {
t.Errorf("ListAndWatchWithContext returned error: %v", err)
}
}

View File

@@ -20,10 +20,12 @@ import (
"context"
"errors"
"fmt"
"maps"
"math/rand"
"net/http"
"reflect"
goruntime "runtime"
"slices"
"strconv"
"sync"
"sync/atomic"
@@ -1962,7 +1964,7 @@ func TestReflectorReplacesStoreOnUnsafeDelete(t *testing.T) {
s := NewFIFO(MetaNamespaceKeyFunc)
var replaceInvoked atomic.Int32
store := &fakeStore{
Store: s,
ReflectorStore: s,
beforeReplace: func(list []interface{}, rv string) {
// interested in the Replace call that happens after the Error event
if rv == lastExpectedRV {
@@ -2057,131 +2059,165 @@ func TestReflectorReplacesStoreOnUnsafeDelete(t *testing.T) {
}
func TestReflectorRespectStoreTransformer(t *testing.T) {
mkPod := func(id string, rv string) *v1.Pod {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: id, ResourceVersion: rv},
Spec: v1.PodSpec{
Hostname: "test",
for name, test := range map[string]struct {
storeBuilder func(counter *atomic.Int32) ReflectorStore
items func(rs ReflectorStore) []interface{}
}{
"real-fifo": {
storeBuilder: func(counter *atomic.Int32) ReflectorStore {
return NewRealFIFO(MetaNamespaceKeyFunc, NewStore(MetaNamespaceKeyFunc), func(i interface{}) (interface{}, error) {
counter.Add(1)
cast := i.(*v1.Pod)
cast.Spec.Hostname = "transformed"
return cast, nil
})
},
}
}
preExisting1 := mkPod("foo-1", "1")
preExisting2 := mkPod("foo-2", "2")
pod3 := mkPod("foo-3", "3")
lastExpectedRV := "3"
events := []watch.Event{
{Type: watch.Added, Object: preExisting1},
{Type: watch.Added, Object: preExisting2},
{Type: watch.Bookmark, Object: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: lastExpectedRV,
Annotations: map[string]string{
metav1.InitialEventsAnnotationKey: "true",
},
items: func(rs ReflectorStore) []interface{} {
store := rs.(*RealFIFO)
objects := make(map[string]interface{})
for _, item := range store.getItems() {
key, _ := store.keyFunc(item.Object)
if item.Type == Deleted {
delete(objects, key)
} else {
objects[key] = item.Object
}
}
return slices.Collect(maps.Values(objects))
},
}},
{Type: watch.Added, Object: pod3},
}
s := NewFIFO(MetaNamespaceKeyFunc)
var replaceInvoked atomic.Int32
store := &fakeStore{
Store: s,
beforeReplace: func(list []interface{}, rv string) {
replaceInvoked.Add(1)
// Only two pods are present at the point when Replace is called.
if len(list) != 2 {
t.Errorf("unexpected nb of objects: expected 2 received %d", len(list))
},
"delta-fifo": {
storeBuilder: func(counter *atomic.Int32) ReflectorStore {
return NewDeltaFIFOWithOptions(DeltaFIFOOptions{
KeyFunction: MetaNamespaceKeyFunc,
Transformer: func(i interface{}) (interface{}, error) {
counter.Add(1)
cast := i.(*v1.Pod)
cast.Spec.Hostname = "transformed"
return cast, nil
},
})
},
items: func(rs ReflectorStore) []interface{} {
return rs.(*DeltaFIFO).list()
},
},
} {
t.Run(name, func(t *testing.T) {
mkPod := func(id string, rv string) *v1.Pod {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: id, ResourceVersion: rv},
Spec: v1.PodSpec{
Hostname: "test",
},
}
}
for _, obj := range list {
cast := obj.(*v1.Pod)
preExisting1 := mkPod("foo-1", "1")
preExisting2 := mkPod("foo-2", "2")
pod3 := mkPod("foo-3", "3")
lastExpectedRV := "3"
events := []watch.Event{
{Type: watch.Added, Object: preExisting1},
{Type: watch.Added, Object: preExisting2},
{Type: watch.Bookmark, Object: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: lastExpectedRV,
Annotations: map[string]string{
metav1.InitialEventsAnnotationKey: "true",
},
},
}},
{Type: watch.Added, Object: pod3},
}
var transformerInvoked atomic.Int32
s := test.storeBuilder(&transformerInvoked)
var once sync.Once
lw := &ListWatch{
WatchFunc: func(metav1.ListOptions) (watch.Interface, error) {
fw := watch.NewFake()
go func() {
once.Do(func() {
for _, e := range events {
fw.Action(e.Type, e.Object)
}
})
}()
return fw, nil
},
// ListFunc should never be used in WatchList mode
ListFunc: func(metav1.ListOptions) (runtime.Object, error) {
return nil, errors.New("list call not expected in WatchList mode")
},
}
clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, true)
r := NewReflector(lw, &v1.Pod{}, s, 0)
ctx, cancel := context.WithCancel(context.Background())
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
r.RunWithContext(ctx)
}()
// wait for the RV to sync to the version returned by the final list
err := wait.PollUntilContextTimeout(context.Background(), 100*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (done bool, err error) {
if rv := r.LastSyncResourceVersion(); rv == lastExpectedRV {
return true, nil
}
return false, nil
})
if err != nil {
t.Fatalf("reflector never caught up with expected revision: %q, err: %v", lastExpectedRV, err)
}
if want, got := lastExpectedRV, r.LastSyncResourceVersion(); want != got {
t.Errorf("expected LastSyncResourceVersion to be %q, but got: %q", want, got)
}
informerItems := test.items(s)
if want, got := 3, len(informerItems); want != got {
t.Errorf("expected informer to contain %d objects, but got: %d", want, got)
}
for _, item := range informerItems {
cast := item.(*v1.Pod)
if cast.Spec.Hostname != "transformed" {
t.Error("Object was not transformed prior to replacement")
}
}
},
afterReplace: func(rv string, err error) {},
transformer: func(i interface{}) (interface{}, error) {
cast := i.(*v1.Pod)
cast.Spec.Hostname = "transformed"
return cast, nil
},
}
var once sync.Once
lw := &ListWatch{
WatchFunc: func(metav1.ListOptions) (watch.Interface, error) {
fw := watch.NewFake()
go func() {
once.Do(func() {
for _, e := range events {
fw.Action(e.Type, e.Object)
}
})
}()
return fw, nil
},
// ListFunc should never be used in WatchList mode
ListFunc: func(metav1.ListOptions) (runtime.Object, error) {
return nil, errors.New("list call not expected in WatchList mode")
},
}
// Transformer should have been invoked twice for the initial sync in the informer on the temporary store,
// then twice on replace, then once on the following update.
if want, got := 5, int(transformerInvoked.Load()); want != got {
t.Errorf("expected transformer to be invoked %d times, but got: %d", want, got)
}
clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, true)
r := NewReflector(lw, &v1.Pod{}, store, 0)
ctx, cancel := context.WithCancel(context.Background())
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
r.RunWithContext(ctx)
}()
// wait for the RV to sync to the version returned by the final list
err := wait.PollUntilContextTimeout(context.Background(), 100*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (done bool, err error) {
if rv := r.LastSyncResourceVersion(); rv == lastExpectedRV {
return true, nil
}
return false, nil
})
if err != nil {
t.Fatalf("reflector never caught up with expected revision: %q, err: %v", lastExpectedRV, err)
}
if want, got := lastExpectedRV, r.LastSyncResourceVersion(); want != got {
t.Errorf("expected LastSyncResourceVersion to be %q, but got: %q", want, got)
}
if want, got := 1, int(replaceInvoked.Load()); want != got {
t.Errorf("expected replace to be invoked %d times, but got: %d", want, got)
}
cancel()
select {
case <-doneCh:
case <-time.After(wait.ForeverTestTimeout):
t.Errorf("timed out waiting for Run to return")
cancel()
select {
case <-doneCh:
case <-time.After(wait.ForeverTestTimeout):
t.Errorf("timed out waiting for Run to return")
}
})
}
}
type fakeStore struct {
Store
ReflectorStore
beforeReplace func(list []interface{}, s string)
afterReplace func(rv string, err error)
transformer TransformFunc
}
func (f *fakeStore) Replace(list []interface{}, rv string) error {
f.beforeReplace(list, rv)
err := f.Store.Replace(list, rv)
err := f.ReflectorStore.Replace(list, rv)
f.afterReplace(rv, err)
return err
}
func (f *fakeStore) Transformer() TransformFunc {
return f.transformer
}
func BenchmarkExtractList(b *testing.B) {
_, _, podList := getPodListItems(0, fakeItemsNum)
_, _, configMapList := getConfigmapListItems(0, fakeItemsNum)

View File

@@ -539,20 +539,7 @@ func (s *sharedIndexInformer) RunWithContext(ctx context.Context) {
s.startedLock.Lock()
defer s.startedLock.Unlock()
var fifo Queue
if clientgofeaturegate.FeatureGates().Enabled(clientgofeaturegate.InOrderInformers) {
fifo = NewRealFIFOWithOptions(RealFIFOOptions{
KeyFunction: MetaNamespaceKeyFunc,
KnownObjects: s.indexer,
Transformer: s.transform,
})
} else {
fifo = NewDeltaFIFOWithOptions(DeltaFIFOOptions{
KnownObjects: s.indexer,
EmitDeltaTypeReplaced: true,
Transformer: s.transform,
})
}
fifo := newQueueFIFO(s.indexer, s.transform)
cfg := &Config{
Queue: fifo,

View File

@@ -89,7 +89,8 @@ type RealFIFO struct {
}
var (
_ = Queue(&RealFIFO{}) // RealFIFO is a Queue
_ = Queue(&RealFIFO{}) // RealFIFO is a Queue
_ = TransformingStore(&RealFIFO{}) // RealFIFO implements TransformingStore to allow memory optimizations
)
// Close the queue.

View File

@@ -283,8 +283,57 @@ type ExecConfig struct {
// read user instructions might set this to "used by my-program to read user instructions".
// +k8s:conversion-gen=false
StdinUnavailableMessage string `json:"-"`
// PluginPolicy is the policy governing whether or not the configured
// `Command` may run.
// +k8s:conversion-gen=false
PluginPolicy PluginPolicy `json:"-"`
}
// AllowlistEntry is an entry in the allowlist. For each allowlist item, at
// least one field must be nonempty. A struct with all empty fields is
// considered a misconfiguration error. Each field is a criterion for
// execution. If multiple fields are specified, then the criteria of all
// specified fields must be met. That is, the result of an individual entry is
// the logical AND of all checks corresponding to the specified fields within
// the entry.
type AllowlistEntry struct {
// Name matching is performed by first resolving the absolute path of both
// the plugin and the name in the allowlist entry using `exec.LookPath`. It
// will be called on both, and the resulting strings must be equal. If
// either call to `exec.LookPath` results in an error, the `Name` check
// will be considered a failure.
Name string `json:"-"`
}
// PluginPolicy describes the policy type and allowlist (if any) for client-go
// credential plugins.
type PluginPolicy struct {
// PolicyType specifies the policy governing which, if any, client-go
// credential plugins may be executed. It MUST be one of { "", "AllowAll", "DenyAll", "Allowlist" }.
// If the policy is "", then it falls back to "AllowAll" (this is required
// to maintain backward compatibility). If the policy is DenyAll, no
// credential plugins may run. If the policy is Allowlist, only those
// plugins meeting the criteria specified in the `credentialPluginAllowlist`
// field may run. If the policy is not `Allowlist` but one is provided, it
// is considered a configuration error.
PolicyType PolicyType `json:"-"`
// Allowlist is a slice of allowlist entries. If any of them is a match,
// then the executable in question may execute. That is, the result is the
// logical OR of all entries in the allowlist. This list MUST be nil
// whenever the policy is not "Allowlist".
Allowlist []AllowlistEntry `json:"-"`
}
type PolicyType string
const (
PluginPolicyAllowAll PolicyType = "AllowAll"
PluginPolicyDenyAll PolicyType = "DenyAll"
PluginPolicyAllowlist PolicyType = "Allowlist"
)
var _ fmt.Stringer = new(ExecConfig)
var _ fmt.GoStringer = new(ExecConfig)

View File

@@ -401,6 +401,7 @@ func autoConvert_api_ExecConfig_To_v1_ExecConfig(in *api.ExecConfig, out *ExecCo
out.InteractiveMode = ExecInteractiveMode(in.InteractiveMode)
// INFO: in.StdinUnavailable opted out of conversion generation
// INFO: in.StdinUnavailableMessage opted out of conversion generation
// INFO: in.PluginPolicy opted out of conversion generation
return nil
}

View File

@@ -25,6 +25,22 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowlistEntry) DeepCopyInto(out *AllowlistEntry) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowlistEntry.
func (in *AllowlistEntry) DeepCopy() *AllowlistEntry {
if in == nil {
return nil
}
out := new(AllowlistEntry)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuthInfo) DeepCopyInto(out *AuthInfo) {
*out = *in
@@ -271,6 +287,7 @@ func (in *ExecConfig) DeepCopyInto(out *ExecConfig) {
if in.Config != nil {
out.Config = in.Config.DeepCopyObject()
}
in.PluginPolicy.DeepCopyInto(&out.PluginPolicy)
return
}
@@ -300,6 +317,27 @@ func (in *ExecEnvVar) DeepCopy() *ExecEnvVar {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PluginPolicy) DeepCopyInto(out *PluginPolicy) {
*out = *in
if in.Allowlist != nil {
in, out := &in.Allowlist, &out.Allowlist
*out = make([]AllowlistEntry, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginPolicy.
func (in *PluginPolicy) DeepCopy() *PluginPolicy {
if in == nil {
return nil
}
out := new(PluginPolicy)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Preferences) DeepCopyInto(out *Preferences) {
*out = *in

View File

@@ -25,6 +25,7 @@ import (
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/validation"
authexec "k8s.io/client-go/plugin/pkg/client/auth/exec"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
@@ -327,6 +328,10 @@ func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []err
default:
validationErrors = append(validationErrors, fmt.Errorf("invalid interactiveMode for %v: %q", authInfoName, authInfo.Exec.InteractiveMode))
}
if err := authexec.ValidatePluginPolicy(authInfo.Exec.PluginPolicy); err != nil {
validationErrors = append(validationErrors, fmt.Errorf("allowlist misconfiguration: %w", err))
}
}
// authPath also provides information for the client to identify the server, so allow multiple auth methods in that case

View File

@@ -62,6 +62,12 @@ type CallsMetric interface {
Increment(exitCode int, callStatus string)
}
// CallsMetric counts the success or failure of execution for exec plugins.
type PolicyCallsMetric interface {
// Increment increments a counter per status { "allowed", "denied" }
Increment(status string)
}
// RetryMetric counts the number of retries sent to the server
// partitioned by code, method, and host.
type RetryMetric interface {
@@ -99,6 +105,9 @@ var (
// ExecPluginCalls is the number of calls made to an exec plugin, partitioned by
// exit code and call status.
ExecPluginCalls CallsMetric = noopCalls{}
// ExecPluginPolicyCalls is the number of plugin policy check calls, partitioned
// by {"allowed", "denied"}
ExecPluginPolicyCalls PolicyCallsMetric = noopPolicy{}
// RequestRetry is the retry metric that tracks the number of
// retries sent to the server.
RequestRetry RetryMetric = noopRetry{}
@@ -121,6 +130,7 @@ type RegisterOpts struct {
RateLimiterLatency LatencyMetric
RequestResult ResultMetric
ExecPluginCalls CallsMetric
ExecPluginPolicyCalls PolicyCallsMetric
RequestRetry RetryMetric
TransportCacheEntries TransportCacheMetric
TransportCreateCalls TransportCreateCallsMetric
@@ -157,6 +167,9 @@ func Register(opts RegisterOpts) {
if opts.ExecPluginCalls != nil {
ExecPluginCalls = opts.ExecPluginCalls
}
if opts.ExecPluginPolicyCalls != nil {
ExecPluginCalls = opts.ExecPluginCalls
}
if opts.RequestRetry != nil {
RequestRetry = opts.RequestRetry
}
@@ -198,6 +211,10 @@ type noopCalls struct{}
func (noopCalls) Increment(int, string) {}
type noopPolicy struct{}
func (noopPolicy) Increment(string) {}
type noopRetry struct{}
func (noopRetry) IncrementRetry(context.Context, string, string, string) {}

View File

@@ -45,16 +45,28 @@ func IsDataConsistencyDetectionForWatchListEnabled() bool {
return dataConsistencyDetectionForWatchListEnabled
}
// SetDataConsistencyDetectionForWatchListEnabledForTest allows to enable/disable data consistency detection for testing purposes.
// It returns a function that restores the original value.
func SetDataConsistencyDetectionForWatchListEnabledForTest(enabled bool) func() {
original := dataConsistencyDetectionForWatchListEnabled
dataConsistencyDetectionForWatchListEnabled = enabled
return func() {
dataConsistencyDetectionForWatchListEnabled = original
}
}
type RetrieveItemsFunc[U any] func() []U
type ListFunc[T runtime.Object] func(ctx context.Context, options metav1.ListOptions) (T, error)
type TransformFunc func(interface{}) (interface{}, error)
// CheckDataConsistency exists solely for testing purposes.
// we cannot use checkWatchListDataConsistencyIfRequested because
// it is guarded by an environmental variable.
// we cannot manipulate the environmental variable because
// it will affect other tests in this package.
func CheckDataConsistency[T runtime.Object, U any](ctx context.Context, identity string, lastSyncedResourceVersion string, listFn ListFunc[T], listOptions metav1.ListOptions, retrieveItemsFn RetrieveItemsFunc[U]) {
func CheckDataConsistency[T runtime.Object, U any](ctx context.Context, identity string, lastSyncedResourceVersion string, listFn ListFunc[T], listItemTransformFunc TransformFunc, listOptions metav1.ListOptions, retrieveItemsFn RetrieveItemsFunc[U]) {
if !canFormAdditionalListCall(lastSyncedResourceVersion, listOptions) {
klog.V(4).Infof("data consistency check for %s is enabled but the parameters (RV, ListOptions) doesn't allow for creating a valid LIST request. Skipping the data consistency check.", identity)
return
@@ -84,6 +96,15 @@ func CheckDataConsistency[T runtime.Object, U any](ctx context.Context, identity
if err != nil {
panic(err) // this should never happen
}
if listItemTransformFunc != nil {
for i := range rawListItems {
obj, err := listItemTransformFunc(rawListItems[i])
if err != nil {
panic(err)
}
rawListItems[i] = obj.(runtime.Object)
}
}
listItems := toMetaObjectSliceOrDie(rawListItems)
sort.Sort(byUID(listItems))

View File

@@ -215,10 +215,10 @@ func TestDataConsistencyChecker(t *testing.T) {
if scenario.expectPanic {
require.Panics(t, func() {
CheckDataConsistency(ctx, "", scenario.lastSyncedResourceVersion, fakeLister.List, scenario.requestOptions, retrievedItemsFunc)
CheckDataConsistency(ctx, "", scenario.lastSyncedResourceVersion, fakeLister.List, nil, scenario.requestOptions, retrievedItemsFunc)
})
} else {
CheckDataConsistency(ctx, "", scenario.lastSyncedResourceVersion, fakeLister.List, scenario.requestOptions, retrievedItemsFunc)
CheckDataConsistency(ctx, "", scenario.lastSyncedResourceVersion, fakeLister.List, nil, scenario.requestOptions, retrievedItemsFunc)
}
require.Equal(t, scenario.expectedListRequests, fakeLister.counter)
@@ -235,7 +235,7 @@ func TestDataConsistencyCheckerRetry(t *testing.T) {
stopListErrorAfter := 5
fakeErrLister := &errorLister{stopErrorAfter: stopListErrorAfter}
CheckDataConsistency(ctx, "", "", fakeErrLister.List, metav1.ListOptions{}, retrievedItemsFunc)
CheckDataConsistency(ctx, "", "", fakeErrLister.List, nil, metav1.ListOptions{}, retrievedItemsFunc)
require.Equal(t, fakeErrLister.listCounter, fakeErrLister.stopErrorAfter)
}

View File

@@ -169,12 +169,13 @@ func newQueue[T comparable](c clock.WithTicker, queue Queue[T], metrics queueMet
cond: sync.NewCond(&sync.Mutex{}),
metrics: metrics,
unfinishedWorkUpdatePeriod: updatePeriod,
stopCh: make(chan struct{}),
}
// Don't start the goroutine for a type of noMetrics so we don't consume
// resources unnecessarily
if _, ok := metrics.(noMetrics[T]); !ok {
go t.updateUnfinishedWorkLoop()
t.wg.Go(t.updateUnfinishedWorkLoop)
}
return t
@@ -210,6 +211,14 @@ type Typed[t comparable] struct {
unfinishedWorkUpdatePeriod time.Duration
clock clock.WithTicker
// wg manages goroutines started by the queue to allow graceful shutdown
// ShutDown() will wait for goroutines to exit before returning.
wg sync.WaitGroup
stopCh chan struct{}
// stopOnce guarantees we only signal shutdown a single time
stopOnce sync.Once
}
// Add marks item as needing processing. When the queue is shutdown new
@@ -296,6 +305,11 @@ func (q *Typed[T]) Done(item T) {
// goroutines will continue processing items in the queue until it is
// empty and then receive the shutdown signal.
func (q *Typed[T]) ShutDown() {
defer q.wg.Wait()
q.stopOnce.Do(func() {
defer close(q.stopCh)
})
q.cond.L.Lock()
defer q.cond.L.Unlock()
@@ -311,6 +325,10 @@ func (q *Typed[T]) ShutDown() {
// Workers must call Done on an item after processing it, otherwise
// ShutDownWithDrain will block indefinitely.
func (q *Typed[T]) ShutDownWithDrain() {
defer q.wg.Wait()
q.stopOnce.Do(func() {
defer close(q.stopCh)
})
q.cond.L.Lock()
defer q.cond.L.Unlock()
@@ -330,20 +348,22 @@ func (q *Typed[T]) ShuttingDown() bool {
return q.shuttingDown
}
func (q *Typed[T]) updateUnfinishedWork() {
q.cond.L.Lock()
defer q.cond.L.Unlock()
if !q.shuttingDown {
q.metrics.updateUnfinishedWork()
}
}
func (q *Typed[T]) updateUnfinishedWorkLoop() {
t := q.clock.NewTicker(q.unfinishedWorkUpdatePeriod)
defer t.Stop()
for range t.C() {
if !func() bool {
q.cond.L.Lock()
defer q.cond.L.Unlock()
if !q.shuttingDown {
q.metrics.updateUnfinishedWork()
return true
}
return false
}() {
for {
select {
case <-t.C():
q.updateUnfinishedWork()
case <-q.stopCh:
return
}
}