Skip to content
This repository has been archived by the owner on Aug 12, 2024. It is now read-only.

Commit

Permalink
Add ConfigMapSyncer controller
Browse files Browse the repository at this point in the history
The ConfigMapSyncer syncs secret data to configmaps based on injection
annotations present in configmaps in watched namespaces.

We include a rukpak-ca configmap with these annotations present so
that cluster administrators can share rukpak-ca trust without
exposing the CA key that's present in the rukpak-ca secret.

This commit also updates the rukpakctl binary to use the configmap
rather than the secret to load the rukpak CA. This is helpful for rukpak
users that might have access to read configmaps but not secrets in the
rukpak system namespace.

Signed-off-by: Joe Lanford <joe.lanford@gmail.com>
  • Loading branch information
joelanford committed Oct 28, 2022
1 parent edc58d2 commit 30e6bf1
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 19 deletions.
30 changes: 30 additions & 0 deletions cmd/core/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ import (
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/cluster"
crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"
"github.com/operator-framework/rukpak/internal/configmapsyncer"
"github.com/operator-framework/rukpak/internal/finalizer"
"github.com/operator-framework/rukpak/internal/provisioner/bundle"
"github.com/operator-framework/rukpak/internal/provisioner/bundledeployment"
Expand Down Expand Up @@ -139,6 +141,26 @@ func main() {
}

ns := util.PodNamespace(systemNamespace)

// TODO: use cache.MultiNamespacedCacheWithOptionsBuilder for core controller cache
// When https://github.com/kubernetes-sigs/controller-runtime/pull/1962
// merges, use cache.MultiNamespacedCacheWithOptionsBuilder so that
// the system namespace can use cache.New and watch all objects in its own
// namespace. Once we switch to that cache, we can avoid using a separate
// controller-runtime "cluster".
systemNamespaceClstr, err := cluster.New(cfg, func(options *cluster.Options) {
options.Namespace = ns
})
if err != nil {
setupLog.Error(err, "unable to create manager")
os.Exit(1)
}

if err := mgr.Add(systemNamespaceClstr); err != nil {
setupLog.Error(err, "unable to add system namespace cluster to manager")
os.Exit(1)
}

storageURL, err := url.Parse(fmt.Sprintf("%s/bundles/", httpExternalAddr))
if err != nil {
setupLog.Error(err, "unable to parse bundle content server URL")
Expand Down Expand Up @@ -247,6 +269,14 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", rukpakv1alpha1.BundleDeploymentKind, "provisionerID", plain.ProvisionerID)
os.Exit(1)
}

if err = (&configmapsyncer.Reconciler{
Client: systemNamespaceClstr.GetClient(),
Cache: systemNamespaceClstr.GetCache(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ConfigMap")
os.Exit(1)
}
//+kubebuilder:scaffold:builder

if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions cmd/rukpakctl/cmd/alpha_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func newAlphaBootstrapCmd() *cobra.Command {
var (
systemNamespace string
uploadServiceName string
caSecretName string
caConfigMapName string
)
cmd := &cobra.Command{
Use: "bootstrap <bundleDeploymentName>",
Expand Down Expand Up @@ -97,7 +97,7 @@ under the management of a rukpak BundleDeployment.'
Config: cfg,
SystemNamespace: systemNamespace,
UploadServiceName: uploadServiceName,
CASecretName: caSecretName,
CAConfigMapName: caConfigMapName,
}
modified, err := r.Run(ctx, bundleDeploymentName, bundleFS, rukpakctl.RunOptions{
BundleDeploymentProvisionerClassName: plain.ProvisionerID,
Expand All @@ -115,6 +115,6 @@ under the management of a rukpak BundleDeployment.'
}
cmd.Flags().StringVar(&systemNamespace, "system-namespace", util.DefaultSystemNamespace, "Namespace in which the core rukpak provisioners are running.")
cmd.Flags().StringVar(&uploadServiceName, "upload-service-name", util.DefaultUploadServiceName, "the name of the service of the upload manager.")
cmd.Flags().StringVar(&caSecretName, "ca-secret-name", "rukpak-ca", "the name of the secret in the system namespace containing the root CAs used to authenticate the upload service.")
cmd.Flags().StringVar(&caConfigMapName, "ca-configmap-name", util.DefaultCAConfigMapName, "the name of the configmap in the system namespace containing the root CAs used to authenticate the upload service.")
return cmd
}
6 changes: 3 additions & 3 deletions cmd/rukpakctl/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func newRunCmd() *cobra.Command {
var (
systemNamespace string
uploadServiceName string
caSecretName string
caConfigMapName string
bundleDeploymentProvisionerClassName string
bundleProvisionerClassName string
)
Expand Down Expand Up @@ -69,7 +69,7 @@ one version to the next.
Config: cfg,
SystemNamespace: systemNamespace,
UploadServiceName: uploadServiceName,
CASecretName: caSecretName,
CAConfigMapName: caConfigMapName,
}
_, err := r.Run(ctx, bundleDeploymentName, os.DirFS(bundleDir), rukpakctl.RunOptions{
BundleDeploymentProvisionerClassName: bundleDeploymentProvisionerClassName,
Expand All @@ -83,7 +83,7 @@ one version to the next.
}
cmd.Flags().StringVar(&systemNamespace, "system-namespace", util.DefaultSystemNamespace, "the namespace in which the rukpak controllers are deployed.")
cmd.Flags().StringVar(&uploadServiceName, "upload-service-name", util.DefaultUploadServiceName, "the name of the service of the upload manager.")
cmd.Flags().StringVar(&caSecretName, "ca-secret-name", "rukpak-ca", "the name of the secret in the system namespace containing the root CAs used to authenticate the upload service.")
cmd.Flags().StringVar(&caConfigMapName, "ca-configmap-name", util.DefaultCAConfigMapName, "the name of the configmap in the system namespace containing the root CAs used to authenticate the upload service.")
cmd.Flags().StringVar(&bundleDeploymentProvisionerClassName, "bundle-deployment-provisioner-class", plain.ProvisionerID, "Provisioner class name to set on bundle deployment.")
cmd.Flags().StringVar(&bundleProvisionerClassName, "bundle-provisioner-class", plain.ProvisionerID, "Provisioner class name to set on bundle.")
return cmd
Expand Down
158 changes: 158 additions & 0 deletions internal/configmapsyncer/configmapsyncer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package configmapsyncer

import (
"context"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)

const (
configMapInjectFromSecretName = "core.rukpak.io/inject-from-secret-name"
configMapInjectFromSecretKey = "core.rukpak.io/inject-from-secret-key"
configMapInjectToDataKey = "core.rukpak.io/inject-to-data-key"
configMapInjectToBinaryDataKey = "core.rukpak.io/inject-to-binarydata-key"
)

// Reconciler syncs secret fields to configmaps.
type Reconciler struct {
Client client.Client
Cache cache.Cache
}

// Reconcile syncs a secret field to a configmap field based on the presence
// of annotations "core.rukpak.io/inject-from-secret-name" and
// "core.rukpak.io/inject-from-secret-key" (configmaps without BOTH of these
// annotations are ignored). When these annotations are present, this
// reconciler manages all content in the data and binaryData fields.
//
// If the annotation "core.rukpak.io/inject-to-data-key" is present, Reconcile
// creates a data key containing the secret value. Otherwise, the configmap
// data will be empty.
//
// If the annotation "core.rukpak.io/inject-to-binarydata-key" is present,
// Reconcile creates a binaryData key containing the secret value. Otherwise,
// the configmap binaryData will be empty.
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
l := log.FromContext(ctx)
l.V(1).Info("starting reconciliation")
defer l.V(1).Info("ending reconciliation")

// Get configmap from cache and lookup its secret name annotation
cm := &corev1.ConfigMap{}
if err := r.Client.Get(ctx, req.NamespacedName, cm); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}

secretName, ok := cm.Annotations[configMapInjectFromSecretName]
if !ok {
return ctrl.Result{}, nil
}

// Get referenced secret name (in the same namespace as the configmap)
secret := &corev1.Secret{}
l.V(1).Info("inject from secret", "secretName", secretName)
if err := r.Client.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: secretName}, secret); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}

// Get the referenced secret's key and value
secretKey, ok := cm.Annotations[configMapInjectFromSecretKey]
if !ok {
return ctrl.Result{}, nil
}
secretValue := secret.Data[secretKey]

// If the configmap asks for injection into a binaryData key
// generate the expected binary data.
cmBinaryDataKey := cm.Annotations[configMapInjectToBinaryDataKey]
expectedBinaryData := map[string][]byte(nil)
if cmBinaryDataKey != "" {
expectedBinaryData = map[string][]byte{cmBinaryDataKey: secretValue}
}

// If the configmap asks for injection into a data key
// generate the expected data.
cmDataKey := cm.Annotations[configMapInjectToDataKey]
expectedData := map[string]string(nil)
if cmDataKey != "" {
expectedData = map[string]string{cmDataKey: string(secretValue)}
}

// If binaryData and data already have the expected contents,
// there's no need to do anything, so return early.
if equality.Semantic.DeepEqual(cm.BinaryData, expectedBinaryData) && equality.Semantic.DeepEqual(cm.Data, expectedData) {
return ctrl.Result{}, nil
}

// Set the expected binaryData and data fields on the configmap
// and then update it.
cm.BinaryData = expectedBinaryData
cm.Data = expectedData
return ctrl.Result{}, r.Client.Update(ctx, cm)
}

