/
hosts_controller.go
195 lines (154 loc) · 6.13 KB
/
hosts_controller.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"context"
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/klog/v2"
"k8s.io/kops/cmd/kops-controller/pkg/config"
"k8s.io/kops/pkg/apis/kops"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
)
// HostsReconciler populates an /etc/hosts style file in the CoreDNS config map,
// supporting in-pod resolution of our k8s.local entries.
type HostsReconciler struct {
// clusterName identifies the kOps cluster
clusterName string
// configMapID identifies the configmap we should update
configMapID types.NamespacedName
// client is the controller-runtime client
client client.Client
// log is a logr
log logr.Logger
// dynamicClient is a client-go client for patching ConfigMaps
dynamicClient dynamic.Interface
// lastUpdate holds the last value we updated, to reduce spurious updates.
lastUpdate *managedConfigMap
}
// NewHostsReconciler is the constructor for a HostsReconciler
func NewHostsReconciler(mgr manager.Manager, opt *config.Options, configMapID types.NamespacedName) (*HostsReconciler, error) {
r := &HostsReconciler{
clusterName: opt.ClusterName,
client: mgr.GetClient(),
log: ctrl.Log.WithName("controllers").WithName("Hosts"),
configMapID: configMapID,
}
dynamicClient, err := dynamic.NewForConfig(mgr.GetConfig())
if err != nil {
return nil, fmt.Errorf("error building dynamic client: %v", err)
}
r.dynamicClient = dynamicClient
return r, nil
}
// +kubebuilder:rbac:groups=,resources=endpoints,verbs=get;list;watch
// +kubebuilder:rbac:groups=,resources=configmaps,namespace=kube-system,resourceNames=coredns,verbs=get;patch
// Reconcile is the main reconciler function that observes node changes.
func (r *HostsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = r.log.WithValues("endpoints", req.NamespacedName)
// Although we label the service, the labels get copied to the endpoints by the kube-controller-manager.
endpointsLabels := client.HasLabels([]string{kops.DiscoveryLabelKey})
endpointsList := &corev1.EndpointsList{}
// For security, we only process endpoints in kube-system
if err := r.client.List(ctx, endpointsList, endpointsLabels, client.InNamespace("kube-system")); err != nil {
klog.Warningf("unable to list endpoints: %v", err)
return ctrl.Result{}, err
}
return ctrl.Result{}, r.updateHosts(ctx, endpointsList)
}
func (r *HostsReconciler) updateHosts(ctx context.Context, endpointsList *corev1.EndpointsList) error {
addrToHosts := make(map[string][]string)
for i := range endpointsList.Items {
endpoints := &endpointsList.Items[i]
hostname := endpoints.Labels[kops.DiscoveryLabelKey]
hostname = strings.TrimSuffix(hostname, ".")
if hostname == "" {
klog.Warningf("endpoints %s/%s found without discovery label %q; filtering is not working correctly", endpoints.Name, endpoints.Namespace, kops.DiscoveryLabelKey)
continue
}
suffix := ".internal." + r.clusterName
if !strings.HasSuffix(hostname, suffix) {
hostname = hostname + suffix
} else {
klog.Warningf("endpoints %s/%s found with full internal name for discovery label %q; use short name %q instead", endpoints.Name, endpoints.Namespace, kops.DiscoveryLabelKey, strings.TrimSuffix(hostname, suffix))
}
for j := range endpoints.Subsets {
subset := &endpoints.Subsets[j]
for k := range subset.Addresses {
address := &subset.Addresses[k]
ip := address.IP
if ip != "" {
addrToHosts[ip] = append(addrToHosts[ip], hostname)
}
}
}
}
return r.updateConfigMap(ctx, addrToHosts)
}
// managedConfigMap holds the fields we manage
type managedConfigMap struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Data map[string]string `json:"data"`
}
func (r *HostsReconciler) updateConfigMap(ctx context.Context, addrToHosts map[string][]string) error {
var block []string
for addr, hosts := range addrToHosts {
sort.Strings(hosts)
block = append(block, addr+"\t"+strings.Join(hosts, " "))
}
// Sort into a consistent order to minimize updates
sort.Strings(block)
hosts := strings.Join(block, "\n")
data := &managedConfigMap{}
data.APIVersion = "v1"
data.Kind = "ConfigMap"
data.Data = map[string]string{"hosts": hosts}
if r.lastUpdate != nil && reflect.DeepEqual(r.lastUpdate, data) {
klog.V(8).Infof("skipping hosts configmap update (unchanged): %#v", data)
return nil
}
klog.V(4).Infof("patching hosts configmap: %#v", data)
configmapGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}
patch, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal patch: %w", err)
}
// It is strongly recommended for controllers to always "force" conflicts, since they might not be able to resolve or act on these conflicts.
force := true
patchOpts := metav1.PatchOptions{
FieldManager: "kops-controller.kops.k8s.io/hosts",
Force: &force,
}
if _, err := r.dynamicClient.Resource(configmapGVR).Namespace(r.configMapID.Namespace).Patch(ctx, r.configMapID.Name, types.ApplyPatchType, patch, patchOpts); err != nil {
return fmt.Errorf("failed to patch configmap: %w", err)
}
r.lastUpdate = data
return nil
}
func (r *HostsReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Endpoints{}).
Complete(r)
}