Compare commits

..

11 Commits

Author SHA1 Message Date
Kubernetes Publisher
7c4f8c88f6 Update dependencies to v0.27.2 tag 2023-05-18 01:57:04 +00:00
Kubernetes Publisher
015caa2eed Merge pull request #117708 from Jefftree/automated-cherry-pick-of-#117705-upstream-release-1.27
Automated cherry pick of #117705: Update kube-openapi to fix race

Kubernetes-commit: 7d93cc600f4c6dbd5c0b9e37ae7f351cfc59fde7
2023-05-02 17:54:10 +00:00
Jefftree
fb5347d849 Update kube-openapi to fix race
Kubernetes-commit: 86904a7c580d369edc60c5807ef1377d1ff647d2
2023-05-01 17:19:03 +00:00
Kubernetes Publisher
353c489f83 Merge pull request #117685 from ardaguclu/automated-cherry-pick-of-#117495-upstream-release-1.27
Automated cherry pick of #117495: Use absolute path instead requestURI in openapiv3 discovery

Kubernetes-commit: 110415e329c4aabdff9fb4eed7a5d6aa064e7a04
2023-04-29 10:12:16 -07:00
Arda Güçlü
6ce3e7e915 Use absolute path instead requestURI in openapiv3 discovery
Currently, openapiv3 discovery uses requestURI to discover resources.
However, that does not work when the rest endpoint contains prefixes
(e.g. `http://localhost/test-endpoint/`).
Because requestURI overwrites prefixes also in rest endpoint
(e.g. `http://localhost/openapiv3/apis/apps/v1`).

Since `absPath` keeps the prefixes in the rest endpoint,
this PR changes to absPath instead requestURI.

Kubernetes-commit: ee1d7eb5d82f3b2a76afc57bc33bc7e08c34bf27
2023-04-20 09:53:28 +03:00
Kubernetes Publisher
aab9b0a45a Merge pull request #117637 from seans3/automated-cherry-pick-of-#117571-origin-release-1.27
Automated cherry pick of #117571: Refactors discovery content-type and helper functions

Kubernetes-commit: c766f936b7ada04e76c38671c30ceec6df1b5239
2023-04-27 10:30:48 +00:00
Sean Sullivan
a16d525506 Refactors discovery content-type and helper functions
Kubernetes-commit: 3ce0c108fe9587be2e5195ad872578877970a7a9
2023-04-24 17:40:07 -07:00
Kubernetes Publisher
559da627e8 Merge remote-tracking branch 'origin/master' into release-1.27
Kubernetes-commit: 0d6e44998fe1c0a21d0aba1327f55d60ae8a7c2d
2023-03-27 18:31:52 +00:00
Madhav Jivrajani
012beed4e0 .*: update vendor dir and cleanup
Signed-off-by: Madhav Jivrajani <madhav.jiv@gmail.com>

Kubernetes-commit: 63b5ca69f1f481b2b4b2ee967f5b8b7b58937211
2023-03-27 19:18:05 +05:30
Kubernetes Publisher
1517ffb8d3 Merge pull request #116623 from lavalamp/xfrmr
change where transformers are called

Kubernetes-commit: 76d351065e504e23b7962158ef0e9067b6bd96ed
2023-03-17 00:11:43 +00:00
Daniel Smith
c3b84f0438 Change where transformers are called.
Kubernetes-commit: e76dff38cf74c3c8ad9ed4d3bc6e3641d9b64565
2023-03-14 23:05:20 +00:00
13 changed files with 399 additions and 81 deletions

View File

