mirror of https://github.com/kubernetes/kops.git
196 lines
6.1 KiB
Go
196 lines
6.1 KiB
Go
/*
|
|
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)
|
|
}
|