Merge pull request #129081 from stlaz/fg_remote_uid

featuregate UID in RequestHeader authenticator
This commit is contained in:
Kubernetes Prow Robot 2024-12-04 23:43:06 +00:00 committed by GitHub
commit 1504f10e79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 345 additions and 65 deletions

View File

@ -35,7 +35,9 @@ import (
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/authentication/request/headerrequest" "k8s.io/apiserver/pkg/authentication/request/headerrequest"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/apiserver/pkg/server/dynamiccertificates"
utilfeature "k8s.io/apiserver/pkg/util/feature"
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
@ -262,10 +264,13 @@ func getConfigMapDataFor(authenticationInfo ClusterAuthenticationInfo) (map[stri
if err != nil { if err != nil {
return nil, err return nil, err
} }
data["requestheader-uid-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderUIDHeaders.Value()) if utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) && len(authenticationInfo.RequestHeaderUIDHeaders.Value()) > 0 {
if err != nil { data["requestheader-uid-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderUIDHeaders.Value())
return nil, err if err != nil {
return nil, err
}
} }
data["requestheader-group-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderGroupHeaders.Value()) data["requestheader-group-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderGroupHeaders.Value())
if err != nil { if err != nil {
return nil, err return nil, err
@ -305,9 +310,12 @@ func getClusterAuthenticationInfoFor(data map[string]string) (ClusterAuthenticat
if err != nil { if err != nil {
return ClusterAuthenticationInfo{}, err return ClusterAuthenticationInfo{}, err
} }
ret.RequestHeaderUIDHeaders, err = jsonDeserializeStringSlice(data["requestheader-uid-headers"])
if err != nil { if utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) {
return ClusterAuthenticationInfo{}, err ret.RequestHeaderUIDHeaders, err = jsonDeserializeStringSlice(data["requestheader-uid-headers"])
if err != nil {
return ClusterAuthenticationInfo{}, err
}
} }
if caBundle := data["requestheader-client-ca-file"]; len(caBundle) > 0 { if caBundle := data["requestheader-client-ca-file"]; len(caBundle) > 0 {

View File

@ -30,11 +30,14 @@ import (
"k8s.io/apimachinery/pkg/util/dump" "k8s.io/apimachinery/pkg/util/dump"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/authentication/request/headerrequest" "k8s.io/apiserver/pkg/authentication/request/headerrequest"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/apiserver/pkg/server/dynamiccertificates"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
corev1listers "k8s.io/client-go/listers/core/v1" corev1listers "k8s.io/client-go/listers/core/v1"
clienttesting "k8s.io/client-go/testing" clienttesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
featuregatetesting "k8s.io/component-base/featuregate/testing"
) )
var ( var (
@ -95,6 +98,7 @@ func TestWriteClientCAs(t *testing.T) {
preexistingObjs []runtime.Object preexistingObjs []runtime.Object
expectedConfigMaps map[string]*corev1.ConfigMap expectedConfigMaps map[string]*corev1.ConfigMap
expectCreate bool expectCreate bool
uidGate bool
}{ }{
{ {
name: "basic", name: "basic",
@ -107,6 +111,32 @@ func TestWriteClientCAs(t *testing.T) {
RequestHeaderCA: anotherRandomCAProvider, RequestHeaderCA: anotherRandomCAProvider,
RequestHeaderAllowedNames: headerrequest.StaticStringSlice{"first", "second"}, RequestHeaderAllowedNames: headerrequest.StaticStringSlice{"first", "second"},
}, },
expectedConfigMaps: map[string]*corev1.ConfigMap{
"extension-apiserver-authentication": {
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{
"client-ca-file": string(someRandomCA),
"requestheader-username-headers": `["alfa","bravo","charlie"]`,
"requestheader-group-headers": `["delta"]`,
"requestheader-extra-headers-prefix": `["echo","foxtrot"]`,
"requestheader-client-ca-file": string(anotherRandomCA),
"requestheader-allowed-names": `["first","second"]`,
},
},
},
expectCreate: true,
},
{
name: "basic with feature gate",
clusterAuthInfo: ClusterAuthenticationInfo{
ClientCA: someRandomCAProvider,
RequestHeaderUsernameHeaders: headerrequest.StaticStringSlice{"alfa", "bravo", "charlie"},
RequestHeaderUIDHeaders: headerrequest.StaticStringSlice{"golf", "hotel", "india"},
RequestHeaderGroupHeaders: headerrequest.StaticStringSlice{"delta"},
RequestHeaderExtraHeaderPrefixes: headerrequest.StaticStringSlice{"echo", "foxtrot"},
RequestHeaderCA: anotherRandomCAProvider,
RequestHeaderAllowedNames: headerrequest.StaticStringSlice{"first", "second"},
},
expectedConfigMaps: map[string]*corev1.ConfigMap{ expectedConfigMaps: map[string]*corev1.ConfigMap{
"extension-apiserver-authentication": { "extension-apiserver-authentication": {
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
@ -122,6 +152,7 @@ func TestWriteClientCAs(t *testing.T) {
}, },
}, },
expectCreate: true, expectCreate: true,
uidGate: true,
}, },
{ {
name: "skip extension-apiserver-authentication", name: "skip extension-apiserver-authentication",
@ -134,7 +165,6 @@ func TestWriteClientCAs(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{ Data: map[string]string{
"requestheader-username-headers": `[]`, "requestheader-username-headers": `[]`,
"requestheader-uid-headers": `[]`,
"requestheader-group-headers": `[]`, "requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`, "requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(anotherRandomCA), "requestheader-client-ca-file": string(anotherRandomCA),
@ -169,7 +199,6 @@ func TestWriteClientCAs(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{ Data: map[string]string{
"requestheader-username-headers": `[]`, "requestheader-username-headers": `[]`,
"requestheader-uid-headers": `[]`,
"requestheader-group-headers": `[]`, "requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`, "requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(anotherRandomCA), "requestheader-client-ca-file": string(anotherRandomCA),
@ -205,7 +234,6 @@ func TestWriteClientCAs(t *testing.T) {
name: "overwrite extension-apiserver-authentication requestheader", name: "overwrite extension-apiserver-authentication requestheader",
clusterAuthInfo: ClusterAuthenticationInfo{ clusterAuthInfo: ClusterAuthenticationInfo{
RequestHeaderUsernameHeaders: headerrequest.StaticStringSlice{}, RequestHeaderUsernameHeaders: headerrequest.StaticStringSlice{},
RequestHeaderUIDHeaders: headerrequest.StaticStringSlice{},
RequestHeaderGroupHeaders: headerrequest.StaticStringSlice{}, RequestHeaderGroupHeaders: headerrequest.StaticStringSlice{},
RequestHeaderExtraHeaderPrefixes: headerrequest.StaticStringSlice{}, RequestHeaderExtraHeaderPrefixes: headerrequest.StaticStringSlice{},
RequestHeaderCA: anotherRandomCAProvider, RequestHeaderCA: anotherRandomCAProvider,
@ -216,7 +244,6 @@ func TestWriteClientCAs(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{ Data: map[string]string{
"requestheader-username-headers": `[]`, "requestheader-username-headers": `[]`,
"requestheader-uid-headers": `[]`,
"requestheader-group-headers": `[]`, "requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`, "requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(someRandomCA), "requestheader-client-ca-file": string(someRandomCA),
@ -229,7 +256,6 @@ func TestWriteClientCAs(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{ Data: map[string]string{
"requestheader-username-headers": `[]`, "requestheader-username-headers": `[]`,
"requestheader-uid-headers": `[]`,
"requestheader-group-headers": `[]`, "requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`, "requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(someRandomCA) + string(anotherRandomCA), "requestheader-client-ca-file": string(someRandomCA) + string(anotherRandomCA),
@ -260,7 +286,6 @@ func TestWriteClientCAs(t *testing.T) {
name: "skip on no change", name: "skip on no change",
clusterAuthInfo: ClusterAuthenticationInfo{ clusterAuthInfo: ClusterAuthenticationInfo{
RequestHeaderUsernameHeaders: headerrequest.StaticStringSlice{}, RequestHeaderUsernameHeaders: headerrequest.StaticStringSlice{},
RequestHeaderUIDHeaders: headerrequest.StaticStringSlice{},
RequestHeaderGroupHeaders: headerrequest.StaticStringSlice{}, RequestHeaderGroupHeaders: headerrequest.StaticStringSlice{},
RequestHeaderExtraHeaderPrefixes: headerrequest.StaticStringSlice{}, RequestHeaderExtraHeaderPrefixes: headerrequest.StaticStringSlice{},
RequestHeaderCA: anotherRandomCAProvider, RequestHeaderCA: anotherRandomCAProvider,
@ -271,7 +296,6 @@ func TestWriteClientCAs(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{ Data: map[string]string{
"requestheader-username-headers": `[]`, "requestheader-username-headers": `[]`,
"requestheader-uid-headers": `[]`,
"requestheader-group-headers": `[]`, "requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`, "requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(anotherRandomCA), "requestheader-client-ca-file": string(anotherRandomCA),
@ -282,10 +306,126 @@ func TestWriteClientCAs(t *testing.T) {
expectedConfigMaps: map[string]*corev1.ConfigMap{}, expectedConfigMaps: map[string]*corev1.ConfigMap{},
expectCreate: false, expectCreate: false,
}, },
{
name: "drop uid without feature gate",
clusterAuthInfo: ClusterAuthenticationInfo{
RequestHeaderUsernameHeaders: headerrequest.StaticStringSlice{},
RequestHeaderUIDHeaders: headerrequest.StaticStringSlice{"panda"},
RequestHeaderGroupHeaders: headerrequest.StaticStringSlice{},
RequestHeaderExtraHeaderPrefixes: headerrequest.StaticStringSlice{},
RequestHeaderCA: anotherRandomCAProvider,
RequestHeaderAllowedNames: headerrequest.StaticStringSlice{},
},
preexistingObjs: []runtime.Object{
&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{
"requestheader-username-headers": `[]`,
"requestheader-uid-headers": `["snorlax"]`,
"requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(anotherRandomCA),
"requestheader-allowed-names": `[]`,
},
},
},
expectedConfigMaps: map[string]*corev1.ConfigMap{
"extension-apiserver-authentication": {
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{
"requestheader-username-headers": `[]`,
"requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(anotherRandomCA),
"requestheader-allowed-names": `[]`,
},
},
},
expectCreate: false,
},
{
name: "add uid with feature gate",
clusterAuthInfo: ClusterAuthenticationInfo{
RequestHeaderUsernameHeaders: headerrequest.StaticStringSlice{},
RequestHeaderUIDHeaders: headerrequest.StaticStringSlice{"panda"},
RequestHeaderGroupHeaders: headerrequest.StaticStringSlice{},
RequestHeaderExtraHeaderPrefixes: headerrequest.StaticStringSlice{},
RequestHeaderCA: anotherRandomCAProvider,
RequestHeaderAllowedNames: headerrequest.StaticStringSlice{},
},
preexistingObjs: []runtime.Object{
&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{
"requestheader-username-headers": `[]`,
"requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(anotherRandomCA),
"requestheader-allowed-names": `[]`,
},
},
},
expectedConfigMaps: map[string]*corev1.ConfigMap{
"extension-apiserver-authentication": {
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{
"requestheader-username-headers": `[]`,
"requestheader-uid-headers": `["panda"]`,
"requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(anotherRandomCA),
"requestheader-allowed-names": `[]`,
},
},
},
expectCreate: false,
uidGate: true,
},
{
name: "append uid with feature gate",
clusterAuthInfo: ClusterAuthenticationInfo{
RequestHeaderUsernameHeaders: headerrequest.StaticStringSlice{},
RequestHeaderUIDHeaders: headerrequest.StaticStringSlice{"panda"},
RequestHeaderGroupHeaders: headerrequest.StaticStringSlice{},
RequestHeaderExtraHeaderPrefixes: headerrequest.StaticStringSlice{},
RequestHeaderCA: anotherRandomCAProvider,
RequestHeaderAllowedNames: headerrequest.StaticStringSlice{},
},
preexistingObjs: []runtime.Object{
&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{
"requestheader-username-headers": `[]`,
"requestheader-uid-headers": `["snorlax"]`,
"requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(anotherRandomCA),
"requestheader-allowed-names": `[]`,
},
},
},
expectedConfigMaps: map[string]*corev1.ConfigMap{
"extension-apiserver-authentication": {
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{
"requestheader-username-headers": `[]`,
"requestheader-uid-headers": `["snorlax","panda"]`,
"requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(anotherRandomCA),
"requestheader-allowed-names": `[]`,
},
},
},
expectCreate: false,
uidGate: true,
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RemoteRequestHeaderUID, test.uidGate)
client := fake.NewSimpleClientset(test.preexistingObjs...) client := fake.NewSimpleClientset(test.preexistingObjs...)
configMapIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) configMapIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
for _, obj := range test.preexistingObjs { for _, obj := range test.preexistingObjs {
@ -341,7 +481,6 @@ func TestWriteConfigMapDeleted(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"},
Data: map[string]string{ Data: map[string]string{
"requestheader-username-headers": `[]`, "requestheader-username-headers": `[]`,
"requestheader-uid-headers": `[]`,
"requestheader-group-headers": `[]`, "requestheader-group-headers": `[]`,
"requestheader-extra-headers-prefix": `[]`, "requestheader-extra-headers-prefix": `[]`,
"requestheader-client-ca-file": string(anotherRandomCA), "requestheader-client-ca-file": string(anotherRandomCA),

View File

@ -306,6 +306,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
{Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, {Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
}, },
genericfeatures.RemoteRequestHeaderUID: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
genericfeatures.ResilientWatchCacheInitialization: { genericfeatures.ResilientWatchCacheInitialization: {
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, {Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
}, },

View File

@ -149,6 +149,13 @@ const (
// to a chunking list request. // to a chunking list request.
RemainingItemCount featuregate.Feature = "RemainingItemCount" RemainingItemCount featuregate.Feature = "RemainingItemCount"
// owner: @stlaz
//
// Enable kube-apiserver to accept UIDs via request header authentication.
// This will also make the kube-apiserver's API aggregator add UIDs via standard
// headers when forwarding requests to the servers serving the aggregated API.
RemoteRequestHeaderUID featuregate.Feature = "RemoteRequestHeaderUID"
// owner: @wojtek-t // owner: @wojtek-t
// //
// Enables resilient watchcache initialization to avoid controlplane // Enables resilient watchcache initialization to avoid controlplane
@ -359,6 +366,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
{Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, {Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
}, },
RemoteRequestHeaderUID: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
ResilientWatchCacheInitialization: { ResilientWatchCacheInitialization: {
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, {Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
}, },

View File

@ -29,8 +29,10 @@ import (
"k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/authenticatorfactory" "k8s.io/apiserver/pkg/authentication/authenticatorfactory"
"k8s.io/apiserver/pkg/authentication/request/headerrequest" "k8s.io/apiserver/pkg/authentication/request/headerrequest"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/apiserver/pkg/server/dynamiccertificates"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
@ -68,9 +70,6 @@ func (s *RequestHeaderAuthenticationOptions) Validate() []error {
if err := checkForWhiteSpaceOnly("requestheader-username-headers", s.UsernameHeaders...); err != nil { if err := checkForWhiteSpaceOnly("requestheader-username-headers", s.UsernameHeaders...); err != nil {
allErrors = append(allErrors, err) allErrors = append(allErrors, err)
} }
if err := checkForWhiteSpaceOnly("requestheader-uid-headers", s.UIDHeaders...); err != nil {
allErrors = append(allErrors, err)
}
if err := checkForWhiteSpaceOnly("requestheader-group-headers", s.GroupHeaders...); err != nil { if err := checkForWhiteSpaceOnly("requestheader-group-headers", s.GroupHeaders...); err != nil {
allErrors = append(allErrors, err) allErrors = append(allErrors, err)
} }
@ -84,10 +83,6 @@ func (s *RequestHeaderAuthenticationOptions) Validate() []error {
if len(s.UsernameHeaders) > 0 && !caseInsensitiveHas(s.UsernameHeaders, "X-Remote-User") { if len(s.UsernameHeaders) > 0 && !caseInsensitiveHas(s.UsernameHeaders, "X-Remote-User") {
klog.Warningf("--requestheader-username-headers is set without specifying the standard X-Remote-User header - API aggregation will not work") klog.Warningf("--requestheader-username-headers is set without specifying the standard X-Remote-User header - API aggregation will not work")
} }
if len(s.UIDHeaders) > 0 && !caseInsensitiveHas(s.UIDHeaders, "X-Remote-Uid") {
// this was added later and so we are able to error out
allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers is set without specifying the standard X-Remote-Uid header - API aggregation will not work"))
}
if len(s.GroupHeaders) > 0 && !caseInsensitiveHas(s.GroupHeaders, "X-Remote-Group") { if len(s.GroupHeaders) > 0 && !caseInsensitiveHas(s.GroupHeaders, "X-Remote-Group") {
klog.Warningf("--requestheader-group-headers is set without specifying the standard X-Remote-Group header - API aggregation will not work") klog.Warningf("--requestheader-group-headers is set without specifying the standard X-Remote-Group header - API aggregation will not work")
} }
@ -95,6 +90,20 @@ func (s *RequestHeaderAuthenticationOptions) Validate() []error {
klog.Warningf("--requestheader-extra-headers-prefix is set without specifying the standard X-Remote-Extra- header prefix - API aggregation will not work") klog.Warningf("--requestheader-extra-headers-prefix is set without specifying the standard X-Remote-Extra- header prefix - API aggregation will not work")
} }
if !utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) {
if len(s.UIDHeaders) > 0 {
allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers requires the %q feature to be enabled", features.RemoteRequestHeaderUID))
}
} else {
if err := checkForWhiteSpaceOnly("requestheader-uid-headers", s.UIDHeaders...); err != nil {
allErrors = append(allErrors, err)
}
if len(s.UIDHeaders) > 0 && !caseInsensitiveHas(s.UIDHeaders, "X-Remote-Uid") {
// this was added later and so we are able to error out
allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers is set without specifying the standard X-Remote-Uid header - API aggregation will not work"))
}
}
return allErrors return allErrors
} }
@ -126,7 +135,7 @@ func (s *RequestHeaderAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
"List of request headers to inspect for usernames. X-Remote-User is common.") "List of request headers to inspect for usernames. X-Remote-User is common.")
fs.StringSliceVar(&s.UIDHeaders, "requestheader-uid-headers", s.UIDHeaders, ""+ fs.StringSliceVar(&s.UIDHeaders, "requestheader-uid-headers", s.UIDHeaders, ""+
"List of request headers to inspect for UIDs. X-Remote-Uid is suggested.") "List of request headers to inspect for UIDs. X-Remote-Uid is suggested. Requires the RemoteRequestHeaderUID feature to be enabled.")
fs.StringSliceVar(&s.GroupHeaders, "requestheader-group-headers", s.GroupHeaders, ""+ fs.StringSliceVar(&s.GroupHeaders, "requestheader-group-headers", s.GroupHeaders, ""+
"List of request headers to inspect for groups. X-Remote-Group is suggested.") "List of request headers to inspect for groups. X-Remote-Group is suggested.")

View File

@ -99,7 +99,7 @@ func SetFeatureGateEmulationVersionDuringTest(tb TB, gate featuregate.FeatureGat
detectParallelOverrideCleanup := detectParallelOverrideEmulationVersion(tb, ver) detectParallelOverrideCleanup := detectParallelOverrideEmulationVersion(tb, ver)
originalEmuVer := gate.(featuregate.MutableVersionedFeatureGate).EmulationVersion() originalEmuVer := gate.(featuregate.MutableVersionedFeatureGate).EmulationVersion()
if err := gate.(featuregate.MutableVersionedFeatureGate).SetEmulationVersion(ver); err != nil { if err := gate.(featuregate.MutableVersionedFeatureGate).SetEmulationVersion(ver); err != nil {
tb.Fatalf("failed to set emulation version to %s during test", ver.String()) tb.Fatalf("failed to set emulation version to %s during test: %v", ver.String(), err)
} }
tb.Cleanup(func() { tb.Cleanup(func() {
tb.Helper() tb.Helper()

View File

@ -159,7 +159,12 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
proxyRoundTripper := handlingInfo.proxyRoundTripper proxyRoundTripper := handlingInfo.proxyRoundTripper
upgrade := httpstream.IsUpgradeRequest(req) upgrade := httpstream.IsUpgradeRequest(req)
proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), user.GetUID(), user.GetGroups(), user.GetExtra(), proxyRoundTripper) var userUID string
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.RemoteRequestHeaderUID) {
userUID = user.GetUID()
}
proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), userUID, user.GetGroups(), user.GetExtra(), proxyRoundTripper)
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerTracing) && !upgrade { if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerTracing) && !upgrade {
tracingWrapper := tracing.WrapperFor(r.tracerProvider) tracingWrapper := tracing.WrapperFor(r.tracerProvider)
@ -170,7 +175,7 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// NOT use the proxyRoundTripper. It's a direct dial that bypasses the proxyRoundTripper. This means that we have to // NOT use the proxyRoundTripper. It's a direct dial that bypasses the proxyRoundTripper. This means that we have to
// attach the "correct" user headers to the request ahead of time. // attach the "correct" user headers to the request ahead of time.
if upgrade { if upgrade {
transport.SetAuthProxyHeaders(newReq, user.GetName(), user.GetUID(), user.GetGroups(), user.GetExtra()) transport.SetAuthProxyHeaders(newReq, user.GetName(), userUID, user.GetGroups(), user.GetExtra())
} }
handler := proxy.NewUpgradeAwareHandler(location, proxyRoundTripper, true, upgrade, &responder{w: w}) handler := proxy.NewUpgradeAwareHandler(location, proxyRoundTripper, true, upgrade, &responder{w: w})

View File

@ -34,31 +34,34 @@ import (
"sync/atomic" "sync/atomic"
"testing" "testing"
"k8s.io/apiserver/pkg/audit" "github.com/google/go-cmp/cmp"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
"k8s.io/client-go/transport"
"golang.org/x/net/websocket"
"go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace" sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest" "go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"golang.org/x/net/websocket"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
utilnet "k8s.io/apimachinery/pkg/util/net" utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/proxy" "k8s.io/apimachinery/pkg/util/proxy"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/filters" "k8s.io/apiserver/pkg/endpoints/filters"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
"k8s.io/apiserver/pkg/server/egressselector" "k8s.io/apiserver/pkg/server/egressselector"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol" utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol"
apiserverproxyutil "k8s.io/apiserver/pkg/util/proxy" apiserverproxyutil "k8s.io/apiserver/pkg/util/proxy"
"k8s.io/client-go/transport"
"k8s.io/component-base/featuregate"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/component-base/metrics" "k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry" "k8s.io/component-base/metrics/legacyregistry"
apiregistration "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" apiregistration "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
"k8s.io/utils/pointer"
"k8s.io/utils/ptr" "k8s.io/utils/ptr"
) )
@ -130,6 +133,8 @@ func TestProxyHandler(t *testing.T) {
expectedBody string expectedBody string
expectedCalled bool expectedCalled bool
expectedHeaders map[string][]string expectedHeaders map[string][]string
enableFeatureGates []featuregate.Feature
}{ }{
"no target": { "no target": {
expectedStatusCode: http.StatusNotFound, expectedStatusCode: http.StatusNotFound,
@ -138,7 +143,7 @@ func TestProxyHandler(t *testing.T) {
apiService: &apiregistration.APIService{ apiService: &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{ Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Port: ptr.To[int32](443)},
Group: "foo", Group: "foo",
Version: "v1", Version: "v1",
}, },
@ -161,7 +166,7 @@ func TestProxyHandler(t *testing.T) {
apiService: &apiregistration.APIService{ apiService: &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{ Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Port: ptr.To[int32](443)},
Group: "foo", Group: "foo",
Version: "v1", Version: "v1",
InsecureSkipTLSVerify: true, InsecureSkipTLSVerify: true,
@ -174,6 +179,40 @@ func TestProxyHandler(t *testing.T) {
}, },
expectedStatusCode: http.StatusOK, expectedStatusCode: http.StatusOK,
expectedCalled: true, expectedCalled: true,
expectedHeaders: map[string][]string{
"X-Forwarded-Proto": {"https"},
"X-Forwarded-Uri": {"/request/path"},
"X-Forwarded-For": {"127.0.0.1"},
"X-Remote-User": {"username"},
"User-Agent": {"Go-http-client/1.1"},
"Accept-Encoding": {"gzip"},
"X-Remote-Group": {"one", "two"},
},
},
"[RemoteRequestHeaderUID] proxy with user, insecure": {
user: &user.DefaultInfo{
Name: "username",
UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78",
Groups: []string{"one", "two"},
},
path: "/request/path",
apiService: &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Port: ptr.To[int32](443)},
Group: "foo",
Version: "v1",
InsecureSkipTLSVerify: true,
},
Status: apiregistration.APIServiceStatus{
Conditions: []apiregistration.APIServiceCondition{
{Type: apiregistration.Available, Status: apiregistration.ConditionTrue},
},
},
},
enableFeatureGates: []featuregate.Feature{features.RemoteRequestHeaderUID},
expectedStatusCode: http.StatusOK,
expectedCalled: true,
expectedHeaders: map[string][]string{ expectedHeaders: map[string][]string{
"X-Forwarded-Proto": {"https"}, "X-Forwarded-Proto": {"https"},
"X-Forwarded-Uri": {"/request/path"}, "X-Forwarded-Uri": {"/request/path"},
@ -195,7 +234,7 @@ func TestProxyHandler(t *testing.T) {
apiService: &apiregistration.APIService{ apiService: &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{ Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: ptr.To[int32](443)},
Group: "foo", Group: "foo",
Version: "v1", Version: "v1",
CABundle: testCACrt, CABundle: testCACrt,
@ -208,6 +247,40 @@ func TestProxyHandler(t *testing.T) {
}, },
expectedStatusCode: http.StatusOK, expectedStatusCode: http.StatusOK,
expectedCalled: true, expectedCalled: true,
expectedHeaders: map[string][]string{
"X-Forwarded-Proto": {"https"},
"X-Forwarded-Uri": {"/request/path"},
"X-Forwarded-For": {"127.0.0.1"},
"X-Remote-User": {"username"},
"User-Agent": {"Go-http-client/1.1"},
"Accept-Encoding": {"gzip"},
"X-Remote-Group": {"one", "two"},
},
},
"[RemoteRequestHeaderUID] proxy with user, cabundle": {
user: &user.DefaultInfo{
Name: "username",
UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78",
Groups: []string{"one", "two"},
},
path: "/request/path",
apiService: &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: ptr.To[int32](443)},
Group: "foo",
Version: "v1",
CABundle: testCACrt,
},
Status: apiregistration.APIServiceStatus{
Conditions: []apiregistration.APIServiceCondition{
{Type: apiregistration.Available, Status: apiregistration.ConditionTrue},
},
},
},
enableFeatureGates: []featuregate.Feature{features.RemoteRequestHeaderUID},
expectedStatusCode: http.StatusOK,
expectedCalled: true,
expectedHeaders: map[string][]string{ expectedHeaders: map[string][]string{
"X-Forwarded-Proto": {"https"}, "X-Forwarded-Proto": {"https"},
"X-Forwarded-Uri": {"/request/path"}, "X-Forwarded-Uri": {"/request/path"},
@ -229,7 +302,7 @@ func TestProxyHandler(t *testing.T) {
apiService: &apiregistration.APIService{ apiService: &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{ Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: ptr.To[int32](443)},
Group: "foo", Group: "foo",
Version: "v1", Version: "v1",
CABundle: testCACrt, CABundle: testCACrt,
@ -253,7 +326,7 @@ func TestProxyHandler(t *testing.T) {
apiService: &apiregistration.APIService{ apiService: &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{ Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Name: "bad-service", Namespace: "test-ns", Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Name: "bad-service", Namespace: "test-ns", Port: ptr.To[int32](443)},
Group: "foo", Group: "foo",
Version: "v1", Version: "v1",
CABundle: testCACrt, CABundle: testCACrt,
@ -276,7 +349,7 @@ func TestProxyHandler(t *testing.T) {
apiService: &apiregistration.APIService{ apiService: &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{ Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Port: ptr.To[int32](443)},
Group: "foo", Group: "foo",
Version: "v1", Version: "v1",
}, },
@ -298,7 +371,7 @@ func TestProxyHandler(t *testing.T) {
apiService: &apiregistration.APIService{ apiService: &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{ Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: ptr.To[int32](443)},
Group: "foo", Group: "foo",
Version: "v1", Version: "v1",
CABundle: testCACrt, CABundle: testCACrt,
@ -320,7 +393,11 @@ func TestProxyHandler(t *testing.T) {
target.Reset() target.Reset()
legacyregistry.Reset() legacyregistry.Reset()
func() { t.Run(name, func(t *testing.T) {
for _, f := range tc.enableFeatureGates {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, f, true)
}
targetServer := httptest.NewUnstartedServer(target) targetServer := httptest.NewUnstartedServer(target)
serviceCert := tc.serviceCertOverride serviceCert := tc.serviceCertOverride
if serviceCert == nil { if serviceCert == nil {
@ -354,37 +431,37 @@ func TestProxyHandler(t *testing.T) {
resp, err := http.Get(server.URL + tc.path) resp, err := http.Get(server.URL + tc.path)
if err != nil { if err != nil {
t.Errorf("%s: %v", name, err) t.Errorf("%v", err)
return return
} }
if e, a := tc.expectedStatusCode, resp.StatusCode; e != a { if e, a := tc.expectedStatusCode, resp.StatusCode; e != a {
body, _ := httputil.DumpResponse(resp, true) body, _ := httputil.DumpResponse(resp, true)
t.Logf("%s: %v", name, string(body)) t.Logf("%v", string(body))
t.Errorf("%s: expected %v, got %v", name, e, a) t.Errorf("expected %v, got %v", e, a)
return return
} }
bytes, err := io.ReadAll(resp.Body) bytes, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
t.Errorf("%s: %v", name, err) t.Errorf("%v", err)
return return
} }
if !strings.Contains(string(bytes), tc.expectedBody) { if !strings.Contains(string(bytes), tc.expectedBody) {
t.Errorf("%s: expected %q, got %q", name, tc.expectedBody, string(bytes)) t.Errorf("expected %q, got %q", tc.expectedBody, string(bytes))
return return
} }
if e, a := tc.expectedCalled, target.called; e != a { if e, a := tc.expectedCalled, target.called; e != a {
t.Errorf("%s: expected %v, got %v", name, e, a) t.Errorf("expected %v, got %v", e, a)
return return
} }
// this varies every test // this varies every test
delete(target.headers, "X-Forwarded-Host") delete(target.headers, "X-Forwarded-Host")
if e, a := tc.expectedHeaders, target.headers; !reflect.DeepEqual(e, a) { if e, a := tc.expectedHeaders, target.headers; !reflect.DeepEqual(e, a) {
t.Errorf("%s: expected %v, got %v", name, e, a) t.Errorf("expected != got %v", cmp.Diff(e, a))
return return
} }
if e, a := targetServer.Listener.Addr().String(), target.host; tc.expectedCalled && !reflect.DeepEqual(e, a) { if e, a := targetServer.Listener.Addr().String(), target.host; tc.expectedCalled && !reflect.DeepEqual(e, a) {
t.Errorf("%s: expected %v, got %v", name, e, a) t.Errorf("expected %v, got %v", e, a)
return return
} }
@ -397,7 +474,7 @@ func TestProxyHandler(t *testing.T) {
t.Errorf("expected the x509_missing_san_total to be 1, but it's %d", errorCounter) t.Errorf("expected the x509_missing_san_total to be 1, but it's %d", errorCounter)
} }
} }
}() })
} }
} }
@ -432,6 +509,8 @@ func newBrokenDialerAndSelector() (*mockEgressDialer, *egressselector.EgressSele
} }
func TestProxyUpgrade(t *testing.T) { func TestProxyUpgrade(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RemoteRequestHeaderUID, true)
upgradeUser := "upgradeUser" upgradeUser := "upgradeUser"
upgradeUID := "upgradeUser-UID" upgradeUID := "upgradeUser-UID"
testcases := map[string]struct { testcases := map[string]struct {
@ -446,7 +525,7 @@ func TestProxyUpgrade(t *testing.T) {
CABundle: testCACrt, CABundle: testCACrt,
Group: "mygroup", Group: "mygroup",
Version: "v1", Version: "v1",
Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: ptr.To[int32](443)},
}, },
Status: apiregistration.APIServiceStatus{ Status: apiregistration.APIServiceStatus{
Conditions: []apiregistration.APIServiceCondition{ Conditions: []apiregistration.APIServiceCondition{
@ -463,7 +542,7 @@ func TestProxyUpgrade(t *testing.T) {
InsecureSkipTLSVerify: true, InsecureSkipTLSVerify: true,
Group: "mygroup", Group: "mygroup",
Version: "v1", Version: "v1",
Service: &apiregistration.ServiceReference{Name: "invalid-service", Namespace: "invalid-ns", Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Name: "invalid-service", Namespace: "invalid-ns", Port: ptr.To[int32](443)},
}, },
Status: apiregistration.APIServiceStatus{ Status: apiregistration.APIServiceStatus{
Conditions: []apiregistration.APIServiceCondition{ Conditions: []apiregistration.APIServiceCondition{
@ -480,7 +559,7 @@ func TestProxyUpgrade(t *testing.T) {
CABundle: testCACrt, CABundle: testCACrt,
Group: "mygroup", Group: "mygroup",
Version: "v1", Version: "v1",
Service: &apiregistration.ServiceReference{Name: "invalid-service", Namespace: "invalid-ns", Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Name: "invalid-service", Namespace: "invalid-ns", Port: ptr.To[int32](443)},
}, },
Status: apiregistration.APIServiceStatus{ Status: apiregistration.APIServiceStatus{
Conditions: []apiregistration.APIServiceCondition{ Conditions: []apiregistration.APIServiceCondition{
@ -497,7 +576,7 @@ func TestProxyUpgrade(t *testing.T) {
CABundle: testCACrt, CABundle: testCACrt,
Group: "mygroup", Group: "mygroup",
Version: "v1", Version: "v1",
Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: ptr.To[int32](443)},
}, },
Status: apiregistration.APIServiceStatus{ Status: apiregistration.APIServiceStatus{
Conditions: []apiregistration.APIServiceCondition{ Conditions: []apiregistration.APIServiceCondition{
@ -515,7 +594,7 @@ func TestProxyUpgrade(t *testing.T) {
CABundle: testCACrt, CABundle: testCACrt,
Group: "mygroup", Group: "mygroup",
Version: "v1", Version: "v1",
Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: ptr.To[int32](443)},
}, },
Status: apiregistration.APIServiceStatus{ Status: apiregistration.APIServiceStatus{
Conditions: []apiregistration.APIServiceCondition{ Conditions: []apiregistration.APIServiceCondition{
@ -996,7 +1075,7 @@ func TestProxyCertReload(t *testing.T) {
apiService := &apiregistration.APIService{ apiService := &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{ Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Name: "test-service2", Namespace: "test-ns", Port: pointer.Int32Ptr(443)}, Service: &apiregistration.ServiceReference{Name: "test-service2", Namespace: "test-ns", Port: ptr.To[int32](443)},
Group: "foo", Group: "foo",
Version: "v1", Version: "v1",
CABundle: backendCaCertificate(), // used to validate backendCertificate() CABundle: backendCaCertificate(), // used to validate backendCertificate()

View File

@ -27,7 +27,7 @@ import (
func main() { func main() {
ctx := genericapiserver.SetupSignalContext() ctx := genericapiserver.SetupSignalContext()
options := server.NewWardleServerOptions(os.Stdout, os.Stderr) options := server.NewWardleServerOptions(os.Stdout, os.Stderr)
cmd := server.NewCommandStartWardleServer(ctx, options) cmd := server.NewCommandStartWardleServer(ctx, options, false)
code := cli.Run(cmd) code := cli.Run(cmd)
os.Exit(code) os.Exit(code)
} }

View File

@ -90,12 +90,15 @@ func NewWardleServerOptions(out, errOut io.Writer) *WardleServerOptions {
// NewCommandStartWardleServer provides a CLI handler for 'start master' command // NewCommandStartWardleServer provides a CLI handler for 'start master' command
// with a default WardleServerOptions. // with a default WardleServerOptions.
func NewCommandStartWardleServer(ctx context.Context, defaults *WardleServerOptions) *cobra.Command { func NewCommandStartWardleServer(ctx context.Context, defaults *WardleServerOptions, skipDefaultComponentGlobalsRegistrySet bool) *cobra.Command {
o := *defaults o := *defaults
cmd := &cobra.Command{ cmd := &cobra.Command{
Short: "Launch a wardle API server", Short: "Launch a wardle API server",
Long: "Launch a wardle API server", Long: "Launch a wardle API server",
PersistentPreRunE: func(*cobra.Command, []string) error { PersistentPreRunE: func(*cobra.Command, []string) error {
if skipDefaultComponentGlobalsRegistrySet {
return nil
}
return featuregate.DefaultComponentGlobalsRegistry.Set() return featuregate.DefaultComponentGlobalsRegistry.Set()
}, },
RunE: func(c *cobra.Command, args []string) error { RunE: func(c *cobra.Command, args []string) error {

View File

@ -1058,6 +1058,12 @@
lockToDefault: true lockToDefault: true
preRelease: GA preRelease: GA
version: "1.29" version: "1.29"
- name: RemoteRequestHeaderUID
versionedSpecs:
- default: false
lockToDefault: false
preRelease: Alpha
version: "1.32"
- name: ResilientWatchCacheInitialization - name: ResilientWatchCacheInitialization
versionedSpecs: versionedSpecs:
- default: true - default: true

View File

@ -27,11 +27,14 @@ import (
authnv1 "k8s.io/api/authentication/v1" authnv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
certutil "k8s.io/client-go/util/cert" certutil "k8s.io/client-go/util/cert"
"k8s.io/client-go/util/keyutil" "k8s.io/client-go/util/keyutil"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options" "k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/test/integration/framework" "k8s.io/kubernetes/test/integration/framework"
testutils "k8s.io/kubernetes/test/utils" testutils "k8s.io/kubernetes/test/utils"
@ -39,6 +42,8 @@ import (
) )
func TestAuthnToKAS(t *testing.T) { func TestAuthnToKAS(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RemoteRequestHeaderUID, true)
tCtx := ktesting.Init(t) tCtx := ktesting.Init(t)
frontProxyCA, frontProxyClient, frontProxyKey, err := newTestCAWithClient( frontProxyCA, frontProxyClient, frontProxyKey, err := newTestCAWithClient(

View File

@ -45,8 +45,10 @@ import (
"k8s.io/apiserver/pkg/authentication/serviceaccount" "k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/apiserver/pkg/server/dynamiccertificates"
genericapiserveroptions "k8s.io/apiserver/pkg/server/options" genericapiserveroptions "k8s.io/apiserver/pkg/server/options"
utilfeature "k8s.io/apiserver/pkg/util/feature"
client "k8s.io/client-go/kubernetes" client "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
@ -54,6 +56,7 @@ import (
"k8s.io/client-go/transport" "k8s.io/client-go/transport"
"k8s.io/client-go/util/cert" "k8s.io/client-go/util/cert"
"k8s.io/component-base/featuregate" "k8s.io/component-base/featuregate"
featuregatetesting "k8s.io/component-base/featuregate/testing"
utilversion "k8s.io/component-base/version" utilversion "k8s.io/component-base/version"
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
@ -260,6 +263,7 @@ func TestFrontProxyConfig(t *testing.T) {
testFrontProxyConfig(t, false) testFrontProxyConfig(t, false)
}) })
t.Run("WithUID", func(t *testing.T) { t.Run("WithUID", func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RemoteRequestHeaderUID, true)
testFrontProxyConfig(t, true) testFrontProxyConfig(t, true)
}) })
} }
@ -282,7 +286,7 @@ func testFrontProxyConfig(t *testing.T, withUID bool) {
kubeBinaryVersion := sampleserver.WardleVersionToKubeVersion(version.MustParse(wardleBinaryVersion)).String() kubeBinaryVersion := sampleserver.WardleVersionToKubeVersion(version.MustParse(wardleBinaryVersion)).String()
// start up the KAS and prepare the options for the wardle API server // start up the KAS and prepare the options for the wardle API server
testKAS, wardleOptions, wardlePort := prepareAggregatedWardleAPIServer(ctx, t, testNamespace, kubeBinaryVersion, wardleBinaryVersion, extraKASFlags) testKAS, wardleOptions, wardlePort := prepareAggregatedWardleAPIServer(ctx, t, testNamespace, kubeBinaryVersion, wardleBinaryVersion, extraKASFlags, withUID)
kubeConfig := getKubeConfig(testKAS) kubeConfig := getKubeConfig(testKAS)
// create the SA that we will use to query the aggregated API // create the SA that we will use to query the aggregated API
@ -337,12 +341,12 @@ func testFrontProxyConfig(t *testing.T, withUID bool) {
transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper { transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper {
return roundTripperFunc(func(req *http.Request) (*http.Response, error) { return roundTripperFunc(func(req *http.Request) (*http.Response, error) {
gotUser, ok := genericapirequest.UserFrom(req.Context()) gotUser, ok := genericapirequest.UserFrom(req.Context())
if !ok || gotUser.GetName() == "system:anonymous" { if !ok {
return nil, fmt.Errorf("got an unauthenticated request") return nil, fmt.Errorf("got an unauthenticated request")
} }
// this is likely the KAS checking the OpenAPI endpoints // this is likely the KAS checking the OpenAPI endpoints
if gotUser.GetName() == "system:anonymous" || gotUser.GetName() == "system:aggregator" { if gotUser.GetName() == "system:anonymous" || gotUser.GetName() == "system:aggregator" || gotUser.GetName() == "system:kube-aggregator" {
return rt.RoundTrip(req) return rt.RoundTrip(req)
} }
@ -368,7 +372,7 @@ func testFrontProxyConfig(t *testing.T, withUID bool) {
wardleCertDir, _ := os.MkdirTemp("", "test-integration-wardle-server") wardleCertDir, _ := os.MkdirTemp("", "test-integration-wardle-server")
defer os.RemoveAll(wardleCertDir) defer os.RemoveAll(wardleCertDir)
runPreparedWardleServer(ctx, t, wardleOptions, wardleCertDir, wardlePort, false, true, wardleBinaryVersion, kubeConfig) runPreparedWardleServer(ctx, t, wardleOptions, wardleCertDir, wardlePort, false, true, wardleBinaryVersion, kubeConfig, withUID)
waitForWardleAPIServiceReady(ctx, t, kubeConfig, wardleCertDir, testNamespace) waitForWardleAPIServiceReady(ctx, t, kubeConfig, wardleCertDir, testNamespace)
// get the wardle API client using our SA token // get the wardle API client using our SA token
@ -401,13 +405,13 @@ func testAggregatedAPIServer(t *testing.T, setWardleFeatureGate, banFlunder bool
// each wardle binary is bundled with a specific kube binary. // each wardle binary is bundled with a specific kube binary.
kubeBinaryVersion := sampleserver.WardleVersionToKubeVersion(version.MustParse(wardleBinaryVersion)).String() kubeBinaryVersion := sampleserver.WardleVersionToKubeVersion(version.MustParse(wardleBinaryVersion)).String()
testKAS, wardleOptions, wardlePort := prepareAggregatedWardleAPIServer(ctx, t, testNamespace, kubeBinaryVersion, wardleBinaryVersion, nil) testKAS, wardleOptions, wardlePort := prepareAggregatedWardleAPIServer(ctx, t, testNamespace, kubeBinaryVersion, wardleBinaryVersion, nil, false)
kubeClientConfig := getKubeConfig(testKAS) kubeClientConfig := getKubeConfig(testKAS)
wardleCertDir, _ := os.MkdirTemp("", "test-integration-wardle-server") wardleCertDir, _ := os.MkdirTemp("", "test-integration-wardle-server")
defer os.RemoveAll(wardleCertDir) defer os.RemoveAll(wardleCertDir)
directWardleClientConfig := runPreparedWardleServer(ctx, t, wardleOptions, wardleCertDir, wardlePort, setWardleFeatureGate, banFlunder, wardleEmulationVersion, kubeClientConfig) directWardleClientConfig := runPreparedWardleServer(ctx, t, wardleOptions, wardleCertDir, wardlePort, setWardleFeatureGate, banFlunder, wardleEmulationVersion, kubeClientConfig, false)
// now we're finally ready to test. These are what's run by default now // now we're finally ready to test. These are what's run by default now
wardleDirectClient := client.NewForConfigOrDie(directWardleClientConfig) wardleDirectClient := client.NewForConfigOrDie(directWardleClientConfig)
@ -681,7 +685,7 @@ func TestAggregatedAPIServerRejectRedirectResponse(t *testing.T) {
} }
} }
func prepareAggregatedWardleAPIServer(ctx context.Context, t *testing.T, namespace, kubebinaryVersion, wardleBinaryVersion string, kubeAPIServerFlags []string) (*kastesting.TestServer, *sampleserver.WardleServerOptions, int) { func prepareAggregatedWardleAPIServer(ctx context.Context, t *testing.T, namespace, kubebinaryVersion, wardleBinaryVersion string, kubeAPIServerFlags []string, withUID bool) (*kastesting.TestServer, *sampleserver.WardleServerOptions, int) {
// makes the kube-apiserver very responsive. it's normally a minute // makes the kube-apiserver very responsive. it's normally a minute
dynamiccertificates.FileRefreshDuration = 1 * time.Second dynamiccertificates.FileRefreshDuration = 1 * time.Second
@ -693,6 +697,11 @@ func prepareAggregatedWardleAPIServer(ctx context.Context, t *testing.T, namespa
// endpoints cannot have loopback IPs so we need to override the resolver itself // endpoints cannot have loopback IPs so we need to override the resolver itself
t.Cleanup(app.SetServiceResolverForTests(staticURLServiceResolver(fmt.Sprintf("https://127.0.0.1:%d", wardlePort)))) t.Cleanup(app.SetServiceResolverForTests(staticURLServiceResolver(fmt.Sprintf("https://127.0.0.1:%d", wardlePort))))
// TODO figure out how to actually make BinaryVersion/EmulationVersion work with Wardle and KAS at the same time when Alpha FG are being set
if withUID {
kubebinaryVersion = ""
}
testServer := kastesting.StartTestServerOrDie(t, testServer := kastesting.StartTestServerOrDie(t,
&kastesting.TestServerInstanceOptions{ &kastesting.TestServerInstanceOptions{
EnableCertAuth: true, EnableCertAuth: true,
@ -751,6 +760,7 @@ func runPreparedWardleServer(
banFlunder bool, banFlunder bool,
emulationVersion string, emulationVersion string,
kubeConfig *rest.Config, kubeConfig *rest.Config,
withUID bool,
) *rest.Config { ) *rest.Config {
// start the wardle server to prove we can aggregate it // start the wardle server to prove we can aggregate it
@ -769,7 +779,8 @@ func runPreparedWardleServer(
if flunderBanningFeatureGate { if flunderBanningFeatureGate {
args = append(args, "--feature-gates", fmt.Sprintf("wardle:BanFlunder=%v", banFlunder)) args = append(args, "--feature-gates", fmt.Sprintf("wardle:BanFlunder=%v", banFlunder))
} }
wardleCmd := sampleserver.NewCommandStartWardleServer(ctx, wardleOptions) // TODO figure out how to actually make BinaryVersion/EmulationVersion work with Wardle and KAS at the same time when Alpha FG are being set
wardleCmd := sampleserver.NewCommandStartWardleServer(ctx, wardleOptions, withUID)
wardleCmd.SetArgs(args) wardleCmd.SetArgs(args)
if err := wardleCmd.Execute(); err != nil { if err := wardleCmd.Execute(); err != nil {
t.Error(err) t.Error(err)