Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: CecileRobertMichon/controller-runtime
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: CecileRobertMichon/controller-runtime
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: release-0.7
Choose a head ref
Able to merge. These branches can be automatically merged.
  • 7 commits
  • 4 files changed
  • 5 contributors

Commits on Dec 15, 2020

  1. 🐛 Cache bypass should take into account List types

    Currently, when we create a new delegating client and we force some
    types to be uncached, we can only specify client.Object(s).
    
    When these objects are passed to the delegating client, and their GVK is
    matched against the internal map, the check doesn't take into account
    that the client might be issuing a List call with a ObjectList instead
    of a metav1.Object.
    
    The proposed solution is taken from the discovery rest mapper, which
    does the same ~hack. While it's not perfect, probably close to all
    generated objects are going to have a list that has the "List" suffix,
    that when removed gives back the original GVK.
    
    If there are other solutions, happy to change this hack.
    
    Signed-off-by: Vince Prignano <vincepri@vmware.com>
    vincepri committed Dec 15, 2020
    Copy the full SHA
    81be036 View commit details
  2. Merge pull request kubernetes-sigs#1298 from vincepri/backport1297

    🐛 Cache bypass should take into account List types
    k8s-ci-robot authored Dec 15, 2020
    Copy the full SHA
    096b2e0 View commit details

Commits on Jan 11, 2021

  1. 🐛 pkg/log: Set Log to NullLogger after 30 seconds

    The delegating Logger will cause a high number of memory allocations
    when no actual Logger is set. This change makes us set a NullLogger
    after 30 seconds if none was set yet to avoid that.
    alvaroaleman authored and vincepri committed Jan 11, 2021
    Copy the full SHA
    fd8a642 View commit details
  2. Do not set PatchType if patch is empty

    In admission v1, API server requires that Patch and PatchType are both
    provided or none are provided.  Meanwhile, admission v1beta1 does not
    have this kind of requirement.
    
    In controller-runtime, PatchResponseFromRaw sets PatchType regardless
    of the existence of patch.  If patch is empty, a response contains
    only PatchType and API server does not like it.  Webhook call fails.
    
    This change fixes this issue by not setting PatchType if patch is
    empty.
    Tatsuhiro Tsujikawa authored and vincepri committed Jan 11, 2021
    Copy the full SHA
    7684ee5 View commit details
  3. Merge pull request kubernetes-sigs#1329 from vincepri/backport-071

    🌱 Backport bug fixes for v0.7.1 release
    k8s-ci-robot authored Jan 11, 2021
    Copy the full SHA
    1d5261b View commit details

Commits on Jan 20, 2021

  1. Copy the full SHA
    0b59b8f View commit details

Commits on Jan 21, 2021

  1. Merge pull request kubernetes-sigs#1351 from estroz/cherry-pick/1332

    🐛 pkg/client: optionally allow caching of unstructured objects
    k8s-ci-robot authored Jan 21, 2021
    Copy the full SHA
    d189419 View commit details
Showing with 178 additions and 73 deletions.
  1. +108 −56 pkg/client/client_test.go
  2. +28 −13 pkg/client/split.go
  3. +34 −2 pkg/log/log.go
  4. +8 −2 pkg/webhook/admission/response.go
