Skip to content

Commit 8b08487

Browse files
authored
Merge pull request kubernetes#130354 from siyuanfoundation/forward-api
KEP-4330: add forward compatibility for compatibility mode
2 parents b00f88e + 3d2d8db commit 8b08487

File tree

16 files changed

+1820
-208
lines changed

16 files changed

+1820
-208
lines changed

cmd/kube-apiserver/app/options/options_test.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ func TestAddFlags(t *testing.T) {
128128
"--service-cluster-ip-range=192.168.128.0/17",
129129
"--lease-reuse-duration-seconds=100",
130130
"--emulated-version=test=1.31",
131+
"--emulation-forward-compatible=true",
132+
"--runtime-config-emulation-forward-compatible=true",
131133
}
132134
fs.Parse(args)
133135
utilruntime.Must(componentGlobalsRegistry.Set())
@@ -136,17 +138,19 @@ func TestAddFlags(t *testing.T) {
136138
expected := &ServerRunOptions{
137139
Options: &controlplaneapiserver.Options{
138140
GenericServerRunOptions: &apiserveroptions.ServerRunOptions{
139-
AdvertiseAddress: netutils.ParseIPSloppy("192.168.10.10"),
140-
CorsAllowedOriginList: []string{"10.10.10.100", "10.10.10.200"},
141-
MaxRequestsInFlight: 400,
142-
MaxMutatingRequestsInFlight: 200,
143-
RequestTimeout: time.Duration(2) * time.Minute,
144-
MinRequestTimeout: 1800,
145-
StorageInitializationTimeout: time.Minute,
146-
JSONPatchMaxCopyBytes: int64(3 * 1024 * 1024),
147-
MaxRequestBodyBytes: int64(3 * 1024 * 1024),
148-
ComponentGlobalsRegistry: componentGlobalsRegistry,
149-
ComponentName: basecompatibility.DefaultKubeComponent,
141+
AdvertiseAddress: netutils.ParseIPSloppy("192.168.10.10"),
142+
CorsAllowedOriginList: []string{"10.10.10.100", "10.10.10.200"},
143+
MaxRequestsInFlight: 400,
144+
MaxMutatingRequestsInFlight: 200,
145+
RequestTimeout: time.Duration(2) * time.Minute,
146+
MinRequestTimeout: 1800,
147+
StorageInitializationTimeout: time.Minute,
148+
JSONPatchMaxCopyBytes: int64(3 * 1024 * 1024),
149+
MaxRequestBodyBytes: int64(3 * 1024 * 1024),
150+
ComponentGlobalsRegistry: componentGlobalsRegistry,
151+
ComponentName: basecompatibility.DefaultKubeComponent,
152+
EmulationForwardCompatible: true,
153+
RuntimeConfigEmulationForwardCompatible: true,
150154
},
151155
Admission: &kubeoptions.AdmissionOptions{
152156
GenericAdmission: &apiserveroptions.AdmissionOptions{

pkg/controlplane/apiserver/apis.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,12 @@ func (s *Server) InstallAPIs(restStorageProviders ...RESTStorageProvider) error
8989
nonLegacy := []*genericapiserver.APIGroupInfo{}
9090

9191
// used later in the loop to filter the served resource by those that have expired.
92-
resourceExpirationEvaluator, err := genericapiserver.NewResourceExpirationEvaluator(s.GenericAPIServer.EffectiveVersion.EmulationVersion())
92+
resourceExpirationEvaluatorOpts := genericapiserver.ResourceExpirationEvaluatorOptions{
93+
CurrentVersion: s.GenericAPIServer.EffectiveVersion.EmulationVersion(),
94+
EmulationForwardCompatible: s.GenericAPIServer.EmulationForwardCompatible,
95+
RuntimeConfigEmulationForwardCompatible: s.GenericAPIServer.RuntimeConfigEmulationForwardCompatible,
96+
}
97+
resourceExpirationEvaluator, err := genericapiserver.NewResourceExpirationEvaluatorFromOptions(resourceExpirationEvaluatorOpts)
9398
if err != nil {
9499
return err
95100
}
@@ -107,10 +112,13 @@ func (s *Server) InstallAPIs(restStorageProviders ...RESTStorageProvider) error
107112
continue
108113
}
109114

110-
// Remove resources that serving kinds that are removed.
115+
// Remove resources that serving kinds that are removed or not introduced yet at the current version.
111116
// We do this here so that we don't accidentally serve versions without resources or openapi information that for kinds we don't serve.
112117
// This is a spot above the construction of individual storage handlers so that no sig accidentally forgets to check.
113-
resourceExpirationEvaluator.RemoveDeletedKinds(groupName, apiGroupInfo.Scheme, apiGroupInfo.VersionedResourcesStorageMap)
118+
err = resourceExpirationEvaluator.RemoveUnavailableKinds(groupName, apiGroupInfo.Scheme, apiGroupInfo.VersionedResourcesStorageMap, s.APIResourceConfigSource)
119+
if err != nil {
120+
return err
121+
}
114122
if len(apiGroupInfo.VersionedResourcesStorageMap) == 0 {
115123
klog.V(1).Infof("Removing API group %v because it is time to stop serving it because it has no versions per APILifecycle.", groupName)
116124
continue

staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ type ObjectDefaulter interface {
259259

260260
type ObjectVersioner interface {
261261
ConvertToVersion(in Object, gv GroupVersioner) (out Object, err error)
262+
PrioritizedVersionsForGroup(group string) []schema.GroupVersion
262263
}
263264

264265
// ObjectConvertor converts an object to a different version.

staging/src/k8s.io/apiserver/pkg/server/config.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,15 @@ type Config struct {
154154
// EffectiveVersion determines which apis and features are available
155155
// based on when the api/feature lifecyle.
156156
EffectiveVersion basecompatibility.EffectiveVersion
157+
// EmulationForwardCompatible is an option to implicitly enable all APIs which are introduced after the emulation version and
158+
// have higher priority than APIs of the same group resource enabled at the emulation version.
159+
// If true, all APIs that have higher priority than the APIs of the same group resource enabled at the emulation version will be installed.
160+
// This is needed when a controller implementation migrates to newer API versions, for the binary version, and also uses the newer API versions even when emulation version is set.
161+
EmulationForwardCompatible bool
162+
// RuntimeConfigEmulationForwardCompatible is an option to explicitly enable specific APIs introduced after the emulation version through the runtime-config.
163+
// If true, APIs identified by group/version that are enabled in the --runtime-config flag will be installed even if it is introduced after the emulation version. --runtime-config flag values that identify multiple APIs, such as api/all,api/ga,api/beta, are not influenced by this flag and will only enable APIs available at the current emulation version.
164+
// If false, error would be thrown if any GroupVersion or GroupVersionResource explicitly enabled in the --runtime-config flag is introduced after the emulation version.
165+
RuntimeConfigEmulationForwardCompatible bool
157166
// FeatureGate is a way to plumb feature gate through if you have them.
158167
FeatureGate featuregate.FeatureGate
159168
// AuditBackend is where audit events are sent to.
@@ -839,8 +848,10 @@ func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*G
839848
StorageReadinessHook: NewStorageReadinessHook(c.StorageInitializationTimeout),
840849
StorageVersionManager: c.StorageVersionManager,
841850

842-
EffectiveVersion: c.EffectiveVersion,
843-
FeatureGate: c.FeatureGate,
851+
EffectiveVersion: c.EffectiveVersion,
852+
EmulationForwardCompatible: c.EmulationForwardCompatible,
853+
RuntimeConfigEmulationForwardCompatible: c.RuntimeConfigEmulationForwardCompatible,
854+
FeatureGate: c.FeatureGate,
844855

845856
muxAndDiscoveryCompleteSignals: map[string]<-chan struct{}{},
846857
}

staging/src/k8s.io/apiserver/pkg/server/deleted_kinds.go

Lines changed: 153 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package server
1919
import (
2020
"fmt"
2121
"os"
22+
"regexp"
2223
"strconv"
2324
"strings"
2425

@@ -28,13 +29,18 @@ import (
2829
"k8s.io/apimachinery/pkg/util/sets"
2930
apimachineryversion "k8s.io/apimachinery/pkg/util/version"
3031
"k8s.io/apiserver/pkg/registry/rest"
32+
serverstorage "k8s.io/apiserver/pkg/server/storage"
3133
"k8s.io/klog/v2"
3234
)
3335

36+
var alphaPattern = regexp.MustCompile(`^v\d+alpha\d+$`)
37+
3438
// resourceExpirationEvaluator holds info for deciding if a particular rest.Storage needs to excluded from the API
3539
type resourceExpirationEvaluator struct {
36-
currentVersion *apimachineryversion.Version
37-
isAlpha bool
40+
currentVersion *apimachineryversion.Version
41+
emulationForwardCompatible bool
42+
runtimeConfigEmulationForwardCompatible bool
43+
isAlpha bool
3844
// Special flag checking for the existence of alpha.0
3945
// alpha.0 is a special case where everything merged to master is auto propagated to the release-1.n branch
4046
isAlphaZero bool
@@ -50,20 +56,41 @@ type resourceExpirationEvaluator struct {
5056

5157
// ResourceExpirationEvaluator indicates whether or not a resource should be served.
5258
type ResourceExpirationEvaluator interface {
53-
// RemoveDeletedKinds inspects the storage map and modifies it in place by removing storage for kinds that have been deleted.
59+
// RemoveUnavailableKinds inspects the storage map and modifies it in place by removing storage for kinds that have been deleted or are introduced after the current version.
5460
// versionedResourcesStorageMap mirrors the field on APIGroupInfo, it's a map from version to resource to the storage.
55-
RemoveDeletedKinds(groupName string, versioner runtime.ObjectVersioner, versionedResourcesStorageMap map[string]map[string]rest.Storage)
61+
RemoveUnavailableKinds(groupName string, versioner runtime.ObjectVersioner, versionedResourcesStorageMap map[string]map[string]rest.Storage, apiResourceConfigSource serverstorage.APIResourceConfigSource) error
5662
// ShouldServeForVersion returns true if a particular version cut off is after the current version
5763
ShouldServeForVersion(majorRemoved, minorRemoved int) bool
5864
}
5965

66+
type ResourceExpirationEvaluatorOptions struct {
67+
// CurrentVersion is the current version of the apiserver.
68+
CurrentVersion *apimachineryversion.Version
69+
// EmulationForwardCompatible indicates whether the apiserver should serve resources that are introduced after the current version,
70+
// when resources of the same group and resource name but with lower priority are served.
71+
EmulationForwardCompatible bool
72+
// RuntimeConfigEmulationForwardCompatible indicates whether the apiserver should serve resources that are introduced after the current version,
73+
// when the resource is explicitly enabled in runtime-config.
74+
RuntimeConfigEmulationForwardCompatible bool
75+
}
76+
6077
func NewResourceExpirationEvaluator(currentVersion *apimachineryversion.Version) (ResourceExpirationEvaluator, error) {
78+
opts := ResourceExpirationEvaluatorOptions{
79+
CurrentVersion: currentVersion,
80+
}
81+
return NewResourceExpirationEvaluatorFromOptions(opts)
82+
}
83+
84+
func NewResourceExpirationEvaluatorFromOptions(opts ResourceExpirationEvaluatorOptions) (ResourceExpirationEvaluator, error) {
85+
currentVersion := opts.CurrentVersion
6186
if currentVersion == nil {
6287
return nil, fmt.Errorf("empty NewResourceExpirationEvaluator currentVersion")
6388
}
6489
klog.V(1).Infof("NewResourceExpirationEvaluator with currentVersion: %s.", currentVersion)
6590
ret := &resourceExpirationEvaluator{
66-
strictRemovedHandlingInAlpha: false,
91+
strictRemovedHandlingInAlpha: false,
92+
emulationForwardCompatible: opts.EmulationForwardCompatible,
93+
runtimeConfigEmulationForwardCompatible: opts.RuntimeConfigEmulationForwardCompatible,
6794
}
6895
// Only keeps the major and minor versions from input version.
6996
ret.currentVersion = apimachineryversion.MajorMinor(currentVersion.Major(), currentVersion.Minor())
@@ -89,7 +116,8 @@ func NewResourceExpirationEvaluator(currentVersion *apimachineryversion.Version)
89116
return ret, nil
90117
}
91118

92-
func (e *resourceExpirationEvaluator) shouldServe(gv schema.GroupVersion, versioner runtime.ObjectVersioner, resourceServingInfo rest.Storage) bool {
119+
// isNotRemoved checks if a resource is removed due to the APILifecycleRemoved information.
120+
func (e *resourceExpirationEvaluator) isNotRemoved(gv schema.GroupVersion, versioner runtime.ObjectVersioner, resourceServingInfo rest.Storage) bool {
93121
internalPtr := resourceServingInfo.New()
94122

95123
target := gv
@@ -104,15 +132,6 @@ func (e *resourceExpirationEvaluator) shouldServe(gv schema.GroupVersion, versio
104132
return false
105133
}
106134

107-
introduced, ok := versionedPtr.(introducedInterface)
108-
if ok {
109-
majorIntroduced, minorIntroduced := introduced.APILifecycleIntroduced()
110-
verIntroduced := apimachineryversion.MajorMinor(uint(majorIntroduced), uint(minorIntroduced))
111-
if e.currentVersion.LessThan(verIntroduced) {
112-
return false
113-
}
114-
}
115-
116135
removed, ok := versionedPtr.(removedInterface)
117136
if !ok {
118137
return true
@@ -155,13 +174,13 @@ type introducedInterface interface {
155174

156175
// removeDeletedKinds inspects the storage map and modifies it in place by removing storage for kinds that have been deleted.
157176
// versionedResourcesStorageMap mirrors the field on APIGroupInfo, it's a map from version to resource to the storage.
158-
func (e *resourceExpirationEvaluator) RemoveDeletedKinds(groupName string, versioner runtime.ObjectVersioner, versionedResourcesStorageMap map[string]map[string]rest.Storage) {
177+
func (e *resourceExpirationEvaluator) removeDeletedKinds(groupName string, versioner runtime.ObjectVersioner, versionedResourcesStorageMap map[string]map[string]rest.Storage) {
159178
versionsToRemove := sets.NewString()
160179
for apiVersion := range sets.StringKeySet(versionedResourcesStorageMap) {
161180
versionToResource := versionedResourcesStorageMap[apiVersion]
162181
resourcesToRemove := sets.NewString()
163182
for resourceName, resourceServingInfo := range versionToResource {
164-
if !e.shouldServe(schema.GroupVersion{Group: groupName, Version: apiVersion}, versioner, resourceServingInfo) {
183+
if !e.isNotRemoved(schema.GroupVersion{Group: groupName, Version: apiVersion}, versioner, resourceServingInfo) {
165184
resourcesToRemove.Insert(resourceName)
166185
}
167186
}
@@ -189,6 +208,123 @@ func (e *resourceExpirationEvaluator) RemoveDeletedKinds(groupName string, versi
189208
}
190209
}
191210

211+
func (e *resourceExpirationEvaluator) RemoveUnavailableKinds(groupName string, versioner runtime.ObjectVersioner, versionedResourcesStorageMap map[string]map[string]rest.Storage, apiResourceConfigSource serverstorage.APIResourceConfigSource) error {
212+
e.removeDeletedKinds(groupName, versioner, versionedResourcesStorageMap)
213+
return e.removeUnintroducedKinds(groupName, versioner, versionedResourcesStorageMap, apiResourceConfigSource)
214+
}
215+
216+
// removeUnintroducedKinds inspects the storage map and modifies it in place by removing storage for kinds that are introduced after the current version.
217+
// versionedResourcesStorageMap mirrors the field on APIGroupInfo, it's a map from version to resource to the storage.
218+
func (e *resourceExpirationEvaluator) removeUnintroducedKinds(groupName string, versioner runtime.ObjectVersioner, versionedResourcesStorageMap map[string]map[string]rest.Storage, apiResourceConfigSource serverstorage.APIResourceConfigSource) error {
219+
versionsToRemove := sets.NewString()
220+
prioritizedVersions := versioner.PrioritizedVersionsForGroup(groupName)
221+
enabledResources := sets.NewString()
222+
223+
// iterate from the end to the front, so that we remove the lower priority versions first.
224+
for i := len(prioritizedVersions) - 1; i >= 0; i-- {
225+
apiVersion := prioritizedVersions[i].Version
226+
versionToResource := versionedResourcesStorageMap[apiVersion]
227+
if len(versionToResource) == 0 {
228+
continue
229+
}
230+
resourcesToRemove := sets.NewString()
231+
for resourceName, resourceServingInfo := range versionToResource {
232+
// we check the resource enablement from low priority to high priority.
233+
// If the same resource with a different version that we have checked so far is already enabled, that means some resource with the same resourceName and a lower priority version has been enabled.
234+
// Then emulation forward compatibility for the version being checked now is made based on this information.
235+
lowerPriorityEnabled := enabledResources.Has(resourceName)
236+
shouldKeep, err := e.shouldServeBasedOnVersionIntroduced(schema.GroupVersionResource{Group: groupName, Version: apiVersion, Resource: resourceName},
237+
versioner, resourceServingInfo, apiResourceConfigSource, lowerPriorityEnabled)
238+
if err != nil {
239+
return err
240+
}
241+
if !shouldKeep {
242+
resourcesToRemove.Insert(resourceName)
243+
} else if !alphaPattern.MatchString(apiVersion) {
244+
// enabledResources is passed onto the next iteration to check the enablement of higher priority resources for emulation forward compatibility.
245+
// But enablement alpha apis do not affect the enablement of other versions because emulation forward compatibility is not applicable to alpha apis.
246+
enabledResources.Insert(resourceName)
247+
}
248+
}
249+
250+
for resourceName := range versionedResourcesStorageMap[apiVersion] {
251+
if !shouldRemoveResourceAndSubresources(resourcesToRemove, resourceName) {
252+
continue
253+
}
254+
255+
klog.V(1).Infof("Removing resource %v.%v.%v because it is introduced after the current version %s per APILifecycle.", resourceName, apiVersion, groupName, e.currentVersion.String())
256+
storage := versionToResource[resourceName]
257+
storage.Destroy()
258+
delete(versionToResource, resourceName)
259+
}
260+
versionedResourcesStorageMap[apiVersion] = versionToResource
261+
262+
if len(versionedResourcesStorageMap[apiVersion]) == 0 {
263+
versionsToRemove.Insert(apiVersion)
264+
}
265+
}
266+
267+
for _, apiVersion := range versionsToRemove.List() {
268+
gv := schema.GroupVersion{Group: groupName, Version: apiVersion}
269+
if apiResourceConfigSource != nil && apiResourceConfigSource.VersionExplicitlyEnabled(gv) {
270+
return fmt.Errorf(
271+
"cannot enable version %s in runtime-config because all the resources have been introduced after the current version %s. Consider setting --runtime-config-emulation-forward-compatible=true",
272+
gv, e.currentVersion)
273+
}
274+
klog.V(1).Infof("Removing version %v.%v because it is introduced after the current version %s and because it has no resources per APILifecycle.", apiVersion, groupName, e.currentVersion.String())
275+
delete(versionedResourcesStorageMap, apiVersion)
276+
}
277+
return nil
278+
}
279+
280+
func (e *resourceExpirationEvaluator) shouldServeBasedOnVersionIntroduced(gvr schema.GroupVersionResource, versioner runtime.ObjectVersioner, resourceServingInfo rest.Storage,
281+
apiResourceConfigSource serverstorage.APIResourceConfigSource, lowerPriorityEnabled bool) (bool, error) {
282+
verIntroduced := apimachineryversion.MajorMinor(0, 0)
283+
internalPtr := resourceServingInfo.New()
284+
285+
target := gvr.GroupVersion()
286+
// honor storage that overrides group version (used for things like scale subresources)
287+
if versionProvider, ok := resourceServingInfo.(rest.GroupVersionKindProvider); ok {
288+
target = versionProvider.GroupVersionKind(target).GroupVersion()
289+
}
290+
291+
versionedPtr, err := versioner.ConvertToVersion(internalPtr, target)
292+
if err != nil {
293+
utilruntime.HandleError(err)
294+
return false, err
295+
}
296+
297+
introduced, ok := versionedPtr.(introducedInterface)
298+
if ok {
299+
majorIntroduced, minorIntroduced := introduced.APILifecycleIntroduced()
300+
verIntroduced = apimachineryversion.MajorMinor(uint(majorIntroduced), uint(minorIntroduced))
301+
}
302+
// should serve resource introduced at or before the current version.
303+
if e.currentVersion.AtLeast(verIntroduced) {
304+
return true, nil
305+
}
306+
// the rest of the function is to determine if a resource introduced after current version should be served. (only applicable in emulation mode.)
307+
308+
// if a lower priority version of the resource has been enabled, the same resource with higher priority
309+
// should also be enabled if emulationForwardCompatible = true.
310+
if e.emulationForwardCompatible && lowerPriorityEnabled {
311+
return true, nil
312+
}
313+
if apiResourceConfigSource == nil {
314+
return false, nil
315+
}
316+
// could explicitly enable future resources in runtime-config forward compatible mode.
317+
if e.runtimeConfigEmulationForwardCompatible && (apiResourceConfigSource.ResourceExplicitlyEnabled(gvr) || apiResourceConfigSource.VersionExplicitlyEnabled(gvr.GroupVersion())) {
318+
return true, nil
319+
}
320+
// return error if a future resource is explicit enabled in runtime-config but runtimeConfigEmulationForwardCompatible is false.
321+
if apiResourceConfigSource.ResourceExplicitlyEnabled(gvr) {
322+
return false, fmt.Errorf("cannot enable resource %s in runtime-config because it is introduced at %s after the current version %s. Consider setting --runtime-config-emulation-forward-compatible=true",
323+
gvr, verIntroduced, e.currentVersion)
324+
}
325+
return false, nil
326+
}
327+
192328
func shouldRemoveResourceAndSubresources(resourcesToRemove sets.String, resourceName string) bool {
193329
for _, resourceToRemove := range resourcesToRemove.List() {
194330
if resourceName == resourceToRemove {

0 commit comments

Comments
 (0)