Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CA bundle injector #274

Merged
merged 3 commits into from
Aug 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pkg/controller/add_networkconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -13,5 +14,6 @@ func init() {
operconfig.Add,
clusterconfig.Add,
operconfig.AddConfigMapReconciler,
configmapcainjector.Add,
)
}
196 changes: 196 additions & 0 deletions pkg/controller/configmap_ca_injector/controller.go
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
JacobTanenbaum marked this conversation as resolved.
Show resolved Hide resolved
}
_, trustedCAbundleData, err := validation.TrustBundleConfigMap(trustedCAbundleConfigMap)

if err != nil {
return reconcile.Result{}, err
}
// Build a list of configMaps.
configMapsToChange := []corev1.ConfigMap{}
squeed marked this conversation as resolved.
Show resolved Hide resolved

// 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
JacobTanenbaum marked this conversation as resolved.
Show resolved Hide resolved
}
configMapsToChange = configMapList.Items
JacobTanenbaum marked this conversation as resolved.
Show resolved Hide resolved
log.Printf("%s changed, updating %d configMaps", names.TRUSTED_CA_BUNDLE_CONFIGMAP, len(configMapsToChange))
} else {
JacobTanenbaum marked this conversation as resolved.
Show resolved Hide resolved
// 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)
JacobTanenbaum marked this conversation as resolved.
Show resolved Hide resolved
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 {
JacobTanenbaum marked this conversation as resolved.
Show resolved Hide resolved
squeed marked this conversation as resolved.
Show resolved Hide resolved
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
JacobTanenbaum marked this conversation as resolved.
Show resolved Hide resolved
}
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)
JacobTanenbaum marked this conversation as resolved.
Show resolved Hide resolved
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)
}
26 changes: 26 additions & 0 deletions pkg/names/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,36 @@ 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 {
return types.NamespacedName{
Name: PROXY_CONFIG,
JacobTanenbaum marked this conversation as resolved.
Show resolved Hide resolved
}
}

// 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,
}
}
62 changes: 62 additions & 0 deletions pkg/util/validation/trustbundle.go
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
}