@@ -20,6 +20,7 @@ import (
"context"
"encoding/json"
"fmt"
"mime"
"net/http"
"net/url"
"sort"
@@ -58,8 +59,9 @@ const (
defaultBurst = 300
AcceptV1 = runtime.ContentTypeJSON
// Aggregated discovery content-type (currently v2beta1). NOTE: Currently, we are assuming the order
// for "g", "v", and "as" from the server. We can only compare this string if we can make that assumption.
// 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"
// Prioritize aggregated discovery by placing first in the order of discovery accept types.
acceptDiscoveryFormats = AcceptV2Beta1 + "," + AcceptV1
@@ -259,8 +261,16 @@ func (d *DiscoveryClient) downloadLegacy() (
var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList
// Switch on content-type server responded with: aggregated or unaggregated.
switch responseContentType {
case AcceptV1:
switch {
case isV2Beta1ContentType(responseContentType):
var aggregatedDiscovery apidiscovery.APIGroupDiscoveryList
err = json.Unmarshal(body, &aggregatedDiscovery)
if err != nil {
return nil, nil, nil, err
}
apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResources(aggregatedDiscovery)
default:
// Default is unaggregated discovery v1.
var v metav1.APIVersions
err = json.Unmarshal(body, &v)
if err != nil {
@@ -271,15 +281,6 @@ func (d *DiscoveryClient) downloadLegacy() (
apiGroup = apiVersionsToAPIGroup(&v)
}
apiGroupList.Groups = []metav1.APIGroup{apiGroup}
case AcceptV2Beta1:
var aggregatedDiscovery apidiscovery.APIGroupDiscoveryList
err = json.Unmarshal(body, &aggregatedDiscovery)
if err != nil {
return nil, nil, nil, err
}
apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResources(aggregatedDiscovery)
default:
return nil, nil, nil, fmt.Errorf("Unknown discovery response content-type: %s", responseContentType)
}
return apiGroupList, resourcesByGV, failedGVs, nil
@@ -313,13 +314,8 @@ func (d *DiscoveryClient) downloadAPIs() (
failedGVs := map[schema.GroupVersion]error{}
var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList
// Switch on content-type server responded with: aggregated or unaggregated.
switch responseContentType {
case AcceptV1:
err = json.Unmarshal(body, apiGroupList)
if err != nil {
return nil, nil, nil, err
}
case AcceptV2Beta1:
switch {
case isV2Beta1ContentType(responseContentType):
var aggregatedDiscovery apidiscovery.APIGroupDiscoveryList
err = json.Unmarshal(body, &aggregatedDiscovery)
if err != nil {
@@ -327,12 +323,38 @@ func (d *DiscoveryClient) downloadAPIs() (
}
apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResources(aggregatedDiscovery)
default:
return nil, nil, nil, fmt.Errorf("Unknown discovery response content-type: %s", responseContentType)
// Default is unaggregated discovery v1.
err = json.Unmarshal(body, apiGroupList)
if err != nil {
return nil, nil, nil, err
}
}
return apiGroupList, resourcesByGV, failedGVs, nil
}
// isV2Beta1ContentType checks of the content-type string is both
// "application/json" and contains the v2beta1 content-type params.
// NOTE: This function is resilient to the ordering of the
// content-type parameters, as well as parameters added by
// intermediaries such as proxies or gateways. Examples:
//
// "application/json; g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList" = true
// "application/json; as=APIGroupDiscoveryList;v=v2beta1;g=apidiscovery.k8s.io" = true
// "application/json; as=APIGroupDiscoveryList;v=v2beta1;g=apidiscovery.k8s.io;charset=utf-8" = true
// "application/json" = false
// "application/json; charset=UTF-8" = false
func isV2Beta1ContentType(contentType string) bool {
base, params, err := mime.ParseMediaType(contentType)
if err != nil {
return false
}
return runtime.ContentTypeJSON == base &&
params["g"] == "apidiscovery.k8s.io" &&
params["v"] == "v2beta1" &&
params["as"] == "APIGroupDiscoveryList"
}
// ServerGroups returns the supported groups, with information like supported versions and the
// preferred version.
func (d *DiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {

View File

@@ -1395,8 +1395,9 @@ func TestAggregatedServerGroups(t *testing.T) {
}
output, err := json.Marshal(agg)
require.NoError(t, err)
// Content-type is "aggregated" discovery format.
w.Header().Set("Content-Type", AcceptV2Beta1)
// Content-Type is "aggregated" discovery format. Add extra parameter
// to ensure we are resilient to these extra parameters.
w.Header().Set("Content-Type", AcceptV2Beta1+"; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(output)
}))
@@ -1985,8 +1986,9 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) {
}
output, err := json.Marshal(agg)
require.NoError(t, err)
// Content-type is "aggregated" discovery format.
w.Header().Set("Content-Type", AcceptV2Beta1)
// Content-type is "aggregated" discovery format. Add extra parameter
// to ensure we are resilient to these extra parameters.
w.Header().Set("Content-Type", AcceptV2Beta1+"; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(output)
}))
@@ -2125,8 +2127,9 @@ func TestAggregatedServerGroupsAndResourcesWithErrors(t *testing.T) {
}
output, err := json.Marshal(agg)
require.NoError(t, err)
// Content-type is "aggregated" discovery format.
w.Header().Set("Content-Type", AcceptV2Beta1)
// Content-type is "aggregated" discovery format. Add extra parameter
// to ensure we are resilient to these extra parameters.
w.Header().Set("Content-Type", AcceptV2Beta1+"; charset=utf-8")
w.WriteHeader(status)
w.Write(output)
}))
@@ -2733,8 +2736,9 @@ func TestAggregatedServerPreferredResources(t *testing.T) {
}
output, err := json.Marshal(agg)
require.NoError(t, err)
// Content-type is "aggregated" discovery format.
w.Header().Set("Content-Type", AcceptV2Beta1)
// Content-type is "aggregated" discovery format. Add extra parameter
// to ensure we are resilient to these extra parameters.
w.Header().Set("Content-Type", AcceptV2Beta1+"; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(output)
}))
@@ -2758,6 +2762,58 @@ func TestAggregatedServerPreferredResources(t *testing.T) {
}
}
func TestDiscoveryContentTypeVersion(t *testing.T) {
tests := []struct {
contentType string
isV2Beta1 bool
}{
{
contentType: "application/json; g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList",
isV2Beta1: true,
},
{
// content-type parameters are not in correct order, but comparison ignores order.
contentType: "application/json; v=v2beta1;as=APIGroupDiscoveryList;g=apidiscovery.k8s.io",
isV2Beta1: true,
},
{
// content-type parameters are not in correct order, but comparison ignores order.
contentType: "application/json; as=APIGroupDiscoveryList;g=apidiscovery.k8s.io;v=v2beta1",
isV2Beta1: true,
},
{
// Ignores extra parameter "charset=utf-8"
contentType: "application/json; g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList;charset=utf-8",
isV2Beta1: true,
},
{
contentType: "application/json",
isV2Beta1: false,
},
{
contentType: "application/json; charset=UTF-8",
isV2Beta1: false,
},
{
contentType: "text/json",
isV2Beta1: false,
},
{
contentType: "text/html",
isV2Beta1: false,
},
{
contentType: "",
isV2Beta1: false,
},
}
for _, test := range tests {
isV2Beta1 := isV2Beta1ContentType(test.contentType)
assert.Equal(t, test.isV2Beta1, isV2Beta1)
}
}
func TestUseLegacyDiscovery(t *testing.T) {
// Default client sends aggregated discovery accept format (first) as well as legacy format.
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

10
go.mod
View File

@@ -24,10 +24,10 @@ require (
golang.org/x/term v0.6.0
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
google.golang.org/protobuf v1.28.1
k8s.io/api v0.27.0-beta.0
k8s.io/apimachinery v0.27.0-beta.0
k8s.io/api v0.27.2
k8s.io/apimachinery v0.27.2
k8s.io/klog/v2 v2.90.1
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f
k8s.io/utils v0.0.0-20230209194617-a36077c30491
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd
sigs.k8s.io/structured-merge-diff/v4 v4.2.3
@@ -59,6 +59,6 @@ require (
)
replace (
k8s.io/api => k8s.io/api v0.27.0-beta.0
k8s.io/apimachinery => k8s.io/apimachinery v0.27.0-beta.0
k8s.io/api => k8s.io/api v0.27.2
k8s.io/apimachinery => k8s.io/apimachinery v0.27.2
)

14
go.sum
View File

@@ -177,7 +177,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
@@ -477,14 +477,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.27.0-beta.0 h1:9F3EuGgqS7SN+DdrfTl1imHRH6CqA1e52/Yr0IYJ9Wc=
k8s.io/api v0.27.0-beta.0/go.mod h1:46qWzqeYHNdjX0W19dogSvEKASLYH0MVO6DMdR++gCI=
k8s.io/apimachinery v0.27.0-beta.0 h1:oaNAx4OA7nrM/s6RGUx81PleoVXwwmhhPligkvP8KmE=
k8s.io/apimachinery v0.27.0-beta.0/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM=
k8s.io/api v0.27.2 h1:+H17AJpUMvl+clT+BPnKf0E3ksMAzoBBg7CntpSuADo=
k8s.io/api v0.27.2/go.mod h1:ENmbocXfBT2ADujUXcBhHV55RIT31IIEvkntP6vZKS4=
k8s.io/apimachinery v0.27.2 h1:vBjGaKKieaIreI+oQwELalVG4d8f3YAMNpWLzDXkxeg=
k8s.io/apimachinery v0.27.2/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg=
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY=
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=
k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

View File

@@ -19,6 +19,7 @@ package openapi
import (
"context"
"encoding/json"
"strings"
"k8s.io/client-go/rest"
"k8s.io/kube-openapi/pkg/handler3"
@@ -58,7 +59,11 @@ func (c *client) Paths() (map[string]GroupVersion, error) {
// Create GroupVersions for each element of the result
result := map[string]GroupVersion{}
for k, v := range discoMap.Paths {
result[k] = newGroupVersion(c, v)
// If the server returned a URL rooted at /openapi/v3, preserve any additional client-side prefix.
// If the server returned a URL not rooted at /openapi/v3, treat it as an actual server-relative URL.
// See https://github.com/kubernetes/kubernetes/issues/117463 for details
useClientPrefix := strings.HasPrefix(v.ServerRelativeURL, "/openapi/v3")
result[k] = newGroupVersion(c, v, useClientPrefix)
}
return result, nil
}

View File

@@ -18,6 +18,7 @@ package openapi
import (
"context"
"net/url"
"k8s.io/kube-openapi/pkg/handler3"
)
@@ -29,18 +30,41 @@ type GroupVersion interface {
}
type groupversion struct {
client *client
item handler3.OpenAPIV3DiscoveryGroupVersion
client *client
item handler3.OpenAPIV3DiscoveryGroupVersion
useClientPrefix bool
}
func newGroupVersion(client *client, item handler3.OpenAPIV3DiscoveryGroupVersion) *groupversion {
return &groupversion{client: client, item: item}
func newGroupVersion(client *client, item handler3.OpenAPIV3DiscoveryGroupVersion, useClientPrefix bool) *groupversion {
return &groupversion{client: client, item: item, useClientPrefix: useClientPrefix}
}
func (g *groupversion) Schema(contentType string) ([]byte, error) {
return g.client.restClient.Get().
RequestURI(g.item.ServerRelativeURL).
SetHeader("Accept", contentType).
Do(context.TODO()).
Raw()
if !g.useClientPrefix {
return g.client.restClient.Get().
RequestURI(g.item.ServerRelativeURL).
SetHeader("Accept", contentType).
Do(context.TODO()).
Raw()
}
locator, err := url.Parse(g.item.ServerRelativeURL)
if err != nil {
return nil, err
}
path := g.client.restClient.Get().
AbsPath(locator.Path).
SetHeader("Accept", contentType)
// Other than root endpoints(openapiv3/apis), resources have hash query parameter to support etags.
// However, absPath does not support handling query parameters internally,
// so that hash query parameter is added manually
for k, value := range locator.Query() {
for _, v := range value {
path.Param(k, v)
}
}
return path.Do(context.TODO()).Raw()
}

View File

@@ -0,0 +1,106 @@
/*
Copyright 2023 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 openapi
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
)
func TestGroupVersion(t *testing.T) {
tests := []struct {
name string
prefix string
serverReturnsPrefix bool
}{
{
name: "no prefix",
prefix: "",
serverReturnsPrefix: false,
},
{
name: "prefix not in discovery",
prefix: "/test-endpoint",
serverReturnsPrefix: false,
},
{
name: "prefix in discovery",
prefix: "/test-endpoint",
serverReturnsPrefix: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == test.prefix+"/openapi/v3/apis/apps/v1" && r.URL.RawQuery == "hash=014fbff9a07c":
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"openapi":"3.0.0","info":{"title":"Kubernetes","version":"unversioned"}}`))
case r.URL.Path == test.prefix+"/openapi/v3":
// return root content
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if test.serverReturnsPrefix {
w.Write([]byte(fmt.Sprintf(`{"paths":{"apis/apps/v1":{"serverRelativeURL":"%s/openapi/v3/apis/apps/v1?hash=014fbff9a07c"}}}`, test.prefix)))
} else {
w.Write([]byte(`{"paths":{"apis/apps/v1":{"serverRelativeURL":"/openapi/v3/apis/apps/v1?hash=014fbff9a07c"}}}`))
}
default:
t.Errorf("unexpected request: %s", r.URL.String())
w.WriteHeader(http.StatusNotFound)
return
}
}))
defer server.Close()
c, err := rest.RESTClientFor(&rest.Config{
Host: server.URL + test.prefix,
ContentConfig: rest.ContentConfig{
NegotiatedSerializer: scheme.Codecs,
GroupVersion: &appsv1.SchemeGroupVersion,
},
})
if err != nil {
t.Fatalf("unexpected error occurred: %v", err)
}
openapiClient := NewClient(c)
paths, err := openapiClient.Paths()
if err != nil {
t.Fatalf("unexpected error occurred: %v", err)
}
schema, err := paths["apis/apps/v1"].Schema(runtime.ContentTypeJSON)
if err != nil {
t.Fatalf("unexpected error occurred: %v", err)
}
expectedResult := `{"openapi":"3.0.0","info":{"title":"Kubernetes","version":"unversioned"}}`
if string(schema) != expectedResult {
t.Fatalf("unexpected result actual: %s expected: %s", string(schema), expectedResult)
}
})
}
}

View File

@@ -394,17 +394,6 @@ func NewIndexerInformer(
return clientState, newInformer(lw, objType, resyncPeriod, h, clientState, nil)
}
// TransformFunc allows for transforming an object before it will be processed
// and put into the controller cache and before the corresponding handlers will
// be called on it.
// TransformFunc (similarly to ResourceEventHandler functions) should be able
// to correctly handle the tombstone of type cache.DeletedFinalStateUnknown
//
// The most common usage pattern is to clean-up some parts of the object to
// reduce component memory usage if a given component doesn't care about them.
// given controller doesn't care for them
type TransformFunc func(interface{}) (interface{}, error)
// NewTransformingInformer returns a Store and a controller for populating
// the store while also providing event notifications. You should only used
// the returned Store for Get/List operations; Add/Modify/Deletes will cause
@@ -452,20 +441,12 @@ func processDeltas(
// Object which receives event notifications from the given deltas
handler ResourceEventHandler,
clientState Store,
transformer TransformFunc,
deltas Deltas,
isInInitialList bool,
) error {
// from oldest to newest
for _, d := range deltas {
obj := d.Object
if transformer != nil {
var err error
obj, err = transformer(obj)
if err != nil {
return err
}
}
switch d.Type {
case Sync, Replaced, Added, Updated:
@@ -517,6 +498,7 @@ func newInformer(
fifo := NewDeltaFIFOWithOptions(DeltaFIFOOptions{
KnownObjects: clientState,
EmitDeltaTypeReplaced: true,
Transformer: transformer,
})
cfg := &Config{
@@ -528,7 +510,7 @@ func newInformer(
Process: func(obj interface{}, isInInitialList bool) error {
if deltas, ok := obj.(Deltas); ok {
return processDeltas(h, clientState, transformer, deltas, isInInitialList)
return processDeltas(h, clientState, deltas, isInInitialList)
}
return errors.New("object given as Process argument is not Deltas")
},

View File

@@ -23,7 +23,7 @@ import (
"testing"
"time"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -32,7 +32,7 @@ import (
"k8s.io/apimachinery/pkg/watch"
fcache "k8s.io/client-go/tools/cache/testing"
"github.com/google/gofuzz"
fuzz "github.com/google/gofuzz"
)
func Example() {

View File

@@ -51,6 +51,10 @@ type DeltaFIFOOptions struct {
// When true, `Replaced` events will be sent for items passed to a Replace() call.
// When false, `Sync` events will be sent instead.
EmitDeltaTypeReplaced bool
// If set, will be called for objects before enqueueing them. Please
// see the comment on TransformFunc for details.
Transformer TransformFunc
}
// DeltaFIFO is like FIFO, but differs in two ways. One is that the
@@ -129,8 +133,32 @@ type DeltaFIFO struct {
// emitDeltaTypeReplaced is whether to emit the Replaced or Sync
// DeltaType when Replace() is called (to preserve backwards compat).
emitDeltaTypeReplaced bool
// Called with every object if non-nil.
transformer TransformFunc
}
// TransformFunc allows for transforming an object before it will be processed.
// TransformFunc (similarly to ResourceEventHandler functions) should be able
// to correctly handle the tombstone of type cache.DeletedFinalStateUnknown.
//
// New in v1.27: In such cases, the contained object will already have gone
// through the transform object separately (when it was added / updated prior
// to the delete), so the TransformFunc can likely safely ignore such objects
// (i.e., just return the input object).
//
// The most common usage pattern is to clean-up some parts of the object to
// reduce component memory usage if a given component doesn't care about them.
//
// New in v1.27: unless the object is a DeletedFinalStateUnknown, TransformFunc
// sees the object before any other actor, and it is now safe to mutate the
// object in place instead of making a copy.
//
// Note that TransformFunc is called while inserting objects into the
// notification queue and is therefore extremely performance sensitive; please
// do not do anything that will take a long time.
type TransformFunc func(interface{}) (interface{}, error)
// DeltaType is the type of a change (addition, deletion, etc)
type DeltaType string
@@ -227,6 +255,7 @@ func NewDeltaFIFOWithOptions(opts DeltaFIFOOptions) *DeltaFIFO {
knownObjects: opts.KnownObjects,
emitDeltaTypeReplaced: opts.EmitDeltaTypeReplaced,
transformer: opts.Transformer,
}
f.cond.L = &f.lock
return f
@@ -415,6 +444,21 @@ func (f *DeltaFIFO) queueActionLocked(actionType DeltaType, obj interface{}) err
if err != nil {
return KeyError{obj, err}
}
// Every object comes through this code path once, so this is a good
// place to call the transform func. If obj is a
// DeletedFinalStateUnknown tombstone, then the containted inner object
// will already have gone through the transformer, but we document that
// this can happen. In cases involving Replace(), such an object can
// come through multiple times.
if f.transformer != nil {
var err error
obj, err = f.transformer(obj)
if err != nil {
return err
}
}
oldDeltas := f.items[id]
newDeltas := append(oldDeltas, Delta{actionType, obj})
newDeltas = dedupDeltas(newDeltas)

View File

@@ -327,6 +327,88 @@ func TestDeltaFIFO_addUpdate(t *testing.T) {
}
}
type rvAndXfrm struct {
rv int
xfrm int
}
func TestDeltaFIFO_transformer(t *testing.T) {
mk := func(name string, rv int) testFifoObject {
return mkFifoObj(name, &rvAndXfrm{rv, 0})
}
xfrm := TransformFunc(func(obj interface{}) (interface{}, error) {
switch v := obj.(type) {
case testFifoObject:
v.val.(*rvAndXfrm).xfrm++
case DeletedFinalStateUnknown:
if x := v.Obj.(testFifoObject).val.(*rvAndXfrm).xfrm; x != 1 {
return nil, fmt.Errorf("object has been transformed wrong number of times: %#v", obj)
}
default:
return nil, fmt.Errorf("unexpected object: %#v", obj)
}
return obj, nil
})
must := func(err error) {
if err != nil {
t.Fatal(err)
}
}
f := NewDeltaFIFOWithOptions(DeltaFIFOOptions{
KeyFunction: testFifoObjectKeyFunc,
Transformer: xfrm,
})
must(f.Add(mk("foo", 10)))
must(f.Add(mk("bar", 11)))
must(f.Update(mk("foo", 12)))
must(f.Delete(mk("foo", 15)))
must(f.Replace([]interface{}{}, ""))
must(f.Add(mk("bar", 16)))
must(f.Replace([]interface{}{}, ""))
// Should be empty
if e, a := []string{"foo", "bar"}, f.ListKeys(); !reflect.DeepEqual(e, a) {
t.Errorf("Expected %+v, got %+v", e, a)
}
for i := 0; i < 2; i++ {
obj, err := f.Pop(func(o interface{}, isInInitialList bool) error { return nil })
if err != nil {
t.Fatalf("got nothing on try %v?", i)
}
obj = obj.(Deltas).Newest().Object
switch v := obj.(type) {
case testFifoObject:
if v.name != "foo" {
t.Errorf("expected regular deletion of foo, got %q", v.name)
}
rx := v.val.(*rvAndXfrm)
if rx.rv != 15 {
t.Errorf("expected last message, got %#v", obj)
}
if rx.xfrm != 1 {
t.Errorf("obj %v transformed wrong number of times.", obj)
}
case DeletedFinalStateUnknown:
tf := v.Obj.(testFifoObject)
rx := tf.val.(*rvAndXfrm)
if tf.name != "bar" {
t.Errorf("expected tombstone deletion of bar, got %q", tf.name)
}
if rx.rv != 16 {
t.Errorf("expected last message, got %#v", obj)
}
if rx.xfrm != 1 {
t.Errorf("tombstoned obj %v transformed wrong number of times.", obj)
}
default:
t.Errorf("unknown item %#v", obj)
}
}
}
func TestDeltaFIFO_enqueueingNoLister(t *testing.T) {
f := NewDeltaFIFOWithOptions(DeltaFIFOOptions{KeyFunction: testFifoObjectKeyFunc})
f.Add(mkFifoObj("foo", 10))

View File

@@ -205,10 +205,7 @@ type SharedInformer interface {
//
// Must be set before starting the informer.
//
// Note: Since the object given to the handler may be already shared with
// other goroutines, it is advisable to copy the object being
// transform before mutating it at all and returning the copy to prevent
// data races.
// Please see the comment on TransformFunc for more details.
SetTransform(handler TransformFunc) error
// IsStopped reports whether the informer has already been stopped.
@@ -465,6 +462,7 @@ func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) {
fifo := NewDeltaFIFOWithOptions(DeltaFIFOOptions{
KnownObjects: s.indexer,
EmitDeltaTypeReplaced: true,
Transformer: s.transform,
})
cfg := &Config{
@@ -637,7 +635,7 @@ func (s *sharedIndexInformer) HandleDeltas(obj interface{}, isInInitialList bool
defer s.blockDeltas.Unlock()
if deltas, ok := obj.(Deltas); ok {
return processDeltas(s, s.indexer, s.transform, deltas, isInInitialList)
return processDeltas(s, s.indexer, deltas, isInInitialList)
}
return errors.New("object given as Process argument is not Deltas")
}

View File

@@ -406,9 +406,8 @@ func TestSharedInformerTransformer(t *testing.T) {
name := pod.GetName()
if upper := strings.ToUpper(name); upper != name {
copied := pod.DeepCopyObject().(*v1.Pod)
copied.SetName(upper)
return copied, nil
pod.SetName(upper)
return pod, nil
}
}
return obj, nil