-
Notifications
You must be signed in to change notification settings - Fork 235
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #274 from JacobTanenbaum/SDN-496
Add CA bundle injector
- Loading branch information
Showing
5 changed files
with
290 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
package configmapcainjector | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"log" | ||
|
||
configv1 "github.com/openshift/api/config/v1" | ||
"github.com/openshift/cluster-network-operator/pkg/controller/statusmanager" | ||
"github.com/openshift/cluster-network-operator/pkg/names" | ||
"github.com/openshift/cluster-network-operator/pkg/util/validation" | ||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/labels" | ||
"k8s.io/apimachinery/pkg/types" | ||
|
||
corev1 "k8s.io/api/core/v1" | ||
"k8s.io/apimachinery/pkg/api/equality" | ||
"k8s.io/apimachinery/pkg/api/errors" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
"k8s.io/client-go/util/retry" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
"sigs.k8s.io/controller-runtime/pkg/controller" | ||
"sigs.k8s.io/controller-runtime/pkg/event" | ||
"sigs.k8s.io/controller-runtime/pkg/handler" | ||
"sigs.k8s.io/controller-runtime/pkg/manager" | ||
"sigs.k8s.io/controller-runtime/pkg/predicate" | ||
"sigs.k8s.io/controller-runtime/pkg/reconcile" | ||
"sigs.k8s.io/controller-runtime/pkg/source" | ||
) | ||
|
||
func Add(mgr manager.Manager, status *statusmanager.StatusManager) error { | ||
reconciler := newReconciler(mgr, status) | ||
if reconciler == nil { | ||
return fmt.Errorf("failed to create reconciler") | ||
} | ||
|
||
return add(mgr, reconciler) | ||
} | ||
|
||
func newReconciler(mgr manager.Manager, status *statusmanager.StatusManager) reconcile.Reconciler { | ||
if err := configv1.Install(mgr.GetScheme()); err != nil { | ||
return nil | ||
} | ||
|
||
return &ReconcileConfigMapInjector{client: mgr.GetClient(), scheme: mgr.GetScheme(), status: status} | ||
} | ||
|
||
func add(mgr manager.Manager, r reconcile.Reconciler) error { | ||
// Create a new controller. | ||
c, err := controller.New("configmap-trust-bundle-injector-controller", mgr, controller.Options{Reconciler: r}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// The events fire for changes/creation of the trusted-ca-bundle and any configmaps with the | ||
// label "config.openshift.io/inject-trusted-cabundle". | ||
pred := predicate.Funcs{ | ||
UpdateFunc: func(e event.UpdateEvent) bool { | ||
return shouldUpdateConfigMaps(e.MetaNew) | ||
}, | ||
DeleteFunc: func(e event.DeleteEvent) bool { | ||
return false | ||
}, | ||
CreateFunc: func(e event.CreateEvent) bool { | ||
return shouldUpdateConfigMaps(e.Meta) | ||
}, | ||
GenericFunc: func(e event.GenericEvent) bool { | ||
return shouldUpdateConfigMaps(e.Meta) | ||
}, | ||
} | ||
|
||
err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, &handler.EnqueueRequestForObject{}, pred) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
var _ reconcile.Reconciler = &ReconcileConfigMapInjector{} | ||
|
||
type ReconcileConfigMapInjector struct { | ||
client client.Client | ||
scheme *runtime.Scheme | ||
status *statusmanager.StatusManager | ||
} | ||
|
||
// Reconcile expects requests to refers to configmaps of two different types. | ||
// 1. a configmap named trusted-ca-bundle in namespace openshift-config-managed and will ensure that all configmaps with the label | ||
// config.openshift.io/inject-trusted-cabundle = true have the certificate information stored in trusted-ca-bundle's ca-bundle.crt entry. | ||
// 2. a configmap in any namespace with the label config.openshift.io/inject-trusted-cabundle = true and will insure that it contains the ca-bundle.crt | ||
// entry in the configmap named trusted-ca-bundle in namespace openshift-config-managed. | ||
func (r *ReconcileConfigMapInjector) Reconcile(request reconcile.Request) (reconcile.Result, error) { | ||
log.Printf("Reconciling configmap from %s/%s\n", request.Name, request.Namespace) | ||
|
||
trustedCAbundleConfigMap := &corev1.ConfigMap{} | ||
trustedCAbundleConfigMapName := types.NamespacedName{ | ||
Namespace: names.TRUSTED_CA_BUNDLE_CONFIGMAP_NS, | ||
Name: names.TRUSTED_CA_BUNDLE_CONFIGMAP, | ||
} | ||
err := r.client.Get(context.TODO(), trustedCAbundleConfigMapName, trustedCAbundleConfigMap) | ||
if err != nil { | ||
if errors.IsNotFound(err) { | ||
return reconcile.Result{}, nil | ||
} | ||
log.Println(err) | ||
return reconcile.Result{}, err | ||
} | ||
_, trustedCAbundleData, err := validation.TrustBundleConfigMap(trustedCAbundleConfigMap) | ||
|
||
if err != nil { | ||
return reconcile.Result{}, err | ||
} | ||
// Build a list of configMaps. | ||
configMapsToChange := []corev1.ConfigMap{} | ||
|
||
// The trusted-ca-bundle changed. | ||
if request.Name == names.TRUSTED_CA_BUNDLE_CONFIGMAP && request.Namespace == names.TRUSTED_CA_BUNDLE_CONFIGMAP_NS { | ||
|
||
configMapList := &corev1.ConfigMapList{} | ||
selector := labels.Set(map[string]string{names.TRUSTED_CA_BUNDLE_CONFIGMAP_LABEL: "true"}).AsSelector() | ||
err = r.client.List(context.TODO(), &client.ListOptions{LabelSelector: selector}, configMapList) | ||
if err != nil { | ||
log.Println(err) | ||
return reconcile.Result{}, err | ||
} | ||
configMapsToChange = configMapList.Items | ||
log.Printf("%s changed, updating %d configMaps", names.TRUSTED_CA_BUNDLE_CONFIGMAP, len(configMapsToChange)) | ||
} else { | ||
// Changing a single labeled configmap. | ||
|
||
// Get the requested object. | ||
requestedCAbundleConfigMap := &corev1.ConfigMap{} | ||
requestedCAbundleConfigMapName := types.NamespacedName{ | ||
Namespace: request.Namespace, | ||
Name: request.Name, | ||
} | ||
err = r.client.Get(context.TODO(), requestedCAbundleConfigMapName, requestedCAbundleConfigMap) | ||
if err != nil { | ||
log.Println(err) | ||
if apierrors.IsNotFound(err) { | ||
return reconcile.Result{}, nil | ||
} | ||
return reconcile.Result{}, err | ||
} | ||
configMapsToChange = append(configMapsToChange, *requestedCAbundleConfigMap) | ||
} | ||
|
||
errs := []error{} | ||
|
||
for _, configMap := range configMapsToChange { | ||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { | ||
retrievedConfigMap := &corev1.ConfigMap{} | ||
err = r.client.Get(context.TODO(), types.NamespacedName{Namespace: configMap.Namespace, Name: configMap.Name}, retrievedConfigMap) | ||
if err != nil { | ||
if errors.IsNotFound(err) { | ||
return nil | ||
} | ||
log.Println(err) | ||
return err | ||
} | ||
configMapToUpdate := retrievedConfigMap.DeepCopy() | ||
if configMapToUpdate.Data == nil { | ||
configMapToUpdate.Data = map[string]string{names.TRUSTED_CA_BUNDLE_CONFIGMAP_KEY: string(trustedCAbundleData)} | ||
} else { | ||
configMapToUpdate.Data[names.TRUSTED_CA_BUNDLE_CONFIGMAP_KEY] = string(trustedCAbundleData) | ||
} | ||
if equality.Semantic.DeepEqual(configMapToUpdate, retrievedConfigMap) { | ||
// Nothing to update the new and old configmap object would be the same. | ||
return nil | ||
} | ||
err = r.client.Update(context.TODO(), configMapToUpdate) | ||
if err != nil { | ||
log.Println(err) | ||
return err | ||
} | ||
return nil | ||
}) | ||
if err != nil { | ||
errs = append(errs, err) | ||
if len(errs) > 5 { | ||
return reconcile.Result{}, fmt.Errorf("Too many errors attempting to update configmaps with CA cert. data") | ||
} | ||
} | ||
} | ||
if len(errs) > 0 { | ||
return reconcile.Result{}, fmt.Errorf("some configmaps didn't fully update with CA cert. data") | ||
} | ||
return reconcile.Result{}, nil | ||
} | ||
|
||
func shouldUpdateConfigMaps(meta metav1.Object) bool { | ||
return meta.GetLabels()[names.TRUSTED_CA_BUNDLE_CONFIGMAP_LABEL] == "true" || | ||
(meta.GetName() == names.TRUSTED_CA_BUNDLE_CONFIGMAP && meta.GetNamespace() == names.TRUSTED_CA_BUNDLE_CONFIGMAP_NS) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package validation | ||
|
||
import ( | ||
"crypto/x509" | ||
"encoding/pem" | ||
"fmt" | ||
|
||
"github.com/openshift/cluster-network-operator/pkg/names" | ||
|
||
corev1 "k8s.io/api/core/v1" | ||
) | ||
|
||
const ( | ||
// certPEMBlock is the type taken from the preamble of a PEM-encoded structure. | ||
certPEMBlock = "CERTIFICATE" | ||
) | ||
|
||
// TrustBundleConfigMap validates that ConfigMap contains a | ||
// trust bundle named "ca-bundle.crt" and that "ca-bundle.crt" | ||
// contains one or more valid PEM encoded certificates, returning | ||
// a byte slice of "ca-bundle.crt" contents upon success. | ||
func TrustBundleConfigMap(cfgMap *corev1.ConfigMap) ([]*x509.Certificate, []byte, error) { | ||
if _, ok := cfgMap.Data[names.TRUSTED_CA_BUNDLE_CONFIGMAP_KEY]; !ok { | ||
return nil, nil, fmt.Errorf("ConfigMap %q is missing %q", cfgMap.Name, names.TRUSTED_CA_BUNDLE_CONFIGMAP_KEY) | ||
} | ||
trustBundleData := []byte(cfgMap.Data[names.TRUSTED_CA_BUNDLE_CONFIGMAP_KEY]) | ||
if len(trustBundleData) == 0 { | ||
return nil, nil, fmt.Errorf("data key %q is empty from ConfigMap %q", names.TRUSTED_CA_BUNDLE_CONFIGMAP_KEY, cfgMap.Name) | ||
} | ||
certBundle, _, err := CertificateData(trustBundleData) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("failed parsing certificate data from ConfigMap %q: %v", cfgMap.Name, err) | ||
} | ||
|
||
return certBundle, trustBundleData, nil | ||
} | ||
|
||
// CertificateData decodes certData, ensuring each PEM block is type | ||
// "CERTIFICATE" and the block can be parsed as an x509 certificate, | ||
// returning slices of parsed certificates and parsed certificate data. | ||
func CertificateData(certData []byte) ([]*x509.Certificate, []byte, error) { | ||
var block *pem.Block | ||
certBundle := []*x509.Certificate{} | ||
for len(certData) != 0 { | ||
block, certData = pem.Decode(certData) | ||
if block == nil { | ||
return nil, nil, fmt.Errorf("failed to parse certificate PEM") | ||
} | ||
if block.Type != certPEMBlock { | ||
return nil, nil, fmt.Errorf("invalid certificate PEM, must be of type %q", certPEMBlock) | ||
|
||
} | ||
|
||
cert, err := x509.ParseCertificate(block.Bytes) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("failed to parse certificate: %v", err) | ||
} | ||
certBundle = append(certBundle, cert) | ||
} | ||
|
||
return certBundle, certData, nil | ||
} |