Skip to content
4 changes: 4 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pkg/controller/add_networkconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package controller
import (
"github.com/openshift/cluster-network-operator/pkg/controller/clusterconfig"
"github.com/openshift/cluster-network-operator/pkg/controller/operconfig"
"github.com/openshift/cluster-network-operator/pkg/controller/proxyconfig"
)

func init() {
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
AddToManagerFuncs = append(AddToManagerFuncs,
proxyconfig.Add,
operconfig.Add,
clusterconfig.Add,
operconfig.AddConfigMapReconciler,
Expand Down
225 changes: 225 additions & 0 deletions pkg/controller/proxyconfig/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package proxyconfig

import (
"context"
"fmt"
"log"

configv1 "github.com/openshift/api/config/v1"
"github.com/openshift/cluster-network-operator/pkg/controller/statusmanager"
"github.com/openshift/cluster-network-operator/pkg/names"
"github.com/openshift/cluster-network-operator/pkg/util/validation"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)

// and Start it when the Manager is Started.
func Add(mgr manager.Manager, status *statusmanager.StatusManager) error {
reconciler := newReconciler(mgr, status)
if reconciler == nil {
return fmt.Errorf("failed to create reconciler")
}

return add(mgr, reconciler)
}

// newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager, status *statusmanager.StatusManager) reconcile.Reconciler {
if err := configv1.Install(mgr.GetScheme()); err != nil {
return &ReconcileProxyConfig{}
}

return &ReconcileProxyConfig{client: mgr.GetClient(), scheme: mgr.GetScheme(), status: status}
}

// add adds a new Controller to mgr with r as the reconcile.Reconciler
func add(mgr manager.Manager, r reconcile.Reconciler) error {
// Create a new controller
c, err := controller.New("proxyconfig-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}

// We only care about a configmap source with a specific name/namespace,
// so filter events before they are provided to the controller event handlers.
pred := predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
return e.MetaOld.GetName() == names.TRUST_BUNDLE_CONFIGMAP &&
e.MetaOld.GetNamespace() == names.TRUST_BUNDLE_CONFIGMAP_NS
},
DeleteFunc: func(e event.DeleteEvent) bool {
return e.Meta.GetName() == names.TRUST_BUNDLE_CONFIGMAP &&
e.Meta.GetNamespace() == names.TRUST_BUNDLE_CONFIGMAP_NS
},
CreateFunc: func(e event.CreateEvent) bool {
return e.Meta.GetName() == names.TRUST_BUNDLE_CONFIGMAP &&
e.Meta.GetNamespace() == names.TRUST_BUNDLE_CONFIGMAP_NS
},
GenericFunc: func(e event.GenericEvent) bool {
return e.Meta.GetName() == names.TRUST_BUNDLE_CONFIGMAP &&
e.Meta.GetNamespace() == names.TRUST_BUNDLE_CONFIGMAP_NS
},
}

// Watch for changes to the trust bundle configmap.
err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, &handler.EnqueueRequestForObject{}, pred)
if err != nil {
return err
}

// Watch for changes to the proxy resource.
err = c.Watch(&source.Kind{Type: &configv1.Proxy{}}, &handler.EnqueueRequestForObject{})
if err != nil {
return err
}

return nil
}

// ReconcileProxyConfig reconciles a Proxy object
type ReconcileProxyConfig struct {
// This client, initialized using mgr.Client() above, is a split client
// that reads objects from the cache and writes to the apiserver.
client client.Client
scheme *runtime.Scheme
status *statusmanager.StatusManager
}

