-
Notifications
You must be signed in to change notification settings - Fork 244
Adds Proxy Controller #245
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
Changes from all commits
0830ac3
095cd15
d577466
4443f4c
eaf1158
5689933
9e17a43
4885506
488b7fb
6b189c8
42dbcf8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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 | ||
} | ||
|
||
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 | ||
} |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
Uh oh!
There was an error while loading. Please reload this page.