diff --git a/internal/controller/certificates/OWNERS b/internal/controller/certificates/OWNERS new file mode 100644 index 000000000..a846a6112 --- /dev/null +++ b/internal/controller/certificates/OWNERS @@ -0,0 +1,4 @@ +filters: + "^apply(_test)?\\.go$": + required_reviewers: + - joshvanl diff --git a/internal/controller/certificates/apply.go b/internal/controller/certificates/apply.go new file mode 100644 index 000000000..a9d0d0add --- /dev/null +++ b/internal/controller/certificates/apply.go @@ -0,0 +1,67 @@ +/* +Copyright 2020 The cert-manager 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 internal + +import ( + "context" + "encoding/json" + "fmt" + + cmclient "github.com/jetstack/cert-manager/pkg/client/clientset/versioned" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apitypes "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" +) + +// ApplyStatus will make a Apply 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 +// given fieldManager is will be used as the FieldManager in the Apply call. +// Always sets Force Apply to true. +func ApplyStatus(ctx context.Context, cl cmclient.Interface, fieldManager string, crt *cmapi.Certificate) error { + crtData, err := serializeApplyStatus(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}, "status", + ) + + return err +} + +// 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. +// TypeMeta will be populated with the Kind "Certificate" and API Version +// "cert-manager.io/v1" respectively. +func serializeApplyStatus(crt *cmapi.Certificate) ([]byte, error) { + crt = &cmapi.Certificate{ + TypeMeta: metav1.TypeMeta{Kind: cmapi.CertificateKind, APIVersion: cmapi.SchemeGroupVersion.Identifier()}, + ObjectMeta: metav1.ObjectMeta{Namespace: crt.Namespace, Name: crt.Name}, + Status: crt.Status, + } + crtData, err := json.Marshal(crt) + if err != nil { + return nil, fmt.Errorf("failed to marshal certificate object: %w", err) + } + return crtData, nil +} diff --git a/internal/controller/certificates/apply_test.go b/internal/controller/certificates/apply_test.go new file mode 100644 index 000000000..6a2035b0f --- /dev/null +++ b/internal/controller/certificates/apply_test.go @@ -0,0 +1,80 @@ +/* +Copyright 2020 The cert-manager 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 internal + +import ( + "strconv" + "sync" + "testing" + + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/assert" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" +) + +// 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 +// also ensure that all Certificate status fields are tagged omitempty, and are +// not serialized if unset. +func Test_serializeApplyStatus(t *testing.T) { + // Expected serialized Certificate Apply object. Should only contain base + // meta/type object, empty spec. Status should be matched both via regex, and + // when empty. + const ( + expReg = `^{"kind":"Certificate","apiVersion":"cert-manager.io/v1","metadata":{"name":"foo","namespace":"bar","creationTimestamp":null},"spec":{"secretName":"","issuerRef":{"name":""}},"status":{.*}$` + expEmpty = `{"kind":"Certificate","apiVersion":"cert-manager.io/v1","metadata":{"name":"foo","namespace":"bar","creationTimestamp":null},"spec":{"secretName":"","issuerRef":{"name":""}},"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.Name = "foo" + crt.Namespace = "bar" + + // Test regex with non-empty status. + crtData, err := serializeApplyStatus(&crt) + assert.NoError(t, err) + assert.Regexp(t, expReg, string(crtData)) + + // String match on empty status. + crt.Status = cmapi.CertificateStatus{} + crtData, err = serializeApplyStatus(&crt) + assert.NoError(t, err) + assert.Equal(t, expEmpty, string(crtData)) + + wg.Done() + }) + } + }() + } + + for i := 0; i < numJobs; i++ { + jobs <- i + } + close(jobs) + wg.Wait() +}