164 changes: 108 additions & 56 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
@@ -22,8 +22,6 @@ import (
"sync/atomic"
"time"

"k8s.io/apimachinery/pkg/types"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
@@ -33,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
kscheme "k8s.io/client-go/kubernetes/scheme"

"sigs.k8s.io/controller-runtime/pkg/client"
@@ -3106,48 +3105,79 @@ var _ = Describe("DelegatingClient", func() {
Expect(1).To(Equal(cachedReader.Called))
})

It("should call client reader when unstructured object", func() {
cachedReader := &fakeReader{}
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
dReader, err := client.NewDelegatingClient(client.NewDelegatingClientInput{
CacheReader: cachedReader,
Client: cl,
})
Expect(err).NotTo(HaveOccurred())
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "deployment1",
Labels: map[string]string{"app": "frontend"},
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "frontend"},
When("getting unstructured objects", func() {
var dep *appsv1.Deployment

BeforeEach(func() {
dep = &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "deployment1",
Labels: map[string]string{"app": "frontend"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "x", Image: "x"}}},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "frontend"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "x", Image: "x"}}},
},
},
},
}
dep, err = clientset.AppsV1().Deployments("default").Create(context.Background(), dep, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
}
var err error
dep, err = clientset.AppsV1().Deployments("default").Create(context.Background(), dep, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
})
AfterEach(func() {
Expect(clientset.AppsV1().Deployments("default").Delete(
context.Background(),
dep.Name,
metav1.DeleteOptions{},
)).To(Succeed())
})
It("should call client reader when not cached", func() {
cachedReader := &fakeReader{}
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
dReader, err := client.NewDelegatingClient(client.NewDelegatingClientInput{
CacheReader: cachedReader,
Client: cl,
})
Expect(err).NotTo(HaveOccurred())

actual := &unstructured.Unstructured{}
actual.SetGroupVersionKind(schema.GroupVersionKind{
Group: "apps",
Kind: "Deployment",
Version: "v1",
})
actual.SetName(dep.Name)
key := client.ObjectKey{Namespace: dep.Namespace, Name: dep.Name}
Expect(dReader.Get(context.TODO(), key, actual)).To(Succeed())
Expect(0).To(Equal(cachedReader.Called))
})
It("should call cache reader when cached", func() {
cachedReader := &fakeReader{}
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
dReader, err := client.NewDelegatingClient(client.NewDelegatingClientInput{
CacheReader: cachedReader,
Client: cl,
CacheUnstructured: true,
})
Expect(err).NotTo(HaveOccurred())

actual := &unstructured.Unstructured{}
actual.SetGroupVersionKind(schema.GroupVersionKind{
Group: "apps",
Kind: "Deployment",
Version: "v1",
})
actual.SetName(dep.Name)
key := client.ObjectKey{Namespace: dep.Namespace, Name: dep.Name}
Expect(dReader.Get(context.TODO(), key, actual)).To(Succeed())
Expect(0).To(Equal(cachedReader.Called))
Expect(clientset.AppsV1().Deployments("default").Delete(
context.Background(),
dep.Name,
metav1.DeleteOptions{},
)).To(Succeed())
actual := &unstructured.Unstructured{}
actual.SetGroupVersionKind(schema.GroupVersionKind{
Group: "apps",
Kind: "Deployment",
Version: "v1",
})
actual.SetName(dep.Name)
key := client.ObjectKey{Namespace: dep.Namespace, Name: dep.Name}
Expect(dReader.Get(context.TODO(), key, actual)).To(Succeed())
Expect(1).To(Equal(cachedReader.Called))
})
})
})
Describe("List", func() {
@@ -3165,24 +3195,46 @@ var _ = Describe("DelegatingClient", func() {
Expect(1).To(Equal(cachedReader.Called))
})

It("should call client reader when unstructured object", func() {
cachedReader := &fakeReader{}
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
dReader, err := client.NewDelegatingClient(client.NewDelegatingClientInput{
CacheReader: cachedReader,
Client: cl,
When("listing unstructured objects", func() {
It("should call client reader when not cached", func() {
cachedReader := &fakeReader{}
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
dReader, err := client.NewDelegatingClient(client.NewDelegatingClientInput{
CacheReader: cachedReader,
Client: cl,
})
Expect(err).NotTo(HaveOccurred())

actual := &unstructured.UnstructuredList{}
actual.SetGroupVersionKind(schema.GroupVersionKind{
Group: "apps",
Kind: "DeploymentList",
Version: "v1",
})
Expect(dReader.List(context.Background(), actual)).To(Succeed())
Expect(0).To(Equal(cachedReader.Called))
})
Expect(err).NotTo(HaveOccurred())
It("should call cache reader when cached", func() {
cachedReader := &fakeReader{}
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
dReader, err := client.NewDelegatingClient(client.NewDelegatingClientInput{
CacheReader: cachedReader,
Client: cl,
CacheUnstructured: true,
})
Expect(err).NotTo(HaveOccurred())

actual := &unstructured.UnstructuredList{}
actual.SetGroupVersionKind(schema.GroupVersionKind{
Group: "apps",
Kind: "DeploymentList",
Version: "v1",
actual := &unstructured.UnstructuredList{}
actual.SetGroupVersionKind(schema.GroupVersionKind{
Group: "apps",
Kind: "DeploymentList",
Version: "v1",
})
Expect(dReader.List(context.Background(), actual)).To(Succeed())
Expect(1).To(Equal(cachedReader.Called))
})
Expect(dReader.List(context.Background(), actual)).To(Succeed())
Expect(0).To(Equal(cachedReader.Called))
})
})
})
41 changes: 28 additions & 13 deletions pkg/client/split.go
Original file line number Diff line number Diff line change
@@ -18,19 +18,22 @@ package client

import (
"context"
"strings"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"

"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)

// NewDelegatingClientInput encapsulates the input parameters to create a new delegating client.
type NewDelegatingClientInput struct {
CacheReader Reader
Client Client
UncachedObjects []Object
CacheReader Reader
Client Client
UncachedObjects []Object
CacheUnstructured bool
}

// NewDelegatingClient creates a new delegating client.
@@ -52,10 +55,11 @@ func NewDelegatingClient(in NewDelegatingClientInput) (Client, error) {
scheme: in.Client.Scheme(),
mapper: in.Client.RESTMapper(),
Reader: &delegatingReader{
CacheReader: in.CacheReader,
ClientReader: in.Client,
scheme: in.Client.Scheme(),
uncachedGVKs: uncachedGVKs,
CacheReader: in.CacheReader,
ClientReader: in.Client,
scheme: in.Client.Scheme(),
uncachedGVKs: uncachedGVKs,
cacheUnstructured: in.CacheUnstructured,
},
Writer: in.Client,
StatusClient: in.Client,
@@ -90,19 +94,30 @@ type delegatingReader struct {
CacheReader Reader
ClientReader Reader

uncachedGVKs map[schema.GroupVersionKind]struct{}
scheme *runtime.Scheme
uncachedGVKs map[schema.GroupVersionKind]struct{}
scheme *runtime.Scheme
cacheUnstructured bool
}

func (d *delegatingReader) shouldBypassCache(obj runtime.Object) (bool, error) {
gvk, err := apiutil.GVKForObject(obj, d.scheme)
if err != nil {
return false, err
}
_, isUncached := d.uncachedGVKs[gvk]
_, isUnstructured := obj.(*unstructured.Unstructured)
_, isUnstructuredList := obj.(*unstructured.UnstructuredList)
return isUncached || isUnstructured || isUnstructuredList, nil
// TODO: this is producing unsafe guesses that don't actually work,
// but it matches ~99% of the cases out there.
if meta.IsListType(obj) {
gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")
}
if _, isUncached := d.uncachedGVKs[gvk]; isUncached {
return true, nil
}
if !d.cacheUnstructured {
_, isUnstructured := obj.(*unstructured.Unstructured)
_, isUnstructuredList := obj.(*unstructured.UnstructuredList)
return isUnstructured || isUnstructuredList, nil
}
return false, nil
}

// Get retrieves an obj for a given object key from the Kubernetes Cluster.
36 changes: 34 additions & 2 deletions pkg/log/log.go
Original file line number Diff line number Diff line change
@@ -35,18 +35,50 @@ package log

import (
"context"
"sync"
"time"

"github.com/go-logr/logr"
)

// SetLogger sets a concrete logging implementation for all deferred Loggers.
func SetLogger(l logr.Logger) {
loggerWasSetLock.Lock()
defer loggerWasSetLock.Unlock()

loggerWasSet = true
Log.Fulfill(l)
}

// It is safe to assume that if this wasn't set within the first 30 seconds of a binaries
// lifetime, it will never get set. The DelegatingLogger causes a high number of memory
// allocations when not given an actual Logger, so we set a NullLogger to avoid that.
//
// We need to keep the DelegatingLogger because we have various inits() that get a logger from
// here. They will always get executed before any code that imports controller-runtime
// has a chance to run and hence to set an actual logger.
func init() {
// Init is blocking, so start a new goroutine
go func() {
time.Sleep(30 * time.Second)
loggerWasSetLock.Lock()
defer loggerWasSetLock.Unlock()
if !loggerWasSet {
Log.Fulfill(NullLogger{})
}
}()
}

var (
loggerWasSetLock sync.Mutex
loggerWasSet bool
)

// Log is the base logger used by kubebuilder. It delegates
// to another logr.Logger. You *must* call SetLogger to
// get any actual logging.
// to another logr.Logger. You *must* call SetLogger to
// get any actual logging. If SetLogger is not called within
// the first 30 seconds of a binaries lifetime, it will get
// set to a NullLogger.
var Log = NewDelegatingLogger(NullLogger{})

// FromContext returns a logger with predefined values from a context.Context.
10 changes: 8 additions & 2 deletions pkg/webhook/admission/response.go
Original file line number Diff line number Diff line change
@@ -90,8 +90,14 @@ func PatchResponseFromRaw(original, current []byte) Response {
return Response{
Patches: patches,
AdmissionResponse: admissionv1.AdmissionResponse{
Allowed: true,
PatchType: func() *admissionv1.PatchType { pt := admissionv1.PatchTypeJSONPatch; return &pt }(),
Allowed: true,
PatchType: func() *admissionv1.PatchType {
if len(patches) == 0 {
return nil
}
pt := admissionv1.PatchTypeJSONPatch
return &pt
}(),
},
}
}