apiserver: add warnings for deprecated APIs

Kubernetes-commit: e06b0635de5983aa115e2073c7335ad2bc8985cb
This commit is contained in:
Jordan Liggitt 2020-04-14 16:11:21 -04:00 committed by Kubernetes Publisher
parent d7d5d84691
commit 7add3b408a
3 changed files with 470 additions and 2 deletions

View File

@ -0,0 +1,133 @@
/*
Copyright 2020 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 deprecation
import (
"fmt"
"regexp"
"strconv"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
)
type apiLifecycleDeprecated interface {
APILifecycleDeprecated() (major, minor int)
}
type apiLifecycleRemoved interface {
APILifecycleRemoved() (major, minor int)
}
type apiLifecycleReplacement interface {
APILifecycleReplacement() schema.GroupVersionKind
}
// extract all digits at the beginning of the string
var leadingDigits = regexp.MustCompile(`^(\d+)`)
// MajorMinor parses a numeric major/minor version from the provided version info.
// The minor version drops all characters after the first non-digit character:
// version.Info{Major:"1", Minor:"2+"} -> 1,2
// version.Info{Major:"1", Minor:"2.3-build4"} -> 1,2
func MajorMinor(v version.Info) (int, int, error) {
major, err := strconv.Atoi(v.Major)
if err != nil {
return 0, 0, err
}
minor, err := strconv.Atoi(leadingDigits.FindString(v.Minor))
if err != nil {
return 0, 0, err
}
return major, minor, nil
}
// IsDeprecated returns true if obj implements APILifecycleDeprecated() and returns
// a major/minor version that is non-zero and is <= the specified current major/minor version.
func IsDeprecated(obj runtime.Object, currentMajor, currentMinor int) bool {
deprecated, isDeprecated := obj.(apiLifecycleDeprecated)
if !isDeprecated {
return false
}
deprecatedMajor, deprecatedMinor := deprecated.APILifecycleDeprecated()
// no deprecation version expressed
if deprecatedMajor == 0 && deprecatedMinor == 0 {
return false
}
// no current version info available
if currentMajor == 0 && currentMinor == 0 {
return true
}
// compare deprecation version to current version
if deprecatedMajor > currentMajor {
return false
}
if deprecatedMajor == currentMajor && deprecatedMinor > currentMinor {
return false
}
return true
}
// RemovedRelease returns the major/minor version in which the given object is unavailable (in the form "<major>.<minor>")
// if the object implements APILifecycleRemoved() to indicate a non-zero removal version, and returns an empty string otherwise.
func RemovedRelease(obj runtime.Object) string {
if removed, hasRemovalInfo := obj.(apiLifecycleRemoved); hasRemovalInfo {
removedMajor, removedMinor := removed.APILifecycleRemoved()
if removedMajor != 0 || removedMinor != 0 {
return fmt.Sprintf("%d.%d", removedMajor, removedMinor)
}
}
return ""
}
// WarningMessage returns a human-readable deprecation warning if the object implements APILifecycleDeprecated()
// to indicate a non-zero deprecated major/minor version and has a populated GetObjectKind().GroupVersionKind().
func WarningMessage(obj runtime.Object) string {
deprecated, isDeprecated := obj.(apiLifecycleDeprecated)
if !isDeprecated {
return ""
}
deprecatedMajor, deprecatedMinor := deprecated.APILifecycleDeprecated()
if deprecatedMajor == 0 && deprecatedMinor == 0 {
return ""
}
gvk := obj.GetObjectKind().GroupVersionKind()
if gvk.Empty() {
return ""
}
deprecationWarning := fmt.Sprintf("%s %s is deprecated in v%d.%d+", gvk.GroupVersion().String(), gvk.Kind, deprecatedMajor, deprecatedMinor)
if removed, hasRemovalInfo := obj.(apiLifecycleRemoved); hasRemovalInfo {
removedMajor, removedMinor := removed.APILifecycleRemoved()
if removedMajor != 0 || removedMinor != 0 {
deprecationWarning = deprecationWarning + fmt.Sprintf(", unavailable in v%d.%d+", removedMajor, removedMinor)
}
}
if replaced, hasReplacement := obj.(apiLifecycleReplacement); hasReplacement {
replacement := replaced.APILifecycleReplacement()
if !replacement.Empty() {
deprecationWarning = deprecationWarning + fmt.Sprintf("; use %s %s", replacement.GroupVersion().String(), replacement.Kind)
}
}
return deprecationWarning
}

View File

@ -0,0 +1,319 @@
/*
Copyright 2020 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 deprecation
import (
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
)
func TestMajorMinor(t *testing.T) {
tests := []struct {
name string
v version.Info
expectMajor int
expectMinor int
expectErr bool
}{
{
name: "empty",
v: version.Info{Major: "", Minor: ""},
expectMajor: 0,
expectMinor: 0,
expectErr: true,
},
{
name: "non-numeric major",
v: version.Info{Major: "A", Minor: "0"},
expectMajor: 0,
expectMinor: 0,
expectErr: true,
},
{
name: "non-numeric minor",
v: version.Info{Major: "1", Minor: "A"},
expectMajor: 0,
expectMinor: 0,
expectErr: true,
},
{
name: "valid",
v: version.Info{Major: "1", Minor: "2"},
expectMajor: 1,
expectMinor: 2,
expectErr: false,
},
{
name: "valid zero",
v: version.Info{Major: "0", Minor: "0"},
expectMajor: 0,
expectMinor: 0,
expectErr: false,
},
{
name: "valid zero decimal",
v: version.Info{Major: "01", Minor: "02"},
expectMajor: 1,
expectMinor: 2,
expectErr: false,
},
{
name: "valid with extra minor",
v: version.Info{Major: "1", Minor: "2+"},
expectMajor: 1,
expectMinor: 2,
expectErr: false,
},
{
name: "valid with extra minor",
v: version.Info{Major: "1", Minor: "2.3"},
expectMajor: 1,
expectMinor: 2,
expectErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
major, minor, err := MajorMinor(tt.v)
if (err != nil) != tt.expectErr {
t.Errorf("MajorMinor() error = %v, wantErr %v", err, tt.expectErr)
return
}
if major != tt.expectMajor {
t.Errorf("MajorMinor() major = %v, want %v", major, tt.expectMajor)
}
if minor != tt.expectMinor {
t.Errorf("MajorMinor() minor = %v, want %v", minor, tt.expectMinor)
}
})
}
}
func TestIsDeprecated(t *testing.T) {
tests := []struct {
name string
obj runtime.Object
currentMajor int
currentMinor int
want bool
}{
{
name: "no interface",
obj: &fakeObject{},
currentMajor: 0,
currentMinor: 0,
want: false,
},
{
name: "interface, zero-value",
obj: &fakeDeprecatedObject{},
currentMajor: 0,
currentMinor: 0,
want: false,
},
{
name: "interface, non-zero-value, no current value",
obj: &fakeDeprecatedObject{major: 10, minor: 20},
currentMajor: 0,
currentMinor: 0,
want: true,
},
{
name: "interface, non-zero-value matching major, minor",
obj: &fakeDeprecatedObject{major: 10, minor: 20},
currentMajor: 10,
currentMinor: 20,
want: true,
},
{
name: "interface, non-zero-value after major, after minor",
obj: &fakeDeprecatedObject{major: 10, minor: 20},
currentMajor: 9,
currentMinor: 19,
want: false,
},
{
name: "interface, non-zero-value after major, before minor",
obj: &fakeDeprecatedObject{major: 10, minor: 20},
currentMajor: 9,
currentMinor: 21,
want: false,
},
{
name: "interface, non-zero-value before major, after minor",
obj: &fakeDeprecatedObject{major: 10, minor: 20},
currentMajor: 11,
currentMinor: 19,
want: true,
},
{
name: "interface, non-zero-value before major, before minor",
obj: &fakeDeprecatedObject{major: 10, minor: 20},
currentMajor: 11,
currentMinor: 21,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsDeprecated(tt.obj, tt.currentMajor, tt.currentMinor); got != tt.want {
t.Errorf("IsDeprecated() = %v, want %v", got, tt.want)
}
})
}
}
func TestRemovedRelease(t *testing.T) {
tests := []struct {
name string
obj runtime.Object
want string
}{
{
name: "no interface",
obj: &fakeObject{},
want: "",
},
{
name: "interface, zero-value",
obj: &fakeRemovedObject{removedMajor: 0, removedMinor: 0},
want: "",
},
{
name: "interface, non-zero major",
obj: &fakeRemovedObject{removedMajor: 1, removedMinor: 0},
want: "1.0",
},
{
name: "interface, non-zero minor",
obj: &fakeRemovedObject{removedMajor: 0, removedMinor: 1},
want: "0.1",
},
{
name: "interface, non-zero",
obj: &fakeRemovedObject{removedMajor: 1, removedMinor: 2},
want: "1.2",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := RemovedRelease(tt.obj); got != tt.want {
t.Errorf("RemovedRelease() = %v, want %v", got, tt.want)
}
})
}
}
func TestWarningMessage(t *testing.T) {
tests := []struct {
name string
obj runtime.Object
gvk schema.GroupVersionKind
want string
}{
{
name: "no interface, zero-value",
obj: &fakeObject{},
want: "",
},
{
name: "deprecated interface, zero-value",
obj: &fakeDeprecatedObject{major: 0, minor: 0},
gvk: schema.GroupVersionKind{},
want: "",
},
{
name: "deprecated interface, non-zero-value",
obj: &fakeDeprecatedObject{major: 1, minor: 2},
gvk: schema.GroupVersionKind{Group: "mygroup", Version: "v1", Kind: "MyKind"},
want: "mygroup/v1 MyKind is deprecated in v1.2+",
},
{
name: "removed interface, zero-value removal version",
obj: &fakeRemovedObject{major: 1, minor: 2},
gvk: schema.GroupVersionKind{Group: "mygroup", Version: "v1", Kind: "MyKind"},
want: "mygroup/v1 MyKind is deprecated in v1.2+",
},
{
name: "removed interface, non-zero-value removal version",
obj: &fakeRemovedObject{major: 1, minor: 2, removedMajor: 3, removedMinor: 4},
gvk: schema.GroupVersionKind{Group: "mygroup", Version: "v1", Kind: "MyKind"},
want: "mygroup/v1 MyKind is deprecated in v1.2+, unavailable in v3.4+",
},
{
name: "replaced interface, zero-value replacement",
obj: &fakeReplacedObject{major: 1, minor: 2, removedMajor: 3, removedMinor: 4},
gvk: schema.GroupVersionKind{Group: "mygroup", Version: "v1", Kind: "MyKind"},
want: "mygroup/v1 MyKind is deprecated in v1.2+, unavailable in v3.4+",
},
{
name: "replaced interface, non-zero-value replacement",
obj: &fakeReplacedObject{major: 1, minor: 2, removedMajor: 3, removedMinor: 4, replacement: schema.GroupVersionKind{Group: "anothergroup", Version: "v2", Kind: "AnotherKind"}},
gvk: schema.GroupVersionKind{Group: "mygroup", Version: "v1", Kind: "MyKind"},
want: "mygroup/v1 MyKind is deprecated in v1.2+, unavailable in v3.4+; use anothergroup/v2 AnotherKind",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.obj.GetObjectKind().SetGroupVersionKind(tt.gvk)
if got := WarningMessage(tt.obj); got != tt.want {
t.Errorf("WarningMessage() = %v, want %v", got, tt.want)
}
})
}
}
type fakeObject struct {
unstructured.Unstructured
}
type fakeDeprecatedObject struct {
unstructured.Unstructured
major int
minor int
}
func (f *fakeDeprecatedObject) APILifecycleDeprecated() (int, int) { return f.major, f.minor }
type fakeRemovedObject struct {
unstructured.Unstructured
major int
minor int
removedMajor int
removedMinor int
}
func (f *fakeRemovedObject) APILifecycleDeprecated() (int, int) { return f.major, f.minor }
func (f *fakeRemovedObject) APILifecycleRemoved() (int, int) { return f.removedMajor, f.removedMinor }
type fakeReplacedObject struct {
unstructured.Unstructured
major int
minor int
replacement schema.GroupVersionKind
removedMajor int
removedMinor int
}
func (f *fakeReplacedObject) APILifecycleDeprecated() (int, int) { return f.major, f.minor }
func (f *fakeReplacedObject) APILifecycleRemoved() (int, int) { return f.removedMajor, f.removedMinor }
func (f *fakeReplacedObject) APILifecycleReplacement() schema.GroupVersionKind { return f.replacement }

View File

@ -34,6 +34,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/endpoints/deprecation"
"k8s.io/apiserver/pkg/endpoints/discovery"
"k8s.io/apiserver/pkg/endpoints/handlers"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
@ -43,6 +44,7 @@ import (
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
versioninfo "k8s.io/component-base/version"
)
const (
@ -624,8 +626,22 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
verbOverrider, needOverride := storage.(StorageMetricsOverride)
// accumulate endpoint-level warnings
enableWarningHeaders := utilfeature.DefaultFeatureGate.Enabled(features.WarningHeaders)
warnings := []string{}
var (
enableWarningHeaders = utilfeature.DefaultFeatureGate.Enabled(features.WarningHeaders)
warnings []string
deprecated bool
)
{
versionedPtrWithGVK := versionedPtr.DeepCopyObject()
versionedPtrWithGVK.GetObjectKind().SetGroupVersionKind(fqKindToRegister)
currentMajor, currentMinor, _ := deprecation.MajorMinor(versioninfo.Get())
deprecated = deprecation.IsDeprecated(versionedPtrWithGVK, currentMajor, currentMinor)
if deprecated {
warnings = append(warnings, deprecation.WarningMessage(versionedPtrWithGVK))
}
}
switch action.Verb {
case "GET": // Get a resource.