Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Proxy Controller #245

Merged
merged 11 commits into from
Aug 8, 2019
2 changes: 2 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
156 changes: 156 additions & 0 deletions pkg/controller/proxyconfig/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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"

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/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"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
}

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

danehans marked this conversation as resolved.
Show resolved Hide resolved
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 and will ensure the proxy object is in the
// desired state.
func (r *ReconcileProxyConfig) Reconcile(request reconcile.Request) (reconcile.Result, error) {
log.Printf("Reconciling proxy '%s'", request.Name)
proxyConfig := &configv1.Proxy{}
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.Printf("proxy '%s' not found; reconciliation will be skipped", request.Name)
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, fmt.Errorf("failed to get proxy '%s': %v", request.Name, err)
}

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

// Only proceed if the required config objects can be collected.
infraConfig := &configv1.Infrastructure{}
netConfig := &configv1.Network{}
clusterCfgMap := &corev1.ConfigMap{}
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: names.CLUSTER_CONFIG}, infraConfig); err != nil {
log.Printf("failed to get infrastructure config '%s': %v", names.CLUSTER_CONFIG, err)
r.status.SetDegraded(statusmanager.ProxyConfig, "InfraConfigError",
fmt.Sprintf("Error getting infrastructure config %s: %v.", names.CLUSTER_CONFIG, err))
return reconcile.Result{}, nil
}
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: names.CLUSTER_CONFIG}, netConfig); err != nil {
log.Printf("failed to get network config '%s': %v", names.CLUSTER_CONFIG, err)
r.status.SetDegraded(statusmanager.ProxyConfig, "NetworkConfigError",
fmt.Sprintf("Error getting network config '%s': %v.", names.CLUSTER_CONFIG, err))
return reconcile.Result{}, nil
}
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: "cluster-config-v1", Namespace: "kube-system"},
clusterCfgMap); err != nil {
log.Printf("failed to get configmap '%s/%s': %v", clusterCfgMap.Namespace, clusterCfgMap.Name, err)
r.status.SetDegraded(statusmanager.ProxyConfig, "ClusterConfigError",
fmt.Sprintf("Error getting cluster config configmap '%s/%s': %v.", clusterCfgMap.Namespace,
clusterCfgMap.Name, err))
return reconcile.Result{}, nil
}
danehans marked this conversation as resolved.
Show resolved Hide resolved
if err := r.ValidateProxyConfig(&proxyConfig.Spec); err != nil {
log.Printf("Failed to validate proxy '%s': %v", proxyConfig.Name, err)
r.status.SetDegraded(statusmanager.ProxyConfig, "InvalidProxyConfig",
fmt.Sprintf("The configuration is invalid for proxy '%s' (%v). "+
"Use 'oc edit proxy.config.openshift.io %s' to fix.", proxyConfig.Name, proxyConfig.Name, err))
return reconcile.Result{}, nil
}

// Update proxy status.
if err := r.syncProxyStatus(proxyConfig, infraConfig, netConfig, clusterCfgMap); err != nil {
log.Printf("Could not sync proxy '%s' status: %v", proxyConfig.Name, err)
r.status.SetDegraded(statusmanager.ProxyConfig, "StatusError",
fmt.Sprintf("Could not update proxy '%s' status: %v", proxyConfig.Name, err))
return reconcile.Result{}, err
}
log.Printf("Reconciling proxy '%s' complete", request.Name)

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 {
danehans marked this conversation as resolved.
Show resolved Hide resolved
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
}
124 changes: 124 additions & 0 deletions pkg/controller/proxyconfig/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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 of 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) {
danehans marked this conversation as resolved.
Show resolved Hide resolved
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"]
danehans marked this conversation as resolved.
Show resolved Hide resolved
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, configv1.GCPPlatformType, configv1.AzurePlatformType, configv1.OpenStackPlatformType:
set.Insert("169.254.169.254", ic.Networking.MachineCIDR)
}

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)
danehans marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
56 changes: 56 additions & 0 deletions pkg/controller/proxyconfig/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package proxyconfig

import (
"fmt"
"net"
"strings"

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

const (
proxyHTTPScheme = "http"
proxyHTTPSScheme = "https"
// noProxyWildcard is the string used to as a wildcard attached to a
// domain suffix in proxy.spec.noProxy to bypass proxying.
noProxyWildcard = "*"
)

// ValidateProxyConfig ensures that proxyConfig is valid.
func (r *ReconcileProxyConfig) ValidateProxyConfig(proxyConfig *configv1.ProxySpec) error {
danehans marked this conversation as resolved.
Show resolved Hide resolved
if isSpecHTTPProxySet(proxyConfig) {
scheme, err := validation.URI(proxyConfig.HTTPProxy)
if err != nil {
return fmt.Errorf("invalid httpProxy URI: %v", err)
}
if scheme != proxyHTTPScheme {
return fmt.Errorf("httpProxy requires an %q URI scheme", proxyHTTPScheme)
}
}

if isSpecHTTPSProxySet(proxyConfig) {
scheme, err := validation.URI(proxyConfig.HTTPSProxy)
if err != nil {
return fmt.Errorf("invalid httpsProxy URI: %v", err)
}
if scheme != proxyHTTPScheme && scheme != proxyHTTPSScheme {
return fmt.Errorf("httpsProxy requires a %q or %s URI scheme", proxyHTTPScheme, proxyHTTPSScheme)
Copy link

Choose a reason for hiding this comment

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

why is one of these %q and the other %s?

}
}

if isSpecNoProxySet(proxyConfig) {
if proxyConfig.NoProxy != noProxyWildcard {
for _, v := range strings.Split(proxyConfig.NoProxy, ",") {
v = strings.TrimSpace(v)
errDomain := validation.DomainName(v, false)
_, _, errCIDR := net.ParseCIDR(v)
if errDomain != nil && errCIDR != nil {
return fmt.Errorf("invalid noProxy: %v", v)
}
}
}
}

return nil
}
1 change: 1 addition & 0 deletions pkg/controller/statusmanager/status_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type StatusLevel int
const (
ClusterConfig StatusLevel = iota
OperatorConfig StatusLevel = iota
ProxyConfig StatusLevel = iota
PodDeployment StatusLevel = iota
maxStatusLevel StatusLevel = iota
)
Expand Down
13 changes: 13 additions & 0 deletions pkg/names/names.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package names

import "k8s.io/apimachinery/pkg/types"

// some names

// OperatorConfig is the name of the CRD that defines the complete
Expand All @@ -10,6 +12,9 @@ const OPERATOR_CONFIG = "cluster"
// and status object.
const CLUSTER_CONFIG = "cluster"

// PROXY_CONFIG is the name of the default proxy object.
const PROXY_CONFIG = "cluster"

// APPLIED_PREFIX is the prefix applied to the config maps
// where we store previously applied configuration
const APPLIED_PREFIX = "applied-"
Expand All @@ -31,3 +36,11 @@ const SERVICE_CA_CONFIGMAP = "openshift-service-ca"
// MULTUS_VALIDATING_WEBHOOK is the name of the ValidatingWebhookConfiguration for multus-admission-controller
// that is used in multus admission controller deployment
const MULTUS_VALIDATING_WEBHOOK = "multus.openshift.io"

// Proxy returns the namespaced name "cluster" in the
// default namespace.
func Proxy() types.NamespacedName {
return types.NamespacedName{
Name: PROXY_CONFIG,
}
}
File renamed without changes.
File renamed without changes.
Loading