// Reconcile expects request to refer to a proxy object named "cluster"
// in the default namespace or to a configmap object named
// "trusted-ca-bundle" in namespace "openshift-config-managed", and will
// ensure the proxy object is in the desired state.
func (r *ReconcileProxyConfig) Reconcile(request reconcile.Request) (reconcile.Result, error) {
switch {
case request.NamespacedName == names.Proxy():
// Collect required config objects for proxy reconciliation.
proxyConfig := &configv1.Proxy{}
infraConfig := &configv1.Infrastructure{}
netConfig := &configv1.Network{}
clusterConfig := &corev1.ConfigMap{}

log.Printf("Reconciling proxy: %s\n", request.Name)
err := r.client.Get(context.TODO(), request.NamespacedName, proxyConfig)
if err != nil {
if apierrors.IsNotFound(err) {
// Request object not found, could have been deleted after reconcile request.
// Return and don't requeue
log.Println("proxy not found; reconciliation will be skipped", "request", request)
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, fmt.Errorf("failed to get proxy %q: %v", request, err)
}

// A nil proxy is generated by upgrades and installs not requiring a proxy.
validate := true
if !isSpecHTTPProxySet(&proxyConfig.Spec) && !isSpecHTTPSProxySet(&proxyConfig.Spec) {
log.Printf("httpProxy and httpsProxy not defined; validation will be skipped for proxy: %s\n",
request.Name)
validate = false
}

// Only proceed if the required config objects can be collected.
if validate {
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: "cluster"}, infraConfig); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to get infrastructure config 'cluster': %v", err)
}
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: "cluster"}, netConfig); err != nil {
log.Printf("failed to get network config 'cluster': %v", err)
return reconcile.Result{}, err
}
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: "cluster-config-v1", Namespace: "kube-system"}, clusterConfig); err != nil {
log.Printf("failed to get configmap 'cluster': %v", err)
return reconcile.Result{}, err
}
if err := r.ValidateProxyConfig(&proxyConfig.Spec); err != nil {
log.Printf("Failed to validate Proxy.Spec: %v", err)
r.status.SetDegraded(statusmanager.ProxyConfig, "InvalidProxyConfig",
fmt.Sprintf("The proxy configuration is invalid (%v). Use 'oc edit proxy.config.openshift.io cluster' to fix.", err))
return reconcile.Result{}, nil
}
}

// Update proxy status.
if err := r.syncProxyStatus(proxyConfig, infraConfig, netConfig, clusterConfig); err != nil {
log.Printf("Could not sync proxy status: %v", err)
r.status.SetDegraded(statusmanager.ProxyConfig, "StatusError",
fmt.Sprintf("Could not update proxy configuration status: %v", err))
return reconcile.Result{}, err
}
log.Printf("Reconciling proxy: %s complete\n", request.Name)
case request.NamespacedName == names.TrustBundleConfigMap():
cfgMap := &corev1.ConfigMap{}
log.Printf("Reconciling configmap: %s/%s\n", request.Namespace, request.Name)
err := r.client.Get(context.TODO(), request.NamespacedName, cfgMap)
if err != nil {
if apierrors.IsNotFound(err) {
// Request object not found, could have been deleted after reconcile request.
// Return and don't requeue
log.Println("configmap not found; reconciliation will be skipped", "request", request)
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, fmt.Errorf("failed to get configmap %q: %v", request, err)
}
if _, _, err := validation.TrustBundleConfigMap(cfgMap); err != nil {
log.Printf("Failed to validate configmap: %v", err)
r.status.SetDegraded(statusmanager.ProxyConfig, "InvalidTrustedCAConfigMap",
fmt.Sprintf("The configmap is invalid (%v). Use 'oc edit configmap %s -n %s' to fix.", err,
request.Name, request.Namespace))
return reconcile.Result{}, nil
}
log.Printf("Reconciling configmap: %s/%s complete\n", request.Namespace, request.Name)
default:
// unknown object
log.Println("Ignoring unknown object, reconciliation will be skipped", "request", request)
}
r.status.SetNotDegraded(statusmanager.ProxyConfig)

return reconcile.Result{}, nil
}

// isSpecHTTPProxySet returns true if spec.httpProxy of
// proxyConfig is set.
func isSpecHTTPProxySet(proxyConfig *configv1.ProxySpec) bool {
return len(proxyConfig.HTTPProxy) > 0
}

// isSpecHTTPSProxySet returns true if spec.httpsProxy of
// proxyConfig is set.
func isSpecHTTPSProxySet(proxyConfig *configv1.ProxySpec) bool {
return len(proxyConfig.HTTPSProxy) > 0
}

// isSpecNoProxySet returns true if spec.NoProxy of proxyConfig is set.
func isSpecNoProxySet(proxyConfig *configv1.ProxySpec) bool {
return len(proxyConfig.NoProxy) > 0
}

// isSpecTrustedCASet returns true if spec.trustedCA of proxyConfig is set.
func isSpecTrustedCASet(proxyConfig *configv1.ProxySpec) bool {
return len(proxyConfig.TrustedCA.Name) > 0
}

// isTrustedCAConfigMap returns true if the ConfigMap name in
// spec.trustedCA is "proxy-ca-bundle".
func isTrustedCAConfigMap(proxyConfig *configv1.ProxySpec) bool {
return proxyConfig.TrustedCA.Name == names.TRUST_BUNDLE_CONFIGMAP
}

