diff --git a/internal/controller/certificates/apply.go b/internal/controller/certificates/apply.go index 81c9b6ce9..a3f09effc 100644 --- a/internal/controller/certificates/apply.go +++ b/internal/controller/certificates/apply.go @@ -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. diff --git a/internal/controller/certificates/apply_test.go b/internal/controller/certificates/apply_test.go index 9fae02006..0928b2908 100644 --- a/internal/controller/certificates/apply_test.go +++ b/internal/controller/certificates/apply_test.go @@ -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() }) } diff --git a/pkg/controller/certificate-shim/BUILD.bazel b/pkg/controller/certificate-shim/BUILD.bazel index 6954d5232..59f42da88 100644 --- a/pkg/controller/certificate-shim/BUILD.bazel +++ b/pkg/controller/certificate-shim/BUILD.bazel @@ -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", diff --git a/pkg/controller/certificate-shim/gateways/controller.go b/pkg/controller/certificate-shim/gateways/controller.go index b8123889a..af5af5c88 100644 --- a/pkg/controller/certificate-shim/gateways/controller.go +++ b/pkg/controller/certificate-shim/gateways/controller.go @@ -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 diff --git a/pkg/controller/certificate-shim/ingresses/controller.go b/pkg/controller/certificate-shim/ingresses/controller.go index c2bb42b64..53f287548 100644 --- a/pkg/controller/certificate-shim/ingresses/controller.go +++ b/pkg/controller/certificate-shim/ingresses/controller.go @@ -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) diff --git a/pkg/controller/certificate-shim/sync.go b/pkg/controller/certificate-shim/sync.go index 604b698e8..9d038b0f3 100644 --- a/pkg/controller/certificate-shim/sync.go +++ b/pkg/controller/certificate-shim/sync.go @@ -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) } diff --git a/pkg/controller/certificate-shim/sync_test.go b/pkg/controller/certificate-shim/sync_test.go index 3e6bdbe49..1b0ce62c2 100644 --- a/pkg/controller/certificate-shim/sync_test.go +++ b/pkg/controller/certificate-shim/sync_test.go @@ -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)