diff --git a/Gopkg.lock b/Gopkg.lock index 99f92f232e..e573f49f36 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1002,6 +1002,7 @@ "k8s.io/apimachinery/pkg/api/meta", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured", + "k8s.io/apimachinery/pkg/labels", "k8s.io/apimachinery/pkg/runtime", "k8s.io/apimachinery/pkg/runtime/schema", "k8s.io/apimachinery/pkg/types", @@ -1017,6 +1018,7 @@ "k8s.io/client-go/rest", "k8s.io/client-go/restmapper", "k8s.io/client-go/tools/clientcmd", + "k8s.io/client-go/util/retry", "k8s.io/code-generator/cmd/client-gen", "k8s.io/code-generator/cmd/conversion-gen", "k8s.io/code-generator/cmd/deepcopy-gen", @@ -1031,8 +1033,10 @@ "sigs.k8s.io/controller-runtime/pkg/client/fake", "sigs.k8s.io/controller-runtime/pkg/controller", "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil", + "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/runtime/signals", "sigs.k8s.io/controller-runtime/pkg/source", diff --git a/pkg/controller/add_networkconfig.go b/pkg/controller/add_networkconfig.go index b1cfe0d34f..d8b02c022d 100644 --- a/pkg/controller/add_networkconfig.go +++ b/pkg/controller/add_networkconfig.go @@ -2,6 +2,7 @@ package controller import ( "github.com/openshift/cluster-network-operator/pkg/controller/clusterconfig" + "github.com/openshift/cluster-network-operator/pkg/controller/configmap_ca_injector" "github.com/openshift/cluster-network-operator/pkg/controller/operconfig" "github.com/openshift/cluster-network-operator/pkg/controller/proxyconfig" ) @@ -13,5 +14,6 @@ func init() { operconfig.Add, clusterconfig.Add, operconfig.AddConfigMapReconciler, + configmapcainjector.Add, ) } diff --git a/pkg/controller/configmap_ca_injector/controller.go b/pkg/controller/configmap_ca_injector/controller.go new file mode 100644 index 0000000000..e452f217af --- /dev/null +++ b/pkg/controller/configmap_ca_injector/controller.go @@ -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) +} diff --git a/pkg/names/names.go b/pkg/names/names.go index e278639099..96a9fd3406 100644 --- a/pkg/names/names.go +++ b/pkg/names/names.go @@ -37,6 +37,23 @@ const SERVICE_CA_CONFIGMAP = "openshift-service-ca" // that is used in multus admission controller deployment const MULTUS_VALIDATING_WEBHOOK = "multus.openshift.io" +// TRUSTED_CA_BUNDLE_CONFIGMAP_KEY is the name of the data key containing +// the PEM encoded trust bundle. +const TRUSTED_CA_BUNDLE_CONFIGMAP_KEY = "ca-bundle.crt" + +// TRUSTED_CA_BUNDLE_CONFIGMAP is the name of the ConfigMap +// containing the combined user/system trust bundle. +const TRUSTED_CA_BUNDLE_CONFIGMAP = "trusted-ca-bundle" + +// TRUSTED_CA_BUNDLE_CONFIGMAP_NS is the namespace that hosts the +// ADDL_TRUST_BUNDLE_CONFIGMAP and TRUST_BUNDLE_CONFIGMAP +// ConfigMaps. +const TRUSTED_CA_BUNDLE_CONFIGMAP_NS = "openshift-config-managed" + +// TRUSTED_CA_BUNDLE_CONFIGMAP_LABEL is the name of the label that +// determines whether or not to inject the combined ca certificate +const TRUSTED_CA_BUNDLE_CONFIGMAP_LABEL = "config.openshift.io/inject-trusted-cabundle" + // Proxy returns the namespaced name "cluster" in the // default namespace. func Proxy() types.NamespacedName { @@ -44,3 +61,12 @@ func Proxy() types.NamespacedName { Name: PROXY_CONFIG, } } + +// AddlTrustBundleConfigMapNS returns the namespaced name of the +// namespace containing the user-provided trust bundle ConfigMap. +func AddlTrustBundleConfigMap() types.NamespacedName { + return types.NamespacedName{ + Namespace: TRUSTED_CA_BUNDLE_CONFIGMAP_NS, + Name: TRUSTED_CA_BUNDLE_CONFIGMAP, + } +} diff --git a/pkg/util/validation/trustbundle.go b/pkg/util/validation/trustbundle.go new file mode 100644 index 0000000000..9ebfbbd69c --- /dev/null +++ b/pkg/util/validation/trustbundle.go @@ -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 +}