Merge pull request #4811 from JoshVanL/controllers-server-side-apply-certificates-shim
Server Side Apply: Adds support for certificate-shim controllers to use SSA with Feature Gate
This commit is contained in:
commit
bfcc204c2b
@ -29,6 +29,26 @@ import (
|
||||
cmclient "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned"
|
||||
)
|
||||
|
||||
// Apply will make a Apply API call with the given client to the certificates
|
||||
// resource endpoint. All data in the given Certificate's status field is
|
||||
// dropped.
|
||||
// The given fieldManager is will be used as the FieldManager in the Apply
|
||||
// call.
|
||||
// Always sets Force Apply to true.
|
||||
func Apply(ctx context.Context, cl cmclient.Interface, fieldManager string, crt *cmapi.Certificate) error {
|
||||
crtData, err := serializeApply(crt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = cl.CertmanagerV1().Certificates(crt.Namespace).Patch(
|
||||
ctx, crt.Name, apitypes.ApplyPatchType, crtData,
|
||||
metav1.PatchOptions{Force: pointer.Bool(true), FieldManager: fieldManager},
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ApplyStatus will make a Patch API call with the given client to the
|
||||
// certificates status sub-resource endpoint. All data in the given Certificate
|
||||
// object is dropped; expect for the name, namespace, and status object. The
|
||||
@ -48,6 +68,24 @@ func ApplyStatus(ctx context.Context, cl cmclient.Interface, fieldManager string
|
||||
return err
|
||||
}
|
||||
|
||||
// serializeApply converts the given Certificate object in JSON.
|
||||
// The status field will be set empty before serializing.
|
||||
// TypeMeta will be populated with the Kind "Certificate" and API Version
|
||||
// "cert-manager.io/v1" respectively.
|
||||
func serializeApply(crt *cmapi.Certificate) ([]byte, error) {
|
||||
crt = &cmapi.Certificate{
|
||||
TypeMeta: metav1.TypeMeta{Kind: cmapi.CertificateKind, APIVersion: cmapi.SchemeGroupVersion.Identifier()},
|
||||
ObjectMeta: *crt.ObjectMeta.DeepCopy(),
|
||||
Spec: *crt.Spec.DeepCopy(),
|
||||
Status: cmapi.CertificateStatus{},
|
||||
}
|
||||
crtData, err := json.Marshal(crt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal certificate object: %w", err)
|
||||
}
|
||||
return crtData, nil
|
||||
}
|
||||
|
||||
// serializeApplyStatus converts the given Certificate object in JSON. Only the
|
||||
// name, namespace, and status field values will be copied and encoded into the
|
||||
// serialized slice. All other fields will be left at their zero value.
|
||||
|
||||
@ -17,6 +17,7 @@ limitations under the License.
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
@ -27,6 +28,46 @@ import (
|
||||
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
|
||||
)
|
||||
|
||||
func Test_serializeApply(t *testing.T) {
|
||||
const (
|
||||
expReg = `^{"kind":"Certificate","apiVersion":"cert-manager.io/v1","metadata":{.*},"spec":{.*},"status":{}}$`
|
||||
numJobs = 10000
|
||||
)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
jobs := make(chan int)
|
||||
|
||||
wg.Add(numJobs)
|
||||
for i := 0; i < 3; i++ {
|
||||
go func() {
|
||||
for j := range jobs {
|
||||
t.Run("fuzz_"+strconv.Itoa(j), func(t *testing.T) {
|
||||
var crt cmapi.Certificate
|
||||
fuzz.New().NilChance(0.5).Fuzz(&crt)
|
||||
crt.ManagedFields = nil
|
||||
|
||||
crtData, err := serializeApply(&crt)
|
||||
assert.NoError(t, err)
|
||||
assert.Regexp(t, expReg, string(crtData))
|
||||
|
||||
// Test round trip serializing Certificate preserved the spec.
|
||||
var rtCrt cmapi.Certificate
|
||||
assert.NoError(t, json.Unmarshal(crtData, &rtCrt))
|
||||
assert.Equal(t, rtCrt.Spec, crt.Spec)
|
||||
|
||||
wg.Done()
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < numJobs; i++ {
|
||||
jobs <- i
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// This test ensures that when a Certificate object is serialized in
|
||||
// preparation for a Certificate status Apply call. Only the required
|
||||
// metadata/type fields are present, and only empty spec fields are set. We
|
||||
@ -66,6 +107,11 @@ func Test_serializeApplyStatus(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expEmpty, string(crtData))
|
||||
|
||||
// Test round trip serializing Certificate preserved the status.
|
||||
var rtCrt cmapi.Certificate
|
||||
assert.NoError(t, json.Unmarshal(crtData, &rtCrt))
|
||||
assert.Equal(t, rtCrt.Status, crt.Status)
|
||||
|
||||
wg.Done()
|
||||
})
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ go_library(
|
||||
importpath = "github.com/cert-manager/cert-manager/pkg/controller/certificate-shim",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//internal/controller/certificates:go_default_library",
|
||||
"//internal/controller/feature:go_default_library",
|
||||
"//internal/ingress:go_default_library",
|
||||
"//pkg/api/util:go_default_library",
|
||||
"//pkg/apis/acme/v1:go_default_library",
|
||||
@ -18,6 +20,7 @@ go_library(
|
||||
"//pkg/client/listers/certmanager/v1:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//pkg/logs:go_default_library",
|
||||
"//pkg/util/feature:go_default_library",
|
||||
"@com_github_go_logr_logr//:go_default_library",
|
||||
"@io_k8s_api//core/v1:go_default_library",
|
||||
"@io_k8s_api//networking/v1:go_default_library",
|
||||
|
||||
@ -55,7 +55,7 @@ type controller struct {
|
||||
func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) {
|
||||
c.gatewayLister = ctx.GWShared.Gateway().V1alpha2().Gateways().Lister()
|
||||
log := logf.FromContext(ctx.RootContext, ControllerName)
|
||||
c.sync = shimhelper.SyncFnFor(ctx.Recorder, log, ctx.CMClient, ctx.SharedInformerFactory.Certmanager().V1().Certificates().Lister(), ctx.IngressShimOptions)
|
||||
c.sync = shimhelper.SyncFnFor(ctx.Recorder, log, ctx.CMClient, ctx.SharedInformerFactory.Certmanager().V1().Certificates().Lister(), ctx.IngressShimOptions, ctx.FieldManager)
|
||||
|
||||
// We don't need to requeue Gateways on "Deleted" events, since our Sync
|
||||
// function does nothing when the Gateway lister returns "not found". But we
|
||||
|
||||
@ -53,7 +53,7 @@ func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitin
|
||||
c.ingressLister = internalIngressLister
|
||||
|
||||
log := logf.FromContext(ctx.RootContext, ControllerName)
|
||||
c.sync = shimhelper.SyncFnFor(ctx.Recorder, log, ctx.CMClient, cmShared.Certmanager().V1().Certificates().Lister(), ctx.IngressShimOptions)
|
||||
c.sync = shimhelper.SyncFnFor(ctx.Recorder, log, ctx.CMClient, cmShared.Certmanager().V1().Certificates().Lister(), ctx.IngressShimOptions, ctx.FieldManager)
|
||||
|
||||
queue := workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), ControllerName)
|
||||
|
||||
|
||||
@ -37,6 +37,8 @@ import (
|
||||
"k8s.io/client-go/tools/record"
|
||||
gwapi "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
internalcertificates "github.com/cert-manager/cert-manager/internal/controller/certificates"
|
||||
"github.com/cert-manager/cert-manager/internal/controller/feature"
|
||||
ingress "github.com/cert-manager/cert-manager/internal/ingress"
|
||||
cmacme "github.com/cert-manager/cert-manager/pkg/apis/acme/v1"
|
||||
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
|
||||
@ -45,6 +47,7 @@ import (
|
||||
cmlisters "github.com/cert-manager/cert-manager/pkg/client/listers/certmanager/v1"
|
||||
"github.com/cert-manager/cert-manager/pkg/controller"
|
||||
logf "github.com/cert-manager/cert-manager/pkg/logs"
|
||||
utilfeature "github.com/cert-manager/cert-manager/pkg/util/feature"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -75,6 +78,7 @@ func SyncFnFor(
|
||||
cmClient clientset.Interface,
|
||||
cmLister cmlisters.CertificateLister,
|
||||
defaults controller.IngressShimOptions,
|
||||
fieldManager string,
|
||||
) SyncFn {
|
||||
return func(ctx context.Context, ingLike metav1.Object) error {
|
||||
log := logf.WithResource(log, ingLike)
|
||||
@ -121,7 +125,7 @@ func SyncFnFor(
|
||||
}
|
||||
|
||||
for _, crt := range newCrts {
|
||||
_, err := cmClient.CertmanagerV1().Certificates(crt.Namespace).Create(ctx, crt, metav1.CreateOptions{})
|
||||
_, err := cmClient.CertmanagerV1().Certificates(crt.Namespace).Create(ctx, crt, metav1.CreateOptions{FieldManager: fieldManager})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -129,10 +133,29 @@ func SyncFnFor(
|
||||
}
|
||||
|
||||
for _, crt := range updateCrts {
|
||||
_, err := cmClient.CertmanagerV1().Certificates(crt.Namespace).Update(ctx, crt, metav1.UpdateOptions{})
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(feature.ServerSideApply) {
|
||||
err = internalcertificates.Apply(ctx, cmClient, fieldManager, &cmapi.Certificate{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: crt.Name,
|
||||
Namespace: crt.Namespace,
|
||||
Labels: crt.Labels,
|
||||
OwnerReferences: crt.OwnerReferences,
|
||||
},
|
||||
Spec: cmapi.CertificateSpec{
|
||||
DNSNames: crt.Spec.DNSNames,
|
||||
SecretName: crt.Spec.SecretName,
|
||||
IssuerRef: crt.Spec.IssuerRef,
|
||||
Usages: crt.Spec.Usages,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
_, err = cmClient.CertmanagerV1().Certificates(crt.Namespace).Update(ctx, crt, metav1.UpdateOptions{})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rec.Eventf(ingLikeObj, corev1.EventTypeNormal, reasonUpdateCertificate, "Successfully updated Certificate %q", crt.Name)
|
||||
}
|
||||
|
||||
|
||||
@ -2486,7 +2486,7 @@ func TestSync(t *testing.T) {
|
||||
DefaultIssuerKind: test.DefaultIssuerKind,
|
||||
DefaultIssuerGroup: test.DefaultIssuerGroup,
|
||||
DefaultAutoCertificateAnnotations: []string{"kubernetes.io/tls-acme"},
|
||||
})
|
||||
}, "cert-manager-test")
|
||||
b.Start()
|
||||
|
||||
err := sync(context.Background(), test.IngressLike)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user