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) {
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