switch to SSA for cainjector

Co-authored-by: joshvanl <vleeuwenjoshua@gmail.com>
Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com>
This commit is contained in:
Tim Ramlot 2023-04-26 17:04:11 +02:00
parent 15e05b705b
commit 927cef3c22
No known key found for this signature in database
GPG Key ID: 47428728E0C2878D
8 changed files with 182 additions and 27 deletions

View File

@ -143,7 +143,7 @@ func NewCommandStartInjectorController(ctx context.Context, out, errOut io.Write
o := NewInjectorControllerOptions(out, errOut)
cmd := &cobra.Command{
Use: "ca-injector",
Use: "cainjector",
Short: fmt.Sprintf("CA Injection Controller for Kubernetes (%s) (%s)", util.AppVersion, util.AppGitCommit),
Long: `
cert-manager CA injector is a Kubernetes addon to automate the injection of CA data into
@ -155,7 +155,7 @@ servers and webhook servers.`,
// TODO: Refactor this function from this package
RunE: func(cmd *cobra.Command, args []string) error {
o.log = logf.Log.WithName("ca-injector")
o.log = logf.Log.WithName("cainjector")
logf.V(logf.InfoLevel).InfoS("starting", "version", util.AppVersion, "revision", util.AppGitCommit)
return o.RunInjectorController(ctx)
@ -169,19 +169,21 @@ servers and webhook servers.`,
}
func (o InjectorControllerOptions) RunInjectorController(ctx context.Context) error {
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: api.Scheme,
Namespace: o.Namespace,
LeaderElection: o.LeaderElect,
LeaderElectionNamespace: o.LeaderElectionNamespace,
LeaderElectionID: "cert-manager-cainjector-leader-election",
LeaderElectionReleaseOnCancel: true,
LeaderElectionResourceLock: resourcelock.LeasesResourceLock,
LeaseDuration: &o.LeaseDuration,
RenewDeadline: &o.RenewDeadline,
RetryPeriod: &o.RetryPeriod,
MetricsBindAddress: "0",
})
mgr, err := ctrl.NewManager(
util.RestConfigWithUserAgent(ctrl.GetConfigOrDie(), "cainjector"),
ctrl.Options{
Scheme: api.Scheme,
Namespace: o.Namespace,
LeaderElection: o.LeaderElect,
LeaderElectionNamespace: o.LeaderElectionNamespace,
LeaderElectionID: "cert-manager-cainjector-leader-election",
LeaderElectionReleaseOnCancel: true,
LeaderElectionResourceLock: resourcelock.LeasesResourceLock,
LeaseDuration: &o.LeaseDuration,
RenewDeadline: &o.RenewDeadline,
RetryPeriod: &o.RetryPeriod,
MetricsBindAddress: "0",
})
if err != nil {
return fmt.Errorf("error creating manager: %v", err)
}

View File

@ -22,13 +22,13 @@ rules:
verbs: ["get", "create", "update", "patch"]
- apiGroups: ["admissionregistration.k8s.io"]
resources: ["validatingwebhookconfigurations", "mutatingwebhookconfigurations"]
verbs: ["get", "list", "watch", "update"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: ["apiregistration.k8s.io"]
resources: ["apiservices"]
verbs: ["get", "list", "watch", "update"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch", "update"]
verbs: ["get", "list", "watch", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@ -112,7 +112,7 @@ cert-manager-certificates-[issuing,trigger,keymanager,readiness]
cert-manager-certificaterequests-[acme,approver,ca,selfsigned,vault,venafi]
cert-manager-clusterissuers-[acme,ca,selfsigned,vault,venafi]
cert-manager-issuers-[acme,ca,selfsigned,vault,venafi]
cert-manager-cainjector # base field manager of cert-manager ca-injector
cert-manager-cainjector # base field manager of cert-manager cainjector
cert-manager-webhook # base field manager of cert-manager webhook
cert-manager-cmctl
```

View File

@ -237,7 +237,7 @@ comma = ,
# for "--set featureGates". That's why we have "\$(comma)".
feature_gates_controller := $(subst $(space),\$(comma),$(filter AllAlpha=% AllBeta=% AdditionalCertificateOutputFormats=% ValidateCAA=% ExperimentalCertificateSigningRequestControllers=% ExperimentalGatewayAPISupport=% ServerSideApply=% LiteralCertificateSubject=% UseCertificateRequestBasicConstraints=% SecretsFilteredCaching=%, $(subst $(comma),$(space),$(FEATURE_GATES))))
feature_gates_webhook := $(subst $(space),\$(comma),$(filter AllAlpha=% AllBeta=% AdditionalCertificateOutputFormats=% LiteralCertificateSubject=%, $(subst $(comma),$(space),$(FEATURE_GATES))))
feature_gates_cainjector := $(subst $(space),\$(comma),$(filter AllAlpha=% AllBeta=%, $(subst $(comma),$(space),$(FEATURE_GATES))))
feature_gates_cainjector := $(subst $(space),\$(comma),$(filter AllAlpha=% AllBeta=% ServerSideApply=%, $(subst $(comma),$(space),$(FEATURE_GATES))))
# Install cert-manager with E2E specific images and deployment settings.
# The values.best-practice.yaml file is applied for compliance with the

View File

@ -17,8 +17,13 @@ limitations under the License.
package cainjector
import (
"encoding/json"
admissionreg "k8s.io/api/admissionregistration/v1"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/types"
applyadmissionreg "k8s.io/client-go/applyconfigurations/admissionregistration/v1"
applymetav1 "k8s.io/client-go/applyconfigurations/meta/v1"
apireg "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
@ -61,12 +66,38 @@ type InjectTarget interface {
// It should be a pointer suitable for mutation.
AsObject() client.Object
// AsApplyObject returns this injectable as an object that only contains
// fields which are managed by the cainjector (CA Data) and immutable fields
// that must be present in Apply calls; intended for use for Apply Patch
// calls.
AsApplyObject() (client.Object, client.Patch)
// SetCA sets the CA of this target to the given certificate data (in the standard
// PEM format used across Kubernetes). In cases where multiple CA fields exist per
// target (like admission webhook configs), all CAs are set to the given value.
SetCA(data []byte)
}
type ssaPatch struct {
patch []byte
err error
}
func newSSAPatch(patch interface{}) *ssaPatch {
jsonPatch, err := json.Marshal(patch)
return &ssaPatch{patch: jsonPatch, err: err}
}
func (p *ssaPatch) Type() types.PatchType {
return types.ApplyPatchType
}
func (p *ssaPatch) Data(obj client.Object) ([]byte, error) {
return p.patch, nil
}
var _ client.Patch = &ssaPatch{}
// mutatingWebhookTarget knows how to set CA data for all the webhooks
// in a mutatingWebhookConfiguration.
type mutatingWebhookTarget struct {
@ -76,12 +107,32 @@ type mutatingWebhookTarget struct {
func (t *mutatingWebhookTarget) AsObject() client.Object {
return &t.obj
}
func (t *mutatingWebhookTarget) SetCA(data []byte) {
for ind := range t.obj.Webhooks {
t.obj.Webhooks[ind].ClientConfig.CABundle = data
}
}
func (t *mutatingWebhookTarget) AsApplyObject() (client.Object, client.Patch) {
patch := applyadmissionreg.MutatingWebhookConfiguration(t.obj.Name)
for i := range t.obj.Webhooks {
patch = patch.WithWebhooks(
applyadmissionreg.
MutatingWebhook().
WithName(t.obj.Webhooks[i].Name). // Name is used as slice key.
WithClientConfig(
&applyadmissionreg.WebhookClientConfigApplyConfiguration{
CABundle: t.obj.Webhooks[i].ClientConfig.CABundle,
},
),
)
}
return &t.obj, newSSAPatch(patch)
}
// validatingWebhookTarget knows how to set CA data for all the webhooks
// in a validatingWebhookConfiguration.
type validatingWebhookTarget struct {
@ -98,6 +149,25 @@ func (t *validatingWebhookTarget) SetCA(data []byte) {
}
}
func (t *validatingWebhookTarget) AsApplyObject() (client.Object, client.Patch) {
patch := applyadmissionreg.ValidatingWebhookConfiguration(t.obj.Name)
for i := range t.obj.Webhooks {
patch = patch.WithWebhooks(
applyadmissionreg.
ValidatingWebhook().
WithName(t.obj.Webhooks[i].Name). // Name is used as slice key.
WithClientConfig(
&applyadmissionreg.WebhookClientConfigApplyConfiguration{
CABundle: t.obj.Webhooks[i].ClientConfig.CABundle,
},
),
)
}
return &t.obj, newSSAPatch(patch)
}
// apiServiceTarget knows how to set CA data for the CA bundle in
// the APIService spec.
type apiServiceTarget struct {
@ -112,6 +182,31 @@ func (t *apiServiceTarget) SetCA(data []byte) {
t.obj.Spec.CABundle = data
}
type apiServiceTargetPatch struct {
applymetav1.TypeMetaApplyConfiguration `json:",inline"`
*applymetav1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"`
Spec *apiServiceTargetSpecPatch `json:"spec,omitempty"`
}
type apiServiceTargetSpecPatch struct {
CABundle []byte `json:"caBundle,omitempty"`
}
func (t *apiServiceTarget) AsApplyObject() (client.Object, client.Patch) {
return &t.obj, newSSAPatch(&apiServiceTargetPatch{
TypeMetaApplyConfiguration: *applymetav1.
TypeMeta().
WithAPIVersion(apireg.SchemeGroupVersion.String()).
WithKind("APIService"),
ObjectMetaApplyConfiguration: applymetav1.
ObjectMeta().
WithName(t.obj.Name),
Spec: &apiServiceTargetSpecPatch{
CABundle: t.obj.Spec.CABundle,
},
})
}
// crdConversionTarget knows how to set CA data for the conversion webhook in CRDs
type crdConversionTarget struct {
obj apiext.CustomResourceDefinition
@ -133,3 +228,46 @@ func (t *crdConversionTarget) SetCA(data []byte) {
}
t.obj.Spec.Conversion.Webhook.ClientConfig.CABundle = data
}
type customResourceDefinitionPatch struct {
applymetav1.TypeMetaApplyConfiguration `json:",inline"`
*applymetav1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"`
Spec *customResourceDefinitionSpecPatch `json:"spec,omitempty"`
}
type customResourceDefinitionSpecPatch struct {
Conversion *customResourceConversionPatch `json:"conversion,omitempty"`
}
type customResourceConversionPatch struct {
Webhook *customResourceWebhookConversionPatch `json:"webhook,omitempty"`
}
type customResourceWebhookConversionPatch struct {
ClientConfig *customResourceWebhookClientConfigPatch `json:"clientConfig,omitempty"`
}
type customResourceWebhookClientConfigPatch struct {
CABundle []byte `json:"caBundle,omitempty"`
}
func (t *crdConversionTarget) AsApplyObject() (client.Object, client.Patch) {
return &t.obj, newSSAPatch(&customResourceDefinitionPatch{
TypeMetaApplyConfiguration: *applymetav1.
TypeMeta().
WithAPIVersion(apiext.SchemeGroupVersion.String()).
WithKind("CustomResourceDefinition"),
ObjectMetaApplyConfiguration: applymetav1.
ObjectMeta().
WithName(t.obj.Name),
Spec: &customResourceDefinitionSpecPatch{
Conversion: &customResourceConversionPatch{
Webhook: &customResourceWebhookConversionPatch{
ClientConfig: &customResourceWebhookClientConfigPatch{
CABundle: t.obj.Spec.Conversion.Webhook.ClientConfig.CABundle,
},
},
},
},
})
}

View File

@ -21,12 +21,15 @@ import (
"fmt"
"strings"
"github.com/cert-manager/cert-manager/internal/controller/feature"
utilfeature "github.com/cert-manager/cert-manager/pkg/util/feature"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@ -57,6 +60,9 @@ type reconciler struct {
// if set, the reconciler is namespace scoped
namespace string
// fieldManager is the manager name used for the Apply operations.
fieldManager string
resourceName string // just used for logging
}
@ -117,10 +123,20 @@ func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
target.SetCA(caData)
// actually update with injected CA data
if err := r.Client.Update(ctx, target.AsObject()); err != nil {
if utilfeature.DefaultFeatureGate.Enabled(feature.ServerSideApply) {
obj, patch := target.AsApplyObject()
err = r.Client.Patch(ctx, obj, patch, &client.PatchOptions{
Force: pointer.Bool(true), FieldManager: r.fieldManager,
})
} else {
err = r.Client.Update(ctx, target.AsObject())
}
if err != nil {
log.Error(err, "unable to update target object with new CA data")
return ctrl.Result{}, err
}
log.V(logf.InfoLevel).Info("Updated object")
return ctrl.Result{}, nil

View File

@ -35,6 +35,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/source"
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
"github.com/cert-manager/cert-manager/pkg/util"
)
// this file contains the logic to set up the different reconcile loops run by cainjector
@ -90,8 +91,6 @@ var (
listType: &apiext.CustomResourceDefinitionList{},
objType: &apiext.CustomResourceDefinition{},
}
injectorSetups = []setup{MutatingWebhookSetup, ValidatingWebhookSetup, APIServiceSetup, CRDSetup}
)
// registerAllInjectors registers all injectors and based on the
@ -134,6 +133,7 @@ func RegisterAllInjectors(ctx context.Context, mgr ctrl.Manager, opts SetupOptio
cds,
kds,
},
fieldManager: util.PrefixFromUserAgent(mgr.GetConfig().UserAgent),
}
// Index injectable with a new field. If the injectable's CA is
@ -196,9 +196,8 @@ func RegisterAllInjectors(ctx context.Context, mgr ctrl.Manager, opts SetupOptio
toInjectable: buildCertToInjectableFunc(setup.listType, setup.resourceName),
}).Map))
}
err := b.Complete(r)
if err != nil {
err = fmt.Errorf("error registering controller for %s: %w", setup.objType.GetName(), err)
if err := b.Complete(r); err != nil {
return fmt.Errorf("error registering controller for %s: %w", setup.objType.GetName(), err)
}
}
return nil

View File

@ -49,7 +49,7 @@ type injectableTest struct {
}
var _ = framework.CertManagerDescribe("CA Injector", func() {
f := framework.NewDefaultFramework("ca-injector")
f := framework.NewDefaultFramework("cainjector")
issuerName := "inject-cert-issuer"
secretName := "serving-certs-data"