// SetupWithManager sets up the controller with the Manager.
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
ctrlr, err := controller.New("configmapsyncer", mgr, controller.Options{
Reconciler: r,
})
if err != nil {
return err
}

if err := ctrlr.Watch(
source.NewKindWithCache(&corev1.ConfigMap{}, r.Cache),
&handler.EnqueueRequestForObject{},
configMapHasInjectionAnnotationsPredicate(),
); err != nil {
return err
}

if err := ctrlr.Watch(
source.NewKindWithCache(&corev1.Secret{}, r.Cache),
secretToConfigMapMapper(r.Client),
); err != nil {
return err
}
return nil
}

func configMapHasInjectionAnnotationsPredicate() predicate.Predicate {
return predicate.NewPredicateFuncs(func(object client.Object) bool {
cm := object.(*corev1.ConfigMap)
if _, ok := cm.Annotations[configMapInjectFromSecretName]; !ok {
return false
}
if _, ok := cm.Annotations[configMapInjectFromSecretKey]; !ok {
return false
}
return true
})
}

func secretToConfigMapMapper(cl client.Reader) handler.EventHandler {
return handler.EnqueueRequestsFromMapFunc(func(object client.Object) []reconcile.Request {
cmList := &corev1.ConfigMapList{}
if err := cl.List(context.TODO(), cmList, client.InNamespace(object.GetNamespace())); err != nil {
return nil
}
reqs := []reconcile.Request{}
for _, cm := range cmList.Items {
if cm.Annotations[configMapInjectFromSecretName] == object.GetName() {
reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&cm)})
}
}
return reqs
})
}
8 changes: 4 additions & 4 deletions internal/rukpakctl/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import (

// GetClusterCA returns an x509.CertPool by reading the contents of a Kubernetes Secret. It uses the provided
// client to get the requested secret and then loads the contents of the secret's "ca.crt" key into the cert pool.
func GetClusterCA(ctx context.Context, cl client.Reader, secretKey types.NamespacedName) (*x509.CertPool, error) {
caSecret := &corev1.Secret{}
if err := cl.Get(ctx, secretKey, caSecret); err != nil {
func GetClusterCA(ctx context.Context, cl client.Reader, configmapKey types.NamespacedName) (*x509.CertPool, error) {
caConfigMap := &corev1.ConfigMap{}
if err := cl.Get(ctx, configmapKey, caConfigMap); err != nil {
return nil, fmt.Errorf("get rukpak certificate authority: %v", err)
}
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(caSecret.Data["ca.crt"]) {
if !certPool.AppendCertsFromPEM([]byte(caConfigMap.Data["ca-bundle.crt"])) {
return nil, errors.New("failed to load certificate authority into cert pool: malformed PEM?")
}
return certPool, nil
Expand Down
4 changes: 2 additions & 2 deletions internal/rukpakctl/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Run struct {

SystemNamespace string
UploadServiceName string
CASecretName string
CAConfigMapName string
}

// RunOptions define extra options used for Run.
Expand Down Expand Up @@ -88,7 +88,7 @@ func (r *Run) Run(ctx context.Context, bundleDeploymentName string, bundle fs.FS
return false, fmt.Errorf("failed to get bundle name: %v", err)
}

rukpakCA, err := GetClusterCA(ctx, cl, types.NamespacedName{Namespace: r.SystemNamespace, Name: r.CASecretName})
rukpakCA, err := GetClusterCA(ctx, cl, types.NamespacedName{Namespace: r.SystemNamespace, Name: r.CAConfigMapName})
if err != nil {
return false, err
}
Expand Down
1 change: 1 addition & 0 deletions internal/util/defaults_upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package util

const (
DefaultSystemNamespace = "rukpak-system"
DefaultCAConfigMapName = "rukpak-ca"
DefaultUnpackImage = "quay.io/operator-framework/rukpak:latest"
DefaultUploadServiceName = "core"
)
12 changes: 11 additions & 1 deletion manifests/core/resources/rukpak_issuer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,14 @@ metadata:
namespace: rukpak-system
spec:
ca:
secretName: rukpak-ca
secretName: rukpak-ca
---
apiVersion: v1
kind: ConfigMap
metadata:
annotations:
core.rukpak.io/inject-from-secret-name: rukpak-ca
core.rukpak.io/inject-from-secret-key: tls.crt
core.rukpak.io/inject-to-data-key: ca-bundle.crt
name: rukpak-ca
namespace: rukpak-system
36 changes: 36 additions & 0 deletions test/e2e/configmap_syncer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package e2e

import (
"context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
)

var _ = Describe("ConfigMapSyncer", func() {
ctx := context.Background()
It("should populate rukpak-ca configmap", func() {
By("fetching rukpak-ca secret")
secret := &corev1.Secret{}
Eventually(func() (map[string][]byte, error) {
err := c.Get(ctx, types.NamespacedName{Namespace: defaultSystemNamespace, Name: "rukpak-ca"}, secret)
return secret.Data, err
}).Should(And(
HaveKey("ca.crt"),
HaveKey("tls.crt"),
HaveKey("tls.key"),
))

By("fetching rukpak-ca configmap")
cm := &corev1.ConfigMap{}
Eventually(func() (map[string]string, error) {
err := c.Get(ctx, types.NamespacedName{Namespace: defaultSystemNamespace, Name: defaultCAConfigMapName}, cm)
return cm.Data, err
}).Should(HaveKey("ca-bundle.crt"))

By("comparing expected injected value")
Expect(string(secret.Data["tls.crt"])).To(Equal(cm.Data["ca-bundle.crt"]))
})
})
8 changes: 8 additions & 0 deletions test/e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"
"github.com/operator-framework/rukpak/internal/util"
)

const (
defaultSystemNamespace = util.DefaultSystemNamespace
defaultUploadServiceName = util.DefaultUploadServiceName
defaultCAConfigMapName = util.DefaultCAConfigMapName
testdataDir = "../../testdata"
)

var (
Expand Down
6 changes: 0 additions & 6 deletions test/e2e/plain_provisioner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@ import (
"github.com/operator-framework/rukpak/internal/util"
)

const (
defaultSystemNamespace = util.DefaultSystemNamespace
defaultUploadServiceName = util.DefaultUploadServiceName
testdataDir = "../../testdata"
)

func Logf(f string, v ...interface{}) {
if !strings.HasSuffix(f, "\n") {
f += "\n"
Expand Down

0 comments on commit 30e6bf1

Please sign in to comment.