// isSpecReadinessEndpoints returns true if spec.readinessEndpoints of
// proxyConfig is set.
func isSpecReadinessEndpoints(proxyConfig *configv1.ProxySpec) bool {
return len(proxyConfig.ReadinessEndpoints) > 0
}
126 changes: 126 additions & 0 deletions pkg/controller/proxyconfig/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package proxyconfig

import (
"context"
"fmt"
"net/url"
"strconv"
"strings"

"github.com/ghodss/yaml"

configv1 "github.com/openshift/api/config/v1"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
)

// syncProxyStatus computes the current status of proxy and
// updates status if any changes since last sync.
func (r *ReconcileProxyConfig) syncProxyStatus(proxy *configv1.Proxy, infra *configv1.Infrastructure, network *configv1.Network, cluster *corev1.ConfigMap) error {
var err error
var noProxy string
updated := proxy.DeepCopy()

if isSpecNoProxySet(&proxy.Spec) {
if proxy.Spec.NoProxy == noProxyWildcard {
noProxy = proxy.Spec.NoProxy
} else {
noProxy, err = mergeUserSystemNoProxy(proxy, infra, network, cluster)
if err != nil {
return fmt.Errorf("failed to merge user/system noProxy settings: %v", err)
}
}
}

updated.Status.HTTPProxy = proxy.Spec.HTTPProxy
updated.Status.HTTPSProxy = proxy.Spec.HTTPSProxy
updated.Status.NoProxy = noProxy

if !proxyStatusesEqual(proxy.Status, updated.Status) {
if err := r.client.Status().Update(context.TODO(), updated); err != nil {
return fmt.Errorf("failed to update proxy status: %v", err)
}
}

return nil
}

// mergeUserSystemNoProxy merges user-supplied noProxy settings from proxy
// with cluster-wide noProxy settings, returning a merged, comma-separated
// string of noProxy settings.
func mergeUserSystemNoProxy(proxy *configv1.Proxy, infra *configv1.Infrastructure, network *configv1.Network, cluster *corev1.ConfigMap) (string, error) {
apiServerURL, err := url.Parse(infra.Status.APIServerURL)
if err != nil {
return "", fmt.Errorf("failed to parse API server URL")
}

internalAPIServer, err := url.Parse(infra.Status.APIServerInternalURL)
if err != nil {
return "", fmt.Errorf("failed to parse API server internal URL")
}

set := sets.NewString(
"127.0.0.1",
"localhost",
network.Status.ServiceNetwork[0],
apiServerURL.Hostname(),
internalAPIServer.Hostname(),
)

// TODO: This will be flexible when master machine management is more dynamic.
type installConfig struct {
ControlPlane struct {
Replicas string `json:"replicas"`
} `json:"controlPlane"`
Networking struct {
MachineCIDR string `json:"machineCIDR"`
} `json:"networking"`
}
var ic installConfig
data, ok := cluster.Data["install-config"]
if !ok {
return "", fmt.Errorf("missing install-config in configmap")
}
if err := yaml.Unmarshal([]byte(data), &ic); err != nil {
return "", fmt.Errorf("invalid install-config: %v\njson:\n%s", err, data)
}

switch infra.Status.PlatformStatus.Type {
case configv1.AWSPlatformType:
set.Insert("169.254.169.254", ic.Networking.MachineCIDR)
default:
return "", fmt.Errorf("unsupported infrastructure provider: %s", infra.Status.PlatformStatus.Type)
}

replicas, err := strconv.Atoi(ic.ControlPlane.Replicas)
if err != nil {
return "", fmt.Errorf("failed to parse install config replicas: %v", err)
}

for i := int64(0); i < int64(replicas); i++ {
etcdHost := fmt.Sprintf("etcd-%d.%s", i, infra.Status.EtcdDiscoveryDomain)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crawford - will these names be fixed with etcd operator in future?

set.Insert(etcdHost)
}

for _, clusterNetwork := range network.Status.ClusterNetwork {
set.Insert(clusterNetwork.CIDR)
}

for _, userValue := range strings.Split(proxy.Spec.NoProxy, ",") {
set.Insert(userValue)
}

return strings.Join(set.List(), ","), nil
}

// proxyStatusesEqual compares two ProxyStatus values. Returns true if the
// provided values should be considered equal for the purpose of determining
// whether an update is necessary, false otherwise.
func proxyStatusesEqual(a, b configv1.ProxyStatus) bool {
if a.HTTPProxy != b.HTTPProxy || a.HTTPSProxy != b.HTTPSProxy || a.NoProxy != b.NoProxy {
return false
}

return true
}
Loading