...

Source file src/edge-infra.dev/pkg/edge/controllers/envctl/configmap_replication_controller.go

Documentation: edge-infra.dev/pkg/edge/controllers/envctl

     1  package envctl
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	unstructuredutil "edge-infra.dev/pkg/k8s/unstructured"
    10  
    11  	"github.com/fluxcd/pkg/ssa"
    12  	"github.com/go-logr/logr"
    13  
    14  	corev1 "k8s.io/api/core/v1"
    15  	k8errors "k8s.io/apimachinery/pkg/api/errors"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    18  	"k8s.io/apimachinery/pkg/types"
    19  
    20  	"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
    21  	ctrl "sigs.k8s.io/controller-runtime"
    22  	"sigs.k8s.io/controller-runtime/pkg/builder"
    23  	"sigs.k8s.io/controller-runtime/pkg/client"
    24  	"sigs.k8s.io/controller-runtime/pkg/handler"
    25  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    26  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    27  )
    28  
    29  const (
    30  	CMNamespaceAnnotation  = "injector.edge.ncr.com/configmap"
    31  	CMReplicatedAnnotation = "injector.edge.ncr.com/configmap-replication"
    32  )
    33  
    34  var (
    35  	configMapMapping = map[string]types.NamespacedName{
    36  		"bsl-info":  {Namespace: "kube-public", Name: "bsl-info"},
    37  		"edge-info": {Namespace: "kube-public", Name: "edge-info"},
    38  	}
    39  )
    40  
    41  type ConfigMapReplicationReconciler struct {
    42  	client.Client
    43  	Name            string
    44  	RequeueTime     time.Duration
    45  	ResourceManager *ssa.ResourceManager
    46  }
    47  
    48  func (r *ConfigMapReplicationReconciler) SetupWithManager(mgr ctrl.Manager) error {
    49  	r.ResourceManager = ssa.NewResourceManager(
    50  		r.Client,
    51  		polling.NewStatusPoller(r.Client, r.Client.RESTMapper(), polling.Options{}), ssa.Owner{Field: r.Name},
    52  	)
    53  
    54  	return ctrl.NewControllerManagedBy(mgr).
    55  		For(&corev1.Namespace{}, namespacePredicates()).
    56  		Watches(
    57  			&corev1.ConfigMap{},
    58  			handler.EnqueueRequestsFromMapFunc(r.namespacesToEnqueue),
    59  			builder.WithPredicates(isConfigMapCopyable())).
    60  		Complete(r)
    61  }
    62  
    63  func namespacePredicates() builder.Predicates {
    64  	return builder.WithPredicates(
    65  		predicate.AnnotationChangedPredicate{},
    66  		predicate.NewPredicateFuncs(func(obj client.Object) bool {
    67  			return obj.GetAnnotations()[CMNamespaceAnnotation] != ""
    68  		}))
    69  }
    70  
    71  func isConfigMapCopyable() predicate.Funcs {
    72  	return predicate.NewPredicateFuncs(func(obj client.Object) bool {
    73  		for _, item := range configMapMapping {
    74  			if item.Namespace == obj.GetNamespace() &&
    75  				item.Name == obj.GetName() {
    76  				return true
    77  			}
    78  		}
    79  		return false
    80  	})
    81  }
    82  
    83  func (r *ConfigMapReplicationReconciler) namespacesToEnqueue(_ context.Context, _ client.Object) []reconcile.Request {
    84  	nss := &corev1.NamespaceList{}
    85  	if err := r.Client.List(context.Background(), nss, &client.ListOptions{}); err != nil {
    86  		return nil
    87  	}
    88  
    89  	var validNS []corev1.Namespace
    90  	for _, ns := range nss.Items {
    91  		if ns.GetAnnotations()[CMNamespaceAnnotation] != "" {
    92  			validNS = append(validNS, ns)
    93  		}
    94  	}
    95  
    96  	var reqs []reconcile.Request
    97  	for _, ns := range validNS {
    98  		reqs = append(reqs, reconcile.Request{NamespacedName: types.NamespacedName{Name: ns.Name}})
    99  	}
   100  	return reqs
   101  }
   102  
   103  func (r *ConfigMapReplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
   104  	log := ctrl.LoggerFrom(ctx).WithName(r.Name).WithValues("req", req)
   105  
   106  	ns := &corev1.Namespace{}
   107  	if err := r.Client.Get(ctx, req.NamespacedName, ns); err != nil {
   108  		return ctrl.Result{}, client.IgnoreNotFound(err)
   109  	}
   110  
   111  	log.Info("configmap replication started")
   112  
   113  	ctx = logr.NewContext(ctx, log)
   114  	if res, err := r.reconcile(ctx, ns); err != nil {
   115  		log.Error(err, "fail to replicate configmaps")
   116  		return res, err
   117  	}
   118  
   119  	log.Info("successfully replicated configmaps")
   120  	return ctrl.Result{}, nil
   121  }
   122  
   123  func (r *ConfigMapReplicationReconciler) reconcile(ctx context.Context, ns *corev1.Namespace) (ctrl.Result, error) { //nolint:unparam
   124  	log := ctrl.LoggerFrom(ctx)
   125  
   126  	nns, err := parseAnnotation(ns)
   127  	if err != nil {
   128  		log.Error(err, "invalid annotation, will not retry")
   129  		return ctrl.Result{Requeue: false}, nil
   130  	}
   131  
   132  	var uns []*unstructured.Unstructured
   133  	for _, nn := range nns {
   134  		cm := &corev1.ConfigMap{}
   135  		err = r.Client.Get(ctx, nn, cm)
   136  		if err != nil {
   137  			if k8errors.IsNotFound(err) {
   138  				log.Info("configmap not found, it will be ignored", "cm", nn)
   139  				continue
   140  			}
   141  			log.Error(err, "cannot get configmap to replicate", "cm", nn)
   142  			return ctrl.Result{Requeue: true, RequeueAfter: r.RequeueTime}, nil
   143  		}
   144  
   145  		un, err := r.copyCM(cm, types.NamespacedName{Name: cm.Name, Namespace: ns.Name}, nn)
   146  		if err != nil {
   147  			log.Error(err, "interval error: cannot convert to unstructured", "key", nn)
   148  			return ctrl.Result{Requeue: false}, nil
   149  		}
   150  		uns = append(uns, un)
   151  	}
   152  
   153  	_, err = r.ResourceManager.ApplyAll(ctx, uns, ssa.ApplyOptions{})
   154  	if err != nil {
   155  		log.Error(err, "applying configmaps failed")
   156  		return ctrl.Result{Requeue: true, RequeueAfter: r.RequeueTime}, nil
   157  	}
   158  	return ctrl.Result{}, nil
   159  }
   160  
   161  func (r *ConfigMapReplicationReconciler) copyCM(existingCM *corev1.ConfigMap, nn, from types.NamespacedName) (*unstructured.Unstructured, error) {
   162  	cm := &corev1.ConfigMap{
   163  		TypeMeta: metav1.TypeMeta{
   164  			APIVersion: "v1",
   165  			Kind:       "ConfigMap",
   166  		},
   167  		ObjectMeta: metav1.ObjectMeta{
   168  			Name:      nn.Name,
   169  			Namespace: nn.Namespace,
   170  			Annotations: map[string]string{
   171  				CMReplicatedAnnotation: fmt.Sprintf("%s/%s", from.Namespace, from.Name),
   172  			},
   173  		},
   174  		Data: existingCM.Data,
   175  	}
   176  	return unstructuredutil.ToUnstructured(cm)
   177  }
   178  
   179  func parseAnnotation(ns *corev1.Namespace) (map[string]types.NamespacedName, error) {
   180  	anno := ns.Annotations[CMNamespaceAnnotation]
   181  	if anno == "" {
   182  		return nil, fmt.Errorf("no annotation found for configmap replication")
   183  	}
   184  
   185  	values := strings.Split(anno, ",")
   186  
   187  	result := make(map[string]types.NamespacedName)
   188  	for _, val := range values {
   189  		nn, ok := configMapMapping[val]
   190  		if !ok {
   191  			return nil, fmt.Errorf("invalid annotation found for comfigmap replication")
   192  		}
   193  		result[val] = nn
   194  	}
   195  	return result, nil
   196  }
   197  

View as plain text